Skip to content

feat(t27c): gen-js, and the spec explorer's compiler gets its source back - #4472

Merged
gHashTag merged 3 commits into
masterfrom
feat/gen-js
Sep 20, 2026
Merged

gHashTag merged 3 commits into
masterfrom
feat/gen-js

Conversation

@gHashTag

@gHashTag gHashTag commented Sep 20, 2026

Copy link
Copy Markdown
Owner

Closes #4471
Closes #4487

Two commits. The first adds a backend; the second gives the spec explorer's
compiler a source tree, and puts the new backend on the page.


1. gen-js — a spec prints its own JavaScript (d5552605e)

t27c gen-js <file.t27> prints an ES module of the spec's declarations
const, enum, struct — on stdout. bootstrap/src/codegen_js.rs, one new file.

Our MCP servers run on JavaScript and a .t27 spec had no way to reach them,
so each one grew a hand-written generator in a foreign language beside it —
and that generator, not the compiler, decided what the artifact said.

Nothing is dropped in silence. A fn, test, bench or invariant is
announced in a comment in the output. An artifact that is quietly missing
something is worse than one that says what it omitted.

The module is data. Nothing in it runs, so a spec cannot smuggle behaviour
into a server through its own description.

The defect this found in itself

pub enum Sev { Info, Warn = 5, Error } came out Warn: 1. The explicit
discriminant lives on the variant node, in value, not in a child — so the
first draft printed the position the variant happened to sit in. A wrong number
that looks like a right one is the worst thing a code generator can emit. The
node is asked first now, and the count resumes from the explicit value, so
Error is 6: the rule C, Rust and Zig already share. Covered by a test.

Found by running the binary on a fixture, not by the unit tests — which all
passed while it was wrong.


2. bindings/wasm-explorer — the page stops running a second compiler (8fee90e6b)

apps/website ships a WebAssembly build of the bootstrap compiler so the spec
explorer can lex, parse, typecheck and generate in the browser. Its driver said,
in a header comment, that this is "the same code the CLI runs, not a
reimplementation."

The source of that artifact was committed in no repository. git log --all -S"t27_analyze" finds nothing here. The sync script's build step fell through
to the copy already vendored beside the corpus — silently, on every run, since
the file first appeared. A binary that cannot be rebuilt cannot be corrected,
and this one had drifted:

Measurement over the 1408-spec website corpus Old artifact This build
Specs given an AST, node count, depth, type verdict and backends that t27c parse refuses 214 0
Of 40 specs where both parse but emitted Zig differs in size, agreement with t27c gen 0 40
Specs the artifact traps on 2 0

The 214 were not sampled — every one was put to the CLI, and the CLI refused
214 of 214. So the page was not showing a spec's compile; it was showing a
second compiler's opinion, which no test ran against and no CI job built.

The fix is the missing source, not a rewrite. The crate reaches
bootstrap/src/{compiler,use_resolve,codegen_js}.rs with #[path] rather than
copying them, so there is exactly one copy of the compiler in the tree.
bootstrap/stage0/FROZEN_HASH is unchanged: no freeze ceremony was needed
and none was performed.

The ABI is treated as a contract, because a browser can be holding
yesterday's bundle. t27_alloc / t27_analyze / t27_free keep their exact
shapes and the JSON keeps its exact field names. t27_analyze_named is
added beside t27_analyze, never in place of it.

A test that failed and was right to

It asserted that Lexer::dropped records characters. It records bytes, so
one em dash arrives as three entries — â and two controls — and the page's
loss figure overcounts every non-ASCII character threefold. The vendored
artifact did the same, so this is neither a regression nor an improvement;
it is the compiler's accounting. The test was rewritten to pin the behaviour as
it is, because compiler.rs is sealed and the repair is a reseal, not a line in
a browser binding. What the test buys is that the day it changes is visible.


Witnesses

Check Result
habr-mcp's committed habr_decls.mjs regenerated from its spec byte-identical
150 corpus specs through gen-js 116 generate · 34 refused by name · 0 panics
cargo test --release --bin t27c codegen_js 11 passed, 0 failed
cd bindings/wasm-explorer && cargo test -- --skip compiler:: 39 passed, 0 failed — the binding's own
the same crate, bare cargo test 713 run, 711 passed, 2 failed — see below
bootstrap/stage0/FROZEN_HASH unchanged — compiler.rs is not touched

