Skip to content

feat(t27c): gen-ts, and one value layer rather than a second copy of it - #4502

Merged
gHashTag merged 2 commits into
masterfrom
feat/gen-ts
Sep 21, 2026
Merged

gHashTag merged 2 commits into
masterfrom
feat/gen-ts

Conversation

@gHashTag

Copy link
Copy Markdown
Owner

Closes #4501

t27c gen-ts <spec> — a seventh backend, emitting a TypeScript module of a
spec's declarations.

Why it is not a copy of gen-js

TypeScript's syntax for a value is JavaScript's, to the character. The
straightforward shape here was ~200 lines of codegen_js copied with a type
layer bolted on — which is the hand-copied list this codebase keeps finding,
in a new place. So codegen_js became the shared value layer instead: the
reserved-word list, the escape table, the array-length check and the
constant-expression walk have one home, and the only thing that differs
between the backends is which subcommand an error names. That difference is
an argument, Target { tag, lang }, not a second function.

Three further would-be duplicates were lifted the same way rather than
written twice:

Lifted Because two copies would mean
struct_fields a consumer reading the JS descriptor and the TS descriptor of one spec gets two answers
ast_for_codegen the two backends stop failing on the same inputs, and the import fallback gets forgotten in the second copy
declarations_ast (wasm binding) the explorer shows a js tab and a ts tab built from two different readings of the same source

What the type layer decided

  • export const N = 20 satisfies number, not const N: number = 20 —
    the annotation widens 20 to number and takes the literal, and with it
    any use as a discriminant, away from the consumer. Arrays get
    as const satisfies.
  • A frozen object plus a union type, not TS enum: enum compiles to a
    runtime IIFE, so the two backends would stop emitting the same artifact,
    and const enum is refused under isolatedModules.
  • interface X and const X together — declaration merging is what makes
    one import both the usable type and the runtime descriptor.
  • Every integer width is number, said plainly rather than dressed in a
    branded alias, because number is what gen-js already prints.
  • A name the module does not declare becomes unknown with a note, never
    a guess. An array past 32 elements drops its tuple length and says so — and
    when its element type is also unknown it reports both losses, which an
    earlier draft of this file silently did not.

Evidence

Claim How it was checked Result
Both backends accept and refuse the same inputs every spec in the tree through both ok=796 refused=154 parity-breaks=0
The output is valid TypeScript tsc 5.8.3 --strict --noEmit over all 796 emitted modules 9 errors in 7 specs — each reproduced by gen-js on the same spec, so pre-existing value-layer defects a type-checker merely made visible. Filed separately, not fixed here
The types reach a caller a consumer file with four @ts-expect-error directives, which tsc errors on when unused exit 0 — a type that quietly widened to any would fail this
Both backends emit the same runtime artifact TS compiled to JS, both imported under node, deep-compared identical export names, identical values, frozen in both
No regressions full bootstrap suite, 114 test binaries all ok, 0 failures

bootstrap/stage0/FROZEN_HASH is untouched; compiler.rs was read, never
edited.

The two compiler::tests_hir_roundtrip::* failures in bindings/wasm-explorer
are the pre-existing pair documented above the #[path] includes — a
manifest-relative fixture path that resolves only from bootstrap. This PR
drops the two test counts from that note: one was wrong the day it was
written and both were wrong a commit later, because every test either crate
gains moves them.

🤖 Generated with Claude Code

gHashTag and others added 2 commits September 21, 2026 14:29
A seventh backend: `t27c gen-ts <spec>` emits a TypeScript module of the
spec's declarations. It exists because `gen-js` throws the types away --
a consumer of the JavaScript artifact can read `Token.fields` but cannot
write `const t: Token`, and cannot be told at compile time that `999` is
not one of the spec's discriminants.

TypeScript's syntax for a *value* is JavaScript's, to the character. So
the tempting shape here was a 200-line copy of `codegen_js` with the type
layer bolted on -- which is the hand-copied list this codebase keeps
finding, in a new place. Instead `codegen_js` became the shared value
layer: the reserved-word list, the array-length check, the escape table
and the constant-expression walk have one home, and the only thing that
differs between the two backends is which subcommand an error names. That
difference is an argument, `Target { tag, lang }`, not a second function.
Three further would-be duplicates were lifted the same way: `struct_fields`
(both backends must agree on the descriptor, or a consumer reading each
gets two answers for one spec), `ast_for_codegen` (both must fail on the
same inputs) and `declarations_ast` in the wasm binding (the page must not
show a `js` tab and a `ts` tab built from two different readings).

