Repository navigation
epic: self-host t27c -- rewrite the compiler from Rust into t27 #5980
Copy link
Copy link
Open
14 / 2114 of 21 issues completed
Copy link
Labels
Description
Goal
Rewrite the t27 compiler (t27c, today ~44.6k lines of Rust in bootstrap/src/compiler.rs) in t27 itself, so that the language compiles its own compiler. Rust stays as the stage-0 bootstrap until the t27-written compiler reaches parity, then retires to a frozen seed.
Why now
- The repo already carries
specs/compiler/{lexer,parser,typechecker,pipeline,stdlib,...}.t27(~4.5k lines). None of them compile throught27c gen+zig testtoday -- they were written ahead of the compiler and describe a language the bootstrap does not accept. They are aspirational, not a compiler. - A spec-first project whose compiler is not a spec is the largest remaining "own language first" gap.
Strategy: grow a C-subset core, gate it by byte identity
- The self-hosted compiler is written in the subset
t27c gen-calready compiles correctly (global arrays, scalar functions,while/if/break/continue, casts, char literals; no structs or strings). - Its first backend emits C, and the gate is byte identity with
t27c gen-con the same input. Byte identity is a stronger oracle than "the output also compiles": every divergence is a bug in one of the two compilers, and the diff points at it. - Fixpoint gate:
core(core.t27) == gen-c(core.t27). Then stage1 (core compiled by Rust) and stage2 (core compiled by itself) are the same C, so the bootstrap is closed for the subset. - Each phase widens the subset the core accepts, and the core is then allowed to use what it just learned to accept.
Phases
- P0 -- MVP core (child issue): lexer + parser + C emitter for the subset, one module under
specs/compiler/core/, byte-identical togen-con fixtures, fixpoint on its own source, gated by abootstrap/testsintegration test. Done in feat(core): t27core -- self-hosted compiler core MVP, fixpoint with gen-c #6041 (merged 2026-10-04), with the gen-c blockers fixed in fix(gen-c): seven self-host blockers #6046-#6052 (Part of #5980) #6939; gatebootstrap/tests/core_selfhost.rs. - P1 -- full statements/expressions:
for,switch, compound assignment,defer, nested arrays,**with a real meaning, numeric literal forms,pub, imports between modules. Done: each construct is accepted byte-identically withgen-cor refused by code with a filed gen-c defect. Compound assignment self-host: t27core accepts compound assignment, byte-identical to gen-c (P1 slice 1) #7313 (<<=/>>=not lexed by t27c: t27c lexer: <<= and >>= are not lexed, so shift compound assignment does not parse #7940). Numeric literal forms t27core: numeric literal forms and pub var, byte-identical to gen-c (Closes #7321) #7323 and t27core: local arrays, separators in dimensions, numeric ** refused (Closes #7929) #7931 (0oThe lexer turns 0o777 into 0; and the nested-literal group needs the parser, not the emitter #2645, malformed literals self-host: gen-c passes leading-zero, bad-binary-digit and misplaced-underscore literals to C #7319).pubself-host P1: pin pub declarations in the t27core fixtures (Closes #7377) #7385. Nested arrays t27core: nested arrays and list initializers, byte-identical to gen-c (Closes #7361) #7374 (nested repeat Self-host: gen-c nests C comments in a nested repeat initializer and zeroes a repeated row #7353).forandswitcht27core: for over one range and switch as a value, byte-identical to gen-c (Closes #7429) #7436 (for-each/multi-range self-host: gen-c runs a for-each or multi-range for body once #7420, switch shapes self-host: gen-c lowers a switch expression with lost grouping, an upper-cased char label and a repeated subject #7419).**fix(gen-c): seven self-host blockers #6046-#6052 (Part of #5980) #6939 and t27core: local arrays, separators in dimensions, numeric ** refused (Closes #7929) #7931: global and local repeats; numerica ** brefused, gen-c: a numeric a ** b is lowered as an array repeat, /* repeat: a ** b */ {0} #7427.deferrefused (Self-host: gen-c writes defer as an unsupported comment and the deferred call never runs #7352). Imports self-host P1: pin the use refusals and the gen-c splice defects (Closes #7378) #7389 and t27core: use between modules, byte-identical to gen-c's splice (Closes #7935) #7950 (splice defects gen-c use splice: identifiers in ; prose pull declarations #7379, gen-c use splice: imported const #define captures a same-named local, broken C with exit 0 #7380, gen-c use splice: imported module var and let are never spliced, undeclared global with exit 0 #7381, gen-c: a module var sized by a constant fromuseis emitted before that constant (t27core_ddc.t27 does not compile with cc) #7702, gen-c use splice: a declaration whose { opens on the next line is spliced as its header, and the import is dropped with exit 0 #7937, gen-c use splice: a spliced declaration reads the importer's namesake, not its own module's #7938, gen-c use splice: an ambiguous name is left unspliced and gen-c exits 0 with an undeclared call #7939, t27c gen: a use it cannot resolve exits 0 and drops the import #7176). Zig-backend defect found on the way: t27c gen (Zig): !(a and b) is written !a and b, the grouping under unary ! is lost #7936. - P2 -- data types: structs, enums, strings/slices,
invariantblocks,benchblocks. The core rewrites its parallel arrays into structs once it can compile them. Done: each construct is accepted byte-identically withgen-cin its lossless subset, or refused by code with a filed gen-c defect. Structs t27core: structs, byte-identical to gen-c (Closes #7962) #8000 (defaults/missing/repeated fields gen-c: a struct literal is written as written -- a field default is dropped, a missing field reads 0, a repeated field keeps the last #7959, non-zero struct repeat gen-c: a repeated struct value [_]S{S{...}} ** N is written {0}, whatever the value (struct case of #6050) #7960, anonymous literal/empty/by-value cycle/mapped name/(-x).fgen-c: struct shapes written as C that is not C or means something else (anonymous literal, empty struct, by-value cycle, mapped name, (-x).f) #7961). Enums t27core: enums, byte-identical to gen-c (Closes #8004) #8007 (member matched by case or with no single owner, shadowing gen-c: an enum member is matched without case and without a single owner, so C reads another name (a const, a parameter, another member) #8001; member value cut at its first token gen-c: an enum member's value is cut at its first token (p = 1 + 2 adds a member named 2; 1_000 is written verbatim) #8002; inferred member in a const or repeat, non-exhaustive or duplicated switch gen-c: inferred enum members lost outside expressions (const .on dropped, [_]E{.on} ** N written {0}), and a non-exhaustive or duplicated enum switch completed silently #8003).invariantandbencht27core: invariant and bench blocks, byte-identical to gen-c (Closes #8011) #8012 (invariant dropped to a comment or an uncalled function gen-c: an invariant it cannot write as _Static_assert becomes a comment or an uncalled function (hex/char literal, enum member, static const, call, statement), so C never checks it #8009; bench binding left undeclared gen-c: a bench body's first top-level assignment is written without a declaration, so cc rejects what the Zig route binds #8010). Strings t27core: strings, byte-identical to gen-c; slices refused (Closes #8014) #8016; slices refused (pointer lowering,.len, escapes, trigraphs gen-c: strings and slices lowered to bare pointers -- == compares addresses, .len on str and on slices is not C, escapes and trigraphs change value, a string where an integer goes compiles #8013). The core's tables became arrays of structs in t27core: its parallel arrays become arrays of structs (Closes #8017) #8020, so the fixpoint runs through structs. Zig-backend defects found on the way: gen (Zig): a field of a struct literal, S{...}.f, is written without parentheses, which Zig does not parse #7997 (S{...}.f), gen (Zig): a cast of an enum value to an integer (e as u8) is written @intCast(e), which Zig rejects #8006 (enum cast). - P3 -- type checker: the semantic passes
t27cruns before codegen (name resolution, type inference, L3/L4 checks), so the core rejects what Rust rejects. Done: each pass is mirrored, or refused by code with a filed defect. typecheck's own rules (E_CHECK) and expression types t27core: t27c's typecheck in the core, and expression types (Closes #8038) #8039: wrong arity anywhere t27c typecheck: a wrong-arity call is a hard error only in initializers, returns and call statements -- conditions, assignments, casts, switches, tests and benches pass #8033, usize rank t27c typecheck: usize (and string) resolve to Custom of rank 0, so u8 -> usize is refused and usize -> u8 or bool is accepted #8034, separated const t27c typecheck: check_const_widths skips a separated, hex or binary literal, so const B: u8 = 1_000 reaches C as 1000 #8035, nested literal gen-c: a struct literal nested in a module var's literal is a nested compound literal, which cc refuses in a static initializer #8036. Coercions typecheck lets through and Zig refuses (E_UNSOUND) t27core: the coercions typecheck lets through and Zig refuses (Closes #8080) #8085: t27c typecheck accepts values the Zig route refuses: initializers, arguments and returns only warn; conditions, operands and indexes go unchecked #8075; gen-c mixed-sign comparison t27c gen-c: a signed-unsigned comparison converts the signed side, so -1 < 0u is false in C and true in Zig #8076; gen-zig comparison folding t27c gen (Zig): constant folding turns a comparison into 0 or 1, which Zig refuses where a bool is expected #8077; t27b local const t27b: a local const known at compile time coerces by value, as Zig does (var b: u8 = local, with const local: i64 = 7) #8079. Name resolution (E_NAME) t27core: every name bound as both backends bind it (Closes #8092) #8129: t27c typecheck resolves no name: undeclared identifiers, duplicates, Zig-refused shadows and missing fields pass with 0 errors #8087; gen-c namespace and order t27c gen-c: C's one namespace and declaration order -- a later const in an initializer, a #define that rewrites a local or field, a shadowed call, test_/bench_ names #8088. Values known at compile time (widths, C int arithmetic, index bounds, shift amounts, divisors) t27core: values known at compile time fit their types, in Zig and in C (Closes #8100) #8159: t27c typecheck checks a constant's width only in a module decimal const: initializers, arguments, operands, casts, indexes, shift amounts and divisors take any constant #8098, t27c gen-c computes constant expressions in C's int: 100000 * 100000, 1 << 40, G * G with a #define G, 0xFFFFFFFF + 1 give other values than Zig #8099. L3/L4 t27core: L3 and L4 as t27c runs them, and under strict_laws as the constitution states them (Closes #8103) #8224: by default as t27c runs them, understrict_lawsas the constitution states them (E_LAW), t27c enforces neither L3 nor L4 before codegen: a non-ASCII comment or string and a spec with no test, invariant or bench all generate #8101. - P4 -- other backends: Zig (
t27c gen), Rust, Verilog, each gated by byte identity with the Rust backend. - P5 -- retire Rust:
t27cCLI dispatches to the self-hosted compiler; the Rust compiler is frozen as the stage-0 seed and the CI gate becomes stage2 == stage3.
gen-c defects found while probing the subset (to be filed / fixed along the way)
**always emits/* repeat: L ** R */ {0}: the repeated value is dropped ([_]u8{5} ** 4becomes zeros) and numerica ** bis miscompiled.assert(...)in a module with notestblocks compiles to C without#include <assert.h>, so the output does not build.- Inside a
testblock, assignment to a name that is not a test-level local (e.g. a global) is emitted as a freshuint64_t x = ...; (void)x;, shadowing the global instead of writing it. const Q: u8 = 'a';(char-literal const initializer) emits nothing.
The MVP core deliberately rejects these shapes instead of reproducing them.
Gate
cargo test -p t27c --test core_selfhost (needs cc; skips cleanly without it): fixtures byte-identical, fixpoint holds, generated C runs its own tests.
Reactions are currently unavailable
Activity
gHashTag commentedon Oct 4, 2026
Three more lossy gen-c shapes, found while building the core MVP (#5981). The core refuses each one rather than copying it; each is reproducible with
t27c gen-c:x = - -1;x = --1;-- C reads a pre-decrementmodule m; endmodule fn g() {}gat all -- declarations afterendmoduleare dropped silentlyconst A: u8 = 5 as u8;/A as u8/true and false#define A 5/A/true-- the top-level const keeps only its first tokenThe last row was already noted for literals; it also covers a leading identifier and
true/false.~Aand!trueas const values emit nothing (also E_CONST).Each refusal is pinned with its code in
bootstrap/tests/core_selfhost.rs(REFUSALS). When gen-c is fixed, the matching refusal can be lifted and the shape moved intoFIXTURES.gHashTag commentedon Oct 4, 2026
Delegated to the Queen: seven gen-c defects the self-hosted core refuses today
Each one is a shape
t27c gen-clowers with loss. #6041's core refuses it rather than copy it. Fixing them in Rust first is what lets the core grow without learning a defect. Every issue below has a## Boundaryand passestools/queen/task_shape.py, so bees on the Railway supervisor pick them up. All seven sharebootstrap/src/compiler.rs, so the Queen runs them one at a time.- -xas--x, a pre-decrement #6046- -xis written as--x, a pre-decrementendmodule#6047 declarations afterendmoduleare silently droppedtrue and false->true;~5and!truevanish)[_]T{v} ** Nrepeat #6050 a global[_]T{v} ** Nis filled with zerosassertwithout#include <assert.h>in a module with no tests #6051assertis used without<assert.h>in a module with no testsEach acceptance criterion is a command with its output today, and each fix comes with a regression test that is red without it.
Not delegated: P1+ of this epic (growing
t27core.t27) needs #6041 merged first. That is the owner's call.Monitoring: watch this checklist and the kanban (issue cards move BACKLOG -> RUNNING -> IN REVIEW -> DONE). Bee PRs say
Closes #N+Part of #5980.gHashTag commentedon Oct 4, 2026
Dispatch status, 2026-10-04 12:45Z (read from the Queen, not inferred)
The Queen on Railway sees all seven tasks every round, and every round skips them for
fileConflict./queen/statuslastTick.skipSummary.fileConflictlists 6052, 6051, 6050, 6049, 6048, 6047 and 6046. Capacity is not the limit: 49 of 70 workers are idle.Why:
CLAIM_RULEinspecs/queen/dispatch.t27says a running bee holds its Boundary files, and so does a PR awaiting review, for up to 48 hours. Five PRs opened today changebootstrap/src/compiler.rsand are still open:_ = call(..);instead of deleting it (Closes #5984) #6003 (gen-zig deletes every top-level_ = call(args);in a fn body (dead-store elimination treats_as a store) #5984)When it unblocks: when those PRs merge or their 48-hour hold expires (by about 2026-10-06 10:00Z). The Queen takes the first eligible issue in GitHub's listing order, newest first, so #6052 goes first, then the rest one at a time.
compiler.rsis the bottleneck of this epic: about 60 open issues name it in their Boundary. Splitting gen-c out ofcompiler.rswould let these tasks run in parallel. That is a separate decision.feat(core): t27core -- self-hosted compiler core MVP, fixpoint with g…
feat(agents): Gamma holds a /self-host skill for epic #5980 (#6054)
fix(t27c): refuse a Rust match block the parser kept as text; rewrite…
gHashTag commentedon Oct 4, 2026
Dispatch status, 2026-10-04 ~16:00Z
The five open PRs that touched
bootstrap/src/compiler.rsare merged: #5971, #5993, #6003, #5902 and #5964. #6041 and #6054 are merged as well.#6046-#6052 still sit in
fileConflict, because the files are held by the Queen's own review rows, not by those PRs. PerCLAIM_RULEinspecs/queen/dispatch.t27, an issue awaiting review holds its Boundary files for 48 hours./queen/public-activityshows these rows inreview / waitwithbootstrap/src/compiler.rsin their Boundary:Accepting or failing any of these rows releases its hold earlier. Once the holds clear,
CHOICE_RULEtakes candidates newest-first, so #6084 goes first and #6052...#6046 follow, one at a time, because they share the file.Check it with:
curl -s https://trios-agent-server-production.up.railway.app/queen/status(lastTick.skipSummary.fileConflict)fix(t27c): refuse macro calls and typeless fields; rewrite eight Mark…
gHashTag commentedon Oct 4, 2026
Port fan-out: 59 one-file tasks for the Queen (2026-10-04)
Each task owns ONE new file under
specs/port/bootstrap/src/, so no two hold the same path and none can hitfileConflict. They run in parallel across idle bees. The bodies come from the roadmap feeder's own template (tools/queen/feed_roadmap.pybuild+shape_problems, the same 6 criteria as #6001:test-reportBLOCKED=0, fn-name grep, test count), with a "Part of #5980" note. The feeder itself skipsbootstrap/.bootstrap/src/*.rsfiles that fit at most 8 fns, 400 lines and 16k chars and contain decisions.compiler.rsslices (40): Port bootstrap/src/compiler.rs lines 1-264 (Rust, 3 functions) to specs/port/bootstrap/src/compiler/Node_default.t27 #6137-Port bootstrap/src/compiler.rs lines 30977-31050 (Rust, 6 functions) to specs/port/bootstrap/src/compiler/LinkSegment_text.t27 #6178, except t27b: lower plain enums to integer tags (first rejection in 89 specs) #6146 and spec(split-reel): v10 -- a client's split reel closes on her call colour, never the house's gold #6149. Each slice is a run of top-level items, at most 8 fns and 400 lines, with a real-line-number gutter. Target isspecs/port/bootstrap/src/compiler/<first_fn>.t27.compiler.rsslices are ready.main.rs,suite.rs,service.rsand the 400-1000-line files still need slicing. Bigimplblocks over 400 lines need a method-level splitter.Queen board 16:0xZ: about 30 of the 59 are already
running.policy: owner-approved exception for bootstrap/src/compiler.rs (#6315)
policy: owner-approved exception for bootstrap/src/compiler.rs (#6315)
Merge pull request #6579 from gHashTag/claude/gen-zig-value-ignored
76 remaining items
t27core: the coercions typecheck lets through and Zig refuses (Closes #…
gHashTag commentedon Oct 9, 2026
P3 slice 2 merged as #8085. Before it merged, a follow-up commit landed in it: a module const or a cast keeps its type against a runtime operand, and usize is the peer type of narrower unsigned operands. The Zig-route fuzzer found both.
P3 slice 3 (name resolution) is up as #8129 (sub-issue #8092), with auto-merge on.
forcapture in scope, a parameter, or a module const, var or array.#defineconst;test_/bench_C names;forcapture. A local const the rest of its block assigns is no longer read as known at compile time; that is a slice-2 follow-up the round found.names_acceptsand 35 refusals);cargo test --test core_selfhostok;t27core_ddc,names_accepts,coercion_acceptseach 1/1;Next, already measured on the lab and waiting for this merge:
t27core: every name bound as both backends bind it (Closes #8092) (#8129
gHashTag commentedon Oct 9, 2026
P3 slice 3 merged as #8129. P3 slice 4 (values known at compile time) is up as #8159 (sub-issue #8100), with auto-merge on.
100000 * 100000,1 << 40,G * Gwith a#define G,0xFFFFFFFF + 1(filed as t27c gen-c computes constant expressions in C's int: 100000 * 100000, 1 << 40, G * G with a #define G, 0xFFFFFFFF + 1 give other values than Zig #8099).CVal: sign, magnitude, Zig type (comptime_int or typed), and the C type its text has.widths_acceptsand 17 refusals);cargo test --test core_selfhostok;t27core_ddcand the three accept fixtures. Repinned: S is 250077 bytes, E is 217125 bytes.Next: slice 5, L3 and L4 (#8103, defect #8101). It is measured and waits for this merge.
t27core: values known at compile time fit their types, in Zig and in C (
gHashTag commentedon Oct 9, 2026
P3 slice 4 merged as #8159. P3 slice 5 (L3 and L4), the last P3 slice, is up as #8224 (sub-issue #8103), with auto-merge on.
strict_lawsmode.compilerefuses a byte past ASCII anywhere and an untested module with the new code E_LAW (18), so a gate can enforce both laws without Rust.cargo test --test core_selfhostok;t27core_ddcpasses on routes B and C. Repinned: S is 253526 bytes, E is 219536 bytes.When #8224 merges, every pass P3 lists is covered or refused by code with a filed defect, and I will tick P3.
t27core: L3 and L4 as t27c runs them, and under strict_laws as the co…
t27b: the node shapes and item scans of lower.rs move to specs/tri/t2…
gHashTag commentedon Oct 11, 2026
Owner's standing rule, 2026-10-11 (translated from chat). Asked: "May PRs that port functions from compiler.rs into .t27 update bootstrap/stage0/FROZEN_HASH without your sign-off on each PR?" The owner answered yes, as a standing approval, under these conditions:
Under those conditions a port PR re-computes FROZEN_HASH itself, and bees may do it with no person in the loop. A change that moves generated output is a fix, not a port. It goes in its own PR, under the usual review.
The unit of work is one cohesive group of compiler.rs functions moved into one
specs/compiler/<name>.t27. That spec is generated tobootstrap/gen/rust/compiler/<name>.rsand mounted with#[path], as #8991 did forcse_hoist. Boundary lines name the functions (bootstrap/src/compiler.rs :: fn NAME, see #9059), so the Queen's region ownership can run ports in parallel.gHashTag commentedon Oct 12, 2026
Three gen-c / evaluator defects found while lowering
specs/claw/gates.t27to C (PR #9277). All have minimal repros; the first two block real specs, the third is a silent wrong value..lenonstris emitted verbatim into C. gen-c lowersstrtoconst char*but emitsx.lenas a member access →request for member 'len' in something not a structure or union.Consequence:
.lencannot appear anywhere in a spec passed to gen-c —including test bodies, which gen-c emits too (that was the surprise;
the assumption had been that tests are zig-only). Workaround in use:
explicit-length idiom (length params + hand-counted tables), with the
.lenpins moved to a companion module that is never gen-c'd(
specs/claw/gates_pins.t27).const-init
.lendoes not fold.pub const L : usize = A.len;emitsstatic const size_t L = A.len;verbatim (same C error), and under thezig runner it silently mis-evaluates: the probe
module ProbeLen; pub const A : str = "agent-runner-docker"; pub const L : usize = A.len;with
assert L == 18;fails. A const initializer that computes awrong value with no diagnostic is worse than a parse error. Probe kept at
/tmp/probe-len.t27; happy to re-attach it anywhere useful.==onstris content-compare in zig but pointer-compare in C.zig lowers
a == btostd.mem.eql; gen-c lowers it top == qon twoconst char*. Same spec, opposite semantics per backend. Any spec thatcompares strings with
==passes its zig tests and then comparesaddresses in C (undefined by content).
specs/claw/gates.t27carriesstr_eqas the cross-backend idiom, but the divergence itself is abackend-contract defect worth a row in the parity table.