← All projects

Compilers · Language tooling

OakLand

A small programming language with an interpreter you can step through and a compiler to WebAssembly you can read.

Role
Design, language, interpreter, type checker, compiler and interface
Stack
TypeScript · WebAssembly · React · CodeMirror 6 · Vitest · Playwright
OakLand: a screenshot of the live project

The problem

The course project could run a program, but you could not see how, it was slow, and nothing proved it was right. I wanted to understand compilers well enough to explain them: what a type checker knows, what a binary module looks like, why compiled code is fast.

The solution

I rebuilt it in three layers. An interpreter records every step of a run so the page can replay it on a timeline. A type checker gives a type to every expression. A compiler turns the checked program into a WebAssembly module, written byte by byte by my own encoder. In Compile mode the page shows your source next to the WebAssembly it became, and Compare runs both modes on the same program.

What it does

  • A Java-like language: classes with inheritance and overriding, functions inside functions, arrays, strings, structs
  • Interpret mode: a timeline of up to 20,000 steps, variables and call stack at any step
  • Compile mode: the module as text, one foldable block per function, each pointing at the source lines it came from
  • Compare: both modes on one program, drawn to scale, with a check that the outputs are identical
  • In the benchmarks, compiled programs ran 18 to 358 times faster than the interpreter (the machine is named in the README)

Decisions

The interpreter is the oracle

Two implementations of one language can disagree silently. So every program of the project, and 200 generated well-typed programs, run in both modes and must print the same text and meet the same first mistake. I also broke the compiler on purpose, swapping one instruction at a time, to check that those same programs notice: they caught all eight.

No toolchain

The binary encoder, the text printer and the runtime are written in the repository. The browser's own WebAssembly.validate is the independent referee. The documentation walks a two-line program down to its 189 bytes, and a test keeps that example true.

Honest numbers

The speed-ups are measured against my own rebuilt interpreter, which does more work than the compiled code. The original course interpreter was actually faster on simple loops. The README says so, and names the machine.