Skip to content

epic: self-host t27c -- rewrite the compiler from Rust into t27 #5980

Open
14 of 21 issues completed
Open

Description

@gHashTag

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 through t27c gen + zig test today -- 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

  1. The self-hosted compiler is written in the subset t27c gen-c already compiles correctly (global arrays, scalar functions, while/if/break/continue, casts, char literals; no structs or strings).
  2. Its first backend emits C, and the gate is byte identity with t27c gen-c on 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.
  3. 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.
  4. Each phase widens the subset the core accepts, and the core is then allowed to use what it just learned to accept.

Phases

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} ** 4 becomes zeros) and numeric a ** b is miscompiled.
  • assert(...) in a module with no test blocks compiles to C without #include <assert.h>, so the output does not build.
  • Inside a test block, assignment to a name that is not a test-level local (e.g. a global) is emitted as a fresh uint64_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.

Activity

  1. gHashTag commented on Oct 4, 2026

    @gHashTag
    OwnerAuthor

    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:

    Input gen-c writes Core
    x = - -1; x = --1; -- C reads a pre-decrement E_SYNTAX (2)
    module m; endmodule fn g() {} no g at all -- declarations after endmodule are dropped silently E_SYNTAX (2)
    const A: u8 = 5 as u8; / A as u8 / true and false #define A 5 / A / true -- the top-level const keeps only its first token E_CONST (7)

    The last row was already noted for literals; it also covers a leading identifier and true/false. ~A and !true as 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 into FIXTURES.

  2. gHashTag commented on Oct 4, 2026

    @gHashTag
    OwnerAuthor

    Delegated to the Queen: seven gen-c defects the self-hosted core refuses today

    Each one is a shape t27c gen-c lowers 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 ## Boundary and passes tools/queen/task_shape.py, so bees on the Railway supervisor pick them up. All seven share bootstrap/src/compiler.rs, so the Queen runs them one at a time.

    Each 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.

  3. gHashTag commented on Oct 4, 2026

    @gHashTag
    OwnerAuthor

    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/status lastTick.skipSummary.fileConflict lists 6052, 6051, 6050, 6049, 6048, 6047 and 6046. Capacity is not the limit: 49 of 70 workers are idle.

    Why: CLAIM_RULE in specs/queen/dispatch.t27 says a running bee holds its Boundary files, and so does a PR awaiting review, for up to 48 hours. Five PRs opened today change bootstrap/src/compiler.rs and are still open:

    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.rs is the bottleneck of this epic: about 60 open issues name it in their Boundary. Splitting gen-c out of compiler.rs would let these tasks run in parallel. That is a separate decision.

  4. gHashTag commented on Oct 4, 2026

    @gHashTag
    OwnerAuthor

    Dispatch status, 2026-10-04 ~16:00Z

    The five open PRs that touched bootstrap/src/compiler.rs are 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. Per CLAIM_RULE in specs/queen/dispatch.t27, an issue awaiting review holds its Boundary files for 48 hours. /queen/public-activity shows these rows in review / wait with bootstrap/src/compiler.rs in their Boundary:

    row dispatched hold ends
    #5716 2026-10-03 ~10:04Z ~2026-10-05 10:04Z
    #5718 2026-10-03 ~10:04Z ~2026-10-05 10:04Z
    #6101 2026-10-04 ~14:41Z ~2026-10-06 14:41Z

    Accepting or failing any of these rows releases its hold earlier. Once the holds clear, CHOICE_RULE takes 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)

  5. gHashTag commented on Oct 4, 2026

    @gHashTag
    OwnerAuthor

    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 hit fileConflict. They run in parallel across idle bees. The bodies come from the roadmap feeder's own template (tools/queen/feed_roadmap.py build + shape_problems, the same 6 criteria as #6001: test-report BLOCKED=0, fn-name grep, test count), with a "Part of #5980" note. The feeder itself skips bootstrap/.

    Queen board 16:0xZ: about 30 of the 59 are already running.

  6. 76 remaining items

  7. gHashTag commented on Oct 9, 2026

    @gHashTag
    OwnerAuthor

    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.

    • Why. t27c's typecheck resolves no name (filed as t27c typecheck resolves no name: undeclared identifiers, duplicates, Zig-refused shadows and missing fields pass with 0 errors #8087). An undeclared identifier, a function or type read as a value, a duplicate declaration, a shadow Zig refuses, and a missing field all pass with 0 errors. gen-c then writes C that cc refuses or reads otherwise.
    • E_NAME (5).
    • E_UNSOUND. An assignment to a for capture. 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.
    • Accepted, as both backends take them: sibling blocks reusing a name, shadowing a module var, a function or a struct (gen-zig renames), forward calls, globals declared after their readers.
    • Measured:
      • 68 probes through typecheck, gen-c+cc and the Zig route;
      • typecheck parity, 600 programs: HOLE 0;
      • the Zig route, 400 programs: the only "core refuses, Zig compiles" cases are Zig laziness.
    • Lab results, on master 2e90d31:
      • core tests 85/85;
      • fixpoint 203003 bytes;
      • all fixtures pass (names_accepts and 35 refusals);
      • cargo test --test core_selfhost ok;
      • corpus 92 accepted, 0 mismatched.
    • DDC:
      • route B (t27b): t27core_ddc, names_accepts, coercion_accepts each 1/1;
      • route C (Zig): the same;
      • repinned: S is 233527 bytes, E is 203003 bytes.

    Next, already measured on the lab and waiting for this merge:

  8. gHashTag commented on Oct 9, 2026

    @gHashTag
    OwnerAuthor

    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.

    Next: slice 5, L3 and L4 (#8103, defect #8101). It is measured and waits for this merge.

  9. gHashTag commented on Oct 9, 2026

    @gHashTag
    OwnerAuthor

    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.

    When #8224 merges, every pass P3 lists is covered or refused by code with a filed defect, and I will tick P3.

  10. gHashTag commented on Oct 11, 2026

    @gHashTag
    OwnerAuthor

    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 to bootstrap/gen/rust/compiler/<name>.rs and mounted with #[path], as #8991 did for cse_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.

  11. gHashTag commented on Oct 12, 2026

    @gHashTag
    OwnerAuthor

    Three gen-c / evaluator defects found while lowering specs/claw/gates.t27 to C (PR #9277). All have minimal repros; the first two block real specs, the third is a silent wrong value.

    1. .len on str is emitted verbatim into C. gen-c lowers str to
      const char* but emits x.len as a member access →
      request for member 'len' in something not a structure or union.
      Consequence: .len cannot 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
      .len pins moved to a companion module that is never gen-c'd
      (specs/claw/gates_pins.t27).

    2. const-init .len does not fold. pub const L : usize = A.len; emits
      static const size_t L = A.len; verbatim (same C error), and under the
      zig 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 a
      wrong value with no diagnostic is worse than a parse error. Probe kept at
      /tmp/probe-len.t27; happy to re-attach it anywhere useful.

    3. == on str is content-compare in zig but pointer-compare in C.
      zig lowers a == b to std.mem.eql; gen-c lowers it to p == q on two
      const char*. Same spec, opposite semantics per backend. Any spec that
      compares strings with == passes its zig tests and then compares
      addresses in C (undefined by content). specs/claw/gates.t27 carries
      str_eq as the cross-backend idiom, but the divergence itself is a
      backend-contract defect worth a row in the parity table.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions