← back 0x0 Self-Hosting Without The C Detour Jul 17, 2026Which compiler built the compiler? What built that compiler? Which artifact is trusted? What happens if the trusted binary disappears? Does recovery go through another language? Does the process finish on a machine I can actually afford?
It is easy to say a compiler compiles itself after one successful demo. It is harder to make the claim precise enough that a clean checkout can prove it.
That is what changed in
0x0.After writing about 0x0 technically, its ABI and ELF boundary, and its distribution model, I ended up back at the least glamorous part of language design: deleting old scaffolding, drawing a stricter trust boundary, and watching a compiler use twelve gigabytes of memory to produce a file smaller than one megabyte.
The work is recorded in ADR-0188 through ADR-0191. Together they changed the answer to a useful question:
Can
0x0recover and reproduce its native compiler without generating C?The answer is now yes.
The answer also comes with footnotes, because trustworthy bootstrapping is mostly footnotes with executable tests.
The first change was not a compiler feature. It was deleting
python-dehosting-roadmap.md.That file planned the migration of the repository's Python tooling into
0x0. It was useful while the migration was moving. At closure, the live ledger reported 176 migrated rows, with zero remaining rows, zero host-only rows, and zero temporary shim rows.Keeping the roadmap after that point would not preserve useful history. It would create a second, stale place where somebody could try to record current state.
The active truth now lives in things that can fail:
- the migration progress ledger;
- the no-Python exceptions ledger;
- the Python de-hosting progress gate;
- the no-Python repository gate;
- release checks and ADRs.
There is still an explicit exceptions ledger for residual non-source helpers that need native JSON, time, or rewrite replacements. That is not hidden behind a completed percentage. It is a named caveat with a gate around it.
I like this distinction more than I expected. A roadmap says what I intended to do. A ledger and a gate say what the checkout can prove now.
ADR-0188 retired the intention document after the proof replaced it.
Before these changes, the full self-host target could pass through a native helper built like this:
compiler/compat-main.0x0
|
v
stage 2 OISA compiler
|
v
build/native/zero-native.c
|
v
host C compiler
|
v
bin/zero-native
There is nothing fake about generating C. It is a useful backend and an excellent compatibility oracle. C gives a young language access to mature optimizers, system headers, linkers, and decades of platform knowledge.
But it proves a different statement.
It proves that 0x0 can translate its compiler through C and that a host C
toolchain can finish the job. It does not prove that the native 0x0 compiler
path can recover itself without that detour.
I did not want make full-selfhost to quietly answer the easier question.
ADR-0189 moved the normal gate to the native ELF compiler builder:
trusted zero-elf-compiler
|
v
zero-elf-compiler
|
v
zero-elf-stage2
|
v
zero-elf-stage3
zero-elf-stage2 == zero-elf-stage3
The comparison is byte-for-byte. Stage 2 must compile the compiler into stage 3 without changing it.
The C route still exists, but now its names tell the truth:
make c-compat-native
make c-compat-full-selfhost
make native-compat-check
Compatibility is useful. Compatibility is not the trusted root.
Removing C from the normal gate exposed the next question.
What if the trusted release compiler is missing?
A self-hosted compiler cannot appear from its own source. There is always a first executable somewhere: an older compiler, an interpreter, a tiny seed, or another language implementation.
0x0 keeps a small x86-64 assembly seed. Its OISA succession looks like this:
seed/zero.s
|
v
stage1.oisa
|
v
stage2.oisa
|
v
stage3.oisa
stage2.oisa == stage3.oisa
OISA is the intermediate instruction set used by the self-host chain. The assembly seed can produce stage 1. Stage 1 compiles the compiler into stage 2. Stage 2 compiles it into stage 3. If stages 2 and 3 differ, the succession is not stable and the bootstrap stops.
The remaining hole was turning that stable stage2.oisa image into the native
ELF compiler artifacts without asking the compatibility backend to emit C.
ADR-0190 added direct seed commands for two executables:
- an ELF OISA compiler, which compiles
compiler/main.0x0back to the expected stage 2 OISA image; - an ELF compiler builder, which emits another native ELF compiler builder and can reproduce itself byte-for-byte.
That gives the recovery chain another branch:
seed/zero.s
|
v
stable stage2.oisa
|
+----> native zero-oisa-compiler ----> expected stage2.oisa
|
+----> native zero-elf-compiler
|
v
zero-elf-stage2
|
v
zero-elf-stage3
stage2 == stage3
No C source is generated in that branch. No C program is compiled. The seed path writes the ELF compiler artifacts directly.
This does not mean 0x0 materializes from nothing. The first trusted boundary
still includes the assembly seed and the host machinery needed to assemble and
run it. The current native target is Linux x86-64. The operating system, CPU,
assembler, linker, and source checkout remain part of the bootstrap story.
The first direct seed implementation worked.
It was also a small memory disaster.
The monolithic compiler-elf-builder-stage2 command loaded the stage 2 image,
parsed the compiler, planned the ELF layout, generated every function, and
held parser, layout, and code-generation temporaries until the process exited.
On the measured compiler source, it peaked at:
12,667,784 KiB RSS
The output executable was around 672 kilobytes.
Chunking only the final text did not help. The problem was not the size of the hex string written to disk. The seed VM retained all the intermediate allocations that had produced it.
This is the kind of bug that makes a feature technically complete and operationally imaginary. A recovery path that needs an undocumented 16 GiB allowance is not a comfortable first root.
ADR-0191 changed the unit of work.
Direct ELF generation has a slightly annoying dependency. A function body may contain an absolute call to another function. To encode that call, the emitter needs the callee's address. To know that address, it needs to know the sizes of all functions before it.
So the compiler cannot safely emit arbitrary functions and discover the layout afterward.
The solution was to separate planning from emission:
normalize and index function order
|
v
measure function sizes in bounded batches
|
v
freeze names, addresses, entry point, file size,
ELF header/start bytes, and ABI note in a plan
|
v
emit function chunks in bounded batches
against the frozen address table
|
v
append ABI note
|
v
verify exact planned size
|
v
chmod executable
The default batch contains 32 functions. Each worker loads the stable stage 2 compiler state, handles its bounded slice, writes its result, and exits. Exit is important: it gives the operating system a chance to reclaim everything the seed VM does not reclaim during a long process.
The first pass measures function sizes. Those measurements produce a raw address table. The plan finalizer then computes the artifact-specific startup stub, entry point, function base, exact file size, and ABI note.
The second pass emits real function bytes using the frozen addresses. The OISA compiler and the compiler builder share function measurement and body emission, while keeping separate startup and plan identities.
This makes every emitted call agree with the same layout without keeping every function's temporary state alive at the same time.
It is slower. Stage 2 loading and normalization repeat across workers. That is an acceptable trade for a recovery and audit path. I would rather wait for a bounded process than discover that the bootstrap proof only works on one large machine.
There is another detail in the new flow that I like.
The output file is not executable while it is being assembled.
The driver writes the header and startup bytes, appends every decoded function chunk, appends the ABI note, and compares the actual file size with the frozen plan. Only after all those checks pass does it apply executable permissions.
That turns chmod into a tiny commit operation.
A killed worker may leave a partial file, but it does not leave a partial file pretending to be a compiler. The filesystem metadata communicates whether the transaction completed.
This is not a replacement for atomic rename or signed release artifacts. It is still a useful local invariant: incomplete compiler bytes should not look runnable.
The long bounded run found a bug outside the compiler.
The RSS limiter samples the worker process group while the build runs. Over a
long self-host, the shell created tens of thousands of short-lived sampler
children. One valid build finished and produced the correct artifact, but the
wrapper surfaced shell wait status 127 instead of the command's real
status.
The compiler had succeeded. The evidence wrapper had lied about it.
The fix was to let the monitored command record its own exit status inside the
process group before the leader exits, with wait retained as a fallback. The
full target was then rerun through the limiter.
This is a useful bootstrap lesson. A proof is only as reliable as the program collecting the proof. Build wrappers, timeout handlers, memory samplers, and comparison commands are part of the trust story too.
The original 729-function builder measurement dropped from 12,667,784 KiB to 3,154,848 KiB peak RSS.
The current 736-function compiler completed with these results:
| Artifact | Peak RSS | Output size | Parity result |
|---|---|---|---|
| ELF compiler builder | 3,345,556 KiB | 672,106 bytes | self-reproduced byte-for-byte |
| ELF OISA compiler | 3,337,388 KiB | 672,023 bytes | emitted the trusted stage 2 image byte-for-byte |
Both ran below the enforced 4 GiB ceiling. Both returned status zero through the fixed process-group limiter. Neither generated C.
The result is not tiny. Three-point-two GiB is still a serious recovery budget for a 672 KiB artifact. The important change is that the budget is now bounded, measured, enforced, and low enough for an ordinary development machine.
There is more memory work to do. There is no reason to turn a successful bound into a permanent excuse.
The project now has three deliberately different paths.
make full-selfhost prefers the trusted release compiler. It builds a native
ELF compiler builder, uses that builder to produce stage 2, uses stage 2 to
produce stage 3, and compares the two executables byte-for-byte.
If the trusted release compiler is absent, the target can recover the native builder from the seed-produced stage 2 OISA image through the bounded driver.
make bootstrap-from-seed explicitly starts from the assembly seed, proves
OISA stage 2/stage 3 equality, emits both native compiler artifacts through the
bounded seed path, and proves the native compiler builder can reproduce itself.
This is the expensive path on purpose. It exists to recover or audit the trusted release root, not to make every edit pay the full bootstrap cost.
The generated-C and GAS-backed paths remain available behind explicit
c-compat-* and native-compat-check targets. They are useful for comparison,
platform experiments, and catching backend disagreement.
They no longer silently define what "full self-host" means.
0x0 can compile its compiler into a native executable, run that compiler like
another program, and reproduce it without generating C.
Glossary
Self-hosting: a compiler written in the language it compiles.
Bootstrap: the process that creates the first usable compiler artifact from a smaller trusted root.
Trusted root: the starting artifact or implementation accepted as the basis of the compiler succession proof.
Seed: the small assembly implementation used to start the 0x0 compiler
chain.
OISA: the intermediate instruction set used by the 0x0 self-host chain.
ELF: Executable and Linkable Format, the native executable format emitted by the current Linux x86-64 backend.
RSS: Resident Set Size, an approximation of the physical memory held by a process.
Parity: byte-for-byte agreement between two independently produced stages or artifacts.
ABI note: the marker appended to a direct ELF artifact to identify the binary contract it claims to implement.