Phase 1 exits when the Lume-written compiler compiles the entire language it is written in,
then does a triple self-build (seed builds compiler, compiler builds compiler, compiler
builds compiler again — the last two bit-identical). After that the external as/ld are gone
(Lume emits ELF itself via lasm) and the C seed lume0 is frozen off the steady-state path —
kept as the auditable reproducibility root, used only for the one-time cold mint, not deleted.
See docs/14: committing a binary seed to “remove C” would reintroduce the Ken-Thompson
trusting-trust hole, so the small auditable C source stays. This document is the concrete route
from where we actually are to there. It is written after reading the code, not the roadmap; where the two disagree, the
code wins.
The briefing framed the blocker as “the native backend compiles Layer B (core), but lumec
and data_* are written in Layer A.” Reading the sources confirms the blocker and sharpens
it in two ways that change the plan:
Every tool in compiler/stage1/ is written in surface Layer A — including the native
backend itself. data_asmgen.lume (the x86-64 code generator, 343 lines), data_parser.lume
(202), layout.lume (115) and lexer.lume all use var, while, for … in, mut
parameters, .push()/.len()/.chars()/.map()/.join(), string interpolation "{c}",
maps (map[str,int]), ??, and char literals. None of that is in the functional ML-core
the native backend accepts (DInt DStr DVar DCons DCall DList DIndex DRec DField DBin DIf
DLetIn DMatch + builtins count/at/slice/toint/str). So the native backend cannot compile
its own source, nor the parser that feeds it.
lumec (the surface→C driver: lumec.lume + token/lexer/ast/parser/check/codegen) is a
dead end for self-hosting. It emits C, not native code, and it only accepts a tiny
expression language (let/fn/if-then-else/calls/binops — not even match or records),
i.e. it compiles far less than the Layer A it is written in. It cannot be the fixpoint
vehicle. The real self-hosting vehicle is the ML-core → native path
(data_parser + data_asmgen + lume0 asm), which already produces a native binary with
no C and no as/ld — proven for calc.src, a complete little calculator compiler written
in the ML-core (bootstrap/lume0/tests/run.sh lines 120-137).
So the honest target is: make the ML-core → native compiler compile its own source. The
self-hosted lumec is the ML-core compiler re-expressed in the ML-core — not the surface→C
lumec.lume, which we retire from the critical path.
Two ways to close the gap between “the language the compiler is written in” and “the language the compiler accepts”:
Teach data_asmgen + the runtime to compile var + reassignment, while/for/break,
mut reference parameters, growable lists with push/pop, maps/hash tables, closures
(captured environments → heap closures + indirect calls), string interpolation lowering,
method dispatch, optionals/??, ranges and the for iterator protocol, and a char type —
then compile the existing Layer A tools unchanged.
data_asmgen is itself Layer A, so until it is rewritten in whatever subset it compiles, it
can only ever run under the C interpreter. (a) does not remove C by itself; it just enlarges
the mountain to climb before you can.Freeze an implementation language — call it Lume-Core: the functional ML-core that
data_asmgen compiles today (ADTs + match, records, lists, strings, ints, let … in,
if/then/else, calls, recursion, builtins), plus a tiny set of additions listed in §2.
Rewrite the lexer, parser and code generator in Lume-Core (recursion and immutable
record-threading instead of var/while/mut/push/maps/closures), exactly the way
calc.src already is. Then the native backend compiles the whole toolchain, including itself.
calc.src proves a scanner + Pratt
parser + codegen already run natively in this subset. The work is a disciplined, testable
rewrite, each piece diffable against the Layer A original. It removes C as a consequence.Recommendation: (b). The functional core is already a proven, running compiler substrate;
(a) is a second language runtime we do not need and that does not, on its own, delete any C.
The one-line alternative worth naming: a hybrid that adds only var + while (no closures,
no maps, no methods) to the backend and keeps the tools imperative. It loses to (b) because the
functional subset works today with zero backend changes — recursion replaces loops cleanly —
so paying for mutable-locals-and-loops codegen buys nothing on the critical path.
Reading data_asmgen.lume, data_parser.lume, data_eval.lume, layout.lume and the lumec
modules, the gap splits into genuine backend/runtime additions (small) and a functional
rewrite of the tools (the bulk, no backend change). Everything the tools need that the backend
already has: fn, let … in, if/then/else, match on PInt/PWild/PBind/PCons, records
(DRec/DField), list literals + DIndex, string +/compare, arithmetic, and/or,
count/at/slice/toint/str, and recursion (incl. mutual — labels resolve across the whole text
section). Those require zero new work; the tools just have to be written using only them.
Genuine additions, smallest-first:
printval). A self-compiling
binary must read its own source. Add a runtime builtin read() (whole stdin → boxed string
via the read syscall into a heap buffer) — and, optionally, argv. Driver shape:
print agen(parse(read())), run as ./lumec < in.src > out.s. Blocks everything else.layout.lume’s scanner stops at the first " (while … src[j]
!= '"') and data_parser’s DStr only strips the quotes — neither handles \", \n, \\,
\t. The rewritten code generator is a giant asm-text emitter full of these; the Lume-Core
lexer must scan past \" and the string value must carry real newline/quote/backslash bytes
(null-terminated strings already hold newlines — asm text has no embedded NUL, so no runtime
change is needed here, only correct lex + unescape). Blocks the codegen rewrite (G6 below).let. agen() ignores top-level DLet (let _ = 0); only fn defs and
print are emitted. Constrain Lume-Core programs to fn-definitions + a single final print
(the calc.src shape), or add top-level-let lowering. Cheapest: adopt the constraint and
add a test that pins it. Also pin two existing limits: ≤ 6 function parameters (areg
covers rdi…r9 only) and string-dispatch via if (no string-literal patterns, no record
patterns, no guards in match).The rewrite itself (no backend change, pure source work) in dependency order:
var stack/push/pop); emit the token
list as a Cons/Nil ADT. Depends on G2.data_parser Pratt logic re-expressed by threading a
{toks, pos} record through returns (the {node, rest} pattern already in calc.src),
replacing every var/mut p/.push. Depends on R4.data_asmgen re-expressed functionally — label counters
and the AC record threaded as return values instead of mut AC; all `var out = “” … out = out
loops become string-building recursion; .chars() becomes index recursion with at/count;
the locals lookup (idx_of) becomes recursion over a Cons list. Depends on G2, R5. Largest
single item — split into (6a) int/let/if/call/recursion core, (6b) ADT/record/list/match`,
(6c) the string-runtime glue — each diffed byte-for-byte against the current Layer A output.Deliberately out of scope for the fixpoint: the surface→C lumec modules
(token/lexer/ast/parser/check/codegen.lume), maps, closures, for, mut. They are not needed
to compile Lume-Core and are not ported.
Numbered from this doc. Keep the interpreter/native parity check alive at every step, and name
native .expected files with a distinct suffix so the generic .lume test loop ignores them
(PROJECT_STATE §8).
read() builtin in the data_asmgen runtime tail + a native test: a .src that reads
stdin and prints a transform of it, through data_asm_cc → lume0 asm, no C.\" and unescape
\n \t \\ \"; regression .src whose output contains quotes and newlines, native == C oracle.print, ≤6 params, if-dispatch; no
backend change, just guard rails so the rewrite cannot drift into unsupported territory.lclex.src): dump its token stream for calc.src; assert it equals
the Layer A layout.lume token dump.lcparse.src): dump the AST (show-style) for a sample .src;
assert it matches the data_parser AST.lcgen.src, integer/let/if/call/recursion): emit asm for nfib.src;
assert byte-identical to current data_asmgen output.lcgen.src; byte-identical asm for
ev.src/reclist.src/list.src.lumec.src = read() → lex → parse → gen →
print): byte-identical asm for calc.src vs the Layer A pipeline.lume0 run data_asm_cc.lume lumec.src lumec.s →
lume0 asm lumec.s lumec. Test: ./lumec < calc.src assembled by lume0 asm runs and
matches calc_asm.expected../lumec < lumec.src > s2.s;
assemble; ./lumec2 < lumec.src > s3.s; assert s2.s == s3.s. This is the triple build at
the asm boundary — the compiler now compiles its own source.write(fd,buf,len) in the runtime (ELF contains NUL bytes, so a
C-string cannot represent it); test emitting raw bytes including NUL.lcasm.src, port of elf.c): instruction encoder + ELF64 layout;
assemble elf_hello.s and lumec.s to byte-identical ELF vs lume0 asm.lumec + native lasm, triple build,
bit-identical binary (stage 2 == stage 3). The C seed is then frozen
(bootstrap/lume0/SEED_FREEZE.sha256, #205) and shadowed off the steady-state path
(make zero-c): it mints the first tools once and is never used again in the loop. Per
docs/14 the seed is kept as auditable C source — not deleted, and not replaced by a
committed binary (a binary seed would reintroduce the trusting-trust hole). Porting the
remaining C-only check diagnostics (resolve.c → Lume) stays worthwhile so check is also
C-free, but it is not gated on deleting the seed. = Phase 1 exit (already reached).elf.c, resolve.c) — what and whenelf.c (390 lines: assembler + ELF64 writer). On the critical path: it is the last C
between asm text and a runnable binary, and “no C” cannot be claimed while it stands. It is the
hardest port, because ELF output is binary with embedded NUL bytes — impossible with the
current null-terminated string value. So it is gated on G4 (byte buffers + sized write) and
comes after the compiler already self-hosts through lume0 asm (PR 13–14). Do not attempt it
before the compiler fixpoint: debugging a new binary writer and a new self-hosted compiler at the
same time violates “one change, one green PR.”resolve.c (373 lines: the semantic check). Not required for the triple self-build —
a compiler self-hosts without a checker. But check is the AI-first selling point (R0200/T0001/
D0001/D0002/A000x/…) and must not be lost when C is deleted. Port it just before the delete
(PR 16), after compiler and assembler self-host, keeping all 9 examples/0*.lume green through
the port. If schedule pressure forces a choice, the delete can ship with check temporarily
absent and restored immediately after — but prefer porting first.Historical estimate (stale — Phase 1 is complete; the triple self-build and the Zero-C staged bootstrap are reached): “remaining work: roughly half of Phase 1 (~45–55%).” What existed is real and load-bearing: a
native backend for the functional core, our own ELF writer, and a proof (calc.src) that a
compiler compiles natively with no C/as/ld. What remains is the harder half: the full functional
rewrite surviving real-program scale, a binary ELF writer in Lume (the single least-proven
piece), and a genuinely bit-identical triple build. Rule of thumb: the last ~20% (byte-buffer ELF
in Lume + bit-identical fixpoint) carries most of the risk, not most of the lines.
Biggest risks, roughly in order of how likely they are to bite:
alloc is a 64 MB bump allocator with no reclaim; every int,
string and cons cell is boxed. A self-compile allocates heavily and never frees. Mitigation:
enlarge the region, or add a simple mark/sweep or arena-reset between phases.elf.c port). Binary layout, section offsets and relocations
must match lume0 asm to the byte for a bit-identical fixpoint. Mitigation: G4 first, then
diff ELF bytes against the C writer on a growing corpus before switching the pipeline.\n/\"/\\; one
wrong unescape and the self-compile diverges silently. Mitigation: byte-identical asm diffs at
PRs 7–9, before any fixpoint.var/while) anyway — collapsing toward
the hybrid — and the plan would add that as an early, mandatory brick instead of an if-needed one.