A language, top to bottom
My own programming language, from parser to machine code.
- built
- April - May 2026
- stack
- Go, C, Rust, Cranelift, LLVM
Why
After twelve years of writing programs, I wanted to understand the thing that runs them, so I went down the stack on purpose: write a language, make it fast, then make it native. It started from Thorsten Ball's Writing an Interpreter in Go and grew well past it.
The pipeline
1. An interpreter that grew up
The book ends with a small language: integers, strings, functions, arrays, hashes. I kept going until it was something I could write real programs in:
- The language: imports,
forandwhileloops,break/continue,++/--,&&/||, floats and bytes, string interpolation, destructuring,try/catch, and line numbers in every error. - Concurrency:
asyncblocks that return futures, withawaitandawait_all. - A static type checker that runs over the AST before anything executes.
- A standard library written in the language itself: JSON, regex, an HTTP/1.1 server with a router, templates, base64, and an
asserttest runner, so the language is tested in the language. - A real program on top: a static blog generator, written in the language, that reads posts and a config file and renders the site through its own templates.
What it looks like
Some of the things I added along the way:
Optional static types
let add = fn(a: int, b: int) -> int { a + b };
add(1, "two");
// type error: cannot pass string as argument 2 of type int
let x: int = null;
// type error: cannot assign null to int
async / await, try / catch
let results = await_all([async { 1 }, async { 2 }, async { 3 }]);
try {
await async { throw "oops" };
} catch(e) {
print("caught: ${e}");
}
Destructuring, interpolation, for-in
let [first, second] = ["hello", "world"];
let n = 5;
print("n squared is ${n * n}"); // n squared is 25
for (name in ["ana", "ben"]) {
print("hi ${name}");
}
A web server, written in the language
import "packages/http.mo";
import "packages/router.mo";
let r = router_new();
router_get(r, "/users/:id", fn(req) {
http_ok("user id is " + req["params"]["id"])
});
http_serve(8080, fn(req) { router_dispatch(r, req) });
Tests, also in the language
import "packages/assert.mo";
suite_begin("async / await");
test_case("await resolves a string", fn() {
let f = async { "hello" };
assert_eq(await f, "hello", "should resolve to hello");
});
The standard library
Every package below is written in the language itself, on top of a handful of builtins from Go.
- json: a parser and serializer, plus a pretty-printer
- http: an HTTP/1.1 request parser and server on raw TCP
- router: routes with path params like
/users/:id - template:
{{name}}style templates, from strings or files - regex: a small pattern matcher:
.,*,^,$ - base64: RFC 4648 encode and decode, and basic auth headers
- string: 23 helpers: search, padding, repeat, character checks
- arrays: 23 helpers: map, filter, reduce, find, take, drop and friends
- assert: a test runner with suites, before/after hooks and assertions
2. Bytecode and a virtual machine
Tree-walking is simple and slow. The second step (following Writing a Compiler in Go) compiles the AST to bytecode and runs it on a stack-based virtual machine: opcodes, a constant pool and stack management, with tests for both the compiler and the VM. This part is still in progress.
3. Down to machine code
Then the last step: no VM at all. A frontend in Go emits a small intermediate representation, and a backend in Rust hands it to Cranelift (the code generator behind Wasmtime) to produce a native object file, linked with a small C runtime into a real executable. In parallel I wrote LLVM IR by hand and compiled it with clang, to see how the two code generators compare.
Around it
- A C compiler, in C, with its own lexer, parser and unit tests. In progress.
- A thread pool in C on pthreads: a task queue, a mutex and a condition variable, the classic way.
- eBPF networking experiments: TCP socket hooks in the kernel streamed to a browser, which led straight into kwatch.
Reading list
What I worked through alongside it: Crafting Interpreters (Nystrom), Writing an Interpreter in Go and Writing a Compiler in Go (Ball), Engineering a Compiler (Cooper, Torczon), Computer Systems: A Programmer's Perspective (Bryant, O'Hallaron), Operating Systems: Three Easy Pieces, The C Programming Language (K&R), and Beej's networking guide.