feat(t27c): gen-ts, and one value layer rather than a second copy of it - #4502
Merged
Merged
Conversation
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>
Contributor
|
📓 NotebookLM Notebook linked to this PR
This notebook contains session context, decisions, and artifacts for this work. |
Contributor
Owner
Author
The five red checks are master's, not this branch'sEach was measured rather than assumed.
The
Nothing in this branch goes near C codegen. The diff is |
This was referenced Sep 21, 2026
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>
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 #4501
t27c gen-ts <spec>— a seventh backend, emitting a TypeScript module of aspec's declarations.
Why it is not a copy of
gen-jsTypeScript's syntax for a value is JavaScript's, to the character. The
straightforward shape here was ~200 lines of
codegen_jscopied with a typelayer bolted on — which is the hand-copied list this codebase keeps finding,
in a new place. So
codegen_jsbecame the shared value layer instead: thereserved-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:
struct_fieldsast_for_codegendeclarations_ast(wasm binding)jstab and atstab built from two different readings of the same sourceWhat the type layer decided
export const N = 20 satisfies number, notconst N: number = 20—the annotation widens
20tonumberand takes the literal, and with itany use as a discriminant, away from the consumer. Arrays get
as const satisfies.enum:enumcompiles to aruntime IIFE, so the two backends would stop emitting the same artifact,
and
const enumis refused underisolatedModules.interface Xandconst Xtogether — declaration merging is what makesone import both the usable type and the runtime descriptor.
number, said plainly rather than dressed in abranded alias, because
numberis whatgen-jsalready prints.unknownwith a note, nevera 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
ok=796 refused=154 parity-breaks=0tsc 5.8.3 --strict --noEmitover all 796 emitted modulesgen-json the same spec, so pre-existing value-layer defects a type-checker merely made visible. Filed separately, not fixed here@ts-expect-errordirectives, whichtscerrors on when unusedanywould fail thisnode, deep-comparedok, 0 failuresbootstrap/stage0/FROZEN_HASHis untouched;compiler.rswas read, neveredited.
The two
compiler::tests_hir_roundtrip::*failures inbindings/wasm-explorerare the pre-existing pair documented above the
#[path]includes — amanifest-relative fixture path that resolves only from
bootstrap. This PRdrops 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