Sandbox bytecode loading: K.load + load-sxbc, bytecode param, web stack sxbc via loadModule
Bytecode modules now load correctly in sandbox mode. HS .sxbc modules
use K.load('(load-sxbc ...)') which syncs defines to eval env. Web stack
.sxbc modules use K.loadModule with import suspension drive loop.
K.eval used directly for expression eval (not thunk wrapper) so bytecode-
defined symbols are visible. Falls back to callFn thunk on IO suspension.
Sandbox now reproduces the bytecode repeat bug: source gives 6/6
suspensions, bytecode gives 4/6. Bug is in bytecode compilation of
when/do across perform boundaries, not the runtime wrapper.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -107,7 +107,9 @@ let register_mcp_jit_hook () =
|
|||||||
if !(Sx_vm._jit_compiling) then None
|
if !(Sx_vm._jit_compiling) then None
|
||||||
else
|
else
|
||||||
(try Some (Sx_vm.call_closure_reuse cl args)
|
(try Some (Sx_vm.call_closure_reuse cl args)
|
||||||
with e ->
|
with
|
||||||
|
| Sx_vm.VmSuspended _ as e -> raise e (* let Fix 1 in jit_try_call handle it *)
|
||||||
|
| e ->
|
||||||
if not (Hashtbl.mem _jit_warned fn_name) then begin
|
if not (Hashtbl.mem _jit_warned fn_name) then begin
|
||||||
Hashtbl.replace _jit_warned fn_name true;
|
Hashtbl.replace _jit_warned fn_name true;
|
||||||
Printf.eprintf "[mcp-jit] %s runtime fallback to CEK: %s\n%!" fn_name (Printexc.to_string e)
|
Printf.eprintf "[mcp-jit] %s runtime fallback to CEK: %s\n%!" fn_name (Printexc.to_string e)
|
||||||
@@ -124,7 +126,9 @@ let register_mcp_jit_hook () =
|
|||||||
| Some cl ->
|
| Some cl ->
|
||||||
l.l_compiled <- Some cl;
|
l.l_compiled <- Some cl;
|
||||||
(try Some (Sx_vm.call_closure_reuse cl args)
|
(try Some (Sx_vm.call_closure_reuse cl args)
|
||||||
with e ->
|
with
|
||||||
|
| Sx_vm.VmSuspended _ as e -> raise e
|
||||||
|
| e ->
|
||||||
Printf.eprintf "[mcp-jit] %s first-call fallback: %s\n%!" fn_name (Printexc.to_string e);
|
Printf.eprintf "[mcp-jit] %s first-call fallback: %s\n%!" fn_name (Printexc.to_string e);
|
||||||
Hashtbl.replace _jit_warned fn_name true;
|
Hashtbl.replace _jit_warned fn_name true;
|
||||||
l.l_compiled <- Some Sx_vm.jit_failed_sentinel;
|
l.l_compiled <- Some Sx_vm.jit_failed_sentinel;
|
||||||
@@ -1572,6 +1576,10 @@ let handle_sx_playwright args =
|
|||||||
let island = args |> member "island" |> to_string_option in
|
let island = args |> member "island" |> to_string_option in
|
||||||
let phase = args |> member "phase" |> to_string_option in
|
let phase = args |> member "phase" |> to_string_option in
|
||||||
let filter = args |> member "filter" |> to_string_option in
|
let filter = args |> member "filter" |> to_string_option in
|
||||||
|
let setup = args |> member "setup" |> to_string_option in
|
||||||
|
let stack = args |> member "stack" |> to_string_option in
|
||||||
|
let bytecode = try args |> member "bytecode" |> to_bool with _ -> false in
|
||||||
|
let files_json = try args |> member "files" with _ -> `Null in
|
||||||
(* Determine whether to run specs or the inspector *)
|
(* Determine whether to run specs or the inspector *)
|
||||||
let use_inspector = match mode with
|
let use_inspector = match mode with
|
||||||
| Some m when m <> "run" -> true
|
| Some m when m <> "run" -> true
|
||||||
@@ -1631,6 +1639,12 @@ let handle_sx_playwright args =
|
|||||||
(match island with Some i -> Some ("island", `String i) | None -> None);
|
(match island with Some i -> Some ("island", `String i) | None -> None);
|
||||||
(match phase with Some p -> Some ("phase", `String p) | None -> None);
|
(match phase with Some p -> Some ("phase", `String p) | None -> None);
|
||||||
(match filter with Some f -> Some ("filter", `String f) | None -> None);
|
(match filter with Some f -> Some ("filter", `String f) | None -> None);
|
||||||
|
(match setup with Some s -> Some ("setup", `String s) | None -> None);
|
||||||
|
(match stack with Some s -> Some ("stack", `String s) | None -> None);
|
||||||
|
(if bytecode then Some ("bytecode", `Bool true) else None);
|
||||||
|
(match files_json with
|
||||||
|
| `List items -> Some ("files", `List (List.map (fun j -> `String (Yojson.Safe.Util.to_string j)) items))
|
||||||
|
| _ -> None);
|
||||||
]) in
|
]) in
|
||||||
let args_json = Yojson.Basic.to_string inspector_args in
|
let args_json = Yojson.Basic.to_string inspector_args in
|
||||||
(* Single-quote shell wrapping — escape any literal single quotes in JSON *)
|
(* Single-quote shell wrapping — escape any literal single quotes in JSON *)
|
||||||
@@ -1690,10 +1704,26 @@ let handle_sx_harness_eval args =
|
|||||||
let session = Sx_ref.cek_call (env_get e "make-harness") mock_arg in
|
let session = Sx_ref.cek_call (env_get e "make-harness") mock_arg in
|
||||||
(* Install interceptors *)
|
(* Install interceptors *)
|
||||||
ignore (call_sx "install-interceptors" [session; Env e]);
|
ignore (call_sx "install-interceptors" [session; Env e]);
|
||||||
|
(* IO-aware eval: drives cek_step_loop + cek_resume to handle perform *)
|
||||||
|
let io_log = ref [] in
|
||||||
|
let eval_with_io expr =
|
||||||
|
let state = Sx_ref.make_cek_state expr (Env e) (List []) in
|
||||||
|
let rec drive st =
|
||||||
|
let final = Sx_ref.cek_step_loop st in
|
||||||
|
match Sx_ref.cek_suspended_p final with
|
||||||
|
| Bool true ->
|
||||||
|
let request = Sx_runtime.get_val final (String "request") in
|
||||||
|
io_log := request :: !io_log;
|
||||||
|
let resumed = Sx_ref.cek_resume final Nil in
|
||||||
|
drive resumed
|
||||||
|
| _ -> Sx_ref.cek_value final
|
||||||
|
in
|
||||||
|
drive state
|
||||||
|
in
|
||||||
(* Evaluate the expression *)
|
(* Evaluate the expression *)
|
||||||
let exprs = Sx_parser.parse_all expr_str in
|
let exprs = Sx_parser.parse_all expr_str in
|
||||||
let result = List.fold_left (fun _acc expr ->
|
let result = List.fold_left (fun _acc expr ->
|
||||||
try Sx_ref.eval_expr expr (Env e)
|
try eval_with_io expr
|
||||||
with exn -> String (Printf.sprintf "Error: %s" (Printexc.to_string exn))
|
with exn -> String (Printf.sprintf "Error: %s" (Printexc.to_string exn))
|
||||||
) Nil exprs in
|
) Nil exprs in
|
||||||
(* Get the IO log *)
|
(* Get the IO log *)
|
||||||
@@ -1705,7 +1735,13 @@ let handle_sx_harness_eval args =
|
|||||||
let args_val = call_sx "get" [entry; String "args"] in
|
let args_val = call_sx "get" [entry; String "args"] in
|
||||||
Printf.sprintf " %s(%s)" op (Sx_types.inspect args_val)
|
Printf.sprintf " %s(%s)" op (Sx_types.inspect args_val)
|
||||||
) items)
|
) items)
|
||||||
| _ -> "\n\n(no IO calls)"
|
| _ ->
|
||||||
|
if !io_log <> [] then
|
||||||
|
"\n\nIO suspensions (" ^ string_of_int (List.length !io_log) ^ "):\n" ^
|
||||||
|
String.concat "\n" (List.rev_map (fun req ->
|
||||||
|
" (perform " ^ Sx_types.inspect req ^ ")"
|
||||||
|
) !io_log)
|
||||||
|
else "\n\n(no IO calls)"
|
||||||
in
|
in
|
||||||
let warn_str = if !warnings = [] then "" else
|
let warn_str = if !warnings = [] then "" else
|
||||||
"\n\nWarnings:\n" ^ String.concat "\n" (List.rev !warnings)
|
"\n\nWarnings:\n" ^ String.concat "\n" (List.rev !warnings)
|
||||||
@@ -2767,14 +2803,18 @@ let tool_definitions = `List [
|
|||||||
[];
|
[];
|
||||||
tool "sx_playwright" "Run Playwright browser tests or inspect SX pages interactively. Modes: run (spec files), inspect (page/island report with leak detection and handler audit), diff (full SSR vs hydrated DOM), hydrate (lake-focused SSR vs hydrated comparison — detects clobbering), eval (JS expression), interact (action sequence), screenshot, listeners (CDP event listener inspection), trace (click + capture console/network/pushState), cdp (raw CDP command), trace-boot (full console capture during boot — ALL prefixes), hydrate-debug (re-run island hydration with full env/state tracing), eval-at (inject eval at a specific boot phase)."
|
tool "sx_playwright" "Run Playwright browser tests or inspect SX pages interactively. Modes: run (spec files), inspect (page/island report with leak detection and handler audit), diff (full SSR vs hydrated DOM), hydrate (lake-focused SSR vs hydrated comparison — detects clobbering), eval (JS expression), interact (action sequence), screenshot, listeners (CDP event listener inspection), trace (click + capture console/network/pushState), cdp (raw CDP command), trace-boot (full console capture during boot — ALL prefixes), hydrate-debug (re-run island hydration with full env/state tracing), eval-at (inject eval at a specific boot phase)."
|
||||||
[("spec", `Assoc [("type", `String "string"); ("description", `String "Spec file to run (run mode). e.g. stepper.spec.js")]);
|
[("spec", `Assoc [("type", `String "string"); ("description", `String "Spec file to run (run mode). e.g. stepper.spec.js")]);
|
||||||
("mode", `Assoc [("type", `String "string"); ("description", `String "Mode: run, inspect, diff, hydrate, eval, interact, screenshot, listeners, trace, cdp, trace-boot, hydrate-debug, eval-at")]);
|
("mode", `Assoc [("type", `String "string"); ("description", `String "Mode: run, inspect, diff, hydrate, eval, interact, screenshot, listeners, trace, cdp, trace-boot, hydrate-debug, eval-at, sandbox (offline WASM kernel — no server needed)")]);
|
||||||
("phase", `Assoc [("type", `String "string"); ("description", `String "Boot phase for eval-at mode: before-modules, after-modules, before-pages, after-pages, before-components, after-components, before-hydrate, after-hydrate, after-boot")]);
|
("phase", `Assoc [("type", `String "string"); ("description", `String "Boot phase for eval-at mode: before-modules, after-modules, before-pages, after-pages, before-components, after-components, before-hydrate, after-hydrate, after-boot")]);
|
||||||
("filter", `Assoc [("type", `String "string"); ("description", `String "Filter prefix for trace-boot mode (e.g. '[sx-platform]')")]);
|
("filter", `Assoc [("type", `String "string"); ("description", `String "Filter prefix for trace-boot mode (e.g. '[sx-platform]')")]);
|
||||||
("url", `Assoc [("type", `String "string"); ("description", `String "URL path to navigate to (default: /)")]);
|
("url", `Assoc [("type", `String "string"); ("description", `String "URL path to navigate to (default: /)")]);
|
||||||
("island", `Assoc [("type", `String "string"); ("description", `String "Filter inspect to a specific island by name (e.g. home/stepper)")]);
|
("island", `Assoc [("type", `String "string"); ("description", `String "Filter inspect to a specific island by name (e.g. home/stepper)")]);
|
||||||
("selector", `Assoc [("type", `String "string"); ("description", `String "CSS selector for screenshot/listeners/trace modes")]);
|
("selector", `Assoc [("type", `String "string"); ("description", `String "CSS selector for screenshot/listeners/trace modes")]);
|
||||||
("expr", `Assoc [("type", `String "string"); ("description", `String "JS expression (eval mode), selector (listeners/trace), or CDP command (cdp mode)")]);
|
("expr", `Assoc [("type", `String "string"); ("description", `String "JS expression (eval mode), selector (listeners/trace), or CDP command (cdp mode)")]);
|
||||||
("actions", `Assoc [("type", `String "string"); ("description", `String "Semicolon-separated action sequence (interact mode). Actions: click:sel, fill:sel:val, wait:ms, text:sel, html:sel, attrs:sel, screenshot, screenshot:sel, count:sel, visible:sel")])]
|
("actions", `Assoc [("type", `String "string"); ("description", `String "Semicolon-separated action sequence (interact mode). Actions: click:sel, fill:sel:val, wait:ms, text:sel, html:sel, attrs:sel, screenshot, screenshot:sel, count:sel, visible:sel")]);
|
||||||
|
("files", `Assoc [("type", `String "array"); ("items", `Assoc [("type", `String "string")]); ("description", `String ".sx files to load (sandbox mode)")]);
|
||||||
|
("setup", `Assoc [("type", `String "string"); ("description", `String "SX setup expression (sandbox mode)")]);
|
||||||
|
("stack", `Assoc [("type", `String "string"); ("description", `String "Module stack for sandbox: core (default), web (full web stack), hs (web + hyperscript), test (web + test framework)")]);
|
||||||
|
("bytecode", `Assoc [("type", `String "boolean"); ("description", `String "Load .sxbc bytecode instead of .sx source in sandbox (default: false)")])]
|
||||||
[];
|
[];
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -1331,7 +1331,7 @@ const SANDBOX_STACKS = {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
async function modeSandbox(page, expr, files, setup, stack) {
|
async function modeSandbox(page, expr, files, setup, stack, bytecode) {
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const PROJECT_ROOT = path.resolve(__dirname, '../..');
|
const PROJECT_ROOT = path.resolve(__dirname, '../..');
|
||||||
@@ -1489,25 +1489,47 @@ async function modeSandbox(page, expr, files, setup, stack) {
|
|||||||
if (window.SxKernel.beginModuleLoad) window.SxKernel.beginModuleLoad();
|
if (window.SxKernel.beginModuleLoad) window.SxKernel.beginModuleLoad();
|
||||||
});
|
});
|
||||||
for (const mod of modules) {
|
for (const mod of modules) {
|
||||||
// Try .sx file in wasm/sx/ dir
|
let loaded = false;
|
||||||
const sxPath = path.join(SX_DIR, mod + '.sx');
|
// Try bytecode first if requested
|
||||||
// Also try lib/hyperscript/ for hs- prefixed modules
|
if (bytecode) {
|
||||||
const libPath = path.join(PROJECT_ROOT, 'lib/hyperscript', mod.replace(/^hs-/, '') + '.sx');
|
const bcPath = path.join(SX_DIR, mod + '.sxbc');
|
||||||
let src;
|
if (fs.existsSync(bcPath)) {
|
||||||
try {
|
const bcSrc = fs.readFileSync(bcPath, 'utf8');
|
||||||
src = fs.existsSync(sxPath)
|
const err = await page.evaluate(src => {
|
||||||
? fs.readFileSync(sxPath, 'utf8')
|
try {
|
||||||
: fs.readFileSync(libPath, 'utf8');
|
const K = window.SxKernel;
|
||||||
} catch(e) {
|
// Use K.load with (load-sxbc ...) wrapper — handles IO suspension in CEK
|
||||||
loadErrors.push(mod + ': file not found');
|
window.__sxbcText = src;
|
||||||
continue;
|
const r = K.load('(load-sxbc (first (parse (host-global "__sxbcText"))))');
|
||||||
|
delete window.__sxbcText;
|
||||||
|
if (typeof r === 'string' && r.startsWith('Error')) return r;
|
||||||
|
return null;
|
||||||
|
} catch(e) { delete window.__sxbcText; return 'CATCH: ' + e.message; }
|
||||||
|
}, bcSrc);
|
||||||
|
if (err) loadErrors.push(mod + '.sxbc: ' + err);
|
||||||
|
else { loaded = true; loadedModules.push(mod + '.sxbc'); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Fall back to .sx source
|
||||||
|
if (!loaded) {
|
||||||
|
const sxPath = path.join(SX_DIR, mod + '.sx');
|
||||||
|
const libPath = path.join(PROJECT_ROOT, 'lib/hyperscript', mod.replace(/^hs-/, '') + '.sx');
|
||||||
|
let src;
|
||||||
|
try {
|
||||||
|
src = fs.existsSync(sxPath)
|
||||||
|
? fs.readFileSync(sxPath, 'utf8')
|
||||||
|
: fs.readFileSync(libPath, 'utf8');
|
||||||
|
} catch(e) {
|
||||||
|
loadErrors.push(mod + ': file not found');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const err = await page.evaluate(src => {
|
||||||
|
try { window.SxKernel.load(src); return null; }
|
||||||
|
catch(e) { return e.message; }
|
||||||
|
}, src);
|
||||||
|
if (err) loadErrors.push(mod + ': ' + err);
|
||||||
|
else loadedModules.push(mod);
|
||||||
}
|
}
|
||||||
const err = await page.evaluate(src => {
|
|
||||||
try { window.SxKernel.load(src); return null; }
|
|
||||||
catch(e) { return e.message; }
|
|
||||||
}, src);
|
|
||||||
if (err) loadErrors.push(mod + ': ' + err);
|
|
||||||
else loadedModules.push(mod);
|
|
||||||
}
|
}
|
||||||
await page.evaluate(() => {
|
await page.evaluate(() => {
|
||||||
if (window.SxKernel.endModuleLoad) window.SxKernel.endModuleLoad();
|
if (window.SxKernel.endModuleLoad) window.SxKernel.endModuleLoad();
|
||||||
@@ -1549,11 +1571,22 @@ async function modeSandbox(page, expr, files, setup, stack) {
|
|||||||
window._asyncPending = 0;
|
window._asyncPending = 0;
|
||||||
let syncResult;
|
let syncResult;
|
||||||
try {
|
try {
|
||||||
K.eval('(define _sandbox_thunk (fn () ' + expr + '))');
|
// First try K.eval directly (handles most expressions)
|
||||||
syncResult = K.callFn(K.eval('_sandbox_thunk'), []);
|
syncResult = K.eval(expr);
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
resolve({ result: 'Error: ' + e.message, ioTrace: window._ioTrace });
|
// If eval fails with IO suspension, retry via callFn thunk
|
||||||
return;
|
if (e.message && e.message.includes('suspension')) {
|
||||||
|
try {
|
||||||
|
K.eval('(define _sandbox_thunk (fn () ' + expr + '))');
|
||||||
|
syncResult = K.callFn(K.eval('_sandbox_thunk'), []);
|
||||||
|
} catch(e2) {
|
||||||
|
resolve({ result: 'Error: ' + e2.message, ioTrace: window._ioTrace });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
resolve({ result: 'Error: ' + e.message, ioTrace: window._ioTrace });
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the thunk itself suspended (direct perform), drive it inline
|
// If the thunk itself suspended (direct perform), drive it inline
|
||||||
@@ -1688,7 +1721,7 @@ async function main() {
|
|||||||
result = await modeEvalAt(browser, url, args.phase || 'before-hydrate', args.expr || '(type-of ~cssx/tw)');
|
result = await modeEvalAt(browser, url, args.phase || 'before-hydrate', args.expr || '(type-of ~cssx/tw)');
|
||||||
break;
|
break;
|
||||||
case 'sandbox':
|
case 'sandbox':
|
||||||
result = await modeSandbox(page, args.expr || '"hello"', args.files || [], args.setup || '', args.stack || '');
|
result = await modeSandbox(page, args.expr || '"hello"', args.files || [], args.setup || '', args.stack || '', !!args.bytecode);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
result = { error: `Unknown mode: ${mode}` };
|
result = { error: `Unknown mode: ${mode}` };
|
||||||
|
|||||||
Reference in New Issue
Block a user