Why More Than One Backend, and What It Means to Keep Them Identical
Mere runs four ways: a tree-walking interpreter, and code generators to C, to LLVM IR, and to WebAssembly. The point isn't just reach — it's that all four must produce byte-identical output on the same program. That parity turns the backends into a test oracle for each other, and it's the language's first principle carried into the implementation: which backend you chose must not be a hidden variable that changes what your program does.
Two parts described what Mere is — a memory model and an effect system, both explicit, both checkable. A language, though, is only a promise until something executes it. Part IV is about execution, and it opens on a choice that looks like over-engineering until you see what it buys: Mere doesn’t run one way, it runs four, and it holds the four in exact agreement.
Four ways to run the same language
The four targets exist because “run this program” means different things in different places:
- A tree-walking interpreter walks the syntax tree and evaluates it directly. It is the fast inner loop — the REPL, the quick test, the thing you reach for while writing code — and it doubles as the reference: the plain, obviously-correct reading of what a program means.
- A C backend emits C source you compile with any C compiler, which is to say it runs anywhere — every platform has a C toolchain.
- An LLVM IR backend emits LLVM’s intermediate representation and hands the program to that ecosystem’s optimizer and native code generation.
- A WebAssembly backend emits Wasm, so the same program runs in a browser or any sandboxed Wasm host.
One language, four execution environments, chosen to cover the range from “evaluate this line right now” to “ship this to a browser.” That alone is a reasonable amount of engineering. But compiling to four targets is the easy claim. The hard one is the next sentence.
The hard part: they must agree, exactly
It is not enough that a program compiles on all four backends. The requirement Mere holds itself to is that all four produce byte-identical output on the same program — not “equivalent,” not “close enough modulo formatting,” but the same bytes. More than a dozen realistic programs — a JSON parser, a small calculator, an s-expression reader, and on up to a SQL-ish engine over a thousand lines long — run on the interpreter and all three code generators and produce output that differs by nothing, a diff of zero across the board.
That is a demanding bar, and meeting it is most of the work in Part IV. But it is worth being clear about why the bar is set there and not somewhere more forgiving, because “byte-identical across four backends” sounds like perfectionism and is actually the first principle of the language, applied one level down.
Parity is the first principle, applied to codegen
Mere exists to make behavior something you can read off the program and be sure of. Now ask what it would mean for a language to run four ways without insisting they match. It would mean the backend is a hidden input to your program’s behavior: the same source, compiled for the browser, might print something subtly different than it does under the interpreter — a rounding, an ordering, an edge case. Which backend you happened to use would be an invisible variable silently changing what your code does. That is exactly the kind of implicit, ambient influence the whole language is built to abolish. A language that fought to make memory and effects explicit, and then let “which compiler” quietly alter results, would be contradicting itself at the last step.
So byte-identical parity is not a quality-assurance luxury bolted on at the end. It is the same commitment as explicit types and explicit effects, carried into the implementation: the choice of backend must not be a semantic variable. The interpreter fixes what a program means; every code generator is then obligated to reproduce that meaning down to the byte, and any divergence is a bug in a backend, not a permitted dialect.
The unexpected dividend: the backends test each other
Holding four backends to identical output has a payoff that more than repays the cost. Run a program four ways and compare the outputs: if any two disagree, at least one backend is wrong, and you have found the bug without having written down what the right answer was. The redundancy is its own test oracle.
This is differential testing, and it is unusually cheap here precisely because the backends are supposed to be identical. Normally, testing a compiler means authoring expected outputs by hand — tedious, and only as good as your imagination for edge cases. With four implementations that must agree, every program is a test that checks all four against each other automatically. A subtle bug in string comparison, in how a variant tag is laid out, in evaluation order — the kind of thing that is agony to find by inspection — shows up the moment two backends print different bytes for the same input. The interpreter, being the simplest, usually plays the role of “the one that’s right,” and the code generators are held to it.
The ledger stays honest
Four-way parity is a large surface, and the project tracks it with a coverage matrix: for every language feature and every built-in, a mark for each of the four backends — done, or partial, or not yet. The honesty of that ledger is part of the discipline. A feature that works only under the C backend is not marked done; it is marked “C only,” visibly incomplete, a debt to be paid when LLVM and Wasm catch up. A handful of things — floating point, a few of the more exotic standard-library functions — remain interpreter-only, and the matrix says so plainly rather than letting “works somewhere” masquerade as “works.” The standard the project sets for itself is that a feature is finished when all four backends agree on it, not when one of them runs it.
That principle scales further than it first appears — a later part will show the self-hosted compiler’s output holding the same byte-for-byte agreement across backends — but the shape is already visible here: build the thing more than once, insist the copies match, and let the matching do the verifying.
With the why in place, the rest of Part IV is the how. Next: the interpreter and the C backend — tree-walking evaluation as the reference, and the first translation to compiled code, where the interesting problem is how to represent a closure in a language that doesn’t have them.