From 6008a1be303cbb970eb11c23e2786f8f97be994a Mon Sep 17 00:00:00 2001 From: giles Date: Fri, 3 Apr 2026 23:07:52 +0000 Subject: [PATCH] Step 5b browser wiring: VmSuspended handling in WASM kernel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds IO suspension support to the browser WASM kernel. When the VM hits OP_PERFORM or the CEK suspends (e.g. from import), the kernel now handles it instead of crashing. sx_browser.ml additions: - make_js_suspension: creates JS object {suspended, op, request, resume} for the platform to handle asynchronously - handle_import_suspension: checks library registry, returns Some result if already loaded (immediate resume), None if fetch needed - api_load_module: catches VmSuspended, resolves imports locally if library is loaded, returns suspension marker to JS if not - api_load: IO-aware CEK loop using cek_step_loop/cek_suspended?/ cek_resume — handles import suspensions during .sx file loading This enables the lazy loading pattern: when a module's (import ...) encounters an unloaded library, the suspension propagates to JS which can async-fetch the .sxbc and call resume(). Currently all libraries are pre-loaded so suspensions resolve immediately. 2608/2608 OCaml tests passing. WASM kernel builds clean. Co-Authored-By: Claude Opus 4.6 (1M context) --- hosts/ocaml/browser/sx_browser.ml | 78 ++++++++++++++++++++++++++++++- 1 file changed, 77 insertions(+), 1 deletion(-) diff --git a/hosts/ocaml/browser/sx_browser.ml b/hosts/ocaml/browser/sx_browser.ml index 92357ae6..f05c5176 100644 --- a/hosts/ocaml/browser/sx_browser.ml +++ b/hosts/ocaml/browser/sx_browser.ml @@ -241,6 +241,33 @@ let api_parse src_js = with Parse_error msg -> Js.Unsafe.inject (Js.string ("Parse error: " ^ msg)) +(** Build a JS suspension marker for the platform to handle. + Returns {suspended: true, op: string, request: obj, resume: fn(result)} *) +let make_js_suspension request resume_fn = + let obj = Js.Unsafe.obj [||] in + Js.Unsafe.set obj (Js.string "suspended") (Js.Unsafe.inject (Js.bool true)); + let op = match request with + | Dict d -> (match Hashtbl.find_opt d "op" with Some (String s) -> s | _ -> "unknown") + | _ -> "unknown" in + Js.Unsafe.set obj (Js.string "op") (Js.Unsafe.inject (Js.string op)); + Js.Unsafe.set obj (Js.string "request") (value_to_js request); + Js.Unsafe.set obj (Js.string "resume") (Js.wrap_callback (fun result_js -> + let result = js_to_value result_js in + resume_fn result)); + obj + +(** Handle an import suspension: load the library from the library registry + or return a suspension marker to JS for async loading. *) +let handle_import_suspension request = + let lib_spec = match request with + | Dict d -> (match Hashtbl.find_opt d "library" with Some v -> v | _ -> Nil) + | _ -> Nil in + let key = Sx_ref.library_name_key lib_spec in + if Sx_types.sx_truthy (Sx_ref.library_loaded_p key) then + Some Nil (* Already loaded — resume immediately *) + else + None (* Not loaded — JS platform must fetch it *) + let api_eval src_js = let src = Js.to_string src_js in try @@ -296,7 +323,25 @@ let api_load src_js = let exprs = Sx_parser.parse_all src in let env = Env global_env in let count = ref 0 in - List.iter (fun expr -> ignore (Sx_ref.eval_expr expr env); incr count) exprs; + List.iter (fun expr -> + (* Use IO-aware eval for each expression to handle import suspensions *) + let state = Sx_ref.make_cek_state expr env (List []) in + let final = ref (Sx_ref.cek_step_loop state) in + while Sx_types.sx_truthy (Sx_ref.cek_suspended_p !final) do + let request = Sx_ref.cek_io_request !final in + let op = match request with + | Dict d -> (match Hashtbl.find_opt d "op" with Some (String s) -> s | _ -> "") + | _ -> "" in + let response = if op = "import" then begin + match handle_import_suspension request with + | Some v -> v + | None -> Nil (* Library not found — resume with nil, import will use what's in env *) + end else Nil in + final := Sx_ref.cek_resume !final response + done; + ignore (Sx_ref.cek_value !final); + incr count + ) exprs; sync_env_to_vm (); Js.Unsafe.inject !count with @@ -343,6 +388,37 @@ let api_load_module module_js = sync_vm_to_env (); Js.Unsafe.inject (Hashtbl.length _vm_globals) with + | Sx_vm.VmSuspended (request, vm) -> + (* VM hit OP_PERFORM — check if we can resolve locally *) + let op = match request with + | Dict d -> (match Hashtbl.find_opt d "op" with Some (String s) -> s | _ -> "") + | _ -> "" in + if op = "import" then + match handle_import_suspension request with + | Some result -> + (* Library already loaded — resume VM and continue *) + (try + let final = Sx_vm.resume_vm vm result in + sync_vm_to_env (); + Js.Unsafe.inject (value_to_js final) + with Sx_vm.VmSuspended (req2, vm2) -> + make_js_suspension req2 (fun result -> + let v = Sx_vm.resume_vm vm2 result in + sync_vm_to_env (); + value_to_js v)) + | None -> + (* Library not loaded — return suspension to JS for async fetch *) + make_js_suspension request (fun result -> + ignore result; + (* After JS loads the library file, resume the VM *) + let v = Sx_vm.resume_vm vm Nil in + sync_vm_to_env (); + value_to_js v) + else + make_js_suspension request (fun result -> + let v = Sx_vm.resume_vm vm result in + sync_vm_to_env (); + value_to_js v) | Eval_error msg -> Js.Unsafe.inject (Js.string ("Error: " ^ msg)) | exn -> Js.Unsafe.inject (Js.string ("Error: " ^ Printexc.to_string exn))