ReviewOS

zig-utils/zig-js

VM: preserve dynamic binding references across with destructuring

#731
Closed chris opened this 9 days ago ·
9 days ago

Parents: #465, #493 Related: #726, #730

Problem

Frame-mode bytecode statically selects local/upvalue/global stores, while a with Object Environment Record can redirect an identifier through observable @@unscopables and HasProperty. Destructuring assignment must evaluate each target Reference before IteratorStep/GetV, and destructuring var BindingInitialization resolves each binding at its own spec point. Deferring name resolution until PutValue or forcing a frame slot changes order and destination, so #726 and #730 correctly keep these functions on the tree walker.

The same boundary must be audited for simple var x = value inside with; a static frame store may bypass the object Environment.

Scope

  • Represent one resolved identifier Reference in activation-owned storage without borrowing a recyclable Environment or object pointer unsafely.
  • Resolve dynamic bindings at the exact assignment/declaration evaluation point, including Symbol.unscopables, Proxy traps, getters, abrupt completion, and strict/sloppy PutValue behavior.
  • Reuse the Reference for the final write without repeating HasBinding or observable lookup.
  • Lower array/object destructuring assignment and var declaration targets inside with, including defaults, computed keys, rest, iterator close, recursion, suspension-capable env-mode paths where applicable, and nested with records.
  • Audit and correct simple var initializer behavior through the same primitive.
  • Preserve lexical declaration binding to its declarative scope, ordinary static frame/upvalue fast paths, moving/concurrent GC, activation reuse, and exact interpreter fallback until the complete path is admitted.

No-workaround rules

  • No delayed re-resolution, duplicated Proxy/@@unscopables observation, source rewriting, synthesized global names, raw Environment pointers without lifetime ownership, heuristic syntax refusal, or tree-walker fallback presented as implementation.
  • No stacks, queues, jobs, Promise, microtask, Worker lifecycle, or Map/Set iterator changes.

Acceptance

  • Forced tree-walker and required bytecode agree on exact Reference-creation/iterator/property/write order and destinations across nested with, Proxy, @@unscopables, defaults, rest, abrupt close, recursion, and GC.
  • Simple and destructuring var initialization plus destructuring assignment use the activation-owned Reference and no longer hit unsupported_lowering.
  • Static bindings retain their current direct bytecodes and native-tier eligibility.
  • Focused compiler/frontend/VM, full units, applicable TSan/no-GIL, and nonzero relevant Test262 subtrees pass with exact accounting.
  • Landing commit cross-links #465, #493, #726, and #730.
9 days ago

Implementation progress:

  • Added immutable chunk capture plans plus per-Exec tagged binding-reference slots (static, exact declarative Environment, exact with object, exact global object, or unresolvable).
  • ResolveBinding now runs before simple var initializers and each array/object/rest identifier target; PutValue consumes the captured base without repeating @@unscopables. Static local/upvalue sites with no intervening Environment Record retain their direct bytecodes.
  • The tree-walker semantic baseline now captures the same exact References instead of merely triggering and discarding with lookup.
  • Active Exec and suspended generator roots trace and relocate Environment/Object reference bases; activation recycling clears slots.
  • Plain destructuring assignment/declaration inside with now compiles without bind_pattern or whole-function fallback.

Validated so far:

  • exact tree-walker/required-bytecode witness: 1/1 passed (nested with, Proxy has, Symbol.unscopables, winning and failed lookup, simple/destructuring var, global/unresolvable capture, suspension, moving GC);
  • compiler admission witnesses: 2/2 passed;
  • existing #726/#730 differential witnesses: 2/2 passed.

The first full sharded run exposed a real heap-trampoline initialization gap: plain activations enter runChunk without the program/generator execLoop setup. Two shards aborted at the first zero-length reference slot. The allocation is now shared by both entry paths and the full gate will be rerun; those aborted results are not claimed as passing evidence.

A separate pre-existing ordinary identifier read/call/compound/update with boundary is now tracked as #732 rather than widening or obscuring this issue.

9 days ago

Implementation is complete locally and the exact final tree is green.

Implemented:

  • activation-owned ResolveBinding plans and VM reference slots for simple var initializers plus array/object/rest destructuring targets;
  • exact declarative, with-object, global-object, static frame/upvalue, and unresolvable Reference bases;
  • tree-walker/VM ordering parity before initializer, IteratorStep, GetV, defaults, and rest collection;
  • precise active-exec and suspended-generator tracing/relocation;
  • moving-GC-safe temporary roots across Proxy has, Symbol.unscopables, and SetMutableBinding operations;
  • required-bytecode admission for the previously rejected with destructuring functions.

Final validation:

  • focused tree-walker + required VM + no-GIL parallel-JS witness: 1 passed, 0 failed, 0 leaked;
  • same focused witness under TSan: 1 passed, 0 failed, 0 leaked, no sanitizer diagnostics;
  • zig build test-parallel: 1,862 passed, 1 platform skip, 0 failed, 0 leaked across 10 shards;
  • live pinned Test262 test/language/statements/variable destructuring slice: #730 parent 97 pass / 0 fail -> #731 candidate 97 pass / 0 fail, so 0 flips and 0 regressions.

The differential witness covers lookup winners and misses, nested with records, Proxy observability, Symbol.unscopables mutation, strict unresolvable and deleted-global references, iterator/GetV mutation, generator suspension, forced moving GC, and parallel JS. Landing next with the issue-linked conventional commit.

Sign in to comment on this issue.