Initial reactions to: bundle Cobblestone's own front end, interpret it with
codexrun, and have it compile fib to IR.
I spent about fifteen minutes measuring before writing this, because the idea's merit turns almost entirely on whether the interpreter can get near it at all. It can get nearer than I expected.
The Rust front end already chews the whole compiler, and it is not close.
desugardump cover codexzig-subject.codex
1 files, 6431 definitions desugared
332811 AST expression nodes, 0 of them the error node (0.000%)
desugar: 3440947 bytes in 0.143s, 22.9 MB/s
0 name(s) the scope pass could not resolve
3.4 MB, 92 chapters, zero error nodes, zero unresolved names, a seventh of a second. Reading, desugaring and scoping the compiler is a solved problem.
The interpreter gets INTO the driver. I took the existing transpiler subject,
dropped its trailing zig harness, appended a four-line one that calls
compile-frontend-cdx on an inline fib and prints emit-ir-chapter, and ran
it. It did not fall over at the door. It ran until:
builtin `__heap-save` has no rule for ()
And that first blocker is a bug we have already diagnosed once. __heap-save
is implemented. What it lacks is a NULLARY rule — the same shape as the
uefi-read-key-ex incident, where a builtin's arity came out of the probe as a
default rather than as a fact and the interpreter guessed one argument for a
function that takes none. The error names the victim, not the culprit.
So the honest status is: not "can the interpreter run the compiler", but "which of about a dozen builtins does it still owe".
This is a test of the interpreter, not of the IR. Worth saying plainly,
because the framing is easy to slide on. If Rust interprets Cobblestone's own
front end, the IR that comes out is Cobblestone's IR by construction — the same
algorithm, the same allocation order, the same (tvar N) numbering. Agreement
with bare metal would not be four independent compilers agreeing. It would be
one compiler, run on a fourth host, agreeing with itself.
That is not a criticism. It is the most demanding interpreter test available
anywhere in this ecosystem, and the oracle is free and exact. But it is worth
being clear that the thing under test is interp.rs, and the compiler is the
subject.
The native Rust check and lower remain the independent road. This does not
replace them and should not be described as doing so.
The subject is enormous and real. The corpus sweep is 982 programs of mostly modest size. This is one program of 6,431 definitions that uses records, variants, effects, higher-order functions, text handling and deep recursion, all at once, with an exact expected answer. Nothing else we could feed the interpreter is this demanding.
It is the same move that unstuck both transpilers. "Use their
opening.codex directly" was the change that got codex-zig-transpiler and
codex-wasm-transpiler to a fixed point at U55. Emulating the driver's phases by
hand is what kept breaking. This is that lesson applied one level further out:
do not reimplement the front end, RUN it.
It defers the type checker honestly. Not "skip the hard part" but "let the hard part be somebody else's code for now". The Rust checker still has 479 effect-row refusals of 1,001; interpreting Cobblestone's unifier sidesteps every one of them while still producing typed IR.
And the ladder rungs are already the staircase. lex, parse, desugar,
scope, check, lower each have a truth file cut against Fib, and the
truths NEST. So this is not one big-bang experiment. It is six entry points into
the same bundle, each with a gold already banked, each of which fails
independently and says which layer the interpreter could not carry. Start at
lex — if the interpreter can run Cobblestone's lexer over fib and reproduce
lex.truth, that alone is a real result.
A tree walker does not need the allocator ceremony, and that is a simplification the other arms cannot have.
The zig harness opens with four lines of it — init-phase-allocator,
__heap-save, __deck-set, __heap-advance — and the subject reaches for
deck-record 1,555 times, __heap-save 114, __deck-pos 59, __heap-restore
47. That machinery exists because the compiled arms manage their own memory. A
tree walker's host does that job for it.
So for this arm most of that family is a no-op: deck-record is the identity,
phase-compact is nothing, the heap positions are bookkeeping integers nobody
reads back. The entire memory campaign — the thing that has eaten the most time
upstream this year — is invisible from here.
That cuts both ways, and the second way is the more interesting one. This arm
cannot see memory defects at all. Every bug the deck bracket has produced,
every mcopy-type misread, every lifetime error — none of it can appear. So
agreement between this arm and bare metal is evidence about the SEMANTICS and
silence about the memory discipline. Which is fine, as long as nobody later
reads a green line here as covering both.
The step count. This is the one real risk. render is 605 million steps and
ride is 1.35 billion; at 37M steps/sec those are 16 and 36 seconds. Compiling
even a twelve-line program through a 6,431-definition front end could plausibly
be anywhere from 10⁷ to 10¹⁰ steps. The bottom of that range is instant; the top
is twenty minutes and breaks the ten-minute rule for this repo. I would measure
it before building anything, and codexrun bench already reports step counts.
Memory. The compiler builds large intermediate structures and our Value is
three words with persistent lists underneath. A front end that allocates
comfortably inside a 512 MB deck may not sit comfortably inside a tree walker's
heap. Also worth a number before a commitment.
Effects. The sweep's short tail includes five effect-handler failures. The
compiler uses [Console] and [FileSystem]; the pure entry point avoids the
second, which is why I inlined the source as a Text literal rather than reading
/dev/stdin.
Three things, in order, none of them longer than an afternoon:
Fix the nullary-arity family and see how much further it gets. The failure mode is already understood and the fix is in the builtin table, not in the interpreter. Each fix buys another few hundred lines of progress and the next error names the next gap. That loop is cheap and the errors are informative.
Measure the step count on the smallest possible subject — the lexer alone
over fib, against lex.truth. If that is a hundred million steps, the whole
idea is affordable. If it is a hundred billion, we learn that for the price of
one run and stop.
Only then decide whether to climb. lex → parse → desugar → scope
→ check → lower, each against its banked truth. The first rung that
refuses tells us something specific about the interpreter, and the last one
would be fib's IR — the thing the native road is also aiming at, arriving by
a completely different route.
My overall reaction: worth doing, and cheaper than it looks. The front end is already there, the interpreter is already in the door, the golds are already banked, and the failures so far are a builtin table rather than anything structural. What I would resist is describing the result as a fourth independent compiler. It is a fourth host for the same compiler, which is a different and still valuable thing.