Self-hosting and fixed pointsalpha
Why does moxzi compile itself, what is a C2==C3 fixed point, and why is it the real correctness gate?
Self-hosting means the compiler's source is written in the language it compiles, and the binary that compiles it is a binary it produced. A fixed point is the point at which generating one more compiler stops changing anything.
The constraint is that self-hosting alone proves very little. A compiler that compiles itself into a broken compiler still "self-hosts". The claim that carries weight is the fixed point plus the requirement that each generation is itself a working compiler, and those two together are the acceptance criterion here.
The generations#
Each generation is the compiler wasm produced by the one before it, compiling the same sources.
| Name | What it is |
|---|---|
| C0 | compiler.wasm built by the reference toolchain (make compiler-canister-wasm) |
| C1 | the compiler wasm produced by C0 compiling the compiler's own sources |
| C2 | produced by C1 compiling the same sources |
| C3 | produced by C2 compiling the same sources |
C1 == C2 says the compiler has stopped injecting its own idiosyncrasies into its output. C2 == C3 says that equality is stable rather than a coincidence of one generation. Both have been verified byte-identical in this project, and the on-chain fixed point recorded in CHANGELOG.md is C2 == C3, established at real mainnet caps rather than with limits relaxed.
Why the fixed point is stronger than the corpus#
A corpus tests the programs it contains. The compiler's own source is a 169-file, 7.6 MB Motoko program that exercises the language the way real code does and the way a test suite does not: deep pattern matches, mutually recursive modules, generic classes, thousands of methods, functions large enough that a single one exceeds an IC message budget.
More importantly, a self-compile closes a loop the corpus cannot. If the compiler miscompiles a construct it uses itself, the corpus may never notice — but the next generation will be built with a broken version of that construct, and either it fails to build, or it builds and produces different bytes. The self-referential structure is the detector.
That is not hypothetical. The method that found several real miscompiles was diffing C0's codegen function against C1's codegen function: when a compiler miscompiles the code that does the compiling, that function is the one to look at. Three Nat64-representation bugs on the enhanced-orthogonal-persistence path were found exactly this way, in code the typer never sees.
Fixed point is not enough on its own#
Byte-identical output between two generations means the codegen agrees. It does not mean the produced binary works, because the compiler's #done output is unlinked: RTS imports unresolved, duplicate keep_memory_reserve exports still present, and a wasm engine will refuse to parse it. moc -no-link has the same two artifacts of the same design.
So the acceptance test is two-part, and the second part is the one that took the longest:
- The generations are byte-identical.
- The linked generation is a valid module that is a working compiler — it compiles programs, and it compiles the corpus with the same results as the generation before it.
A run that fails WebAssembly.validate on an unlinked artifact is a healthy run being misread. So is a run that dies on M0020 unresolved import because the source closure manifest was stale — paths existing does not mean the closure is complete.
The mainnet record#
The self-compile has been run on-chain, under the real 40 billion instruction per-message cap with rate limiting enforced, not in a benchmarking mode with limits lifted.
| Build | Source | Messages | Reported instructions | Cycles |
|---|---|---|---|---|
hello.mo | 1 file, 217 B | 52 | — | ~0.001 T |
| the compiler, self-compiling | 177 files, 8.0 MB | 2,345 | — | ~1.6 T |
evm_engine | 661 files, 15.3 MB | 4,138 | — | ~1.6 T/attempt |
Those figures are recorded in docs/onchain-builds.md, measured 2026-08-21 and 2026-08-25. The artifacts were byte-identical to the local builds of the same sources — which is the whole point: a canister that produces a different compiler than your laptop does gives you nothing to check.
The compiler self-compiling is the pathological case for cost — 200,657 cycles per source byte against 69,677 for evm_engine — because its functions are enormous. Price on-chain compilation from that number, not from the average.
What made it possible#
Three things, none of which changed a byte of output:
- Resumability. No stage may run to completion inside one message. See Resumability.
- Memory discipline. The peak lives near a 6 GiB wasm64 ceiling. Committed wasm memory follows the heap peak and never shrinks, and the incremental collector only runs at message boundaries, so the fight is about garbage inside a pass, not after it. One
debug_showon an IR pattern type generated an 11.5 GB show function and was, by itself, the wall. - Stack discipline.
await*inlines its callee's CPS-lowered body, so a single vestigial no-op yield deep in codegen dragged the entire chain into one unsplittable CPS tree — 841 MB and 12.1 B instructions in a single block, which is what blew the 40 B cap. Removingasync*colouring where the driver already owns yielding was a prerequisite for batching, not an optimization on top of it.
Things you cannot do#
| Not possible | Why |
|---|---|
| Bootstrap without the reference toolchain | C0 is built by moc. There is no independent path to a first compiler; the fixed point starts from a borrowed binary, as every self-hosted compiler's does. |
| Treat a fixed point as a proof of language conformance | It proves internal consistency. Conformance is the reference-moc corpus comparison, which is a separate gate. |
| Skip the linker in the loop | Each generation must be linked before it can compile the next. The linker is itself a Motoko canister compiled by the same compiler. |
| Reproduce the mainnet figures locally | Local wasmtime fuel prices this workload roughly 3× cheaper than mainnet; a local worst message under 15 B landed near 40 B on-chain. Local numbers are a lower bound. |
Next#
- Byte-identical output — the comparison the fixed point is built on.
- Resumability — why a 7.6 MB compile fits inside 40 B instructions per message.
- Trust model — what an on-chain-built artifact proves to a third party.