feat(t27c): gen-js, and the spec explorer's compiler gets its source back - #4472
Merged
Merged
Conversation
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
Contributor
|
📓 NotebookLM Notebook linked to this PR
This notebook contains session context, decisions, and artifacts for this work. |
Contributor
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
Contributor
|
📓 NotebookLM Notebook linked to this PR
This notebook contains session context, decisions, and artifacts for this work. |
Contributor
PR DashboardGenerated at: 2026-09-20 17:23:12 UTC
Summary
Seal Status
|
3 tasks
…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>
Contributor
|
📓 NotebookLM Notebook linked to this PR
This notebook contains session context, decisions, and artifacts for this work. |
Contributor
PR DashboardGenerated at: 2026-09-20 18:01:19 UTC
Summary
Seal Status
|
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>
This was referenced Sep 21, 2026
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
.t27spec 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,benchorinvariantisannounced 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 outWarn: 1. The explicitdiscriminant lives on the variant node, in
value, not in a child — so thefirst 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
Erroris 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/websiteships a WebAssembly build of the bootstrap compiler so the specexplorer 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 throughto 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:
t27c parserefusest27c genThe 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}.rswith#[path]rather thancopying them, so there is exactly one copy of the compiler in the tree.
bootstrap/stage0/FROZEN_HASHis unchanged: no freeze ceremony was neededand none was performed.
The ABI is treated as a contract, because a browser can be holding
yesterday's bundle.
t27_alloc/t27_analyze/t27_freekeep their exactshapes and the JSON keeps its exact field names.
t27_analyze_namedisadded beside
t27_analyze, never in place of it.A test that failed and was right to
It asserted that
Lexer::droppedrecords characters. It records bytes, soone em dash arrives as three entries —
âand two controls — and the page'sloss 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.rsis sealed and the repair is a reseal, not a line ina browser binding. What the test buys is that the day it changes is visible.
Witnesses
habr-mcp's committedhabr_decls.mjsregenerated from its specgen-jscargo test --release --bin t27c codegen_jscd bindings/wasm-explorer && cargo test -- --skip compiler::cargo testbootstrap/stage0/FROZEN_HASHcompiler.rsis not touchedThe crate is excluded from the workspace, so
cargo test -p …does not reachit; run it from its own directory.
The two failures are the compiler's own tests, and they are positional.
#[path]bringscompiler.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}_specread$CARGO_MANIFEST_DIR/../specs/fpga/*.t27. That resolves for any crate exactlyone level below the repo root, which is why they pass in
bootstrapand failfrom
bindings/wasm-explorer. Not worked around: the file is sealed, copyingthe fixtures under
bindings/would be a second home for the truth, and movingthis crate up a level to satisfy a relative path would separate it from
bindings/javascriptandbindings/python. The skip is narrow, named, anddocumented 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.txtmoves 141 → 142 and the move is not this PR's. Thenew row is
queen-publish.yml:81,GH_TOKEN: ${{ secrets.GH_AGENT_TOKEN || github.token }}, which arrived ind7a1530adwithout a re-bless. It is a YAMLfallback expression, not a shell
||swallowing an exit code, so the censusrefuses 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.
duplicate-bodies1e78d703aitselfcoverage1e78d703aitself1e78d703aitselfemit-bitexactpull_requestonlyt27cat322af1b27in a detached worktree and ran the census: identical 12. Also fails on 5 of 5 unrelated open PRsspec-guardspull_requestonlytest-ratchet114 targets ran; 23 failing test(s) (baseline 372)and the failure reason is372 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 testThree 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 gatecould not report five live regressions that landed in September.
What this does NOT establish
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.
codegen_js.rsis the compiler's third, besidezig_escapeandescape_for_fmt. Not a copy of either — the U+2028/U+2029arms 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