A CPU of Its Own

Adding a fifth backend sounds like more capability. The truer story is a loop closing: a self-hosted compiler met a CPU the language had already written, and most of the new power was recombination — not a new target, but two old dogfoods finally allowed to meet.

merelanguage-designessayself-hostingrisc-v

Mere grew a fifth backend. Alongside the interpreter, the C backend, LLVM, and WebAssembly, mere -rv now lowers a program straight to a flat RV32IM binary — real 32-bit RISC-V machine code, no external assembler, no linker. Written down like that it reads as one more column in the capability table. That framing is true and it misses the point. The interesting thing this backend did was not add a target. It closed a loop between two things that already existed, and once they met, the language could stand on a machine it had written itself.

A backend is an assembler with opinions

On its own, a RISC-V code generator is the least surprising backend to build. The value model was already settled — a 32-bit word is either an integer or a heap pointer — and the calling conventions, the closure layout, and the string and Vec and Map runtimes had all been designed once and re-implemented on each earlier backend. Lowering to RV32IM is mostly choosing registers and encoding instructions: two passes, a table of opcodes, the immediate bits scattered into their six formats. It is an assembler with a type checker in front of it. If that were the whole story, it would deserve a changelog line and no essay.

What made it worth the trouble was what it could then be pointed at.

The two halves that already existed

Mere already had a self-hosted compiler: a lexer, a parser, a Hindley–Milner type checker, and a WebAssembly code generator, all written in Mere, each shipped as its own dogfood over previous months. And Mere already had a small ladder of CPU emulators — CHIP-8, a 6502, a Game Boy, and an RV32IM core — also written in Mere, built to learn what makes a processor hard to imitate, each validated against an external reference.

Two piles of code, assembled for unrelated reasons. The fifth backend is the hinge that let them meet. Compile the self-hosted compiler with mere -rv and you get a roughly 380-kilobyte RV32IM binary. Run that binary on the Mere-written RISC-V emulator — feeding it a small Mere program to compile — and it emits WebAssembly that is byte-for-byte identical to what the native interpreter produces from the same input. The compiler, compiling, on a CPU the language itself authored.

The sentence is a mouthful, but the structure under it is simple: language → its own backend → its own CPU → its own compiler running on that CPU. Every layer in that tower is the same language. Nothing about the achievement is that a RISC-V backend is hard; the achievement is that all the pieces were already there, each built to answer a different question, and one modest backend let them line up into a single vertical slice.

Three operations make a console

The loop closing also turned the emulator into something you can play. A fantasy console does not need a framework; it needs a small hardware contract, and here it is three operations the -rv backend lowers to bare stores, loads, and a syscall:

  • fb_set x y v — write a shade into a 64×32 framebuffer at a fixed address.
  • key n — read the held state of a button from a memory-mapped input register.
  • present () — end the frame and yield to the host.

The third one is the quietly interesting one. present compiles to a single ecall; the emulator, seeing it, stops running and hands control back to the browser to blit the framebuffer and poll the keys, then resumes the CPU on the very next instruction. From the cartridge’s point of view, present is a function that blocks for one frame and returns. So the game’s main loop is an ordinary recursion — read input, move, draw, present, recurse — and the player’s position lives on the RISC-V call stack across frames, not in any global or any host-side state. The console loop is a coroutine, and the continuation is just the machine’s own stack, frozen between frames.

Put that emulator through the WebAssembly backend, wire it to a <canvas> with the same DOM foreign-function interface a Game Boy emulator dogfood had needed months earlier, and the whole thing runs in a browser tab: a Mere-authored CPU executing a Mere-authored game, reachable at a URL. Again the pattern — the capability that “grew” was mostly recombination. The input plumbing, the canvas blit, the per-frame callback all already existed, built for a different demo. The new work was two externs and a contract.

Each layer billed for itself

None of this arrived clean, and that is the part worth keeping. A self-hosting tower is a brutal integration test: a bug that a small program never triggers has nowhere to hide when the program is a 380-kilobyte compiler. The self-hosted binary first produced empty output, and the cause was a memory map that overlapped — the globals region sat at 64 kilobytes, the compiler’s code ran past 88, so writing a global corrupted the code and execution jumped into rubble. Small programs, whose code fit under 64 kilobytes, had passed happily for weeks and hidden it. Another layer down, conditional branches only reached ±4 kilobytes; functions in a big program don’t, so every branch had to be rewritten to jump over a longer jump.

And the oldest wound reopened. This journal already has an episode about a 64-megabyte cliff — the WebAssembly backend’s bump allocator never frees, so a big enough run walks off the end of its fixed linear memory. The browser console met the same cliff from a new angle: its emulated RAM is an eight-megabyte array of machine words, and building that array through a doubling allocator peaks near a hundred and eighty megabytes before it settles. The fix was not clever — raise this one module’s memory ceiling and move on — but the cliff was exactly where the earlier episode said it would be, waiting, now reached from a program that had nothing to do with ray tracing. A number written down once kept paying out.

What a closed tower actually proves

It would be easy to oversell this. A compiler running on a CPU both written in the same language does not make that language fast, or production-ready, or even practical for the task — the emulated compiler is orders of magnitude slower than the native one, and no one should ship software this way. What the closed loop buys is narrower and real: confidence and a teaching object. Confidence, because byte-identical output across the interpreter and the emulated RV32IM binary is a stringent cross-check that the backend, the value model, and the runtime all agree down to the last emitted byte. A teaching object, because the whole stack is now legible in one language — you can read the CPU, read the compiler it runs, and read the game the compiler builds, without a context switch, and see exactly where each abstraction sits on the one below it.

That is the honest shape of the thing. The fifth backend did make Mere able to do more, but the “more” was not a new destination. It was a loop — two dogfoods, built apart, finally allowed to meet — and the discovery that when the pieces are all the same language, the distance from a source file to a game running on a CPU you wrote is shorter than it has any right to be.

← Back to Notes