VM import suspension for browser lazy loading
Bytecode compiler now emits OP_PERFORM for (import ...) and compiles
(define-library ...) bodies. The VM stores the import request in
globals["__io_request"] and stops the run loop — no exceptions needed.
vm-execute-module returns a suspension dict, vm-resume-module continues.
Browser: sx_browser.ml detects suspension dicts from execute_module and
returns JS {suspended, op, request, resume} objects. The sx-platform.js
while loop handles cascading suspensions via handleImportSuspension.
13 modules load via .sxbc bytecode in 226ms (manifest-driven), both
islands hydrate, all handlers wired. 2650/2650 tests pass including
6 new vm-import-suspension tests.
Also: consolidated sx-platform-2.js → sx-platform.js, fixed
vm-execute-module missing code-from-value call, fixed bootstrap.py
protocol registry transpiler issues.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -26,7 +26,7 @@ cp dist/sx_browser.bc.wasm.js "$DEST/"
|
||||
cp dist/sx_browser.bc.js "$DEST/"
|
||||
rm -rf "$DEST/sx_browser.bc.wasm.assets"
|
||||
cp -r dist/sx_browser.bc.wasm.assets "$DEST/"
|
||||
cp dist/sx-platform.js "$DEST/sx-platform-2.js"
|
||||
cp dist/sx-platform.js "$DEST/sx-platform.js"
|
||||
cp dist/sx/*.sx "$DEST/sx/"
|
||||
cp dist/sx/*.sxbc "$DEST/sx/" 2>/dev/null || true
|
||||
# Keep assets dir for Node.js WASM tests
|
||||
|
||||
@@ -70,37 +70,26 @@ for (const file of FILES) {
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Strip define-library/import wrappers for bytecode compilation.
|
||||
// Strip define-library wrapper for bytecode compilation.
|
||||
//
|
||||
// The VM's execute_module doesn't handle define-library or import — they're
|
||||
// CEK special forms. So the compiled bytecode should contain just the body
|
||||
// defines. The eval-blob phase (above) already handled library registration
|
||||
// via CEK. The JS loader pre-resolves deps, so no registry needed at runtime.
|
||||
// Keeps (import ...) forms — the compiler emits OP_PERFORM for these, enabling
|
||||
// lazy loading: when the VM hits an import for an unloaded library, it suspends
|
||||
// to the JS platform which fetches the library on demand.
|
||||
//
|
||||
// Strips define-library header (name, export) and (begin ...) wrapper, leaving
|
||||
// the body defines + import instructions as top-level forms.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function stripLibraryWrapper(source) {
|
||||
// Line-based stripping: remove (import ...), unwrap (define-library ... (begin BODY)).
|
||||
// Works with both pre-existing and newly-wrapped formats.
|
||||
// Line-based stripping: unwrap (define-library ... (begin BODY)), keep (import ...).
|
||||
const lines = source.split('\n');
|
||||
const result = [];
|
||||
let skip = false; // inside header region (define-library, export)
|
||||
let importDepth = 0; // tracking multi-line import paren depth
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
const trimmed = line.trim();
|
||||
|
||||
// Skip (import ...) — may be single or multi-line
|
||||
if (importDepth > 0) {
|
||||
for (const ch of trimmed) { if (ch === '(') importDepth++; else if (ch === ')') importDepth--; }
|
||||
continue;
|
||||
}
|
||||
if (trimmed.startsWith('(import ')) {
|
||||
importDepth = 0;
|
||||
for (const ch of trimmed) { if (ch === '(') importDepth++; else if (ch === ')') importDepth--; }
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip (define-library ...) header lines until (begin
|
||||
if (trimmed.startsWith('(define-library ')) { skip = true; continue; }
|
||||
if (skip && trimmed.startsWith('(export')) { continue; }
|
||||
|
||||
@@ -243,7 +243,7 @@ let api_parse src_js =
|
||||
|
||||
(** 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 _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
|
||||
@@ -380,44 +380,36 @@ let sync_vm_to_env () =
|
||||
end
|
||||
) _vm_globals
|
||||
|
||||
(** Recursive suspension handler: resumes VM, catches further suspensions,
|
||||
resolves imports locally when possible, otherwise returns JS suspension
|
||||
objects that the platform's while loop can process. *)
|
||||
let rec resume_with_suspensions vm result =
|
||||
try
|
||||
let v = Sx_vm.resume_vm vm result in
|
||||
(** Convert a VM suspension dict to a JS suspension object for the platform. *)
|
||||
let rec make_js_import_suspension (d : (string, value) Hashtbl.t) =
|
||||
let obj = Js.Unsafe.obj [||] in
|
||||
Js.Unsafe.set obj (Js.string "suspended") (Js.Unsafe.inject Js._true);
|
||||
Js.Unsafe.set obj (Js.string "op") (Js.Unsafe.inject (Js.string "import"));
|
||||
let request = match Hashtbl.find_opt d "request" with Some v -> v | None -> Nil in
|
||||
Js.Unsafe.set obj (Js.string "request") (value_to_js request);
|
||||
(* resume callback: clears __io_request, pushes nil, re-runs VM *)
|
||||
Js.Unsafe.set obj (Js.string "resume") (Js.wrap_callback (fun _result_js ->
|
||||
let resumed = Sx_vm_ref.resume_module (Dict d) in
|
||||
sync_vm_to_env ();
|
||||
value_to_js v
|
||||
with Sx_vm.VmSuspended (request, vm2) ->
|
||||
handle_suspension request vm2
|
||||
|
||||
and handle_suspension request vm =
|
||||
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 and handle further suspensions *)
|
||||
resume_with_suspensions vm result
|
||||
| None ->
|
||||
(* Library not loaded — return suspension to JS for async fetch *)
|
||||
Js.Unsafe.inject (make_js_suspension request (fun _result ->
|
||||
resume_with_suspensions vm Nil))
|
||||
else
|
||||
Js.Unsafe.inject (make_js_suspension request (fun result ->
|
||||
resume_with_suspensions vm result))
|
||||
match resumed with
|
||||
| Dict d2 when (match Hashtbl.find_opt d2 "suspended" with Some (Bool true) -> true | _ -> false) ->
|
||||
Js.Unsafe.inject (make_js_import_suspension d2)
|
||||
| result -> value_to_js result));
|
||||
obj
|
||||
|
||||
let api_load_module module_js =
|
||||
try
|
||||
let code_val = js_to_value module_js in
|
||||
let code = Sx_vm.code_from_value code_val in
|
||||
let _result = Sx_vm_ref.execute_module code _vm_globals in
|
||||
sync_vm_to_env ();
|
||||
Js.Unsafe.inject (Hashtbl.length _vm_globals)
|
||||
let result = Sx_vm_ref.execute_module code _vm_globals in
|
||||
match result with
|
||||
| Dict d when (match Hashtbl.find_opt d "suspended" with Some (Bool true) -> true | _ -> false) ->
|
||||
(* VM suspended on OP_PERFORM (import) — return JS suspension object *)
|
||||
Js.Unsafe.inject (make_js_import_suspension d)
|
||||
| _ ->
|
||||
sync_vm_to_env ();
|
||||
Js.Unsafe.inject (Hashtbl.length _vm_globals)
|
||||
with
|
||||
| Sx_vm.VmSuspended (request, vm) ->
|
||||
handle_suspension request vm
|
||||
| Eval_error msg -> Js.Unsafe.inject (Js.string ("Error: " ^ msg))
|
||||
| exn -> Js.Unsafe.inject (Js.string ("Error: " ^ Printexc.to_string exn))
|
||||
|
||||
@@ -628,7 +620,7 @@ let () =
|
||||
in
|
||||
let module_val = convert_code code_form in
|
||||
let code = Sx_vm.code_from_value module_val in
|
||||
let _result = Sx_vm_ref.execute_module code _vm_globals in
|
||||
let _result = Sx_vm.execute_module code _vm_globals in
|
||||
sync_vm_to_env ();
|
||||
Number (float_of_int (Hashtbl.length _vm_globals))
|
||||
| _ -> raise (Eval_error "load-sxbc: expected (sxbc version hash (code ...))"));
|
||||
|
||||
@@ -95,7 +95,7 @@ node -e '
|
||||
var K = globalThis.SxKernel;
|
||||
if (!K) { console.error("FAIL: SxKernel not found"); process.exit(1); }
|
||||
|
||||
// --- Register 8 FFI host primitives (normally done by sx-platform-2.js) ---
|
||||
// --- Register 8 FFI host primitives (normally done by sx-platform.js) ---
|
||||
K.registerNative("host-global", function(args) {
|
||||
var name = args[0];
|
||||
if (typeof globalThis !== "undefined" && name in globalThis) return globalThis[name];
|
||||
@@ -195,7 +195,7 @@ node -e '
|
||||
assert("is-html-tag? fake", K.eval("(is-html-tag? \"fake\")"), false);
|
||||
|
||||
// =====================================================================
|
||||
// Load web stack modules (same as sx-platform-2.js loadWebStack)
|
||||
// Load web stack modules (same as sx-platform.js loadWebStack)
|
||||
// =====================================================================
|
||||
var fs = require("fs");
|
||||
var webStackFiles = [
|
||||
|
||||
Reference in New Issue
Block a user