Skip to content

fix(engine): destructuring defaults, block function scoping and private accessor offsets - #1003

Open
bardrop wants to merge 3 commits into
trynova:mainfrom
bardrop-oss:fix/engine-scoping-bugs
Open

bardrop wants to merge 3 commits into
trynova:mainfrom
bardrop-oss:fix/engine-scoping-bugs

Conversation

@bardrop

@bardrop bardrop commented Nov 12, 2026

Copy link
Copy Markdown

Fixes the first three bugs from #1002. The fourth one (pop/shift/splice on a non-extensible array) touches the Array internals, so I'll send it as a separate PR.

One commit per bug:

  1. Destructuring default reading an earlier binding (bytecode_compiler.rs). IdentifierReference::compile treated any reference whose ancestor is the binding's own VariableDeclarator / FormalParameter as a TDZ access, so const { a, b = a } = { a: 1 } threw. The new is_declaration_tdz keeps the TDZ only when the reference comes before the binding, or sits in the initializer / default value that the binding gets destructured from. So these still throw: const { a = a } = {}, const [a] = [a], const [{ a } = { a: a }] = [], const { [a]: a } = {}, (x = x) => x. And these now work: const { a, b = a } = o, let [a, b = a] = arr, ({ width, height = width }) => ..., const [[a, b = a] = [7]] = [].

  2. Block function can't see a later let/const (block_declaration_instantiation.rs). Functions were instantiated in source order, before the block's lexical environment was entered for the later escaping binding, so the closure captured the outer environment. Function declarations are now instantiated after the other declarations of the block. The order isn't observable otherwise: the other declarations only create bindings.

  3. Panic with a static private field and a private getter/setter pair (class_definition_evaluation.rs). get #b and set #b share one Private Name, but instance_private_method_count counted both, so the static field offset went past private_env.names.len(). The method counters now only go up for a new name.

Testing

  • New nova_vm/tests/language_tests.rs runs three new files in tests/sources/ (the repros from the issue plus the TDZ cases above). All three fail on main (two ReferenceErrors and the PrivateName panic) and pass with this branch. I checked every case against Node first.
  • cargo test, cargo fmt --check and cargo clippy --all-targets -- -D warnings pass.
  • I ran the full test262 suite with -u. No result changed apart from a few timeouts that pass on my machine (I left those out), so expectations.json is unchanged.

A reference to a binding from inside its own declaration was always
treated as a TDZ access, so `const { a, b = a } = o` (and the same with
let, var and parameter patterns) threw a ReferenceError. Now the
reference is only in the TDZ if it comes before the binding, or if it is
in the initializer or default value that the binding gets destructured
from.
Block function declarations were instantiated in source order, before
the block's lexical environment was entered for a later escaping let or
const. The function then captured the outer environment and could not
see the binding. Instantiate function declarations after the other
declarations of the block.
A private getter/setter pair shares one Private Name but was counted
twice, so static private fields after it got an offset past the end of
the class's Private Environment and the engine panicked.
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