Fix bytecode resume mutation order: isolate VM frames in cek_call_or_suspend

When cek_call_or_suspend runs a CEK machine for a non-bytecoded Lambda
(e.g. a thunk), _active_vm still pointed to the caller's VM. VmClosure
calls inside the CEK (e.g. hs-wait) would merge their frames with the
caller's VM via call_closure_reuse, causing the VM to skip the CEK's
remaining continuation on resume — producing wrong DOM mutation order
(+active, +active, -active instead of +active, -active, +active).

Fix: swap _active_vm with an empty isolation VM before running the CEK,
restore after. This keeps VmClosure calls on their own frame stack while
preserving js_of_ocaml exception identity (Some path, not None).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-08 22:55:26 +00:00
parent 981b6e7560
commit 908f4f80d4
3 changed files with 434 additions and 183 deletions

View File

@@ -270,8 +270,18 @@ let jit_compile_comp ~name ~params ~has_children ~body ~closure globals =
let cek_call_or_suspend vm f args =
incr _vm_cek_count;
let a = match args with Nil -> [] | List l -> l | _ -> [args] in
(* Replace _active_vm with an empty isolation VM so call_closure_reuse
inside the CEK pushes onto an empty frame stack rather than the caller's.
Without this, a VmClosure called from within the CEK (e.g. hs-wait)
merges frames with the caller's VM (e.g. do-repeat), and on resume
the VM skips the CEK's remaining continuation (wrong mutation order).
Using Some(isolation) rather than None keeps the call_closure_reuse
"Some" path which preserves exception identity in js_of_ocaml. *)
let saved_active = !_active_vm in
_active_vm := Some (create vm.globals);
let state = Sx_ref.continue_with_call f (List a) (Env (Sx_types.make_env ())) (List a) (List []) in
let final = Sx_ref.cek_step_loop state in
_active_vm := saved_active;
match Sx_runtime.get_val final (String "phase") with
| String "io-suspended" ->
vm.pending_cek <- Some final;