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:
2026-04-16 13:23:35 +00:00
parent 684a46297d
commit c9634ba649
5 changed files with 93 additions and 16 deletions

View File

@@ -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