The fixed point: a compiler written in its own language just reproduced itself, byte for byte

rust dev.to

Verbose is a small experimental language I'm building — its compiler proves properties about your code (termination, sound types) and emits tiny x86-64 machine code, no runtime, no GC, no libc. This post stands on its own. The news: the compiler, written in Verbose itself, just reached the classic self-hosting fixed point — it reproduced its entire self, byte for byte.

Photocopy a photocopy of a photocopy and each generation degrades. Except there's a magic case where the copy is perfect — identical to the original, forever. That's a fixed point: applying the operation once more changes nothing. Every language whose compiler is written in itself eventually faces one question: the version of me I just produced, when asked to produce me, does it produce exactly me? On 2026-07-11, Verbose answered yes, to the byte. And it's on main now — not a branch — 18 commits closing the arc opened June 13.

Three generations

  gen0  ← the Verbose emitter, compiled by the Rust host compiler.
          (what a fresh checkout builds)
            │  feed it the 855 KB of Verbose source
            ▼
  gen1  ← the emitted ELF: the Verbose emitter, but compiled BY Verbose.
            │  feed it the SAME 855 KB source
            ▼
  gen2  ← the ELF gen1 emits in turn.

  The gate:   gen1  ==  gen2   (byte for byte)
Enter fullscreen mode Exit fullscreen mode

gen0 is the Rust-built bootstrap. gen1 is Verbose compiled by itself once. gen2 is Verbose-compiled-by-Verbose compiling itself again. Verdict: gen1 == gen2 == 1,302,980 bytes, same sha256 001481eb…. Not "it works" — the same binary, bit for bit, two generations running. Verified by an #[ignore]d regression test (needs ~9 GB RAM, ~30 s).

Why byte-identical, not just "it compiles"

"It compiles" only asks for a binary that runs. The fixed point asks more: that the compiler, reproducing itself, doesn't drift by a single byte. If it drifted — even one bit — gen1 and gen0 wouldn't be the same compiler; one would emit something the other doesn't. Byte-identity proves the language stands on its own floor: from here, Verbose no longer needs Rust to rebuild itself identically.

What almost stopped it — and why the diagnosis is the real story

A first attempt crashed. Hypothesis: a 32-bit integer overflow in the emitter's counters. Reasonable, given the input size. It was wrong — and it's written up as wrong in the commit, not quietly dropped. ptrace + objdump at the actual faulting address gave the real cause: arena exhaustion. The faulting instruction wrote a node 40 bytes past the 1 GiB arena. 10.3M nodes × 104 bytes = exactly the limit, crossed by 40 bytes. Not a wild pointer — a clean overflow at the boundary.

Why does gen1 overflow when gen0 holds, on the same input? Two different memory models:

  gen0 (Rust backend)          gen1 (self-hosted emitter)
  stack-passes records         arena-stores EVERY record AND variant
  reuses the space             never reclaims
  ~1.5M nodes at peak          ~84M nodes, ~8.7 GB RSS at peak
Enter fullscreen mode Exit fullscreen mode

gen0 works on a whiteboard it wipes as it goes; gen1 writes in a notebook it never tears a page from. Fine for a factorial; it overflows compiling the whole compiler.

The fix — and the fix that waits, on purpose

PR #102's fix is small: the emitter's ELF prologue now mmaps 16 GiB with MAP_NORESERVE. Reserving is free — like reserving a whole parking lot but paying only for the spots you actually use; only the ~8.7 GB actually touched costs RAM, and small programs touch almost none.

But the real memory fix (dedupe an O(sites × program) recompute, reclaim the arena between procedures) is documented and explicitly deferred. It would melt the 8.7 GB peak toward gen0's footprint. It would not change the byte-identical output.

That decoupling is the point. A classic trap in self-hosted-compiler milestones is coupling the correctness proof to a specific optimization. Here they're separate: byte-identity is a semantic property of the emitter, and it holds today; the memory footprint is a resource property, and it improves later. The proof doesn't depend on the perf. Same discipline as everywhere in these projects: name the limit instead of hiding it — the 8.7 GB peak, the O(sites × program) recompute, all written on the same commit that ships the fixed point.

What it means

The full chain the project was built toward is now demonstrated in itself: parse → verify (four proofs: lints, sound types, purity, termination) → interpret → emit → self-compile byte-identical → reproduce its entire self. The self-hosted checker verifying its own proofs, the self-hosted emitter emitting its own ELF, and the byte-identity gate between generations — three claims holding at once. A perfect photocopy of itself, twice over. Verbose stands on its own.

Full version (in French) on arcker.org.

Source: dev.to

arrow_back Back to Tutorials