Run it.
$ diff compiler.wat compiler2.wat
$
Empty. No output. The diff found nothing.
The compiler -- compiled to WebAssembly by the Zig host -- and the same compiler -- compiled to WebAssembly by itself running as WebAssembly -- produced identical output when given the same input.
The snake has eaten its tail.
### What just happened
Let's trace the full path:
1. We wrote eval("3+5") and got 8. (Problem 4)
2. We added a cursor, multi-digit numbers, whitespace, and precedence. (Problems 6–12)
3. We added typed variables, if/else, while, functions. (Problems 15–27)
4. We wrote a compiler that emits WAT instead of computing values. Same parser, different actions. (Problems 30–44)
5. We built a VM that executes WAT, proving the compiler correct through agreement testing. (Problems 48–62)
6. We added memory and strings so the language could manipulate bytes. (Problems 65–75)
7. We wrote compiler primitives in our own language and showed they worked. (Problems 76–79)
8. We assembled those primitives into a complete compiler -- in our language. (Problems 84–92)
9. We compiled the compiler with the Zig host, ran it as WASM, fed it its own source, and got identical output. (Problems 93–97)
Ninety-seven problems. One file. One language. A compiler that compiles itself.
### Why this matters
Self-hosting isn't just a party trick. It's a proof of completeness.
If the compiler can compile itself, it means the language is powerful enough to express its own implementation. Every feature the compiler uses -- arithmetic, string comparison, memory access, recursive descent, output buffering -- is a feature the language provides. There are no hidden dependencies, no magic behind the curtain. The language stands on its own feet.
It's also a practical tool. Once you have a self-hosting compiler, you can improve the language using the language itself. Add a feature to the compiler source, compile it with the old compiler, and now you have a new compiler that supports the new feature. Bootstrap again and the new compiler compiles itself with the new feature. The language grows itself.
c4 did this in 525 lines of C. We did it in a few hundred lines of our language, targeting WebAssembly instead of custom bytecode. The approach is different -- c4 uses single-pass compilation with no tree, just like we do -- but the principle is the same. The snake must be just long enough to eat itself. Not longer. Every unnecessary feature is code you have to compile, which is code you have to write, which is code the compiler has to handle. Minimalism isn't just aesthetic. It's engineering.
# Appendices
## Appendix A: A Simple Allocator
Everything in our language lives at fixed addresses. Arrays start at 300000. The output buffer starts at 100000. The compiler's string table starts at 200000. We chose these addresses by hand and made sure they don't overlap.
That works for a compiler. It doesn't work for a program that needs to create data structures whose size depends on the input. A text editor doesn't know how big the file is until it opens it. A web server doesn't know how many connections it'll handle. These programs need to request memory at runtime -- and eventually give it back.
That's what an allocator does. alloc(size) returns a pointer to size bytes of usable memory. free(ptr) returns those bytes to the pool. Between the two, a program can build linked lists, hash maps, trees, resizable arrays -- any data structure that grows and shrinks.
We're going to build one. No new language features. Everything we need -- load64, store64, pointer arithmetic, linked lists via addresses -- already exists.
### The bump allocator
The simplest possible allocator: a pointer that moves forward.