From 0caa965de0f7098f103cbdb68e4e829cb549c76f Mon Sep 17 00:00:00 2001 From: giles Date: Mon, 16 Mar 2026 07:13:49 +0000 Subject: [PATCH] OCaml CEK machine compiled to WebAssembly for browser execution - wasm_of_ocaml compiles OCaml SX engine to WASM (722/722 spec tests) - js_of_ocaml fallback also working (722/722 spec tests) - Thin JS platform layer (sx-platform.js) with ~80 DOM/browser natives - Lambda callback bridge: SX lambdas callable from JS via handle table - Side-channel pattern bypasses js_of_ocaml return-value property stripping - Web adapters (signals, deps, router, adapter-html) load as SX source - Render mode dispatch: HTML tags + fragments route to OCaml renderer - Island/component accessors handle both Component and Island types - Dict-based signal support (signals.sx creates dicts, not native Signal) - Scope stack implementation (collect!/collected/emit!/emitted/context) - Bundle script embeds web adapters + WASM loader + platform layer - SX_USE_WASM env var toggles WASM engine in dev/production - Bootstrap extended: --web flag transpiles web adapters, :effects stripping Co-Authored-By: Claude Opus 4.6 (1M context) --- docker-compose.dev-sx.yml | 1 + hosts/ocaml/bin/debug_set.ml | 6 +- hosts/ocaml/bin/run_tests.ml | 9 +- hosts/ocaml/bin/sx_server.ml | 7 - hosts/ocaml/bootstrap.py | 249 +- hosts/ocaml/browser/build.sh | 37 + hosts/ocaml/browser/bundle.sh | 139 + hosts/ocaml/browser/dune | 5 + hosts/ocaml/browser/run_tests_js.js | 149 + hosts/ocaml/browser/run_tests_wasm.js | 146 + hosts/ocaml/browser/sx-platform.js | 676 +++++ hosts/ocaml/browser/sx_browser.ml | 946 ++++++ hosts/ocaml/dune-project | 2 +- hosts/ocaml/lib/dune | 3 +- hosts/ocaml/lib/sx_ref.ml | 128 +- hosts/ocaml/lib/sx_render.ml | 63 +- hosts/ocaml/lib/sx_runtime.ml | 168 +- hosts/ocaml/lib/sx_types.ml | 11 +- .../dune__exe__Sx_browser-041274e0.wasm | Bin 0 -> 41576 bytes .../dune__exe__Sx_browser-29ad0c09.wasm | Bin 0 -> 40663 bytes .../dune__exe__Sx_browser-4aee88de.wasm | Bin 0 -> 38610 bytes .../dune__exe__Sx_browser-5f19f371.wasm | Bin 0 -> 42748 bytes .../dune__exe__Sx_browser-7ff11249.wasm | Bin 0 -> 41175 bytes .../dune__exe__Sx_browser-ab096041.wasm | Bin 0 -> 41393 bytes .../dune__exe__Sx_browser-d41a9499.wasm | Bin 0 -> 40663 bytes .../dune__exe__Sx_browser-d592375f.wasm | Bin 0 -> 37621 bytes .../dune__exe__Sx_browser-ec68e6f9.wasm | Bin 0 -> 38739 bytes .../dune__exe__Sx_browser-fc461d88.wasm | Bin 0 -> 39836 bytes .../sx-wasm-assets/js_of_ocaml-651f6707.wasm | Bin 0 -> 159783 bytes .../sx-wasm-assets/jsoo_runtime-f96b44a8.wasm | Bin 0 -> 1958 bytes .../sx-wasm-assets/prelude-d7e4b000.wasm | Bin 0 -> 2035 bytes .../sx-wasm-assets/runtime-0db9b496.wasm | Bin 0 -> 102568 bytes .../sx-wasm-assets/start-9afa06f6.wasm | Bin 0 -> 1617 bytes .../sx-wasm-assets/std_exit-10fb8830.wasm | Bin 0 -> 439 bytes .../sx-wasm-assets/stdlib-23ce0836.wasm | Bin 0 -> 439334 bytes .../scripts/sx-wasm-assets/sx-2f171299.wasm | Bin 0 -> 216629 bytes .../scripts/sx-wasm-assets/sx-340f03ca.wasm | Bin 0 -> 217039 bytes .../scripts/sx-wasm-assets/sx-4d3c7bfa.wasm | Bin 0 -> 215612 bytes .../scripts/sx-wasm-assets/sx-a462ed04.wasm | Bin 0 -> 218529 bytes .../scripts/sx-wasm-assets/sx-ca2dce12.wasm | Bin 0 -> 215835 bytes .../scripts/sx-wasm-assets/sx-fc47a7a0.wasm | Bin 0 -> 216010 bytes shared/static/scripts/sx-wasm.js | 2584 +++++++++++++++++ shared/sx/helpers.py | 6 +- shared/sx/templates/shell.sx | 3 +- 44 files changed, 5167 insertions(+), 171 deletions(-) create mode 100755 hosts/ocaml/browser/build.sh create mode 100755 hosts/ocaml/browser/bundle.sh create mode 100644 hosts/ocaml/browser/dune create mode 100644 hosts/ocaml/browser/run_tests_js.js create mode 100644 hosts/ocaml/browser/run_tests_wasm.js create mode 100644 hosts/ocaml/browser/sx-platform.js create mode 100644 hosts/ocaml/browser/sx_browser.ml create mode 100644 shared/static/scripts/sx-wasm-assets/dune__exe__Sx_browser-041274e0.wasm create mode 100644 shared/static/scripts/sx-wasm-assets/dune__exe__Sx_browser-29ad0c09.wasm create mode 100644 shared/static/scripts/sx-wasm-assets/dune__exe__Sx_browser-4aee88de.wasm create mode 100644 shared/static/scripts/sx-wasm-assets/dune__exe__Sx_browser-5f19f371.wasm create mode 100644 shared/static/scripts/sx-wasm-assets/dune__exe__Sx_browser-7ff11249.wasm create mode 100644 shared/static/scripts/sx-wasm-assets/dune__exe__Sx_browser-ab096041.wasm create mode 100644 shared/static/scripts/sx-wasm-assets/dune__exe__Sx_browser-d41a9499.wasm create mode 100644 shared/static/scripts/sx-wasm-assets/dune__exe__Sx_browser-d592375f.wasm create mode 100644 shared/static/scripts/sx-wasm-assets/dune__exe__Sx_browser-ec68e6f9.wasm create mode 100644 shared/static/scripts/sx-wasm-assets/dune__exe__Sx_browser-fc461d88.wasm create mode 100644 shared/static/scripts/sx-wasm-assets/js_of_ocaml-651f6707.wasm create mode 100644 shared/static/scripts/sx-wasm-assets/jsoo_runtime-f96b44a8.wasm create mode 100644 shared/static/scripts/sx-wasm-assets/prelude-d7e4b000.wasm create mode 100644 shared/static/scripts/sx-wasm-assets/runtime-0db9b496.wasm create mode 100644 shared/static/scripts/sx-wasm-assets/start-9afa06f6.wasm create mode 100644 shared/static/scripts/sx-wasm-assets/std_exit-10fb8830.wasm create mode 100644 shared/static/scripts/sx-wasm-assets/stdlib-23ce0836.wasm create mode 100644 shared/static/scripts/sx-wasm-assets/sx-2f171299.wasm create mode 100644 shared/static/scripts/sx-wasm-assets/sx-340f03ca.wasm create mode 100644 shared/static/scripts/sx-wasm-assets/sx-4d3c7bfa.wasm create mode 100644 shared/static/scripts/sx-wasm-assets/sx-a462ed04.wasm create mode 100644 shared/static/scripts/sx-wasm-assets/sx-ca2dce12.wasm create mode 100644 shared/static/scripts/sx-wasm-assets/sx-fc47a7a0.wasm create mode 100644 shared/static/scripts/sx-wasm.js diff --git a/docker-compose.dev-sx.yml b/docker-compose.dev-sx.yml index 1fb0d61..43e2112 100644 --- a/docker-compose.dev-sx.yml +++ b/docker-compose.dev-sx.yml @@ -16,6 +16,7 @@ services: SX_USE_OCAML: "1" SX_OCAML_BIN: "/app/bin/sx_server" SX_BOUNDARY_STRICT: "1" + SX_USE_WASM: "1" SX_DEV: "1" volumes: - /root/rose-ash/_config/dev-sh-config.yaml:/app/config/app-config.yaml:ro diff --git a/hosts/ocaml/bin/debug_set.ml b/hosts/ocaml/bin/debug_set.ml index 2f68b4b..9cd67d3 100644 --- a/hosts/ocaml/bin/debug_set.ml +++ b/hosts/ocaml/bin/debug_set.ml @@ -1,6 +1,6 @@ -module T = Sx.Sx_types -module P = Sx.Sx_parser -module R = Sx.Sx_ref +module T = Sx_types +module P = Sx_parser +module R = Sx_ref open T let () = diff --git a/hosts/ocaml/bin/run_tests.ml b/hosts/ocaml/bin/run_tests.ml index 35d7a08..3707735 100644 --- a/hosts/ocaml/bin/run_tests.ml +++ b/hosts/ocaml/bin/run_tests.ml @@ -10,13 +10,6 @@ dune exec bin/run_tests.exe -- test-primitives # specific test dune exec bin/run_tests.exe -- --foundation # foundation only *) -module Sx_types = Sx.Sx_types -module Sx_parser = Sx.Sx_parser -module Sx_primitives = Sx.Sx_primitives -module Sx_runtime = Sx.Sx_runtime -module Sx_ref = Sx.Sx_ref -module Sx_render = Sx.Sx_render - open Sx_types open Sx_parser open Sx_primitives @@ -267,7 +260,7 @@ let make_test_env () = | _ -> raise (Eval_error "append!: expected list and value")); (* --- HTML Renderer (from sx_render.ml library module) --- *) - Sx.Sx_render.setup_render_env env; + Sx_render.setup_render_env env; (* --- Missing primitives referenced by tests --- *) diff --git a/hosts/ocaml/bin/sx_server.ml b/hosts/ocaml/bin/sx_server.ml index 058c3a2..9228fff 100644 --- a/hosts/ocaml/bin/sx_server.ml +++ b/hosts/ocaml/bin/sx_server.ml @@ -14,13 +14,6 @@ IO primitives (query, action, request-arg, request-method, ctx) yield (io-request ...) and block on stdin for (io-response ...). *) -module Sx_types = Sx.Sx_types -module Sx_parser = Sx.Sx_parser -module Sx_primitives = Sx.Sx_primitives -module Sx_runtime = Sx.Sx_runtime -module Sx_ref = Sx.Sx_ref -module Sx_render = Sx.Sx_render - open Sx_types diff --git a/hosts/ocaml/bootstrap.py b/hosts/ocaml/bootstrap.py index 89d5bbf..48bcc10 100644 --- a/hosts/ocaml/bootstrap.py +++ b/hosts/ocaml/bootstrap.py @@ -22,14 +22,22 @@ from shared.sx.types import Symbol def extract_defines(source: str) -> list[tuple[str, list]]: - """Parse .sx source, return list of (name, define-expr) for top-level defines.""" + """Parse .sx source, return list of (name, define-expr) for top-level defines. + Strips :effects [...] annotations from defines.""" + from shared.sx.types import Keyword exprs = parse_all(source) defines = [] for expr in exprs: if isinstance(expr, list) and expr and isinstance(expr[0], Symbol): if expr[0].name == "define": name = expr[1].name if isinstance(expr[1], Symbol) else str(expr[1]) - defines.append((name, expr)) + # Strip :effects [...] annotation if present + # (define name :effects [...] body) → (define name body) + cleaned = list(expr) + if (len(cleaned) >= 4 and isinstance(cleaned[2], Keyword) + and cleaned[2].name == "effects"): + cleaned = [cleaned[0], cleaned[1]] + cleaned[4:] + defines.append((name, cleaned)) return defines @@ -50,7 +58,7 @@ let trampoline v = v (* CEK machine doesn't produce thunks *) """ -# OCaml fixups — override iterative CEK run +# OCaml fixups — override iterative CEK run + reactive subscriber fix FIXUPS = """\ (* Override recursive cek_run with iterative loop *) @@ -61,6 +69,40 @@ let cek_run_iterative state = done; cek_value !s +(* Strict mode refs — used by test runner, stubbed here *) +let _strict_ref = ref Nil +let _prim_param_types_ref = ref Nil +let value_matches_type_p _v _t = Bool true + +(* Override reactive_shift_deref to wrap subscriber as NativeFn. + The transpiler emits bare OCaml closures for (fn () ...) but + signal_add_sub_b expects SX values. *) +let reactive_shift_deref sig' env kont = + let scan_result = kont_capture_to_reactive_reset kont in + let captured_frames = first scan_result in + let reset_frame = nth scan_result (Number 1.0) in + let remaining_kont = nth scan_result (Number 2.0) in + let update_fn = get reset_frame (String "update-fn") in + let sub_disposers = ref (List []) in + let subscriber_fn () = + List.iter (fun d -> ignore (cek_call d Nil)) (sx_to_list !sub_disposers); + sub_disposers := List []; + let new_reset = make_reactive_reset_frame env update_fn (Bool false) in + let new_kont = prim_call "concat" [captured_frames; List [new_reset]; remaining_kont] in + ignore (with_island_scope + (fun d -> sub_disposers := sx_append_b !sub_disposers d; Nil) + (fun () -> cek_run (make_cek_value (signal_value sig') env new_kont))); + Nil + in + let subscriber = NativeFn ("reactive-subscriber", fun _args -> subscriber_fn ()) in + ignore (signal_add_sub_b sig' subscriber); + ignore (register_in_scope (fun () -> + ignore (signal_remove_sub_b sig' subscriber); + List.iter (fun d -> ignore (cek_call d Nil)) (sx_to_list !sub_disposers); + Nil)); + let initial_kont = prim_call "concat" [captured_frames; List [reset_frame]; remaining_kont] in + make_cek_value (signal_value sig') env initial_kont + """ @@ -96,8 +138,15 @@ def compile_spec_to_ml(spec_dir: str | None = None) -> str: src = f.read() defines = extract_defines(src) - # Skip defines provided by preamble or fixups - skip = {"trampoline"} + # Skip defines provided by preamble/fixups or that belong in web module + skip = {"trampoline", + # Freeze functions depend on signals.sx (web spec) + "freeze-registry", "freeze-signal", "freeze-scope", + "cek-freeze-scope", "cek-freeze-all", + "cek-thaw-scope", "cek-thaw-all", + "freeze-to-sx", "thaw-from-sx", + "freeze-to-cid", "thaw-from-cid", + "content-hash", "content-put", "content-get", "content-store"} defines = [(n, e) for n, e in defines if n not in skip] # Deduplicate — keep last definition for each name (CEK overrides tree-walk) @@ -125,6 +174,160 @@ def compile_spec_to_ml(spec_dir: str | None = None) -> str: return "\n".join(parts) +WEB_PREAMBLE = """\ +(* sx_web.ml — Auto-generated from web adapters by hosts/ocaml/bootstrap.py *) +(* Do not edit — regenerate with: python3 hosts/ocaml/bootstrap.py --web *) + +[@@@warning "-26-27"] + +open Sx_types +open Sx_runtime + +""" + +# Web adapter files to transpile (dependency order) +WEB_ADAPTER_FILES = [ + ("signals.sx", "signals (reactive signal runtime)"), + ("deps.sx", "deps (component dependency analysis)"), + ("page-helpers.sx", "page-helpers (pure data transformation helpers)"), + ("router.sx", "router (client-side route matching)"), + ("adapter-html.sx", "adapter-html (HTML rendering adapter)"), +] + + +def compile_web_to_ml(web_dir: str | None = None) -> str: + """Compile web adapter SX files to OCaml source.""" + from shared.sx.ref.sx_ref import eval_expr, trampoline, make_env, sx_parse + + if web_dir is None: + web_dir = os.path.join(_PROJECT, "web") + + # Load the transpiler + env = make_env() + transpiler_path = os.path.join(_HERE, "transpiler.sx") + with open(transpiler_path) as f: + transpiler_src = f.read() + for expr in sx_parse(transpiler_src): + trampoline(eval_expr(expr, env)) + + # Also load the evaluator defines so the transpiler knows about them + spec_dir = os.path.join(_PROJECT, "spec") + eval_path = os.path.join(spec_dir, "evaluator.sx") + if os.path.exists(eval_path): + with open(eval_path) as f: + eval_defines = extract_defines(f.read()) + eval_names = [n for n, _ in eval_defines] + else: + eval_names = [] + + parts = [WEB_PREAMBLE] + + # Collect all web adapter defines + all_defines = [] + for filename, label in WEB_ADAPTER_FILES: + filepath = os.path.join(web_dir, filename) + if not os.path.exists(filepath): + print(f"Warning: {filepath} not found, skipping", file=sys.stderr) + continue + + with open(filepath) as f: + src = f.read() + defines = extract_defines(src) + + # Deduplicate within file + seen = {} + for i, (n, e) in enumerate(defines): + seen[n] = i + defines = [(n, e) for i, (n, e) in enumerate(defines) if seen[n] == i] + + all_defines.extend(defines) + print(f" {filename}: {len(defines)} defines", file=sys.stderr) + + # Deduplicate across files (last wins) + seen = {} + for i, (n, e) in enumerate(all_defines): + seen[n] = i + all_defines = [(n, e) for i, (n, e) in enumerate(all_defines) if seen[n] == i] + + print(f" Total: {len(all_defines)} unique defines", file=sys.stderr) + + # Build the defines list for the transpiler + defines_list = [[name, expr] for name, expr in all_defines] + env["_defines"] = defines_list + + # Known defines = evaluator names + web adapter names + env["_known_defines"] = eval_names + [name for name, _ in all_defines] + + # Translate + translate_expr = sx_parse("(ml-translate-file _defines)")[0] + result = trampoline(eval_expr(translate_expr, env)) + + parts.append("\n(* === Transpiled from web adapters === *)\n") + parts.append(result) + + # Registration function — extract actual OCaml names from transpiled output + # by using the same transpiler mangling. + # Ask the transpiler for the mangled name of each define. + name_map = {} + for name, _ in all_defines: + mangle_expr = sx_parse(f'(ml-mangle "{name}")')[0] + mangled = trampoline(eval_expr(mangle_expr, env)) + name_map[name] = mangled + + def count_params(expr): + """Count actual params from a (define name [annotations] (fn (params...) body)) form.""" + # Find the (fn ...) form — it might be at index 2, 3, or 4 depending on annotations + fn_expr = None + for i in range(2, min(len(expr), 6)): + if (isinstance(expr[i], list) and expr[i] and + isinstance(expr[i][0], Symbol) and expr[i][0].name in ("fn", "lambda")): + fn_expr = expr[i] + break + if fn_expr is None: + return -1 # not a function + params = fn_expr[1] if isinstance(fn_expr[1], list) else [] + n = 0 + skip = False + for p in params: + if skip: + skip = False + continue + if isinstance(p, Symbol) and p.name in ("&key", "&rest"): + skip = True + continue + if isinstance(p, list) and len(p) >= 3: # (name :as type) + n += 1 + elif isinstance(p, Symbol): + n += 1 + return n + + parts.append("\n\n(* Register all web adapter functions into an environment *)\n") + parts.append("let register_web_adapters env =\n") + for name, expr in all_defines: + mangled = name_map[name] + n = count_params(expr) + if n < 0: + # Non-function define (constant) + parts.append(f' ignore (Sx_types.env_bind env "{name}" {mangled});\n') + elif n == 0: + parts.append(f' ignore (Sx_types.env_bind env "{name}" ' + f'(NativeFn ("{name}", fun _args -> {mangled} Nil)));\n') + else: + # Generate match with correct arity + arg_names = [chr(97 + i) for i in range(n)] # a, b, c, ... + pat = "; ".join(arg_names) + call = " ".join(arg_names) + # Pad with Nil for partial application + pad_call = " ".join(arg_names[:1] + ["Nil"] * (n - 1)) if n > 1 else arg_names[0] + parts.append(f' ignore (Sx_types.env_bind env "{name}" ' + f'(NativeFn ("{name}", fun args -> match args with ' + f'| [{pat}] -> {mangled} {call} ' + f'| _ -> raise (Eval_error "{name}: expected {n} args"))));\n') + parts.append(" ()\n") + + return "\n".join(parts) + + def main(): import argparse parser = argparse.ArgumentParser(description="Bootstrap SX spec -> OCaml") @@ -133,17 +336,37 @@ def main(): default=None, help="Output file (default: stdout)", ) + parser.add_argument( + "--web", + action="store_true", + help="Compile web adapters instead of evaluator spec", + ) + parser.add_argument( + "--web-output", + default=None, + help="Output file for web adapters (default: stdout)", + ) args = parser.parse_args() - result = compile_spec_to_ml() - - if args.output: - with open(args.output, "w") as f: - f.write(result) - size = os.path.getsize(args.output) - print(f"Wrote {args.output} ({size} bytes)", file=sys.stderr) + if args.web or args.web_output: + result = compile_web_to_ml() + out = args.web_output or args.output + if out: + with open(out, "w") as f: + f.write(result) + size = os.path.getsize(out) + print(f"Wrote {out} ({size} bytes)", file=sys.stderr) + else: + print(result) else: - print(result) + result = compile_spec_to_ml() + if args.output: + with open(args.output, "w") as f: + f.write(result) + size = os.path.getsize(args.output) + print(f"Wrote {args.output} ({size} bytes)", file=sys.stderr) + else: + print(result) if __name__ == "__main__": diff --git a/hosts/ocaml/browser/build.sh b/hosts/ocaml/browser/build.sh new file mode 100755 index 0000000..0299fd2 --- /dev/null +++ b/hosts/ocaml/browser/build.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +# Build the OCaml SX engine for browser use (WASM + JS fallback). +# +# Outputs: +# _build/default/browser/sx_browser.bc.wasm.js WASM loader +# _build/default/browser/sx_browser.bc.wasm.assets/ WASM modules +# _build/default/browser/sx_browser.bc.js JS fallback +# +# Usage: +# cd hosts/ocaml && ./browser/build.sh + +set -euo pipefail +cd "$(dirname "$0")/.." + +eval $(opam env 2>/dev/null || true) + +echo "=== Building OCaml SX browser engine ===" + +# Build all targets: bytecode, JS, WASM +dune build browser/sx_browser.bc.js browser/sx_browser.bc.wasm.js + +echo "" +echo "--- Output sizes ---" +echo -n "JS (unoptimized): "; ls -lh _build/default/browser/sx_browser.bc.js | awk '{print $5}' +echo -n "WASM loader: "; ls -lh _build/default/browser/sx_browser.bc.wasm.js | awk '{print $5}' +echo -n "WASM modules: "; du -sh _build/default/browser/sx_browser.bc.wasm.assets/*.wasm | awk '{s+=$1}END{print s"K"}' + +# Optimized JS build +js_of_ocaml --opt=3 -o _build/default/browser/sx_browser.opt.js _build/default/browser/sx_browser.bc +echo -n "JS (optimized): "; ls -lh _build/default/browser/sx_browser.opt.js | awk '{print $5}' + +echo "" +echo "=== Build complete ===" +echo "" +echo "Test with:" +echo " node hosts/ocaml/browser/run_tests_js.js # JS" +echo " node --experimental-wasm-imported-strings hosts/ocaml/browser/run_tests_wasm.js # WASM" diff --git a/hosts/ocaml/browser/bundle.sh b/hosts/ocaml/browser/bundle.sh new file mode 100755 index 0000000..5cee305 --- /dev/null +++ b/hosts/ocaml/browser/bundle.sh @@ -0,0 +1,139 @@ +#!/usr/bin/env bash +# Bundle the WASM engine + platform + web adapters into shared/static/scripts/ +# +# Usage: hosts/ocaml/browser/bundle.sh + +set -euo pipefail +cd "$(dirname "$0")/../../.." + +WASM_LOADER="hosts/ocaml/_build/default/browser/sx_browser.bc.wasm.js" +WASM_ASSETS="hosts/ocaml/_build/default/browser/sx_browser.bc.wasm.assets" +PLATFORM="hosts/ocaml/browser/sx-platform.js" +OUT="shared/static/scripts/sx-wasm.js" +ASSET_DIR="shared/static/scripts/sx-wasm-assets" + +if [ ! -f "$WASM_LOADER" ]; then + echo "Build first: cd hosts/ocaml && eval \$(opam env) && dune build browser/sx_browser.bc.wasm.js" + exit 1 +fi + +# 1. WASM loader (patched asset path) +sed 's|"src":"sx_browser.bc.wasm.assets"|"src":"sx-wasm-assets"|' \ + "$WASM_LOADER" > "$OUT" + +# 2. Platform layer +echo "" >> "$OUT" +cat "$PLATFORM" >> "$OUT" + +# 3. Embedded web adapters — SX source as JS string constants +echo "" >> "$OUT" +echo "// =========================================================================" >> "$OUT" +echo "// Embedded web adapters (loaded into WASM engine at boot)" >> "$OUT" +echo "// =========================================================================" >> "$OUT" +echo "globalThis.__sxAdapters = {};" >> "$OUT" + +# Adapters to embed (order matters for dependencies) +ADAPTERS="signals deps page-helpers router adapter-html" + +for name in $ADAPTERS; do + file="web/${name}.sx" + if [ -f "$file" ]; then + echo -n "globalThis.__sxAdapters[\"${name}\"] = " >> "$OUT" + # Escape the SX source for embedding in a JS string + python3 -c " +import json, sys +with open('$file') as f: + print(json.dumps(f.read()) + ';') +" >> "$OUT" + fi +done + +# 4. Boot shim +cat >> "$OUT" << 'BOOT' + +// ========================================================================= +// WASM Boot: load adapters, then process inline