Add build-all.sh, bytecode test mode — 5 failures with SX_TEST_BYTECODE=1

The hydration bug root cause: .sxbc bytecoded modules break effect
closures. Source-loaded modules work (15/15), bytecoded fail (10/15).

Run: SX_TEST_BYTECODE=1 node hosts/ocaml/browser/test_wasm_native.js

Failing: scoped static class, signal attr initial, reactive-spread,
CSSX in island scope, reactive attr update.

Also adds build-all.sh (full WASM pipeline) and sx_build target="wasm"
MCP tool integration.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-31 16:26:31 +00:00
parent 42aa6b1e67
commit 4cb4551753
5 changed files with 59 additions and 6 deletions

View File

@@ -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")]);

View File

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

View File

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

View File

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

View File

@@ -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")))'),