How it works
The compiler is a classic pipeline, with one Rust crate per stage:
source ─► lexer ─► parser ─► HIR ─► MIR ─► codegen ─► IC10
(AST) types, CFG + register
checks optimizations allocation
| stage | what it does |
|---|---|
| parser | recursive descent with Pratt-style precedence; it recovers from broken statements, so completion still works on half-written code |
| HIR (high-level form) | name resolution, type checks, mutability rules, device checks, constant evaluation, for unrolling |
| MIR (mid-level form) | a control-flow graph of simple instructions on unlimited virtual registers, with each struct field in its own register; constant folding, copy propagation, dead code removal, jump threading, inlining |
| codegen | register allocation for the whole program, then IC10 output and small local clean-ups (peephole optimizations) |
Fitting in 128 lines
- No stack spills. The compiler assigns registers for the whole program at once. A value still needed after a call never shares a register with the called function, so nothing is saved to the stack. Functions that never run at the same time share registers, and most copies between registers are removed (Briggs coalescing).
- Inlining. Small functions, and functions called from only one place, are copied into
the caller. After that, device parameters usually become constants, so an indirect
dr0turns into a plaind0. - Line-number jumps. Jumps go to line numbers by default.
--labelsmakes the output easier to read, at the cost of one line per label.
The same compiler everywhere
The command-line tool, the tests and the browser playground all use the same code. The
playground loads it as a plain WebAssembly module (C ABI, no wasm-bindgen). The
end-to-end tests run the compiled output in an IC10 emulator.