ReviewOS

zig-utils/zig-js

VM: preserve suspendable private and field-initializer eval context

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

Parents: #465. Discovered by the context-field audit in #795; related to #791's suspended/native root audit.

Reproduced defects

Both unmodified main e1afde2 and the #795 parameter-metadata patch fail these probes identically:

class C {
  #x = 7;
  *read(a = eval("this.#x")) { yield a; }
}
new C().read().next().value; // expected 7; SyntaxError: eval: UnexpectedToken at 1:8

The corresponding async-generator method throws the same SyntaxError at its eager parameter initialization.

function* gen(a = eval("arguments.length")) { yield a; }
class C { field = gen().next().value; }
new C().field; // expected 0; SyntaxError: arguments not allowed in a field initializer

Moving that eval into the generator body reproduces the same incorrect field-initializer restriction. An ordinary generator has its own function context even when invoked/resumed from a field initializer.

Root and scope

Ordinary function entry installs the function's private-name map and clears or lexically restores field-initializer context. The suspendable parameter makers and body-resume boundaries do not consistently establish those fields. #795 fixes formal-name/strictness/parameter-phase metadata, not this private/field context gap.

PerformEval derives field-initializer restrictions from the current this-environment's function and takes the direct eval's private environment from its running execution context.

Acceptance

  • Install the callee's exact private environment for generator, async-function, and async-generator parameter evaluation and body execution.
  • Preserve lexical field-initializer context for arrows while preventing ordinary suspendable functions from inheriting the caller's restriction.
  • Restore caller context on normal completion, abrupt completion, suspension/resumption, reentrant eval, and allocation failure.
  • Cover same-spelled private names in unrelated classes, nested classes/calls, genuinely forbidden field eval, and paired tree-walker/required-bytecode execution.
  • Keep all retained managed edges and native borrowed caller state correctly rooted under moving GC and verify relevant no-GIL/TSan paths.
  • Measure the pinned corpus impact, run integration gates, and land a linked conventional commit.

Reuse existing native suspend/resume, stack, queue, and job ownership; no lifecycle redesign or permissive eval fallback.

Local audit evidence: /tmp/zig-js-795-params.qYytM4/context-audit.log and its four audit-*.js probes. These are local diagnostics, not published conformance counts.

Landed implementation and evidence

8a1398ff — fix(eval): retain lexical private contexts across suspension (#797) is on origin/main, authored and committed by Chris, without trailers.

The fix captures lexical private environments for all functions and VM closures, preserves nested-class shadowing, installs/restores callee context at parameter and resume boundaries, validates exact private names before eval side effects, and isolates indirect eval and dynamic constructors. Maps are immutable Context-arena metadata, not new moving managed edges. Existing native resume, stack, queue, job, and root ownership is unchanged; #791 remains a separate native-borrow audit.

Local results against e0ed6eb and pinned Test262 4249661:

  • 24 focused paired-mode witnesses: 5 -> 24 pass. Includes repeated suspension, async completion, private shadowing, escaped closures, forbidden field eval, and indirect/dynamic isolation.
  • 13 language subtrees: 11,494 pass / 0 fail, unchanged. Five eval/dynamic-constructor subtrees: 581 pass / 2 existing fail, unchanged. All 18 diagnostic logs are byte-identical: 0 pinned test262 flips; no regressions in this measured scope.
  • 9,643 eligible positive paired corpus files: 8,475 pass / 1,168 existing required-bytecode fail, with identical failing paths. These failures are retained, not excluded or claimed implemented.
  • Full four-shard Debug suite: 2,017 pass, 1 platform skip, 0 fail/leak, 397.0 s.
  • ReleaseSafe + TSan, no suppressions: 4 private-context, 47 parameter, 45 generator checks pass (overlapping filters). Includes no-GIL calls, actual receiver relocation, and metadata OOM restoration.
  • Seed-1 threadfuzz: 400 default + 300 verify programs, zero failures. GC inventory and format checks pass.

Evidence index: /tmp/zig-js-797-context.2QwDxk/README.md; retained 24 fixtures under witnesses/. Temporary corpus files removed. The initial GC fixture's no_candidates failure is retained; final acceptance proves the receiver address actually changes.

Completed integration

CI run 33113309062 completed successfully on main commit 8a1398ffc04e9e30987f968693667c7271ca3ce1, which contains this fix. All 53 executed jobs succeeded, including all four unit and TSan-unit shards, Linux no-GIL TSan, functional/no-GIL corpus gates, fuzz profiles, ABI, Wasm, Test262 parallel, and docs. The scheduled-only nightly job was skipped, not counted as a pass. The local evidence above plus this descendant integration run completes this issue's acceptance.

9 days ago

The implementation now captures the lexical private-name environment when functions are created (including ordinary nested functions and arrows), restores it at call/resume entry, and retains the field-initializer restriction only for lexical arrows. Nested classes retain outer names without mutating the outer map.

The audit also exposed an existing eval-validation gap: an undeclared private name could be accepted and execute earlier side effects. Direct eval now validates the exact available private names before execution. Indirect eval and all Function constructor kinds explicitly retain a null private environment, so the new lexical capture cannot grant them the caller's private access.

Local evidence against e0ed6eb:

  • 23 paired tree-walker/required-bytecode semantic witnesses: 5 pass / 18 fail → 23 pass.
  • 11,494 Test262 cases across thirteen subtrees remain byte-identical, all passing; flips 0 existing corpus cases.
  • The no-GIL shared-function witness and exact parser-name validation pass.
  • The initial moving-GC fixture had no relocation candidates. It has been tightened to require a real nursery receiver move before checking the suspended generator and escaped arrow; that revised check and full integration/sanitizer verification are still underway.

New retained fields are immutable Context-arena metadata, not managed GC cells or shared mutable state. Existing stacks, queues, jobs, resume ownership, and lifecycle mechanisms are unchanged. No commit has landed yet.

Sign in to comment on this issue.