VM: fix nested IO suspension frame corruption, island hydration preload
VM frame merging bug: call_closure_reuse now saves caller continuations on a reuse_stack instead of merging frames. resume_vm restores them in innermost-first order. Fixes frame count corruption when nested closures suspend via OP_PERFORM. Zero test regressions (3924/3924). Island hydration: hydrate-island now looks up components from (global-env) instead of render-env, triggering the symbol resolve hook. Added JS-level preload-island-defs that scans DOM for data-sx-island and loads definitions from the content-addressed manifest BEFORE hydration — avoids K.load reentrancy when the resolve hook fires inside env_get. loadDefinitionByHash: fixed isMultiDefine check — defcomp/defisland bodies containing nested (define ...) forms no longer suppress name insertion. Added K.load return value checking for silent error string returns. sx_browser.ml: resolve hook falls back to global_env.bindings when _vm_globals miss (sync gap). Snapshot reuse_stack alongside pending_cek. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -36,6 +36,7 @@ type vm = {
|
||||
globals : (string, value) Hashtbl.t; (* live reference to kernel env *)
|
||||
mutable pending_cek : value option; (* suspended CEK state from Component/Lambda call *)
|
||||
mutable handler_stack : handler_entry list; (* exception handler stack *)
|
||||
mutable reuse_stack : (frame list * int) list; (* saved call_closure_reuse continuations *)
|
||||
}
|
||||
|
||||
(** Raised when OP_PERFORM is executed. Carries the IO request dict
|
||||
@@ -74,7 +75,7 @@ let is_jit_failed cl = cl.vm_code.vc_arity = -1
|
||||
let _active_vm : vm option ref = ref None
|
||||
|
||||
let create globals =
|
||||
{ stack = Array.make 4096 Nil; sp = 0; frames = []; globals; pending_cek = None; handler_stack = [] }
|
||||
{ stack = Array.make 4096 Nil; sp = 0; frames = []; globals; pending_cek = None; handler_stack = []; reuse_stack = [] }
|
||||
|
||||
(** Stack ops — inlined for speed. *)
|
||||
let push vm v =
|
||||
@@ -313,10 +314,11 @@ and call_closure_reuse cl args =
|
||||
(try run vm
|
||||
with
|
||||
| VmSuspended _ as e ->
|
||||
(* IO suspension: merge remaining callback frames with caller frames
|
||||
so the VM can be properly resumed. When resumed, it finishes the
|
||||
callback then returns to the caller's frames. *)
|
||||
vm.frames <- vm.frames @ saved_frames;
|
||||
(* IO suspension: save the caller's continuation on the reuse stack.
|
||||
DON'T merge frames — that corrupts the frame chain with nested
|
||||
closures. On resume, restore_reuse in resume_vm processes these
|
||||
in innermost-first order after the callback finishes. *)
|
||||
vm.reuse_stack <- (saved_frames, saved_sp) :: vm.reuse_stack;
|
||||
raise e
|
||||
| e ->
|
||||
vm.frames <- saved_frames;
|
||||
@@ -831,7 +833,10 @@ and run vm =
|
||||
done
|
||||
|
||||
(** Resume a suspended VM by pushing the IO result and continuing.
|
||||
May raise VmSuspended again if the VM hits another OP_PERFORM. *)
|
||||
May raise VmSuspended again if the VM hits another OP_PERFORM.
|
||||
|
||||
After the callback finishes, restores any call_closure_reuse
|
||||
continuations saved on vm.reuse_stack (innermost first). *)
|
||||
let resume_vm vm result =
|
||||
(match vm.pending_cek with
|
||||
| Some cek_state ->
|
||||
@@ -846,6 +851,32 @@ let resume_vm vm result =
|
||||
| None ->
|
||||
push vm result);
|
||||
run vm;
|
||||
(* Restore call_closure_reuse continuations saved during suspension.
|
||||
reuse_stack is in catch order (outermost first from prepend) —
|
||||
reverse to get innermost first, matching callback→caller unwinding. *)
|
||||
let rec restore_reuse pending =
|
||||
match pending with
|
||||
| [] -> ()
|
||||
| (saved_frames, _saved_sp) :: rest ->
|
||||
let callback_result = pop vm in
|
||||
vm.frames <- saved_frames;
|
||||
push vm callback_result;
|
||||
(try
|
||||
run vm;
|
||||
(* Check for new reuse entries added by nested call_closure_reuse *)
|
||||
let new_pending = List.rev vm.reuse_stack in
|
||||
vm.reuse_stack <- [];
|
||||
restore_reuse (new_pending @ rest)
|
||||
with VmSuspended _ as e ->
|
||||
(* Re-suspension: save unprocessed entries back for next resume.
|
||||
rest is innermost-first; vm.reuse_stack is outermost-first.
|
||||
Combine so next resume's reversal yields: new_inner, old_inner→outer. *)
|
||||
vm.reuse_stack <- (List.rev rest) @ vm.reuse_stack;
|
||||
raise e)
|
||||
in
|
||||
let pending = List.rev vm.reuse_stack in
|
||||
vm.reuse_stack <- [];
|
||||
restore_reuse pending;
|
||||
pop vm
|
||||
|
||||
(** Execute a compiled module (top-level bytecode). *)
|
||||
@@ -967,18 +998,20 @@ 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. *)
|
||||
(* Snapshot pending_cek and reuse_stack NOW — a nested cek_call_or_suspend
|
||||
on the same VM may overwrite them before our resume function is called. *)
|
||||
let saved_cek = vm.pending_cek in
|
||||
let saved_reuse = vm.reuse_stack 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. *)
|
||||
(* Restore saved state before resuming — may have been overwritten
|
||||
by a nested suspension on the same VM. *)
|
||||
vm.pending_cek <- saved_cek;
|
||||
vm.reuse_stack <- saved_reuse;
|
||||
(try resume_vm vm result
|
||||
with exn2 ->
|
||||
match !_vm_suspension_to_dict exn2 with
|
||||
|
||||
Reference in New Issue
Block a user