Fix bytecode when/do/perform: snapshot pending_cek in resume closure

Root cause: nested cek_call_or_suspend calls on the same VM (from
synchronous callbacks like dom-listen firing handler immediately)
overwrote pending_cek before the first resume ran.

Fix: _vm_suspension_to_dict snapshots pending_cek at capture time
and restores it in the resume closure before calling resume_vm.
This ensures each suspension's CEK state is preserved regardless
of nested overwrite.

test_bytecode_repeat.js: 4/4 pass (was 3/4).
Source: 6 suspensions ✓  Bytecode: 6 suspensions ✓

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-08 21:34:10 +00:00
parent 67d2f32512
commit 6456bd927a

View File

@@ -814,12 +814,10 @@ and run vm =
let resume_vm vm result =
(match vm.pending_cek with
| Some cek_state ->
(* Resume the suspended CEK evaluation first *)
vm.pending_cek <- None;
let final = Sx_ref.cek_resume cek_state result in
(match Sx_runtime.get_val final (String "phase") with
| String "io-suspended" ->
(* CEK suspended again — re-suspend the VM *)
vm.pending_cek <- Some final;
raise (VmSuspended (Sx_runtime.get_val final (String "request"), vm))
| _ ->
@@ -948,12 +946,18 @@ let () = _vm_call_closure_ref := (fun cl args -> call_closure_reuse cl args)
let () = _vm_suspension_to_dict := (fun exn ->
match exn with
| VmSuspended (request, vm) ->
(* Snapshot pending_cek NOW — a nested cek_call_or_suspend on the same VM
may overwrite it before our resume function is called. *)
let saved_cek = vm.pending_cek in
let d = Hashtbl.create 3 in
Hashtbl.replace d "__vm_suspended" (Bool true);
Hashtbl.replace d "request" request;
Hashtbl.replace d "resume" (NativeFn ("vm-resume", fun args ->
match args with
| [result] ->
(* Restore the saved pending_cek before resuming — it may have been
overwritten by a nested suspension on the same VM. *)
vm.pending_cek <- saved_cek;
(try resume_vm vm result
with exn2 ->
match !_vm_suspension_to_dict exn2 with