Rust · zero dependencies · compiler backend
Most build-your-own-compiler projects stop at the front end. Anvil is the backend. It takes an SSA intermediate representation and lowers it to a small target machine with only K physical registers, using a readable Chaitin-Briggs graph coloring register allocator with real spilling. Then it proves the result correct: a reference IR interpreter and the lowered target interpreter must return the same answer, even when a tiny K forces values onto the stack. Paste IR, pick K, and watch liveness, interference, coloring, spills, and the emitted assembly.
Anvil is a compiler backend you can watch: paste SSA IR, pick how many physical registers K to allow, and press Compile and check to see liveness, the interference graph, the register coloring and spills, and the lowered target code, verified against a reference interpreter.
The page runs the whole backend in your browser and mirrors the Rust pipeline.
runs fully client-side, the same passes as the Rust crate: parse, SSA destruction, liveness, interference, Chaitin-Briggs coloring, spilling, lowering, and the round-trip check
Anvil ships two interpreters: an oracle that runs the SSA over unlimited virtual registers, and a target interpreter that runs the lowered program over only K physical registers plus a stack of spill slots. For every program, at K=2, K=3, and a K large enough to need no spilling, Anvil asserts they agree.
# the single invariant that validates regalloc, spilling, phi elimination, and lowering interp_ir(program, args) == interp_target(lower(regalloc(program, K)), args) # sum of 1..n at K=2, where register pressure forces spilling to the stack $ anvil check sum.ir --regs 2 100 IR interpreter: 5050 target interpreter: 5050 registers: 2, spills: 7, slots: 7 OK: results match
If those two ever disagree, the backend is wrong, and the page shows it in the banner. That same check runs here in your browser on every compile, so the playground is not a demo of the pipeline, it is the pipeline, holding itself to the equation.
Anvil sits between the tutorials that never reach a backend and the production backends that are correct but too large to read.
Most stop at the front end: a parser, an AST, maybe a tree-walking interpreter. Register allocation, the hard part of a backend, is the part they skip.
Real graph coloring or linear scan allocators, trusted through enormous test corpora. Correct, but the allocator is buried in a large codebase and hard to read end to end.
A readable Chaitin-Briggs graph coloring allocator with real spilling, in one small Rust crate with zero dependencies, and a round-trip oracle that proves each lowering correct instead of arguing it. The same pipeline runs here in the browser.
The passes that matter
SSA IR flows through these stages to target code. Each is a separate module in the crate and runs identically in the playground.
Parses the SSA text form into blocks, instructions, and terminators, and round-trips with the printer so parse then print then parse is identical.
Checks each value is defined once, every use is dominated by its definition, and each phi has exactly one entry per control flow predecessor.
Turns phi nodes into copies in predecessors, splits critical edges so a copy has a safe home, and sequences parallel copies so a swap cycle does not clobber.
Iterates live-in and live-out sets over the control flow graph to a fixed point, so allocation knows which values are alive at the same time.
Two values interfere when one is live at the other's definition. A copy is the exception: source and destination hold the same value, so they may share a register.
Build, simplify low-degree nodes onto a stack, optimistically push a potential spill, select colors, and on an actual spill insert reload code and run again.
Walks the fully colored IR and maps each instruction to the small target machine, replacing every virtual register with its color and dropping same-register copies.
The IR interpreter over unlimited virtual registers and the target interpreter over K registers plus spill slots must return the same value, on every program and input.
Anvil is a Rust crate with a small command line front end. The surfaces below are the real ways to drive it.
Interpret the IR directly with the reference interpreter and print the result, optionally with function arguments.
Show liveness, the interference graph, the coloring, and the spills at a chosen number of registers K.
Print the lowered target assembly the small target machine runs after allocation.
Run the round-trip oracle at K and report OK when the two interpreters agree or a mismatch when they do not.
The pipeline is a lib crate (ir, parse, ssa, liveness, interference, regalloc, lower, target), so each pass can be called on its own.
The round-trip oracle runs across the test programs at K=2, K=3, and a large K, asserting spilling really happens at K=2 and not at large K.
# build, test, and lint the backend (Rust, zero dependencies) cargo build cargo test cargo clippy --all-targets # interpret the IR and print the result, optionally with arguments anvil run program.ir 5 # show liveness, interference, coloring and spills at K registers anvil regalloc program.ir --regs 2 # print the lowered target assembly anvil emit program.ir --regs 2 # run the round-trip oracle and report OK or mismatch anvil check program.ir --regs 2 100