ReviewOS

zig-utils/zig-js

VM: expose ordinary activation bindings to direct eval

#771
Closed chris opened this 10 days ago ·
10 days ago

Parent: #465 Related: #498

Problem

Ordinary function bytecode has exact frame slots, TDZ metadata, arguments objects, receiver/home-object state, and direct-eval call opcodes, but compilePlainFunctionInner and nested-template compilation still reject every function whose parser metadata sets uses_direct_eval. The interpreter therefore remains the only activation model in which direct eval can observe and mutate a caller function local. This is now a causal universal-bytecode boundary, not a missing parser or eval implementation.

Scope

  • Give a running ordinary bytecode activation an explicit direct-eval binding view over parameters, var/function bindings, lexical slots and TDZ state, mapped/unmapped arguments, this, super, new.target, and private-name context.
  • Preserve EvalDeclarationInstantiation for strict and sloppy direct eval, including deletable eval declarations, var/lexical conflicts, parameter-environment restrictions, catch/Annex B interactions, and mutation of the exact caller binding.
  • Lower fixed and spread direct-eval calls, tail-position eval calls, nested arrows, methods, constructors, recursive/reentrant activations, and eval that defines or invokes closures without effect replay.
  • Keep indirect/member/optional eval global, preserve cross-realm intrinsic identity, and retain causal admission reasons for syntax whose dynamic environment is still incomplete.
  • Make the activation view precise under moving GC, deterministic OOM, debugger/watchdog state, shared-chunk no-GIL execution, and activation teardown.

No-workaround rules

  • No source rewriting, local-value snapshots that lose writes or TDZ identity, eager heap allocation of every frame local, AST callback from required bytecode, globalizing direct eval, permissive policy default, or hidden tree-walker fallback.
  • No stacks, queues, jobs, Promise/microtask, Worker lifecycle, or Map/Set iterator implementation changes.

Acceptance

  • Forced tree-walker and required bytecode agree for reads/writes/declarations across parameters, var, let/const/class, shadowing, TDZ, catch, mapped/unmapped arguments, this, super, new.target, and private names.
  • Fixed, spread, tail-position, arrow, method, constructor, nested/reentrant, closure-producing, strict/sloppy, direct/indirect, and cross-realm eval controls preserve exact results, errors, side effects, and step/debugger ordering.
  • The activation binding view is bounded, moving-GC safe, failure-atomic under injected OOM, race-free under shared no-GIL/TSan execution, and released at activation teardown.
  • Admission inventory removes the broad uses_direct_eval rejection only for complete semantics; unsupported nested syntax remains causally fail-closed with no interpreter callback.
  • Focused compiler/VM/frontend, full units, relevant exact Test262 eval/function/class accounting, TSan, no-GIL, threadfuzz, debugger, watchdog, and moving-GC gates pass with zero regressions.
  • A conventional commit lands directly on main and cross-links #465 and #498; flips and exact measured boundaries are recorded without a performance claim.
10 days ago

First live-activation layer landed in c82295c2:

  • compiler freezes name-sorted, scope-preserving binding plans across defining VM frames; NUL-prefixed compiler temporaries are excluded
  • runtime-Environment gaps remain causally rejected instead of being flattened or reordered
  • Environment resolution reads/writes the real frame cell, including mapped arguments, TDZ/const classification, captured Reference identity, and escaped-frame locking
  • moving GC traces and relocates the live activation slots; escaping eval closures mark the backing frame non-recyclable
  • focused compiler/live-view/relocation tests pass 4/4, and the live-view + relocation witnesses pass under TSan 2/2

The broad uses_direct_eval admission barrier is still intentionally closed. Next is lazy per-call Environment materialization plus opcode plan threading; this issue remains open until the full semantic matrix and gates pass.

10 days ago

Lazy activation-Environment materialization landed in f525dd46: the VM now constructs the compiler-described scope chain over exact defining frames, roots both the closure environment and unpublished records across moving collection, reverses/links without allocation, and publishes only after the complete plan succeeds. Injected first-allocation OOM leaves vm.env unchanged. The focused witness passes 1/1 normally and under TSan with no leaks. Broad uses_direct_eval admission remains deliberately closed until opcode plan threading and the declaration/context matrix are complete.

10 days ago

Activation-aware call-site threading landed in 03f7bc58: distinct fixed, spread, receiver, and tail-position eval opcodes carry validated immutable plan indexes. Runtime materializes live activation views only when the resolved callee is the realm eval intrinsic; shadowed eval remains an ordinary call, and the caller environment is restored after evaluation. Compiler emission passes 1/1 across fixed/spread/tail forms; the exact live-slot opcode passes 1/1 normally and under TSan, no leaks. The universal uses_direct_eval admission barrier remains closed until persistent sloppy declarations and the remaining parameter/named/runtime-scope contexts are modeled.

10 days ago

Landed 1761996e (feat(vm): retain direct eval declaration environments (#771)). Ordinary VM activations now retain one atomically published variable Environment, so sloppy direct-eval declarations survive later eval calls and inner materialization can anchor at an already-materialized defining frame. Call-site lexical wrappers remain transient, complete plans are validated before publication, frame reuse clears the cache, and the new edge is traced/relocated by precise and concurrent GC. Materialization is deferred until parsing and inherited early-error checks succeed, so non-string, empty, and syntax-error eval does not allocate or mutate frame state. Focused materialization/opcode/relocation tests pass 3/3; materialization/opcode pass under TSan 2/2; no leaks. Admission remains fail-closed: re-entrant parent-cache refresh plus parameter, named-function, runtime-environment, and debugger contexts are still unresolved. This commit flips 0 test262 cases.

10 days ago

Re-entrant and concurrent activation-cache convergence landed in 9e9462a8 (fix(vm): converge reentrant direct eval environments (#771)). Cold inner materialization now allocates and relocation-roots the complete scope graph before publishing every defining function Environment outer-to-inner. A later outer eval therefore reuses the same declaration target, and concurrent contenders converge with atomic compare-exchange; losing unpublished children are reparented with managed-edge barriers. External frame-root publication now fires the insertion barrier, and escaped defining scopes retain synchronized Environment access. Focused transactional materialization plus activation-opcode witnesses pass 2/2 normally and 2/2 under TSan, zero leaks; an injected-OOM sweep proves both defining-frame caches and vm.env remain unchanged at every allocation failure. Admission remains fail-closed for the parameter/named/runtime/debugger semantic matrix. This commit flips 0 test262 cases.

10 days ago

Ordinary simple-frame admission landed in a74f2029 after parse-phase classification in 4f792e14. Direct eval now receives immutable live-activation plans and an implicit arguments slot when dynamically observable; fixed, spread, and proper-tail forms compile, while shadowed/indirect eval retain ordinary/global semantics. Non-simple ParameterEnvironment layouts, methods, derived constructors, and runtime Environment gaps remain causally rejected with no callback or fallback. Compiler probes pass 4/4; normal and TSan direct-eval subsets pass 16/16 each with zero failures/leaks, and the forced tree-walker/required-bytecode witness agrees exactly across mapped arguments, live locals, strict isolation, persistent sloppy declarations, shadowing, nesting/reentrancy, and indirect eval. Test262 accounting and the remaining acceptance matrix are still pending, so #771 remains open.

10 days ago

Landed c422cf78 (fix(eval): retain escaping dynamic contexts (#771)).

This closes the escaping-context and moving-GC layer of the acceptance matrix:

  • runtime-created functions now honor required bytecode independently of automatic tier-selection policy;
  • direct eval exposes function lexicals as TDZ-marked activation bindings from entry;
  • eval, $262.evalScript, ShadowRealm.evaluate, and debugger evaluation own parser source retained by escaping ASTs;
  • ShadowRealm instances and wrapped-callable side records now trace and relocate their realm/target edges;
  • the differential covers this, ordinary new.target, named function-expression self binding, nested-arrow arguments, an eval-created closure mutating captured locals, TDZ, evalScript, and ShadowRealm after forced moving compaction.

Measured evidence: direct-eval unit cluster 18/18 in normal and TSan builds; ShadowRealm 5/5; native-private relocation 1/1; no leaks or sanitizer reports. Test262 flips have not yet been measured for this layer.

Still open here: non-simple parameter environments and the method/derived-constructor super/private-brand activation surfaces remain causal compiler rejections pending their exact environment model and probes.

10 days ago

Landed b73106f4 (feat(vm): admit direct eval class contexts (#771)).

Removed the stale method/derived-constructor barrier after exercising the already-explicit VM context: home object, live constructor prototype, new.target, private-name map, and the shared uninitialized-this cell. The differential covers pre-super() this ReferenceError, eval-driven super(), constructor new.target, mutable locals, instance/static private names, and super property calls.

Measured evidence: direct-eval cluster 20/20 normal; method/constructor differential 1/1 under TSan; no leaks or sanitizer reports. Test262 flips are not yet measured. Non-simple parameter environments and catch/environment-gap call sites remain causally rejected.

10 days ago

Landed 52bde55a on main: required bytecode now preserves the declarative-record identity of a simple catch parameter, so Annex B.3.5 direct-eval var/function redeclarations are exempted without weakening destructuring-catch conflicts. The VM validates this metadata fail-closed and restores it on cold and cached activation materialization.\n\nEvidence: all 22 focused direct-eval tests pass with zero leaks; the new tree-walker/required-bytecode catch differential passes under ThreadSanitizer. Default test262 flips: 0 (the tree-walker baseline was already correct; this closes a required-bytecode-only gap).\n\n#771 remains open. The next causal boundaries are non-simple parameter environments and explicitly interleaved runtime Environment Records.

10 days ago

Landed b683c4b6 on main: activation-backed direct eval is now admitted for rest-only and expression-free destructuring parameter lists. These forms share the function VariableEnvironment, so the VM can expose their exact frame slots without inventing a parameter/body boundary. Default-bearing lists remain fail-closed pending the real two-record representation.\n\nEvidence: compiler admission probe passes; all 24 focused direct-eval tests pass with zero leaks; the new tree-walker/required-bytecode differential passes under TSan. It covers unmapped non-simple arguments, live rest/destructured mutation, persistent eval vars, and a rest arrow. Default test262 flips: 0 (required-bytecode path only).

10 days ago

The remaining default-parameter boundary is now tracked explicitly as #776. It owns the real ParameterEnvironment/body VariableEnvironment split, dynamic eval var shadowing of parameter-slot loads, and parameter-phase eval restrictions. Rest-only and expression-free pattern lists remain complete in b683c4b6; #771 stays the integration parent.

10 days ago

CI contract follow-up landed in c058250f: admission witnesses now expect strict direct eval and derived-constructor direct eval to compile, and the activation-eval allocator probe restores its ordinary arena before parsing an empty string program. The three exact focused tests pass with no leaks; flips 0 Test262 cases.

10 days ago

Related parameter-environment baseline fix landed in d814995a under #776. The tree-walker now predeclares every non-simple parameter binding before initialization, including direct-eval-visible later names. Exact evidence: focused normal/TSan 1/1 each; Test262 later-parameter family 32/32 -> 32/32, 0 flips. The VM record split remains in #776.

10 days ago

The default-parameter body-eval child slice is now on main in commit 1f594e7b (#776). It adds distinct live ParameterEnvironment and body VariableEnvironment identities without snapshots, source scanning, global fallback, or tree-walker fallback. Full units: 1,961 passed / 1 skipped / 0 failed / 0 leaked; focused body differential and deterministic OOM pass normal + TSan; exact scored Test262 boundary scope remains 10/10 before and after. #776 remains open for parameter-phase eval.

10 days ago

The parameter-phase portion of the #776 boundary landed in commit f49add08. It adds exact raw-input/formal TDZ ordering, stable ParameterEnvironment identity, phase-aware nested-arrow captures, and normal/TSan/no-GIL/OOM coverage. Exact scored Test262 remains 21/21 -> 21/21 with 0 flips; 15 eligible required-bytecode witnesses pass. #776 remains open for the existing rest-pattern parameter-prologue barrier.

10 days ago

#776 is complete via commit 04913596. The final slice removes the mixed/default/destructuring rest admission barrier: all six scope-param-rest-elem required-bytecode witnesses pass, exact scored coverage remains 10/10 with 0 flips, targeted TSan/shared no-GIL and the 1,968-test full denominator are green. This advances #771 without a performance claim.

10 days ago

The remaining runtime-Environment interleaving boundary is now tracked explicitly in #785. It owns precise Frame closure-environment edges, immutable boundary metadata, identity-preserving declarative/with forwarding records, moving-GC/OOM/no-GIL publication, and the block/class/catch/loop/with differential. #771 remains the integration parent and stays fail-closed at parent_environment_depth/current environment_depth until that representation is complete.

10 days ago

The captured-runtime-boundary slice of #785 landed in 6e212e82: exact lexical interleaving, live forwarding identity, transactional OOM behavior, moving-GC and no-GIL/TSan witnesses. Focused Test262 remains 9,886/9,886 with 0 flips or regressions; full details are on #785. The current-frame environment_depth boundary and full required gates remain open, so this parent is not complete.

10 days ago

Current-runtime scope and moving-caller-root work landed in fix(vm): preserve runtime eval scopes and caller roots (#785, #786).

#785 now covers both captured and current runtime Environment segments, exact eval-created loop captures, with/catch/class ordering, and abrupt/suspended unwind. #786 fixes the saved caller state discovered by an actual moving safepoint inside eval. No stack, queue, job, Promise, Worker, or Map/Set iterator implementation changed.

Final-source gates: 1988 unit passes, one platform skip, zero failures/leaks; TSan direct eval 36/36 plus root relocation 1/1, zero reports; default/verify threadfuzz 400/300 programs; exact-parent relevant Test262 9886 -> 9886 pass, zero flips/regressions. Detailed subtree counts and semantic boundaries are recorded in the commit and child issues.

Required pushed CI remains pending. #787 is the separately tracked provisional activation-root/OOM lifecycle audit, so this parent remains open rather than claiming the entire activation lifetime is proven.

10 days ago

Landed fix(threads): publish environment capture state atomically (#789) on main. This addresses the Linux TSan read/write race in the direct-eval forwarding/capture path: both capture/private flags now have atomic publication/read semantics, and escaped-frame marking precedes captured publication. No suppression or disabled reuse was introduced.

Local evidence: Environment 14/14 and direct eval 36/36 in both normal and TSan builds (zero reports); full normal units 1,993 pass / 1 skip / 0 fail / 0 leak; 2,711 Test262 passes unchanged across eight relevant subtrees; seed-1 default/verify threadfuzz 400/300 programs with zero failures. #789 retains the outstanding pushed Linux required-integration gate; these local results do not close the parent integration requirement.

10 days ago

Required integration for the landed eval/environment/private-context prerequisites is now verified in CI run 33113309062: all 53 executed jobs passed. Scoped children #785/#786/#788/#789/#790/#793/#794/#795/#796/#797 are closed with their evidence; this broader issue remains open.

2865b8b4 (#798) additionally admits exact outer/self/later/arrow-arguments parameter references using existing parameter-phase binding resolution. Required-tier paired coverage improves 8,475 -> 8,809 passes across the same 9,643 files (+334, zero regressions); default-tier 11,494 passes are unchanged. Full units 2,020 pass / 1 skip, focused unsuppressed TSan and 700 seeded fuzz programs pass. Its pushed CI is pending. #799 tracks remaining assignment/update initializer admission; #787/#791 retain unfinished ownership audits. No native stack/queue/job or lifecycle implementation was changed.

10 days ago

Parameter-phase assignment/update admission is now landed in e4f76af6 (#799), with exact retained References through direct-eval RHS effects, escaping closures and parameter/body separation. All 43 paired controls, shared no-GIL calls, compiler OOM, TSan and the combined full unit suite pass; #799 records the exact 29 required-tier gains and unchanged default-tier reports. This is a scoped child result, not closure of the broader direct-eval acceptance. #801 tracks newly reproduced broader borrowed-state moving-GC failures.

10 days ago

Landed f83c4191 — feat(vm): retain live deferred class captures (#803) on origin/main. #803 now retains exact live defining-frame bindings for deferred classes, including later sloppy-eval bindings, without changing native stack/queue/job lifecycle. #804/#805 separately track construction receiver/class-state ownership fixed in the same validated change. Same 9,643 eligible positive paired Test262 files: four gains (statements/class/elements), zero regressions; 11,563 default-tier cases unchanged. Local 41/41 paired witnesses, 20 actual-moving cases, 2,037 full units plus one platform skip, focused TSan/no-GIL/OOM and 700 fuzz programs pass. Full pushed integration is pending in CI 33135419183; children remain open. #619 retains the distinct unfinished classifier-scaling evidence boundary; #802 and reserved native ownership audits are not claimed complete.

10 days ago

#806 removes the duplicate parameter syntax whitelist and uses canonical expression/pattern lowering under the existing parameter binding phase, retaining later-formal TDZ, parameter/body environment separation, exact captures and causal rejection. f2340f27 improves the same 9643-file required-tier inventory by 791 passes with zero regressions; default-tier reports are unchanged. Full/sanitized/moving/OOM/no-GIL evidence is in #806. This does not claim new classifier scaling or benchmark performance; current CI remains pending at https://github.com/zig-utils/zig-js/actions/runs/33137549779 .

10 days ago

Parent-level acceptance audit is complete. The implementation is the linked commit series beginning with c82295c2 and ending with the scoped ownership/admission closures, including f2340f27, be776fef, bcc3e757, and 4f02bbd4. All tracked causal children (#776, #785-#791, #793-#799, #801-#806) are closed. The remaining ten required-tier files reported by #806 stay visible under the broader #465 inventory and are not a uses_direct_eval admission barrier.

Fresh current-main verification:

  • direct-eval parent cluster: 36/36 Debug and 36/36 unsuppressed TSan, 0 failures/leaks/reports
  • the cluster covers tree-walker/required-bytecode equality, parameter/body records, catch/Annex B, methods and derived constructors, runtime-record interleaving, nested/reentrant and escaping closures, actual moving GC, shared no-GIL calls, and debugger source registration
  • zig build vm-corpus-witness: 4/4 Test262 forced tree-walker/required-bytecode pairs, plus 1/1 serialized and 1/1 no-GIL PR-249 required-bytecode controls
  • current full units (from the immediately preceding #752 gate): 2,236 passed, 1 skipped, 0 failed, 0 leaked
  • latest completed pushed integration: CI 33471275361, 54 jobs, zero failed jobs

Exact corpus accounting was retained on the scoped issues: the runtime-environment slice held 9,886/9,886 relevant Test262 passes with zero flips, and the final parameter-expression inventory reached 9,633/9,643 required-tier passes (8,842 -> 9,633, 791 gains, zero regressions) while all 13 default-tier reports remained byte-identical at 11,494 passes. No performance claim is made.

This satisfies #771 itself and preserves #465/#498 as the broader bytecode-tier roadmap boundaries.

Sign in to comment on this issue.