What the type layer decides, and what it refused:

  * `export const N = 20 satisfies number`, not `: number`. The annotation
    widens the inferred type from `20` to `number` and takes the literal --
    and with it any use as a discriminant -- away from the consumer.
  * A frozen object plus a union type, not `enum`. TS `enum` compiles to a
    runtime IIFE, so the two backends would stop emitting the same artifact;
    `const enum` is rejected under `isolatedModules`.
  * `interface X` and `const X` together. Declaration merging is what lets
    one import be both the usable type and the runtime descriptor.
  * Every integer width is `number`, said out loud rather than papered over
    with a branded alias, because `number` is what `gen-js` already prints.
  * A name this module does not declare becomes `unknown` with a note, never
    a guess. An array past 32 elements drops its tuple length and says so --
    and when its element type is also unknown it now reports both losses,
    which an earlier draft of this file silently did not.

Evidence, not assertion. Over every spec in the tree: ok=796 refused=154,
parity-breaks=0 -- the two backends accept and refuse exactly the same
inputs. `tsc 5.8.3 --strict --noEmit` over all 796 emitted modules finds 9
errors in 7 specs; each was run back through `gen-js`, which emits the same
broken line (`export const GF16 = u16;` -- a type alias in value position --
and two forward references). They are pre-existing value-layer defects that
a type-checker merely made visible, and they are worth their own issue. A
consumer file whose four `@ts-expect-error` directives would themselves error
if unused type-checks clean, so the types reach a caller rather than widening
to `any` on the way. Both backends' output was imported under node and
deep-compared: identical export names, identical values, frozen in both.

Also: the note above the `#[path]` includes no longer prints two test counts.
One was wrong the day it was written and both were wrong a commit later. The
two failing test names it already carried are what a reader needs.

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

Closes #4501
Refs #4501

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-21 07:31:26 UTC

Summary

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

These columns do not partition: 2 + 18 + 0 + 0 = 20, and there are 21 open PRs. A PR is being counted twice or not at all.

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

Copy link
Copy Markdown
Owner Author

The five red checks are master's, not this branch's

Each was measured rather than assumed.

check red without this branch evidence
test-ratchet on master itself push run 35557843367 — cc: error: unrecognized command-line option '-ferror-limit=0', then "the failing set grew by 23". Identical message, identical count, four hours before this branch existed.
Corpus ratchet on master itself a5df7c278, plus #4498 #4123 #4116 #4113 #4038
coverage on master itself a5df7c278, plus the same five PRs
emit-bitexact pull_request-only, so no master reading red on #4123 #4116 #4113 #4038
spec-guards pull_request-only, so no master reading red on the same four

The test-ratchet cause is a clang-only flag reaching GCC on the Ubuntu runner.
The same suite is green locally on this branch:

cargo test --release --test c_literal_list_type
→ test result: ok. 13 passed; 0 failed

bootstrap-tests.yml states the position in its own header: "master has 383
failing tests today"
, and "This job is NOT in the required-context set, so it
does not block a merge (see #2376)."
No required context is red here —
mergeStateStatus is UNSTABLE, not BLOCKED.

Nothing in this branch goes near C codegen. The diff is bootstrap/src/codegen_ts.rs
(new), codegen_js.rs, main.rs, bindings/wasm-explorer/src/lib.rs.

@gHashTag
gHashTag merged commit 8017b29 into master Sep 21, 2026
36 of 41 checks passed
@gHashTag
gHashTag deleted the feat/gen-ts branch September 21, 2026 07:45
dmitrii-f-t27 pushed a commit to dmitrii-f-t27/t27 that referenced this pull request Sep 24, 2026
t27c on crates.io is 0.3.0. The compiler gained gen-ts in gHashTag#4502, so the version
a user installs from the registry cannot emit TypeScript while the compiler on
master can. Minor rather than patch: a new backend is added functionality, and
additive -- the six 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 gHashTag#4503

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@gHashTag
gHashTag restored the feat/gen-ts branch September 28, 2026 10:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

gen-js throws the declared types away, so every TypeScript consumer writes them a second time

1 participant