Skip to content

Merge train: #9720, #9722 + stale TLS allowlist entry - #9727

Merged
proggeramlug merged 3 commits into
mainfrom
train118
Sep 4, 2026
Merged

Merge train: #9720, #9722 + stale TLS allowlist entry#9727
proggeramlug merged 3 commits into
mainfrom
train118

Conversation

@proggeramlug

Copy link
Copy Markdown
Contributor

Merge train: #9720, #9722, plus one gate fix.

#9720 — a declarator's own initializer can now see the binding it introduces (#9718). const off = ev.on(() => off()) and const { unmount } = await render({ onDone: () => unmount() }) threw ReferenceError: <name> is not defined. The forward-capture pre-pass recorded a declarator's initializer into seen_closure_refs only after deciding whether that declarator's bindings needed a boxed, TDZ-seeded forward declaration — enough for the later-declarator case, but it left the self-referential shape unregistered, so the reference fell through to js_global_get_or_throw_unresolved.

Found in claude-code: the install subcommand is exactly this shape, so claude install <bad-channel> threw an uncaught ReferenceError.

Recording before the decision is a strict superset — the same cic_expr(init, …), moved earlier in the loop iteration, with later declarators still seeing the refs. Registering more bindings also improves shadowing: let O = (() => O)() under an outer O previously fell through to global lookup and could return the outer value, and now correctly hits TDZ, which is what node does. The gap test covers every declarator form (plain, object pattern, { key } shorthand, array, nested, let/const, with and without await) plus a multi-declarator case pinning that the previously-working path did not regress.

#9722 — ten closure-body registries packed into one 16-byte record (#9707). Module init recorded rest arity + kind, declared ABI arity, ECMAScript .length, the arrow/strict/async/generator flags, and an eligible arrow's direct-call bodies into ten thread-local PtrHashMaps all keyed by the same func_ptr, plus an eleventh memoizing the dispatch strategy.

Its gc_runtime_root_holders.json edits are honest: the new CLOSURE_BODY_REGISTRY and TRUSTED_TARGETS verdicts carry forward the same reasoning the superseded entries held — a func_ptr is a code address the collector neither moves nor traces, and the record is three plain integers, a flags word, and an index — and the ten stale entries are deleted rather than left behind. Both gc_runtime_root_holders.py and gc_rekeyed_key_tables.py agree.

Gate fix (mine). #9697, which I landed in train115, removed readline_helpers.rs's only raw thread_local! block but left its cold-allowlist entry behind, and check_thread_locals.py fails a recorded entry that no longer matches the tree. That check runs in tls-budget.yml, a satellite workflow outside the 64-gate lint set, so a green merge did not catch it.

Removed by hand rather than with the script's --update, which would also have swept five unrelated pre-existing failures (fs/deferred.rs, gc/census.rs, gc/idle_compact.rs, gc/idle_reclaim.rs, gc/oldgen_defrag.rs) into the allowlist as "cold" and hidden them. Those are other authors' raw thread_local! blocks and their call to make; tls-budget stays red on them.

Validation

64/64 lint gates; release build; perry-runtime and perry-hir (RUST_TEST_THREADS=1) — all green.

Ralph Küpper and others added 3 commits September 4, 2026 14:00
A closure created inside a `let`/`const` declarator's initializer that
references the binding that declarator introduces threw
`ReferenceError: <name> is not defined`. Node only has a TDZ window here,
and the closure body does not run until after initialization, so shapes
like `const off = ev.on(() => off())` and
`const { unmount } = await render({ onDone: () => unmount() })` are legal.

`pre_register_forward_captured_lets` recorded a declarator's initializer
into `seen_closure_refs` only after deciding whether that declarator's own
bindings were forward-captured. That ordering serves the later-declarator
case and nothing else, so the self-referential shape was never
pre-registered and the reference fell through to
`js_global_get_or_throw_unresolved`. Recording before the decision is a
strict superset — later declarators still see the same refs.

Reachable from every declarator form: the destructuring path has no
pre-registration of its own, and `ast_expr_contains_function_expr` (which
guards the simple-binding path) does not descend through `await`.

Found in claude-code's `install` subcommand, which is exactly this shape;
the uncaught ReferenceError there dropped the trailing newline and the
exit code (`B60_install_badtarget`, #9575).

Closes #9718
…9707)

Module init recorded each closure body's rest/arity/length/arrow/strict/
async/generator/async-generator attributes and the trusted direct-call
bodies into ten thread-local PtrHashMaps keyed by the same func_ptr, plus a
dispatch-strategy memo map. Replace them with one `CLOSURE_BODY_REGISTRY`
of 16-byte `ClosureBodyRecord`s (24-byte bucket, size-pinned), a dense
`TRUSTED_TARGETS` side array indexed only by eligible arrows, and derive
the dispatch strategy from the record on a miss instead of caching it.

Census on a 20k-function fixture: 2,916,564 -> 1,638,416 bytes (-44 %) for
35,051 bodies, identical output. Projected on cc's recorded census counts:
7.24 MB -> 3.28 MB. Public registration/lookup signatures are unchanged.

Claude-Session: https://claude.ai/code/session_01Dw8cFMegSvvXvGABjSXMQf
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.

1 participant