The crate is excluded from the workspace, so cargo test -p … does not reach
it; run it from its own directory.

The two failures are the compiler's own tests, and they are positional.
#[path] brings compiler.rs's #[cfg(test)] modules along with its code —
which is the point of including rather than copying — and
tests_hir_roundtrip::test_roundtrip_{uart,bridge}_spec read
$CARGO_MANIFEST_DIR/../specs/fpga/*.t27. That resolves for any crate exactly
one level below the repo root, which is why they pass in bootstrap and fail
from bindings/wasm-explorer. Not worked around: the file is sealed, copying
the fixtures under bindings/ would be a second home for the truth, and moving
this crate up a level to satisfy a relative path would separate it from
bindings/javascript and bindings/python. The skip is narrow, named, and
documented at the top of lib.rs.

No workflow builds or tests this crate. That is the same shape as the
defect the PR fixes, one level up, and it is deliberately not fixed here —
filed separately rather than grown onto a PR this size.

The census

tools/census/quiet.txt moves 141 → 142 and the move is not this PR's. The
new row is queen-publish.yml:81, GH_TOKEN: ${{ secrets.GH_AGENT_TOKEN || github.token }}, which arrived in d7a1530ad without a re-bless. It is a YAML
fallback expression, not a shell || swallowing an exit code, so the census
refuses it correctly — and refusals are counted, which is why the total moved.
Re-blessed here because this is the commit the gate stopped.

The six red checks, each measured

Not asserted — here is the method for each.

Check Runs on master? Evidence it is not this PR's
duplicate-bodies yes fails on master HEAD 1e78d703a itself
coverage yes fails on master HEAD 1e78d703a itself
Corpus ratchet yes fails on master HEAD 1e78d703a itself
emit-bitexact nopull_request only built master's own t27c at 322af1b27 in a detached worktree and ran the census: identical 12. Also fails on 5 of 5 unrelated open PRs
spec-guards nopull_request only fails on 5 of 5 unrelated open PRs (#4338, #4123, #4116, #4113, #4038)
test-ratchet no, and path-filtered so no unrelated PR runs it headline is 114 targets ran; 23 failing test(s) (baseline 372) and the failure reason is 372 baselined test(s) now PASS — prune them from the baseline. A stale ledger, not a regression; 0 of the printed names is a js or wasm test

Three of the six therefore have a direct master control, one has a direct
local re-measurement, one has a five-PR control, and one is red for a reason
printed in its own log that names no file this PR touches.

emit-bitexact's 12 are the subject of #4488, which also records why that gate
could not report five live regressions that landed in September.

What this does NOT establish

  • Nothing here says the 214 specs are fine, or broken. It says the page will
    now report them the way the compiler does. Which of them are damaged files
    and which are parser gaps is a separate question, deliberately not answered
    on the way past.
  • The escape table in codegen_js.rs is the compiler's third, beside
    zig_escape and escape_for_fmt. Not a copy of either — the U+2028/U+2029
    arms exist for JavaScript alone — but the overlap is real. Folding all three
    into one table with a target parameter is worth doing, and is deliberately
    not done here, where it would ride into a frozen file on the back of a new
    backend.

🤖 Generated with Claude Code

Our MCP servers run on JavaScript, and a `.t27` spec that describes one had no
way to reach them. So every such spec grew a hand-written generator in a
foreign language beside it -- and that generator, not the compiler, became the
thing that decided what the artifact said. That is a hole in the compiler
wearing the costume of a build script.

`bootstrap/src/codegen_js.rs` lowers DECLARATIONS -- const, enum, struct --
into an ES module. It does not lower function bodies: a `fn`, `test`, `bench`
or `invariant` in the input is ANNOUNCED IN A COMMENT in the output, never
dropped in silence. An artifact that is quietly missing something is worse than
one that says what it left out. The module itself is data: nothing in it runs,
so a spec cannot smuggle behaviour into a server through its own description.

A defect found by running the binary rather than trusting the tests: `pub enum
Sev { Info, Warn = 5, Error }` came out `Warn: 1`. The explicit discriminant
lives on the variant NODE, in `value` -- not in a child, which is where the
first draft looked -- so the backend silently substituted the position the
variant happened to sit in. A wrong number that looks like a right one is the
worst thing a code generator can emit. The node is now asked first and the
count resumes FROM the explicit value, so `Error` is 6 and not 2: the rule C,
Rust and Zig already share.

Witnesses:
- `habr_decls.mjs` in habr-mcp regenerates from its spec BYTE-IDENTICALLY.
- 150 corpus specs: 116 generate, 34 are refused with a sentence naming the
  exact construct, 0 panics.
- `cargo test --release --bin t27c codegen_js` -- 11 passed, 0 failed.

`bootstrap/src/compiler.rs` is not touched and the seal in
`bootstrap/stage0/FROZEN_HASH` is unchanged -- no freeze ceremony was needed.

A census moved, and it is not this backend's. `tools/census/quiet.txt` goes
141 -> 142. The new row is `queen-publish.yml:81`,
`GH_TOKEN: ${{ secrets.GH_AGENT_TOKEN || github.token }}`, which arrived in
d7a1530 and was not re-blessed there. It is a YAML fallback expression, not a
shell `||` swallowing an exit code, so the census REFUSES it correctly -- and
the refusals are counted too, which is why the total moved. Re-blessed here
because this is the commit the gate stopped, and a number that moves without a
sentence is exactly what that ledger exists to prevent.

What this does NOT establish: the escape table in `codegen_js.rs` is the
compiler's third, beside `zig_escape` and `escape_for_fmt`. It is not a copy of
either -- the U+2028/U+2029 arms exist for JavaScript alone -- but the overlap
is real. Folding all three into one table with a target parameter is worth
doing, and is deliberately not done here, where it would ride into a frozen
file on the back of a new backend.

Closes #4471
@github-actions

Copy link
Copy Markdown
Contributor

📓 NotebookLM Notebook linked to this PR

This notebook contains session context, decisions, and artifacts for this work.

@github-actions

Copy link
Copy Markdown
Contributor

PR Dashboard

Generated at: 2026-09-20 16:50:07 UTC

Summary

Status Count
Total Open PRs 21
PRs with Failing Checks 19
PRs with All Checks Green 2
READY 2
FAILING 19
PENDING 0
NO CHECKS YET 0

Seal Status

  • ⚠️ STALE -- sha256(compiler.rs)=403499176a5d != manifest seal=87e5cbd3ad94.
    The committed NMSE numbers were certified against an older compiler.rs.
    Run scripts/reseal-check.sh locally for the two-step reseal command (advisory; not a merge gate).

The wasm the spec explorer loads had no source in any repository. `git log
--all -S"t27_analyze"` finds nothing, so the sync script's build step fell
through to the already-vendored copy on every run, and a binary that cannot be
rebuilt cannot be corrected. It had drifted.

Measured against `t27c` over the 1408-spec corpus the page ships:

  214 specs get an AST, a type verdict and five generated backends from the
      old artifact. `t27c parse` refuses all 214 -- checked one by one, 214
      of 214, not sampled.
   40 specs where both parse and the emitted Zig differs in size: `t27c gen`
      matches the new build 40 times, the old build 0.
    2 specs trap the old artifact and not the new one.

For 15% of the library the page was not showing a compile. It was showing a
second compiler's opinion that nothing tested and no CI job built.

`bindings/wasm-explorer` is that missing source. It reaches bootstrap/src with
`#[path]` instead of copying, so the tree still holds exactly one compiler, and
`bootstrap/stage0/FROZEN_HASH` is unchanged. The ABI is treated as a contract
because a browser may hold yesterday's bundle: `t27_analyze_named` is added
beside `t27_analyze`, never in place of it.

`js` joins the layer strip, printing what `t27c gen-js` prints (#4471) -- which
is what this was started for.

A test written to assert that `Lexer::dropped` records characters failed: it
records bytes, so one em dash is three entries and the page's loss figure
overcounts every non-ASCII character threefold. The old artifact did the same,
so this is neither a regression nor a fix. The test now pins the behaviour as
it IS, because compiler.rs is sealed and the repair is a reseal.

Closes #4487
Refs #4471
@github-actions

Copy link
Copy Markdown
Contributor

📓 NotebookLM Notebook linked to this PR

This notebook contains session context, decisions, and artifacts for this work.

@github-actions

Copy link
Copy Markdown
Contributor

PR Dashboard

Generated at: 2026-09-20 17:23:12 UTC

Summary

Status Count
Total Open PRs 21
PRs with Failing Checks 19
PRs with All Checks Green 2
READY 2
FAILING 19
PENDING 0
NO CHECKS YET 0

Seal Status

  • ⚠️ STALE -- sha256(compiler.rs)=403499176a5d != manifest seal=87e5cbd3ad94.
    The committed NMSE numbers were certified against an older compiler.rs.
    Run scripts/reseal-check.sh locally for the two-step reseal command (advisory; not a merge gate).

@gHashTag gHashTag changed the title feat(t27c): gen-js -- a spec prints its own JavaScript feat(t27c): gen-js, and the spec explorer's compiler gets its source back Sep 20, 2026
…ith it

Refs #4487
Refs #4489

A bare `cargo test` in this crate runs 713 tests and reports two failures.
Neither is the binding's: `#[path]` pulls `compiler.rs`'s own `#[cfg(test)]`
modules in with its code, and two of them read
`$CARGO_MANIFEST_DIR/../specs/fpga/*.t27` -- a path that resolves only for a
crate exactly one level below the repository root. They pass in `bootstrap`.

Not worked around. `compiler.rs` is sealed, a copy of the fixtures under
`bindings/` would be a second home for the truth, and moving the crate up a
level to satisfy a relative path would separate it from `bindings/javascript`
and `bindings/python`.

The crate's own 39 tests run with `cargo test -- --skip compiler::`, and the
comment says so where someone hitting the red will read it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

📓 NotebookLM Notebook linked to this PR

This notebook contains session context, decisions, and artifacts for this work.

@github-actions

Copy link
Copy Markdown
Contributor

PR Dashboard

Generated at: 2026-09-20 18:01:19 UTC

Summary

Status Count
Total Open PRs 21
PRs with Failing Checks 19
PRs with All Checks Green 2
READY 2
FAILING 19
PENDING 0
NO CHECKS YET 0

Seal Status

  • ⚠️ STALE -- sha256(compiler.rs)=403499176a5d != manifest seal=87e5cbd3ad94.
    The committed NMSE numbers were certified against an older compiler.rs.
    Run scripts/reseal-check.sh locally for the two-step reseal command (advisory; not a merge gate).

@gHashTag
gHashTag merged commit 8428160 into master Sep 20, 2026
53 of 59 checks passed
gHashTag added a commit to gHashTag/trinity that referenced this pull request Sep 20, 2026
…gets measured (#1067)

The spec tab now offers JavaScript beside the five existing targets, backed by
the gen-js backend landed as gHashTag/t27#4472.

Refreshing the vendored compiler moved the published health counters a long way
-- ok 1002 -> 975, warn 399 -> 100, fail 6 -> 338 -- and the cause is the
instrument, not the corpus. The wasm served here until now was built from no
committed source, and it accepted anything: fed
bootstrap/tests/fixtures/damage/damage_class_01.t27, a fixture this repository
damages on purpose with an unterminated string literal, it answered tc.ok=true
and emitted C, Rust, Verilog and Zig. The binary now served is built from
bootstrap/src, refuses all six backends on that file, and names the line.

Nothing in the corpus got worse; it got measured, and most of the movement is
warn becoming fail rather than working specs breaking. 145 of the 338 are not
t27 at all -- 68 Zig source files, 49 on the older `spec Name { }` dialect, 14
Markdown prose, 23 damage fixtures -- and the largest single class, 122, is
gen-js declining by design to emit a const whose initialiser is not a literal.
Those 122 parse, typecheck and emit five other languages.

So the qualifier is published as a constant rather than a footnote:
HEALTH_FAIL_JS_ONLY is computed from the manifest by the generator and bound to
HEALTH_FAIL by two of the spec's own asserts, so it cannot drift away from the
number it qualifies and a reader who meets 338 cannot miss what it is made of.

The one rename here is two unrelated changes git paired by similarity, and both
are correct. public/t27/files/specs/catalog/onboarding.t27 was a vendored copy
of a file gHashTag/t27 no longer has; upstream's specs/catalog/ holds only
discovery.t27. And specs/catalog/onboarding.t27 -- the source that
onboarding-from-spec.mjs and check:onboarding have always read -- was untracked
until now, so a fresh clone could not have run the gate that guards it.

Not done, and said plainly rather than left to be discovered: the sync's
provenance-preservation code has still never run end-to-end. The on-disk
manifest was repaired out of band, so that path remains unexercised.

check:queen-cycle, check:queen-factory, check:queen-review and
check:queen-honesty are red. They are byte-identically red at origin/main --
compared against a pristine worktree, the failing line sets are the same -- and
nothing here touches them.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
github-actions Bot added a commit to gHashTag/trinity that referenced this pull request Sep 20, 2026
feat(website): JavaScript joins the spec tab, and the corpus finally gets measured (#1067)

The spec tab now offers JavaScript beside the five existing targets, backed by
the gen-js backend landed as gHashTag/t27#4472.

Refreshing the vendored compiler moved the published health counters a long way
-- ok 1002 -> 975, warn 399 -> 100, fail 6 -> 338 -- and the cause is the
instrument, not the corpus. The wasm served here until now was built from no
committed source, and it accepted anything: fed
bootstrap/tests/fixtures/damage/damage_class_01.t27, a fixture this repository
damages on purpose with an unterminated string literal, it answered tc.ok=true
and emitted C, Rust, Verilog and Zig. The binary now served is built from
bootstrap/src, refuses all six backends on that file, and names the line.

Nothing in the corpus got worse; it got measured, and most of the movement is
warn becoming fail rather than working specs breaking. 145 of the 338 are not
t27 at all -- 68 Zig source files, 49 on the older `spec Name { }` dialect, 14
Markdown prose, 23 damage fixtures -- and the largest single class, 122, is
gen-js declining by design to emit a const whose initialiser is not a literal.
Those 122 parse, typecheck and emit five other languages.

So the qualifier is published as a constant rather than a footnote:
HEALTH_FAIL_JS_ONLY is computed from the manifest by the generator and bound to
HEALTH_FAIL by two of the spec's own asserts, so it cannot drift away from the
number it qualifies and a reader who meets 338 cannot miss what it is made of.

The one rename here is two unrelated changes git paired by similarity, and both
are correct. public/t27/files/specs/catalog/onboarding.t27 was a vendored copy
of a file gHashTag/t27 no longer has; upstream's specs/catalog/ holds only
discovery.t27. And specs/catalog/onboarding.t27 -- the source that
onboarding-from-spec.mjs and check:onboarding have always read -- was untracked
until now, so a fresh clone could not have run the gate that guards it.

Not done, and said plainly rather than left to be discovered: the sync's
provenance-preservation code has still never run end-to-end. The on-disk
manifest was repaired out of band, so that path remains unexercised.

check:queen-cycle, check:queen-factory, check:queen-review and
check:queen-honesty are red. They are byte-identically red at origin/main --
compared against a pristine worktree, the failing line sets are the same -- and
nothing here touches them.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
gHashTag added a commit that referenced this pull request Sep 21, 2026
t27c on crates.io is 0.2.0. The compiler gained gen-js in #4472, so the version
a user installs from the registry cannot emit JavaScript while the compiler on
master can. Minor rather than patch: a new backend is added functionality, and
additive -- the five existing targets are untouched.

Both files the release pipeline's VERSION TRUTH gate reads for a t27c-v* tag are
bumped together, because the gate refuses a tag whose manifests do not already
say what the tag says, and a half-bumped pair fails the tag rather than the PR.

ZENODO_DEPOSITION_T27C stays unset on purpose, so zenodo-publish.yml skips and
mints no DOI. A DOI cannot be withdrawn; enabling it is its own decision, not a
side effect of cutting a crate release.

Closes #4490

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant