ReviewOS

zig-utils/zig-js

JIT: reconstruct calls, exceptions, catch, and finally on deoptimization

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

Parent: #432 Blocks: #429

Make optimized execution preserve exceptional control flow and handler state rather than restricting deoptimization to empty-handler numeric regions.

Acceptance

  • Model calls, throwing operations, exception edges, and handler stacks in optimizer frame states.
  • Reconstruct accumulator, operand stack, locals, catch/finally records, completion values, and exact resume IP/steps.
  • Preserve termination, watchdog, debugger, and host-trap behavior through optimized frames.
  • Fail closed before observable work for unsupported call or handler shapes.
  • Pass focused differential tests for caught/uncaught throws, nested catch/finally, return/throw through finally, and host exceptions.

Landed checkpoints

  • a8d094a: deoptimization metadata owns ordered catch/finally records; the VM validates and transactionally replaces locals, stack, accumulator, handlers, and IP, while malformed handler metadata leaves the activation untouched.
  • 321b159: optimizer block/edge frame states select owned handler slices; lowering carries them through program ownership into published deoptimization metadata with checked depths and ranges.
  • 1ee7e18: structured normal-flow handler dataflow reaches active-catch side exits end to end; mismatched merges, unbalanced pops, calls, and throwing instructions reject before publication.
  • e95f7be: side-exit reconstruction reserves the optimized operand-stack storage before its transactional commit, fixing the shared CI compile failure without changing hot append behavior.
  • 0f8bc81: explicit throw is an optimizer terminal effect with an exact pre-instruction frame state and mandatory native side exit; caught throws and propagation to a caller handler preserve value, handlers, IP, and step counts under normal and TSan tests.
  • 3c5f856: normally unreachable handler blocks stay bytecode-only after the mandatory throw exit, enabling nested throw-through-finally while reachable unsupported effects still reject.
  • 668bead: abrupt_return publishes the return completion and active finally stack before resuming canonical bytecode unwinding.
  • 96d74a1: direct call publishes a pre-effect frame state; function-valued parameters and arguments survive without irrelevant Number guards, while normal results and caught call exceptions resume in bytecode.
  • c3681fc: new_call uses the same pre-effect reconstruction contract for constructor-valued parameters and arguments.
  • 5fa8ade: the legacy empty-handler reconstruction fixture explicitly initializes the handler slice, and an unfiltered CI-equivalent unit shard passes 326/326.
  • a95fa40: a conditional-loop OSR differential executes native work, reconstructs the exact header before the 1,024-step boundary, and proves bytecode consumes a pending shell-timeout host trap at precisely step 1,024.
  • 89fc073: fresh optimized-loop activations prove cooperative stop, expired-watchdog, and shell-timeout requests all resume bytecode and fire at exactly step 1,024; normal and TSan focused gates pass.
  • ad0b211: optimizer, baseline, and direct native entry now fail closed whenever debugger, host, or profiler statement hooks are installed, even with an existing owner lease and artifact; refusal consumes no steps or native-attempt counters.

Current checkpoint

Complete through 1b812e2, e3cb3bb, and 0cb719d (documentation at c3833eb, bd94239, and 80281cc). Explicit throws own catch/finally SSA edges; deterministic nested catch and finally bodies execute natively; calls, constructors, properties, coercions, and every interpreter-owned effect own exact pre-effect recovery plus exceptional target/unwind metadata. Uncaught throws, caller propagation, nested finally, return and break through finally, caught JS/host call exceptions, property/coercion/environment/iterator exceptions, termination, watchdog, shell timeout, debugger fallback, invalidation, precise stack maps, and moving-GC recovery are covered.

The focused exceptional-target batch passes 4/4 and the combined optimizer TSan filter passes 87/87 with zero failures or leaks. Benchmark scores are unchanged because no published workload changed. Fully executable calls/effects and native dynamic completion dispatch are intentionally tracked as the next implementation layer in #439 rather than weakening this recovery issue acceptance.

9 days ago

Checkpoint a8d094a5 is on main.

  • DeoptMetadata now owns immutable ordered catch/finally records with exact saved stack depths.
  • Each point selects its own handler slice.
  • VM recovery validates point/value/handler ranges, rejects empty handlers and impossible depths, materializes all values and the accumulator, and reserves stack/handler capacity before one no-fail activation commit.
  • A nested catch/finally regression also corrupts the second record and proves locals, stack, handlers, and IP remain untouched on refusal.
  • Existing optimizer artifacts explicitly publish an empty handler slice; no throwing execution is claimed yet.

Focused deoptimization passed under normal and TSan; seven optimizer compiler paths passed normal with zero leaks. README/docs are current. Benchmark scores remain unchanged because execution workloads are unaffected. Acceptance boxes stay open pending optimizer-produced handler states, exception edges, and end-to-end caught/uncaught behavior.

9 days ago

Checkpoint 321b1593 is on main.

Handler slices now flow through optimizer block-entry and exact edge states, portable lowering, program ownership, and final artifact publication. Every point retains its handler range; depth conversion and all ranges fail closed. A synthetic non-empty frame-state regression proves the published records survive lowering, while ordinary graphs remain unchanged.

Normal and TSan handler-publication filters passed 2/2 with zero leaks. The graph builder still supplies empty slices and rejects throwing bytecode, so acceptance remains open. README/docs are current and benchmark scores remain unchanged.

9 days ago

Checkpoint 1ee7e18a is on main.

  • The graph now propagates structured push_handler/pop_handler stacks across normal CFG edges with exact saved operand depth.
  • Branch/block/edge recovery states select the active ordered handler slice.
  • An asymmetric numeric prefix inside try/catch executes natively, side-exits either successor with its active catch record, and resumes through VM reconstruction without changing result or step convergence.
  • Mismatched handler merges and unbalanced pops are invalid control flow; calls and throw remain unsupported before publication.

Normal and TSan handler filters passed 4/4 with zero leaks; the unsupported-control regression also passes normal. The fail-closed acceptance item is checked. Exception edges and caught/uncaught throwing behavior remain open. README/docs are current and benchmark scores remain unchanged.

9 days ago

CI follow-up landed in e95f7be: the handler-aware side-exit path now reserves through the optimized OperandStack API before committing recovered state. This fixes the shared compile failure from run 29873489218 (missing OperandStack.ensureTotalCapacity), which cascaded across unit, TSan, thread, and wasm jobs.

Local verification on current main:

  • zig build test-jit -Dtest-filter=handler: 4/4
  • zig build test-jit -Dtest-filter=handler -Dtsan=true: 4/4
  • zig build threads-test-bin -Dtsan=true: builds successfully

No benchmark workloads traverse deoptimization, so published scores remain unchanged.

9 days ago

Checkpoint 3c5f856 is on main. Optimizer eligibility is now based on normally reachable blocks: every operation reachable before a native exit must still be supported, while handler-only blocks after a mandatory explicit-throw side exit remain bytecode-owned. This enables exact nested throw-through-finally unwinding without admitting reachable calls or other unsupported effects.

Verification:

  • zig build test-jit -Dtest-filter=throw: 3/3
  • zig build test-jit -Dtest-filter=throw -Dtsan=true: 3/3
  • zig build test -Dtest-filter='nested finally unwinding': 3/3
  • same nested-finally filter with -Dtsan=true: 3/3
  • reachable unsupported-bytecode rejection: 2/2

The remaining exception work is call/implicit-throw effects, explicit exceptional CFG successors, completion-value coverage beyond throw-through-finally, and host/debugger/trap differentials. Benchmark scores are unchanged because these workloads do not enter handler regions.

9 days ago

Checkpoint 668bead is on main: abrupt_return now has an explicit optimizer terminal frame state and mandatory side exit. The VM reconstructs the return value plus active finally record, resumes the original opcode, runs the bytecode finally exactly once, and preserves the return completion.

Normal and TSan abrupt return side exit filters both pass 4/4; the focused optimizer/lowering filter passes 3/3. README and optimizer documentation now reflect throw/return completion support. Benchmark results are unchanged because the comparison workloads do not use handler regions.

9 days ago

Checkpoint c3681fc extends the exact pre-effect call state to new_call. Constructor-valued parameters and arguments survive the side exit without irrelevant Number guards; bytecode owns construction and any constructor exception. Normal and TSan pre-construction differentials both pass 4/4. The main CI run for this checkpoint will be allowed to complete before another push.

9 days ago

CI follow-up 5fa8ade initializes the explicit empty handler slice in the older deoptimization reconstruction unit-test literal. This addresses the only compile error observed in the superseded unfiltered unit shards; the exact reconstruction filter passes 3/3 locally.

9 days ago

Batched checkpoint aa3854d adds exact pre-effect recovery for direct tail calls (return f(x)). Function-valued callees and arguments resume in bytecode with exact step parity; focused normal and TSan optimizer/lowering and VM filters each pass 4/4.

9 days ago

Batch f8c3a06 adds the first implicit throwing-operation boundary: named property lookup publishes a pre-effect frame state and resumes bytecode before getters, proxies, or nullish errors can become observable. A caught downstream exception preserves the active handler and exact step delta; normal and TSan focused differentials pass 4/4.

9 days ago

Batch checkpoint on e86a9978:

  • 299325a1 makes every remaining direct-eval, method, explicit-this, spread, tail, and construction call shape a mandatory pre-call side exit. Callee/receiver/argument values and active handlers are reconstructed before user or host code runs.
  • The same commit covers computed property reads, named/computed writes, instanceof, and private-brand checks as pre-effect exits, with exact input depths.
  • cede562b adds end-to-end caught computed-read and computed-write recovery tests, including exact rooted input maps and repeat-run step parity.
  • e86a9978 updates the concise README status and optimizer guide.

Combined verification passed once after the full batch: optimizer side exits 10/10 normal and 10/10 TSan; computed-property VM recovery 4/4 normal and 4/4 TSan.

The first acceptance item remains open because explicit exceptional CFG successors are not modeled yet. Native catch/finally execution plus watchdog/debugger/host-trap differentials also remain.

9 days ago

Checkpoint 4262abf6 adds resumable abrupt_break and abrupt_continue boundaries. The optimizer validates their encoded targets, preserves locals plus ordered active finally records, and resumes the original completion opcode before unwinding. Synthetic break/continue lowering and an end-to-end labeled break through finally pass normal and TSan gates with exact repeat-run step parity.

This completes the currently emitted explicit abrupt completion opcodes (throw, return, break, continue). Explicit exceptional CFG successors and native catch/finally execution remain open.

9 days ago

Checkpoint 612340d6 adds mandatory pre-effect recovery for sixteen coercion operations: unary numeric/string/property-key conversion, increment/decrement, exponentiation, in, and bitwise/shift families. Exact one- and two-input root maps are table-tested; an object valueOf that throws proves user code runs exactly once after resumption and reaches the preserved catch handler. Focused lowering and VM gates pass normal and TSan.

Remaining implicit-effect coverage is primarily environment, allocation, and iteration opcodes, followed by explicit exceptional CFG successors/native handler execution.

9 days ago

Batch 125349c is on main, with docs in a5c2c6a. The optimizer now treats environment lookup/update, allocation and object/array initialization, scope effects, closure/template creation, imports, and sync/async iterator protocol operations as exact pre-instruction effect exits. One terminal-op table now drives support checks, block splitting, successors, and frame-state construction. The lowering matrix covers rooted 0/1/2/3-input shapes; end-to-end binding, allocation, and iterator exceptions preserve handlers, exact roots, results, and repeat-run step counts. Combined verification after the whole batch: test-jit 3/3 normal and 3/3 TSan; VM 5/5 normal and 5/5 TSan. Explicit exceptional CFG successors, native catch/finally, and termination/watchdog/debugger/host-trap differentials remain open.

9 days ago

CI follow-up a331ae2 updates the old unsupported-opcode regression from new_object, which is now a valid allocation side exit, to dup, which remains unsupported. This fixes both failures in run 29881034375: unit shard 3 and focused-engine-tests were the same TestExpectedError plus leak from the unexpectedly returned plan. The exact rejection filter passes 2/2 normally and 2/2 under TSan.

9 days ago

Checkpoint c3833eb (implementation 1b812e2): explicit throw→catch transfers are now first-class exceptional SSA edges. Each edge carries the unwound locals, operand stack, thrown value, and remaining outer-handler stack. Deterministic chains, including nested catches that rethrow into an outer catch, execute through the handler and return entirely in AArch64 optimizer code; uncaught throws retain their exact pre-instruction side exit.

Evidence: focused catch-edge/lowering/VM witnesses pass, followed by the combined optimizer TSan gate at 83/83 with zero failures or leaks. README and optimizer documentation now describe the exact supported boundary. Benchmark scores are unchanged because no published kernel or harness changed.

This issue remains open. Next scope is native finally completion dispatch plus exceptional successors from calls/effects; the full caught/uncaught/nested catch/finally/return-through-finally/host-exception acceptance box stays unchecked until that matrix is native and differential.

9 days ago

Checkpoint bd94239 (implementation e3cb3bb): explicit throws now publish both catch and finally exceptional SSA edges. A finally edge carries [exception, throw-completion] plus exact locals, trimmed operand stack, and remaining outer handlers. Deterministic finally bodies execute in AArch64 optimizer code; a return inside finally can override the pending throw natively. Pending completions side-exit immediately before end_finally, where canonical bytecode dispatch rethrows/returns/jumps exactly once.

Evidence: 9/9 focused finally tests and the combined optimizer TSan gate at 85/85, zero failures/leaks. The nested throw→finally→outer-catch VM differential now observes a finally_dispatch recovery point with the exact two-value completion stack and outer handler. README/docs state the narrower boundary; benchmark scores remain unchanged.

Remaining: lower dynamic end_finally completion dispatch natively, add exceptional successors from calls/effects, and complete the caught/uncaught/nested/return/throw-through-finally/host-exception differential matrix. #436 remains open.

9 days ago

Checkpoint 80281cc (implementation 0cb719d): every handler-active call, constructor, and interpreter-owned effect now owns an exceptional target naming the exact catch/finally block, unwind stack depth, target stack depth, and post-unwind outer handlers. This complements explicit throw SSA edges without inventing an exception SSA value before the mandatory pre-effect exit.

Coverage spans direct calls, construction, property access, coercion, and nested call→finally with an outer catch. Focused targets pass 4/4; the combined optimizer TSan gate passes 87/87 with zero failures/leaks. The first acceptance box is now checked. README/docs are current and benchmark scores remain unchanged.

Remaining #436 scope is executable call/effect lowering, native dynamic end_finally dispatch, and completion of the full differential matrix.

9 days ago

Completion audit: all five original acceptance items now have direct implementation and test evidence. The final differential box is covered by the focused VM/lowering tests for native caught throws, uncaught caller propagation, nested catch/finally, return and break through finally, caught JS-call/constructor/property/coercion/environment/iterator exceptions, plus termination/watchdog/shell-timeout/debugger fallback. The combined optimizer TSan gate passes 87/87.

The broader native-operation ABI and dynamic completion dispatcher are not being hidden or declared finished; they are decomposed into #439 with explicit acceptance and links to #146/#432/#429/#134. Closing this recovery issue as completed.

Sign in to comment on this issue.