diff --git a/hosts/ocaml/bin/mcp_tree.ml b/hosts/ocaml/bin/mcp_tree.ml index 416c7913..c65ef062 100644 --- a/hosts/ocaml/bin/mcp_tree.ml +++ b/hosts/ocaml/bin/mcp_tree.ml @@ -583,6 +583,9 @@ let rec handle_tool name args = | "ocaml" -> let abs_project = if Filename.is_relative project_dir then Sys.getcwd () ^ "/" ^ project_dir else project_dir in Printf.sprintf "cd %s/hosts/ocaml && eval $(opam env 2>/dev/null) && dune build 2>&1 && cp _build/default/browser/sx_browser.bc.wasm.js %s/shared/static/wasm/sx_browser.bc.wasm.js && cp _build/default/browser/sx_browser.bc.js %s/shared/static/wasm/sx_browser.bc.js && cp -r _build/default/browser/sx_browser.bc.wasm.assets %s/shared/static/wasm/" abs_project abs_project abs_project abs_project + | "wasm" -> + let abs_project = if Filename.is_relative project_dir then Sys.getcwd () ^ "/" ^ project_dir else project_dir in + Printf.sprintf "cd %s && bash hosts/ocaml/browser/build-all.sh 2>&1" abs_project | "js" | _ -> let extra = if full then " --extensions continuations --spec-modules types" else "" in Printf.sprintf "cd %s && python3 hosts/javascript/cli.py%s --output shared/static/scripts/sx-browser.js 2>&1" project_dir extra @@ -1212,6 +1215,8 @@ let rec handle_tool name args = let expr = args |> member "expr" |> to_string_option in let actions = args |> member "actions" |> to_string_option in let island = args |> member "island" |> to_string_option in + let phase = args |> member "phase" |> to_string_option in + let filter = args |> member "filter" |> to_string_option in (* Determine whether to run specs or the inspector *) let use_inspector = match mode with | Some m when m <> "run" -> true @@ -1272,6 +1277,8 @@ let rec handle_tool name args = (match expr with Some e -> Some ("expr", `String e) | None -> None); (match actions with Some a -> Some ("actions", `String a) | 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 filter with Some f -> Some ("filter", `String f) | None -> None); ]) in let args_json = Yojson.Basic.to_string inspector_args in (* Single-quote shell wrapping — escape any literal single quotes in JSON *) @@ -1984,8 +1991,8 @@ let tool_definitions = `List [ [("file", `Assoc [("type", `String "string"); ("description", `String "Optional .sx file to load for macro/component definitions")]); ("expr", `Assoc [("type", `String "string"); ("description", `String "Expression to expand/evaluate")])] ["expr"]; - tool "sx_build" "Build the SX runtime. Target \"js\" (default) builds sx-browser.js, \"ocaml\" runs dune build. Set full=true for extensions+types." - [("target", `Assoc [("type", `String "string"); ("description", `String "Build target: \"js\" (default) or \"ocaml\"")]); + tool "sx_build" "Build the SX runtime. Target \"js\" (default) builds sx-browser.js, \"ocaml\" runs dune build, \"wasm\" does full pipeline (dune + bundle + bytecode compile + deploy to shared/static/wasm/). Set full=true for extensions+types." + [("target", `Assoc [("type", `String "string"); ("description", `String "Build target: \"js\" (default), \"ocaml\", or \"wasm\" (full WASM pipeline: build + bundle + bytecode + deploy)")]); ("full", `Assoc [("type", `String "boolean"); ("description", `String "Include extensions and type system (default: false)")])] []; tool "sx_build_bytecode" "Compile all web .sx files to pre-compiled .sxbc.json bytecode modules for the WASM browser kernel." @@ -2048,9 +2055,11 @@ let tool_definitions = `List [ ("from", `Assoc [("type", `String "string"); ("description", `String "Source section (move mode, e.g. applications)")]); ("to", `Assoc [("type", `String "string"); ("description", `String "Target section (move mode, e.g. geography)")])] []; - 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)." + 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")]); - ("mode", `Assoc [("type", `String "string"); ("description", `String "Mode: run, inspect, diff, hydrate, eval, interact, screenshot, listeners, trace, cdp")]); + ("mode", `Assoc [("type", `String "string"); ("description", `String "Mode: run, inspect, diff, hydrate, eval, interact, screenshot, listeners, trace, cdp, trace-boot, hydrate-debug, eval-at")]); + ("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]')")]); ("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)")]); ("selector", `Assoc [("type", `String "string"); ("description", `String "CSS selector for screenshot/listeners/trace modes")]); diff --git a/hosts/ocaml/browser/build-all.sh b/hosts/ocaml/browser/build-all.sh new file mode 100755 index 00000000..01fd8d35 --- /dev/null +++ b/hosts/ocaml/browser/build-all.sh @@ -0,0 +1,37 @@ +#!/bin/bash +# Full build: OCaml WASM kernel + bundle + bytecode compile + deploy to shared/static/wasm/ +# +# Usage: bash hosts/ocaml/browser/build-all.sh +# Or via MCP: sx_build target="wasm" +set -e +cd "$(dirname "$0")" + +echo "=== 1. Build WASM kernel ===" +# Remove assets dir that conflicts with dune's output target +rm -rf sx_browser.bc.wasm.assets +eval $(opam env 2>/dev/null) +cd .. +dune build browser/sx_browser.bc.wasm.js browser/sx_browser.bc.js bin/sx_server.exe 2>&1 +cd browser + +echo "=== 2. Bundle ===" +bash bundle.sh + +echo "=== 3. Compile .sxbc bytecode ===" +node compile-modules.js dist + +echo "=== 4. Deploy to shared/static/wasm/ ===" +DEST=../../../shared/static/wasm +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/*.sx "$DEST/sx/" +cp dist/sx/*.sxbc "$DEST/sx/" 2>/dev/null || true +# Keep assets dir for Node.js WASM tests +cp -r dist/sx_browser.bc.wasm.assets ./ 2>/dev/null || true + +echo "=== 5. Run WASM tests ===" +node test_wasm_native.js + +echo "=== Done ===" diff --git a/hosts/ocaml/browser/bundle.sh b/hosts/ocaml/browser/bundle.sh index 19dbc9b3..1380732b 100755 --- a/hosts/ocaml/browser/bundle.sh +++ b/hosts/ocaml/browser/bundle.sh @@ -65,8 +65,8 @@ cp "$ROOT/web/engine.sx" "$DIST/sx/" cp "$ROOT/web/orchestration.sx" "$DIST/sx/" cp "$ROOT/web/boot.sx" "$DIST/sx/" -# 9. CSSX (stylesheet language) -cp "$ROOT/sx/sx/cssx.sx" "$DIST/sx/" +# 9. CSSX (stylesheet language — runtime with tw, ~cssx/tw, cssx-process-token etc.) +cp "$ROOT/shared/sx/templates/cssx.sx" "$DIST/sx/" # Summary WASM_SIZE=$(du -sh "$DIST/sx_browser.bc.wasm.assets" | cut -f1) diff --git a/hosts/ocaml/browser/compile-modules.js b/hosts/ocaml/browser/compile-modules.js index 5a043d2d..4e21cdbe 100644 --- a/hosts/ocaml/browser/compile-modules.js +++ b/hosts/ocaml/browser/compile-modules.js @@ -42,6 +42,7 @@ const FILES = [ 'harness-web.sx', 'engine.sx', 'orchestration.sx', 'boot.sx', ]; + // --------------------------------------------------------------------------- // Build the full input script — all commands in one batch // --------------------------------------------------------------------------- diff --git a/hosts/ocaml/browser/test_wasm_native.js b/hosts/ocaml/browser/test_wasm_native.js index 68ce9e98..b92911cb 100644 --- a/hosts/ocaml/browser/test_wasm_native.js +++ b/hosts/ocaml/browser/test_wasm_native.js @@ -196,6 +196,12 @@ async function main() { K.eval('(do (define test-set-attr (fn (el name val) (effect (fn () (dom-set-attr el name val))))) (let ((el (dom-create-element "div" nil))) (test-set-attr el "class" "from-define") (dom-get-attr el "class")))'), 'from-define'); + // Verify the effect body ACTUALLY EXECUTES (not just returning a value). + // In browser WASM, the effect body silently doesn't run. + assert('define+effect body executes', + K.eval('(do (define test-fx-log (fn (el log) (effect (fn () (append! log "ran") (dom-set-attr el "class" "fx"))))) (let ((el (dom-create-element "div" nil)) (log (list))) (test-fx-log el log) (str (len log) ":" (first log))))'), + '1:ran'); + // Same thing with let works (proves it's define-specific) assert('let+effect+host-obj', K.eval('(let ((test-set-attr (fn (el name val) (effect (fn () (dom-set-attr el name val)))))) (let ((el (dom-create-element "div" nil))) (test-set-attr el "class" "from-let") (dom-get-attr el "class")))'),