Skip to content

fix(gen-js,gen-ts): a reference nobody checked became a ReferenceError at import - #4529

Merged
gHashTag merged 2 commits into
masterfrom
fix/js-ts-value-layer
Sep 21, 2026
Merged

gHashTag merged 2 commits into
masterfrom
fix/js-ts-value-layer

Conversation

@gHashTag

Copy link
Copy Markdown
Owner

Closes #4528

What was wrong

const_value answered every bare identifier with js_name(name), under a comment reading
"a reference to a const declared above". The comment was the only thing checking. Three
shapes in the corpus walked through it and produced artifacts that throw before a single
line of them runs: a name the backend had itself refused to emit, a name declared
further down the file (a const is not hoisted into a usable state), and a primitive
type name
used where a value was expected.

The module is syntactically perfect, so node --check passes. Only import reaches these,
and it reaches them all at once — one unbound identifier costs the whole module. The Spec
Explorer showed a green backend chip over a file that cannot be loaded.

Three instruments, each catching what the previous could not

instrument found
node --check over 1414 artifacts 3 duplicate export const (SyntaxError, kills the module)
import of every artifact 13 unbound identifiers
tsc --noEmit --strict over all 24 lost omission records, 2 lost enum discriminants

Stopping at any one of the three would have shipped the next two.

The fix

  • Bound carries three sets — emitted, refused, declared — and resolve says which of
    the four cases a lookup fell into, with the line number, instead of producing a name and
    hoping. A name that genuinely comes from another module is still named as such.
  • A variant declared twice keeps the first. compiler/ast.t27 has TokenType.And at
    16 (the operator and) and again at 99 (the Gherkin keyword And). An object
    literal keeps the last of a repeated key, so the artifact silently answered 99 to
    both questions. The first now wins, the displaced one is announced, and the discriminant
    counter still advances past it — But stays 100. TypeScript says this out loud as
    TS1117; JavaScript does not, which is why it is announced in the artifact rather than
    left to a type-checker to notice.
  • __NOT_EMITTED__ becomes a frozen list, not an object, because two entries for the
    same declaration is legal in a list and TS1117 in an object — an error in exactly the
    artifact the TypeScript backend exists to make type-check.
  • generate_reported returns the omission count beside the code, and the wasm bridge
    publishes it as targets.js.notEmitted. A catalog can now mark a spec partial without
    regexing __NOT_EMITTED__ back out of the artifact, which would be a second, weaker
    implementation of something the backend already knows exactly. The five compiled backends
    get no such field: null reads as no such question, where 0 would read as no
    omissions
    .

Measured over the whole corpus, not sampled

All 1414 specs emitted through every backend, every artifact then loaded:

before after
specs where js fails 335 209
specs where ts fails 335 209
artifacts that import cleanly — 1205 / 1205
tsc --strict errors across all artifacts — 1

209 is exactly the set that produces no AST at all — c, rust and zig fail the same
209, and no backend can serve them. The js/ts-only failing family is zero. The one
remaining type error is neg_invalid_annotation, a fixture that exists to be wrong.

69 specs now carry a non-empty __NOT_EMITTED__ — 244 declarations announced. These are
not whole modules and not broken ones, and a catalog no longer has to call them either.

cargo test --bin t27c: 1736 passed, 0 failed, 2 ignored (was 1735 — the new test is
a_variant_declared_twice_keeps_the_first_and_announces_the_second).

What this does NOT establish

The 209 specs that never parse are untouched and unexplained here. Which of them are
damaged files, which are legacy dialects and which are real parser gaps is a separate
question, deliberately not answered on the way past.

🤖 Generated with Claude Code

@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 12:11:23 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 and others added 2 commits September 21, 2026 19:29
…r at import

Closes #4528

`const_value` answered every bare identifier with `js_name(name)` under a
comment that said "a reference to a const declared above". The comment was the
only thing checking. Three shapes in the corpus walked through it and produced
artifacts that throw before a single line of them runs:

  - a name the backend had itself refused to emit,
  - a name declared further down the file, which `const` does not hoist into a
    usable state,
  - a primitive type name used where a value was expected.

`node --check` cannot see any of these -- the module is syntactically perfect.
Only `import` reaches them, and it reaches them all at once, so one unbound
identifier costs the whole module.

`Bound` now carries three sets (emitted, refused, declared) and `resolve`
names which of the four cases a lookup fell into, with the line. An enum that
declares one variant twice -- `TokenType.And` is the operator `and` at 16 and
the Gherkin keyword `And` at 99 in `compiler/ast.t27` -- kept the last of the
repeated key and silently answered 99 to both questions; the first now wins and
the displaced one is announced. The discriminant counter still advances past it,
so `But` stays 100.

`__NOT_EMITTED__` becomes a frozen list rather than an object, because an object
with two entries for the same declaration is a TS1117 error in the artifact the
TypeScript backend exists to make type-check. `generate_reported` returns the
count beside the code, and the wasm bridge publishes it as `targets.js.notEmitted`
so a catalog can mark a spec partial without regexing the artifact back apart.

Measured over the 1414-spec corpus, emitting every backend and loading every
artifact: js and ts each fail 335 specs before and 209 after -- exactly the set
that never parses at all, which no backend can serve. 1205 of 1205 emitted
modules now import cleanly, and `tsc --noEmit --strict` over all of them leaves
one error: `neg_invalid_annotation`, a fixture that exists to be wrong.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…it merely found

`check_specs_generate` calls a spec broken only when EVERY backend refuses it,
so a js/ts change can move it. Fourteen did, and the gate correctly failed
until their ledger lines came out. That half is this pull request's work.

The same regeneration adds ten lines, and they are NOT. Every one of them is a
parse error:

  specs/account/auth.t27                  parse error in fn 'logout' line 127
  specs/ml/optimizer/adamw.t27            parse error in fn 'get_effective_betas'
  specs/port/tools/_prereq.t27            parse error in fn 'skip' line 33
  specs/port/tools/builtin_parity_table.t27    module level line 3, KwPub
  specs/port/tools/check_damage_negatives.t27  parse error in fn 'parse_report'
  specs/port/tools/published_figures.t27       Expected LBrace line 131
  specs/port/tools/wp18_gate_selfconsistent_selftest.t27  module level line 3
  specs/tri/collections/maybe.t27         Expected LBrace line 34
  specs/tri/graph/graph_bfs.t27           parse error in fn 'init' line 31
  specs/tri/utils/config.t27              Expected LBrace line 42

This branch changes codegen_js.rs, codegen_ts.rs and the wasm bridge. It
touches nothing that parses. A file that produces no AST produces none here
either, so these ten fail identically on master.

Why master is green over them: main() prints the `fixed` class and returns 1
before `new` is ever printed, so a run that has repairs to bless never reports
its new failures -- and `.github/workflows/emit-bitexact-gate.yml` is
`on: pull_request` with a paths filter on `bootstrap/**`, so master is never
measured at all. These ten accumulated where nothing was looking. This is the
first branch to touch bootstrap/** since they arrived; it is measuring them,
not causing them.

The remaining three moved lines (portable/relay_observer, ternary/
hybrid_arithmetic, tools/registry) are the same debts with refreshed message
text, not new entries.

Ledger: 80 -> 76. It only fell.

Closes #4528
@gHashTag
gHashTag force-pushed the fix/js-ts-value-layer branch from 2c33a9b to 7599a94 Compare September 21, 2026 12:37
@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 12:38:44 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
gHashTag merged commit ea72eff into master Sep 21, 2026
41 of 45 checks passed
@gHashTag
gHashTag deleted the fix/js-ts-value-layer branch September 21, 2026 12:51
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 and gen-ts emit references to names that are not bound, so 126 specs produce modules that throw at import

1 participant