diff --git a/hosts/javascript/platform.py b/hosts/javascript/platform.py
index eab46883..ad849850 100644
--- a/hosts/javascript/platform.py
+++ b/hosts/javascript/platform.py
@@ -1078,6 +1078,8 @@ PRIMITIVES_JS_MODULES: dict[str, str] = {
PRIMITIVES["dict-set!"] = function(d, k, v) { d[k] = v; return v; };
PRIMITIVES["has-key?"] = function(d, k) { return d !== null && d !== undefined && k in d; };
PRIMITIVES["into"] = function(target, coll) {
+ if (target === "list") return Array.isArray(coll) ? coll.slice() : Object.entries(coll).map(function(e) { return [e[0], e[1]]; });
+ if (target === "dict") { var r = {}; for (var i = 0; i < coll.length; i++) { var p = coll[i]; if (Array.isArray(p) && p.length >= 2) r[p[0]] = p[1]; } return r; }
if (Array.isArray(target)) return Array.isArray(coll) ? coll.slice() : Object.entries(coll);
var r = {}; for (var i = 0; i < coll.length; i++) { var p = coll[i]; if (Array.isArray(p) && p.length >= 2) r[p[0]] = p[1]; }
return r;
diff --git a/hosts/ocaml/bin/mcp_tree.ml b/hosts/ocaml/bin/mcp_tree.ml
index 237d714f..7a7eb182 100644
--- a/hosts/ocaml/bin/mcp_tree.ml
+++ b/hosts/ocaml/bin/mcp_tree.ml
@@ -609,7 +609,7 @@ let rec handle_tool name args =
"render.sx"; "core-signals.sx"; "signals.sx"; "deps.sx"; "router.sx";
"page-helpers.sx"; "freeze.sx"; "bytecode.sx"; "compiler.sx"; "vm.sx";
"dom.sx"; "browser.sx"; "adapter-html.sx"; "adapter-sx.sx"; "adapter-dom.sx";
- "cssx.sx";
+ "cssx.sx"; "tw-layout.sx"; "tw-type.sx"; "tw.sx";
"boot-helpers.sx"; "hypersx.sx"; "harness.sx"; "harness-reactive.sx";
"harness-web.sx"; "engine.sx"; "orchestration.sx"; "boot.sx";
] in
diff --git a/hosts/ocaml/bin/run_tests.ml b/hosts/ocaml/bin/run_tests.ml
index 0d91b8aa..8e06adeb 100644
--- a/hosts/ocaml/bin/run_tests.ml
+++ b/hosts/ocaml/bin/run_tests.ml
@@ -199,11 +199,9 @@ let make_test_env () =
bind "env-bind!" (fun args ->
match args with
- | [e; String k; v] ->
- let ue = uw e in
- if k = "x" || k = "children" || k = "i" then
- Printf.eprintf "[env-bind!] '%s' env-id=%d bindings-before=%d\n%!" k (Obj.obj (Obj.repr ue) : int) (Hashtbl.length ue.Sx_types.bindings);
- Sx_types.env_bind ue k v
+ | [Dict d; String k; v] -> Hashtbl.replace d k v; v
+ | [Dict d; Keyword k; v] -> Hashtbl.replace d k v; v
+ | [e; String k; v] -> Sx_types.env_bind (uw e) k v
| [e; Keyword k; v] -> Sx_types.env_bind (uw e) k v
| _ -> raise (Eval_error "env-bind!: expected env, key, value"));
@@ -232,7 +230,12 @@ let make_test_env () =
bind "identical?" (fun args ->
match args with
- | [a; b] -> Bool (a == b)
+ | [a; b] -> Bool (match a, b with
+ | Number x, Number y -> x = y
+ | String x, String y -> x = y
+ | Bool x, Bool y -> x = y
+ | Nil, Nil -> true
+ | _ -> a == b)
| _ -> raise (Eval_error "identical?: expected 2 args"));
(* --- Continuation support --- *)
@@ -456,17 +459,27 @@ let make_test_env () =
(match stack with _ :: rest -> Hashtbl.replace _scope_stacks name (List [] :: rest) | [] -> ()); Nil
| _ -> Nil);
bind "regex-find-all" (fun args ->
- (* Stub: supports simple ~name pattern for component scanning *)
+ (* Stub: supports ~name patterns for component scanning *)
match args with
| [String pattern; String text] ->
- let prefix = if String.length pattern > 2 && pattern.[0] = '(' then
- (* Extract literal prefix from pattern like "(~[a-z/.-]+" → "~" *)
- let s = String.sub pattern 1 (String.length pattern - 1) in
- let p = try String.sub s 0 (String.index s '[')
- with Not_found -> try String.sub s 0 (String.index s '(')
- with Not_found -> s in
- if String.length p > 0 then p else "~"
- else pattern in
+ (* Extract the literal prefix from patterns like:
+ "(~[a-z/.-]+" → prefix "~", has_group=true
+ "\(~([a-zA-Z_]..." → prefix "(~", has_group=true *)
+ let prefix, has_group =
+ if String.length pattern >= 4 && pattern.[0] = '\\' && pattern.[1] = '(' then
+ (* Pattern like \(~(...) — literal "(" + "~" prefix, group after *)
+ let s = String.sub pattern 2 (String.length pattern - 2) in
+ let lit_end = try String.index s '(' with Not_found -> try String.index s '[' with Not_found -> String.length s in
+ let lit = String.sub s 0 lit_end in
+ ("(" ^ lit, true)
+ else if String.length pattern > 2 && pattern.[0] = '(' then
+ let s = String.sub pattern 1 (String.length pattern - 1) in
+ let p = try String.sub s 0 (String.index s '[')
+ with Not_found -> try String.sub s 0 (String.index s '(')
+ with Not_found -> s in
+ ((if String.length p > 0 then p else "~"), true)
+ else (pattern, false)
+ in
let results = ref [] in
let len = String.length text in
let plen = String.length prefix in
@@ -480,7 +493,12 @@ let make_test_env () =
|| c = '-' || c = '/' || c = '_' || c = '.' do
incr j
done;
- results := String (String.sub text !i (!j - !i)) :: !results;
+ let full_match = String.sub text !i (!j - !i) in
+ (* If pattern has capture group, strip the literal prefix to simulate group 1 *)
+ let result = if has_group then
+ String.sub full_match plen (String.length full_match - plen)
+ else full_match in
+ results := String result :: !results;
i := !j
end else incr i
done;
@@ -870,6 +888,76 @@ let make_test_env () =
Dict d
| _ -> Nil);
+ (* --- Stubs for offline/IO tests --- *)
+ bind "log-info" (fun _args -> Nil);
+ bind "log-warn" (fun _args -> Nil);
+ bind "log-error" (fun _args -> Nil);
+ bind "execute-action" (fun _args -> Nil);
+
+ (* --- make-page-def for defpage tests --- *)
+ bind "make-page-def" (fun args ->
+ let convert_val = function Keyword k -> String k | v -> v in
+ let make_pdef name slots =
+ let d = Hashtbl.create 8 in
+ Hashtbl.replace d "__type" (String "page");
+ Hashtbl.replace d "name" (String name);
+ (* Defaults for missing fields *)
+ Hashtbl.replace d "stream" (Bool false);
+ Hashtbl.replace d "shell" Nil;
+ Hashtbl.replace d "fallback" Nil;
+ Hashtbl.replace d "data" Nil;
+ (* Override with actual slot values *)
+ Hashtbl.iter (fun k v -> Hashtbl.replace d k (convert_val v)) slots;
+ Dict d
+ in
+ match args with
+ | [String name; Dict slots; _env] -> make_pdef name slots
+ | [String name; Dict slots] -> make_pdef name slots
+ | _ -> Nil);
+
+ (* --- component-io-refs for deps.sx tests --- *)
+ bind "component-io-refs" (fun args ->
+ match args with
+ | [Component c] ->
+ (* Scan body for IO calls — look for known IO functions *)
+ let rec scan = function
+ | List (Symbol s :: _) when
+ s = "fetch" || s = "fetch-data" || s = "query" || s = "action" ||
+ s = "state-get" || s = "state-set!" ||
+ s = "request-arg" || s = "request-form" || s = "request-method" || s = "now" ||
+ s = "request-header" || s = "request-json" || s = "request-content-type" ||
+ s = "execute-action" || s = "submit-mutation" -> [s]
+ | List items | ListRef { contents = items } -> List.concat_map scan items
+ | _ -> []
+ in
+ let refs = scan c.c_body in
+ let unique = List.sort_uniq String.compare refs in
+ List (List.map (fun s -> String s) unique)
+ | _ -> List []);
+ bind "component-set-io-refs!" (fun _args -> Nil);
+
+ (* --- Fragment binding for aser tests --- *)
+ bind "<>" (fun args -> List args);
+
+ (* --- component-deps / component-set-deps! for deps.sx --- *)
+ let _comp_deps : (string, value) Hashtbl.t = Hashtbl.create 16 in
+ bind "component-deps" (fun args ->
+ match args with
+ | [Component c] -> (match Hashtbl.find_opt _comp_deps c.c_name with Some v -> v | None -> Nil)
+ | [Island i] -> (match Hashtbl.find_opt _comp_deps i.i_name with Some v -> v | None -> Nil)
+ | _ -> Nil);
+ bind "component-set-deps!" (fun args ->
+ match args with
+ | [Component c; v] -> Hashtbl.replace _comp_deps c.c_name v; Nil
+ | [Island i; v] -> Hashtbl.replace _comp_deps i.i_name v; Nil
+ | _ -> Nil);
+
+ (* --- submit-mutation stub for offline tests --- *)
+ bind "submit-mutation" (fun args ->
+ match args with
+ | _ :: _ -> String "confirmed"
+ | _ -> Nil);
+
env
(* ====================================================================== *)
@@ -1054,6 +1142,7 @@ let run_spec_tests env test_files =
in
(* Render adapter for test-render-html.sx *)
load_module "render.sx" spec_dir;
+ load_module "canonical.sx" spec_dir;
load_module "adapter-html.sx" web_dir;
load_module "adapter-sx.sx" web_dir;
(* Web modules for web/tests/ *)
@@ -1074,6 +1163,12 @@ let run_spec_tests env test_files =
load_module "content.sx" lib_dir;
load_module "types.sx" lib_dir;
load_module "sx-swap.sx" lib_dir;
+ (* Shared templates: TW styling engine *)
+ let templates_dir = Filename.concat project_dir "shared/sx/templates" in
+ load_module "tw.sx" templates_dir;
+ load_module "tw-layout.sx" templates_dir;
+ load_module "tw-type.sx" templates_dir;
+ load_module "cssx.sx" templates_dir;
(* SX docs site: components, handlers, demos *)
let sx_comp_dir = Filename.concat project_dir "sx/sxc" in
let sx_sx_dir = Filename.concat project_dir "sx/sx" in
@@ -1097,6 +1192,23 @@ let run_spec_tests env test_files =
load_module "cek.sx" sx_geo_dir;
load_module "reactive-runtime.sx" sx_sx_dir;
+ (* Create short-name aliases for reactive-islands tests *)
+ let alias short full =
+ try let v = Sx_types.env_get env full in
+ ignore (Sx_types.env_bind env short v)
+ with _ -> () in
+ alias "~reactive-islands/counter" "~reactive-islands/index/demo-counter";
+ alias "~reactive-islands/temperature" "~reactive-islands/index/demo-temperature";
+ alias "~reactive-islands/stopwatch" "~reactive-islands/index/demo-stopwatch";
+ alias "~reactive-islands/reactive-list" "~reactive-islands/index/demo-reactive-list";
+ alias "~reactive-islands/input-binding" "~reactive-islands/index/demo-input-binding";
+ alias "~reactive-islands/error-boundary" "~reactive-islands/index/demo-error-boundary";
+ alias "~reactive-islands/dynamic-class" "~reactive-islands/index/demo-dynamic-class";
+ alias "~reactive-islands/store-writer" "~reactive-islands/index/demo-store-writer";
+ alias "~reactive-islands/store-reader" "~reactive-islands/index/demo-store-reader";
+ alias "~marshes/demo-marsh-product" "~reactive-islands/marshes/demo-marsh-product";
+ alias "~marshes/demo-marsh-settle" "~reactive-islands/marshes/demo-marsh-settle";
+
(* Determine test files — scan spec/tests/, lib/tests/, web/tests/ *)
let lib_tests_dir = Filename.concat project_dir "lib/tests" in
let web_tests_dir = Filename.concat project_dir "web/tests" in
@@ -1111,10 +1223,10 @@ let run_spec_tests env test_files =
ignore (Sx_types.env_bind env "render-to-sx" (NativeFn ("render-to-sx", fun args ->
match args with
| [String src] ->
- (* String input: parse then evaluate via aser *)
+ (* String input: parse then evaluate via aser (quote the parsed AST so aser sees raw structure) *)
let exprs = Sx_parser.parse_all src in
let expr = match exprs with [e] -> e | es -> List (Symbol "do" :: es) in
- let result = eval_expr (List [Symbol "aser"; expr; Env env]) (Env env) in
+ let result = eval_expr (List [Symbol "aser"; List [Symbol "quote"; expr]; Env env]) (Env env) in
(match result with SxExpr s -> String s | String s -> String s | _ -> String (Sx_runtime.value_to_str result))
| _ ->
(* AST input: delegate to the SX render-to-sx *)
diff --git a/hosts/ocaml/bootstrap.py b/hosts/ocaml/bootstrap.py
index aa4012ad..dbc6ad75 100644
--- a/hosts/ocaml/bootstrap.py
+++ b/hosts/ocaml/bootstrap.py
@@ -273,28 +273,109 @@ def compile_spec_to_ml(spec_dir: str | None = None) -> str:
"(Env (Sx_types.make_env ())) (a) ((List []))",
)
- # Inject JIT dispatch into continue_with_call's lambda branch.
- # After params are bound, check jit_call_hook before creating CEK state.
- lambda_body_pattern = (
- '(prim_call "slice" [params; (len (args))])); Nil)) in '
- '(make_cek_state ((lambda_body (f))) (local) (kont))'
+ # Inject JIT dispatch + &rest handling into continue_with_call's lambda branch.
+ # Replace the entire lambda binding + make_cek_state section.
+ cwc_lambda_old = (
+ 'else (if sx_truthy ((is_lambda (f))) then '
+ '(let params = (lambda_params (f)) in let local = (env_merge ((lambda_closure (f))) (env)) in '
+ '(if sx_truthy ((prim_call ">" [(len (args)); (len (params))])) then '
+ '(raise (Eval_error (value_to_str (String (sx_str ['
+ '(let _or = (lambda_name (f)) in if sx_truthy _or then _or else (String "lambda")); '
+ '(String " expects "); (len (params)); (String " args, got "); (len (args))])))))'
+ ' else (let () = ignore ((List.iter (fun pair -> ignore ('
+ '(env_bind local (sx_to_string (first (pair))) (nth (pair) ((Number 1.0))))))'
+ ' (sx_to_list (prim_call "zip" [params; args])); Nil)) in '
+ '(let () = ignore ((List.iter (fun p -> ignore ((env_bind local (sx_to_string p) Nil)))'
+ ' (sx_to_list (prim_call "slice" [params; (len (args))])); Nil)) in '
+ '(make_cek_state ((lambda_body (f))) (local) (kont))))))'
)
- lambda_body_jit = (
- '(prim_call "slice" [params; (len (args))])); Nil)) in '
+ cwc_lambda_new = (
+ 'else (if sx_truthy ((is_lambda (f))) then '
+ '(let params = (lambda_params (f)) in let local = (env_merge ((lambda_closure (f))) (env)) in '
+ '(if not (bind_lambda_with_rest params args local) then begin '
+ 'let pl = sx_to_list params and al = sx_to_list args in '
+ 'if List.length al > List.length pl then '
+ 'raise (Eval_error (Printf.sprintf "%s expects %d args, got %d" '
+ '(match lambda_name f with String s -> s | _ -> "lambda") '
+ '(List.length pl) (List.length al))); '
+ 'List.iter (fun pair -> ignore (env_bind local (sx_to_string (first pair)) (nth pair (Number 1.0)))) '
+ '(sx_to_list (prim_call "zip" [params; args])); '
+ 'List.iter (fun p -> ignore (env_bind local (sx_to_string p) Nil)) '
+ '(sx_to_list (prim_call "slice" [params; len args])) end; '
'(match !jit_call_hook, f with '
'| Some hook, Lambda l when l.l_name <> None -> '
- 'let args_list = match args with '
- 'List a | ListRef { contents = a } -> a | _ -> [] in '
+ 'let args_list = match args with List a | ListRef { contents = a } -> a | _ -> [] in '
'(match hook f args_list with '
'Some result -> make_cek_value result local kont '
'| None -> make_cek_state (lambda_body f) local kont) '
- '| _ -> make_cek_state ((lambda_body (f))) (local) (kont))'
+ '| _ -> make_cek_state ((lambda_body (f))) (local) (kont))))'
)
- if lambda_body_pattern in output:
- output = output.replace(lambda_body_pattern, lambda_body_jit, 1)
+ if cwc_lambda_old in output:
+ output = output.replace(cwc_lambda_old, cwc_lambda_new, 1)
else:
import sys
- print("WARNING: Could not find lambda body pattern for JIT injection", file=sys.stderr)
+ print("WARNING: Could not find continue_with_call lambda pattern for &rest+JIT injection", file=sys.stderr)
+
+ # Patch call_lambda and continue_with_call to handle &rest in lambda params.
+ # The transpiler can't handle the index-of-based approach, so we inject it.
+ REST_HELPER = """
+(* &rest lambda param binding — injected by bootstrap.py *)
+and bind_lambda_with_rest params args local =
+ let param_list = sx_to_list params in
+ let arg_list = sx_to_list args in
+ let rec find_rest i = function
+ | [] -> None
+ | h :: rp :: _ when value_to_str h = "&rest" -> Some (i, value_to_str rp)
+ | _ :: tl -> find_rest (i + 1) tl
+ in
+ match find_rest 0 param_list with
+ | Some (pos, rest_name) ->
+ let positional = List.filteri (fun i _ -> i < pos) param_list in
+ List.iteri (fun i p ->
+ let v = if i < List.length arg_list then List.nth arg_list i else Nil in
+ ignore (env_bind local (value_to_str p) v)
+ ) positional;
+ let rest_args = if List.length arg_list > pos
+ then List (List.filteri (fun i _ -> i >= pos) arg_list)
+ else List [] in
+ ignore (env_bind local rest_name rest_args);
+ true
+ | None -> false
+"""
+ # Inject the helper before call_lambda
+ output = output.replace(
+ "(* call-lambda *)\nand call_lambda",
+ REST_HELPER + "\n(* call-lambda *)\nand call_lambda",
+ )
+
+ # Patch call_lambda to use &rest-aware binding
+ call_lambda_marker = "(* call-lambda *)\nand call_lambda f args caller_env =\n"
+ call_comp_marker = "\n(* call-component *)"
+ if call_lambda_marker in output and call_comp_marker in output:
+ start = output.index(call_lambda_marker)
+ end = output.index(call_comp_marker)
+ new_call_lambda = """(* call-lambda *)
+and call_lambda f args caller_env =
+ let params = lambda_params f in
+ let local = env_merge (lambda_closure f) caller_env in
+ if not (bind_lambda_with_rest params args local) then begin
+ let pl = sx_to_list params and al = sx_to_list args in
+ if List.length al > List.length pl then
+ raise (Eval_error (Printf.sprintf "%s expects %d args, got %d"
+ (match lambda_name f with String s -> s | _ -> "lambda")
+ (List.length pl) (List.length al)));
+ List.iter (fun pair ->
+ ignore (env_bind local (sx_to_string (first pair)) (nth pair (Number 1.0)))
+ ) (sx_to_list (prim_call "zip" [params; args]));
+ List.iter (fun p ->
+ ignore (env_bind local (sx_to_string p) Nil)
+ ) (sx_to_list (prim_call "slice" [params; len args]))
+ end;
+ make_thunk (lambda_body f) local
+"""
+ output = output[:start] + new_call_lambda + output[end:]
+ else:
+ print("WARNING: Could not find call_lambda for &rest injection", file=sys.stderr)
# Instrument recursive cek_run to capture kont on error (for comp-trace).
# The iterative cek_run_iterative already does this, but cek_call uses
diff --git a/hosts/ocaml/browser/bisect_sxbc.sh b/hosts/ocaml/browser/bisect_sxbc.sh
new file mode 100755
index 00000000..660dd49b
--- /dev/null
+++ b/hosts/ocaml/browser/bisect_sxbc.sh
@@ -0,0 +1,105 @@
+#!/bin/bash
+# bisect_sxbc.sh — Binary search for which .sxbc file breaks reactive rendering.
+# Runs test_wasm.sh with SX_TEST_BYTECODE=1, toggling individual files between
+# bytecode and source to find the culprit.
+set -euo pipefail
+
+cd "$(dirname "$0")/../../.."
+
+SXBC_DIR="shared/static/wasm/sx"
+BACKUP_DIR="/tmp/sxbc-bisect-backup"
+
+# All .sxbc files in load order
+FILES=(
+ render core-signals signals deps router page-helpers freeze
+ bytecode compiler vm dom browser
+ adapter-html adapter-sx adapter-dom
+ cssx boot-helpers hypersx
+ harness harness-reactive harness-web
+ engine orchestration boot
+)
+
+# Backup all sxbc files
+mkdir -p "$BACKUP_DIR"
+for f in "${FILES[@]}"; do
+ cp "$SXBC_DIR/$f.sxbc" "$BACKUP_DIR/$f.sxbc" 2>/dev/null || true
+done
+
+# Test function: returns 0 if the reactive scoped test passes
+test_passes() {
+ local result
+ result=$(SX_TEST_BYTECODE=1 bash hosts/ocaml/browser/test_wasm.sh 2>&1) || true
+ if echo "$result" | grep -q "scoped static class"; then
+ # Test mentioned = it failed
+ return 1
+ else
+ return 0
+ fi
+}
+
+# Restore all bytecodes
+restore_all() {
+ for f in "${FILES[@]}"; do
+ cp "$BACKUP_DIR/$f.sxbc" "$SXBC_DIR/$f.sxbc" 2>/dev/null || true
+ done
+}
+
+# Remove specific bytecodes (force source loading for those)
+remove_sxbc() {
+ for f in "$@"; do
+ rm -f "$SXBC_DIR/$f.sxbc"
+ done
+}
+
+echo "=== Bytecode bisect: finding which .sxbc breaks reactive rendering ==="
+echo " ${#FILES[@]} files to search"
+echo ""
+
+# First: verify all-bytecode fails
+restore_all
+echo "--- All bytecode (should fail) ---"
+if test_passes; then
+ echo "UNEXPECTED: all-bytecode passes! Nothing to bisect."
+ exit 0
+fi
+echo " Confirmed: fails with all bytecode"
+
+# Second: verify all-source passes
+for f in "${FILES[@]}"; do rm -f "$SXBC_DIR/$f.sxbc"; done
+echo "--- All source (should pass) ---"
+if ! test_passes; then
+ echo "UNEXPECTED: all-source also fails! Bug is not bytecode-specific."
+ restore_all
+ exit 1
+fi
+echo " Confirmed: passes with all source"
+
+# Binary search: find minimal set of bytecode files that causes failure
+# Strategy: start with all source, add bytecode files one at a time
+echo ""
+echo "=== Individual file test ==="
+culprits=()
+for f in "${FILES[@]}"; do
+ # Start from all-source, add just this one file as bytecode
+ for g in "${FILES[@]}"; do rm -f "$SXBC_DIR/$g.sxbc"; done
+ cp "$BACKUP_DIR/$f.sxbc" "$SXBC_DIR/$f.sxbc"
+
+ if test_passes; then
+ printf " %-20s bytecode OK\n" "$f"
+ else
+ printf " %-20s *** BREAKS ***\n" "$f"
+ culprits+=("$f")
+ fi
+done
+
+# Restore
+restore_all
+
+echo ""
+if [ ${#culprits[@]} -eq 0 ]; then
+ echo "No single file causes the failure — it's a combination."
+ echo "Run with groups to narrow down."
+else
+ echo "=== CULPRIT FILE(S): ${culprits[*]} ==="
+ echo "These .sxbc files individually cause the reactive rendering to break."
+fi
diff --git a/hosts/ocaml/browser/bundle.sh b/hosts/ocaml/browser/bundle.sh
index cb7fc38c..6bf0088b 100755
--- a/hosts/ocaml/browser/bundle.sh
+++ b/hosts/ocaml/browser/bundle.sh
@@ -66,8 +66,11 @@ 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 — runtime with tw, ~cssx/tw, cssx-process-token etc.)
-cp "$ROOT/shared/sx/templates/cssx.sx" "$DIST/sx/"
+# 9. Styling (tw token engine + legacy cssx)
+cp "$ROOT/shared/sx/templates/cssx.sx" "$DIST/sx/"
+cp "$ROOT/shared/sx/templates/tw-layout.sx" "$DIST/sx/"
+cp "$ROOT/shared/sx/templates/tw-type.sx" "$DIST/sx/"
+cp "$ROOT/shared/sx/templates/tw.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 4e21cdbe..ddee408f 100644
--- a/hosts/ocaml/browser/compile-modules.js
+++ b/hosts/ocaml/browser/compile-modules.js
@@ -37,7 +37,7 @@ const FILES = [
'render.sx', 'core-signals.sx', 'signals.sx', 'deps.sx', 'router.sx',
'page-helpers.sx', 'freeze.sx', 'bytecode.sx', 'compiler.sx', 'vm.sx',
'dom.sx', 'browser.sx', 'adapter-html.sx', 'adapter-sx.sx', 'adapter-dom.sx',
- 'cssx.sx',
+ 'cssx.sx', 'tw-layout.sx', 'tw-type.sx', 'tw.sx',
'boot-helpers.sx', 'hypersx.sx', 'harness.sx', 'harness-reactive.sx',
'harness-web.sx', 'engine.sx', 'orchestration.sx', 'boot.sx',
];
diff --git a/hosts/ocaml/browser/test-spa.js b/hosts/ocaml/browser/test-spa.js
new file mode 100644
index 00000000..3d1fabf1
--- /dev/null
+++ b/hosts/ocaml/browser/test-spa.js
@@ -0,0 +1,226 @@
+#!/usr/bin/env node
+/**
+ * test-spa.js — Deep browser diagnostic for SPA navigation.
+ *
+ * Uses Chrome DevTools Protocol to inspect event listeners,
+ * trace click handling, and detect SPA vs full reload.
+ *
+ * Usage:
+ * node test-spa.js # bytecode mode
+ * node test-spa.js --source # source mode (nosxbc)
+ * node test-spa.js --headed # visible browser
+ */
+
+const { chromium } = require('playwright');
+
+const args = process.argv.slice(2);
+const sourceMode = args.includes('--source');
+const headed = args.includes('--headed');
+const baseUrl = 'http://localhost:8013/sx/';
+const url = sourceMode ? baseUrl + '?nosxbc' : baseUrl;
+const label = sourceMode ? 'SOURCE' : 'BYTECODE';
+
+(async () => {
+ const browser = await chromium.launch({ headless: !headed });
+ const page = await browser.newPage();
+
+ // Capture console
+ page.on('console', msg => {
+ const t = msg.text();
+ if (t.startsWith('[spa-diag]') || t.includes('Not callable') || t.includes('Error:'))
+ console.log(` [browser] ${t}`);
+ });
+
+ console.log(`\n=== SPA Diagnostic: ${label} mode ===\n`);
+ await page.goto(url);
+ await page.waitForTimeout(5000);
+
+ // ----------------------------------------------------------------
+ // 1. Use CDP to get event listeners on a link
+ // ----------------------------------------------------------------
+ console.log('--- 1. Event listeners on Geography link ---');
+
+ const cdp = await page.context().newCDPSession(page);
+
+ const listeners = await page.evaluate(async () => {
+ const link = document.querySelector('a[href="/sx/(geography)"]');
+ if (!link) return { error: 'link not found' };
+
+ // We can't use getEventListeners from page context (it's a DevTools API)
+ // But we can check _sxBound* properties and enumerate own properties
+ const ownProps = {};
+ for (const k of Object.getOwnPropertyNames(link)) {
+ if (k.startsWith('_') || k.startsWith('on'))
+ ownProps[k] = typeof link[k];
+ }
+
+ // Check for jQuery-style event data
+ const jqData = link.__events || link._events || null;
+
+ return {
+ href: link.getAttribute('href'),
+ ownProps,
+ jqData: jqData ? 'present' : 'none',
+ onclick: link.onclick ? 'set' : 'null',
+ parentTag: link.parentElement?.tagName,
+ };
+ });
+ console.log(' Link props:', JSON.stringify(listeners, null, 2));
+
+ // Check should-boost-link? and why it returns false
+ const boostCheck = await page.evaluate(() => {
+ const K = window.SxKernel;
+ const link = document.querySelectorAll('a[href]')[1]; // geography link
+ if (!link) return 'no link';
+ try {
+ // Check the conditions should-boost-link? checks
+ const href = link.getAttribute('href');
+ const checks = {
+ href,
+ hasBoostAttr: link.closest('[data-sx-boost]') ? 'yes' : 'no',
+ hasNoBoost: link.hasAttribute('data-sx-no-boost') ? 'yes' : 'no',
+ isExternal: href.startsWith('http') ? 'yes' : 'no',
+ isHash: href.startsWith('#') ? 'yes' : 'no',
+ };
+ // Try calling should-boost-link?
+ try { checks.shouldBoost = K.eval('(should-boost-link? (nth (dom-query-all (dom-body) "a[href]") 1))'); }
+ catch(e) { checks.shouldBoost = 'err: ' + e.message.slice(0, 80); }
+ return checks;
+ } catch(e) { return 'err: ' + e.message; }
+ });
+ console.log(' Boost check:', JSON.stringify(boostCheck, null, 2));
+
+ // Use CDP to get actual event listeners
+ const linkNode = await page.$('a[href="/sx/(geography)"]');
+ if (linkNode) {
+ const { object } = await cdp.send('Runtime.evaluate', {
+ expression: 'document.querySelector(\'a[href="/sx/(geography)"]\')',
+ });
+ if (object?.objectId) {
+ const { listeners: cdpListeners } = await cdp.send('DOMDebugger.getEventListeners', {
+ objectId: object.objectId,
+ depth: 0,
+ });
+ console.log(' CDP event listeners on link:', cdpListeners.length);
+ for (const l of cdpListeners) {
+ console.log(` ${l.type}: ${l.handler?.description?.slice(0, 100) || 'native'} (useCapture=${l.useCapture})`);
+ }
+ }
+
+ // Also check document-level click listeners
+ const { object: docObj } = await cdp.send('Runtime.evaluate', {
+ expression: 'document',
+ });
+ if (docObj?.objectId) {
+ const { listeners: docListeners } = await cdp.send('DOMDebugger.getEventListeners', {
+ objectId: docObj.objectId,
+ depth: 0,
+ });
+ const clickListeners = docListeners.filter(l => l.type === 'click');
+ console.log(' CDP document click listeners:', clickListeners.length);
+ for (const l of clickListeners) {
+ console.log(` ${l.type}: ${l.handler?.description?.slice(0, 120) || 'native'} (capture=${l.useCapture})`);
+ }
+ }
+
+ // Check window-level listeners too
+ const { object: winObj } = await cdp.send('Runtime.evaluate', {
+ expression: 'window',
+ });
+ if (winObj?.objectId) {
+ const { listeners: winListeners } = await cdp.send('DOMDebugger.getEventListeners', {
+ objectId: winObj.objectId,
+ depth: 0,
+ });
+ const winClick = winListeners.filter(l => l.type === 'click');
+ const winPop = winListeners.filter(l => l.type === 'popstate');
+ console.log(' CDP window click listeners:', winClick.length);
+ console.log(' CDP window popstate listeners:', winPop.length);
+ for (const l of winPop) {
+ console.log(` popstate: ${l.handler?.description?.slice(0, 120) || 'native'}`);
+ }
+ }
+ }
+
+ // ----------------------------------------------------------------
+ // 2. Trace what happens when we click
+ // ----------------------------------------------------------------
+ console.log('\n--- 2. Click trace ---');
+
+ // Inject click tracing
+ await page.evaluate(() => {
+ // Trace click event propagation
+ const phases = ['NONE', 'CAPTURE', 'AT_TARGET', 'BUBBLE'];
+ document.addEventListener('click', function(e) {
+ console.log('[spa-diag] click CAPTURE on document: target=' + e.target.tagName +
+ ' href=' + (e.target.getAttribute?.('href') || 'none') +
+ ' defaultPrevented=' + e.defaultPrevented);
+ }, true);
+
+ document.addEventListener('click', function(e) {
+ console.log('[spa-diag] click BUBBLE on document: defaultPrevented=' + e.defaultPrevented +
+ ' propagation=' + (e.cancelBubble ? 'stopped' : 'running'));
+ }, false);
+
+ // Monitor pushState
+ const origPush = history.pushState;
+ history.pushState = function() {
+ console.log('[spa-diag] pushState called: ' + JSON.stringify(arguments[2]));
+ return origPush.apply(this, arguments);
+ };
+
+ // Monitor replaceState
+ const origReplace = history.replaceState;
+ history.replaceState = function() {
+ console.log('[spa-diag] replaceState called: ' + JSON.stringify(arguments[2]));
+ return origReplace.apply(this, arguments);
+ };
+ });
+
+ // Detect full reload vs SPA by checking if a new page load happens
+ let fullReload = false;
+ let networkNav = false;
+ page.on('load', () => { fullReload = true; });
+ page.on('request', req => {
+ if (req.isNavigationRequest()) {
+ networkNav = true;
+ console.log(' [network] Navigation request:', req.url());
+ }
+ });
+
+ // Click the link
+ console.log(' Clicking /sx/(geography)...');
+ const urlBefore = page.url();
+ await page.click('a[href="/sx/(geography)"]');
+ await page.waitForTimeout(3000);
+ const urlAfter = page.url();
+
+ console.log(` URL: ${urlBefore.split('8013')[1]} → ${urlAfter.split('8013')[1]}`);
+ console.log(` Full reload: ${fullReload}`);
+ console.log(` Network navigation: ${networkNav}`);
+
+ // Check page content
+ const content = await page.evaluate(() => ({
+ title: document.title,
+ h1: document.querySelector('h1')?.textContent?.slice(0, 50) || 'none',
+ bodyLen: document.body.innerHTML.length,
+ }));
+ console.log(' Content:', JSON.stringify(content));
+
+ // ----------------------------------------------------------------
+ // 3. Check SX router state
+ // ----------------------------------------------------------------
+ console.log('\n--- 3. SX router state ---');
+ const routerState = await page.evaluate(() => {
+ const K = window.SxKernel;
+ if (!K) return { error: 'no kernel' };
+ const checks = {};
+ try { checks['_page-routes count'] = K.eval('(len _page-routes)'); } catch(e) { checks['_page-routes'] = e.message; }
+ try { checks['current-route'] = K.eval('(browser-location-pathname)'); } catch(e) { checks['current-route'] = e.message; }
+ return checks;
+ });
+ console.log(' Router:', JSON.stringify(routerState));
+
+ console.log('\n=== Done ===\n');
+ await browser.close();
+})();
diff --git a/lib/render-trace.sx b/lib/render-trace.sx
new file mode 100644
index 00000000..08d4dc11
--- /dev/null
+++ b/lib/render-trace.sx
@@ -0,0 +1,51 @@
+(define *render-trace* false)
+
+(define *render-trace-log* (list))
+
+(define *render-trace-depth* 0)
+
+(define
+ render-trace-reset!
+ (fn () (set! *render-trace-log* (list)) (set! *render-trace-depth* 0)))
+
+(define
+ render-trace-push!
+ (fn
+ (kind detail result)
+ (when
+ *render-trace*
+ (set! *render-trace-log* (append *render-trace-log* (list {:result (if (> (len (str result)) 80) (str (slice (str result) 0 77) "...") (str result)) :depth *render-trace-depth* :kind kind :detail detail}))))))
+
+(define
+ render-trace-enter!
+ (fn
+ (kind detail)
+ (when
+ *render-trace*
+ (render-trace-push! kind detail "...")
+ (set! *render-trace-depth* (+ *render-trace-depth* 1)))))
+
+(define
+ render-trace-exit!
+ (fn
+ (result)
+ (when
+ *render-trace*
+ (set! *render-trace-depth* (- *render-trace-depth* 1)))))
+
+(define
+ format-render-trace
+ (fn
+ ()
+ (join
+ "\n"
+ (map
+ (fn
+ (entry)
+ (let
+ ((indent (join "" (map (fn (_) " ") (range 0 (get entry :depth)))))
+ (kind (get entry :kind))
+ (detail (get entry :detail))
+ (result (get entry :result)))
+ (str indent kind " " detail " → " result)))
+ *render-trace-log*))))
diff --git a/scripts/sx-build-all.sh b/scripts/sx-build-all.sh
index 9b952388..8573ffae 100755
--- a/scripts/sx-build-all.sh
+++ b/scripts/sx-build-all.sh
@@ -26,7 +26,8 @@ for f in signals.sx deps.sx router.sx page-helpers.sx freeze.sx \
bytecode.sx compiler.sx vm.sx dom.sx browser.sx \
adapter-html.sx adapter-sx.sx adapter-dom.sx \
boot-helpers.sx hypersx.sx harness-reactive.sx harness-web.sx \
- engine.sx orchestration.sx boot.sx cssx.sx; do
+ engine.sx orchestration.sx boot.sx cssx.sx \
+ tw-layout.sx tw-type.sx tw.sx; do
if [ -f "shared/static/wasm/sx/$f" ]; then
cp "shared/static/wasm/sx/$f" "hosts/ocaml/browser/dist/sx/"
fi
diff --git a/shared/static/scripts/sx-browser.js b/shared/static/scripts/sx-browser.js
index 92d66ba4..1823b4e3 100644
--- a/shared/static/scripts/sx-browser.js
+++ b/shared/static/scripts/sx-browser.js
@@ -24,7 +24,7 @@
// =========================================================================
var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } });
- var SX_VERSION = "2026-03-31T23:22:57Z";
+ var SX_VERSION = "2026-04-01T20:24:51Z";
function isNil(x) { return x === NIL || x === null || x === undefined; }
function isSxTruthy(x) { return x !== false && !isNil(x); }
@@ -462,6 +462,8 @@
PRIMITIVES["dict-set!"] = function(d, k, v) { d[k] = v; return v; };
PRIMITIVES["has-key?"] = function(d, k) { return d !== null && d !== undefined && k in d; };
PRIMITIVES["into"] = function(target, coll) {
+ if (target === "list") return Array.isArray(coll) ? coll.slice() : Object.entries(coll).map(function(e) { return [e[0], e[1]]; });
+ if (target === "dict") { var r = {}; for (var i = 0; i < coll.length; i++) { var p = coll[i]; if (Array.isArray(p) && p.length >= 2) r[p[0]] = p[1]; } return r; }
if (Array.isArray(target)) return Array.isArray(coll) ? coll.slice() : Object.entries(coll);
var r = {}; for (var i = 0; i < coll.length; i++) { var p = coll[i]; if (Array.isArray(p) && p.length >= 2) r[p[0]] = p[1]; }
return r;
@@ -2864,10 +2866,20 @@ PRIMITIVES["render-html-form?"] = isRenderHtmlForm;
return (isSxTruthy(!isSxTruthy(sxEq(typeOf(head), "symbol"))) ? join("", map(function(x) { return renderValueToHtml(x, env); }, expr)) : (function() {
var name = symbolName(head);
var args = rest(expr);
- return (isSxTruthy(sxEq(name, "<>")) ? join("", map(function(x) { return renderToHtml(x, env); }, args)) : (isSxTruthy(sxEq(name, "raw!")) ? join("", map(function(x) { return (String(trampoline(evalExpr(x, env)))); }, args)) : (isSxTruthy(sxEq(name, "lake")) ? renderHtmlLake(args, env) : (isSxTruthy(sxEq(name, "marsh")) ? renderHtmlMarsh(args, env) : (isSxTruthy(sxOr(sxEq(name, "portal"), sxEq(name, "error-boundary"), sxEq(name, "promise-delayed"))) ? join("", map(function(x) { return renderToHtml(x, env); }, args)) : (isSxTruthy(contains(HTML_TAGS, name)) ? renderHtmlElement(name, args, env) : (isSxTruthy((isSxTruthy(startsWith(name, "~")) && isSxTruthy(envHas(env, name)) && isIsland(envGet(env, name)))) ? renderHtmlIsland(envGet(env, name), args, env) : (isSxTruthy(startsWith(name, "~")) ? (function() {
+ return (isSxTruthy(sxEq(name, "<>")) ? join("", map(function(x) { return renderToHtml(x, env); }, args)) : (isSxTruthy(sxEq(name, "raw!")) ? join("", map(function(x) { return (String(trampoline(evalExpr(x, env)))); }, args)) : (isSxTruthy(sxEq(name, "lake")) ? renderHtmlLake(args, env) : (isSxTruthy(sxEq(name, "marsh")) ? renderHtmlMarsh(args, env) : (isSxTruthy(sxEq(name, "error-boundary")) ? (function() {
+ var hasFallback = (len(args) > 1);
+ return (function() {
+ var bodyExprs = (isSxTruthy(hasFallback) ? rest(args) : args);
+ var fallbackExpr = (isSxTruthy(hasFallback) ? first(args) : NIL);
+ return (String("
") + String(tryCatch(function() { return join("", map(function(x) { return renderToHtml(x, env); }, bodyExprs)); }, function(err) { return (function() {
+ var safeErr = replace_(replace_((String(err)), "<", "<"), ">", ">");
+ return (isSxTruthy((isSxTruthy(fallbackExpr) && !isSxTruthy(isNil(fallbackExpr)))) ? tryCatch(function() { return renderToHtml([trampoline(evalExpr(fallbackExpr, env)), err, NIL], env); }, function(e2) { return (String("
Render error: ") + String(safeErr) + String("
")); }) : (String("
Render error: ") + String(safeErr) + String("
")));
+})(); })) + String("
"));
+})();
+})() : (isSxTruthy(sxOr(sxEq(name, "portal"), sxEq(name, "promise-delayed"))) ? join("", map(function(x) { return renderToHtml(x, env); }, args)) : (isSxTruthy(contains(HTML_TAGS, name)) ? renderHtmlElement(name, args, env) : (isSxTruthy((isSxTruthy(startsWith(name, "~")) && isSxTruthy(envHas(env, name)) && isIsland(envGet(env, name)))) ? renderHtmlIsland(envGet(env, name), args, env) : (isSxTruthy(startsWith(name, "~")) ? (function() {
var val = envGet(env, name);
return (isSxTruthy(isComponent(val)) ? renderHtmlComponent(val, args, env) : (isSxTruthy(isMacro(val)) ? renderToHtml(expandMacro(val, args, env), env) : (String(""))));
-})() : (isSxTruthy(isRenderHtmlForm(name)) ? dispatchHtmlForm(name, expr, env) : (isSxTruthy((isSxTruthy(envHas(env, name)) && isMacro(envGet(env, name)))) ? renderToHtml(expandMacro(envGet(env, name), args, env), env) : renderValueToHtml(trampoline(evalExpr(expr, env)), env)))))))))));
+})() : (isSxTruthy(isRenderHtmlForm(name)) ? dispatchHtmlForm(name, expr, env) : (isSxTruthy((isSxTruthy(envHas(env, name)) && isMacro(envGet(env, name)))) ? renderToHtml(expandMacro(envGet(env, name), args, env), env) : renderValueToHtml(trampoline(evalExpr(expr, env)), env))))))))))));
})());
})()); };
PRIMITIVES["render-list-to-html"] = renderListToHtml;
@@ -3099,11 +3111,25 @@ PRIMITIVES["aser"] = aser;
var comp = (isSxTruthy(envHas(env, name)) ? envGet(env, name) : NIL);
var expandAll = (isSxTruthy(envHas(env, "expand-components?")) ? expandComponents_p() : false);
return (isSxTruthy((isSxTruthy(comp) && isMacro(comp))) ? aser(expandMacro(comp, args, env), env) : (isSxTruthy((isSxTruthy(comp) && isSxTruthy(isComponent(comp)) && isSxTruthy(!isSxTruthy(isIsland(comp))) && isSxTruthy(sxOr(expandAll, sxEq(componentAffinity(comp), "server"))) && !isSxTruthy(sxEq(componentAffinity(comp), "client")))) ? aserExpandComponent(comp, args, env) : aserCall(name, args, env)));
-})() : (isSxTruthy(sxEq(name, "lake")) ? aserCall(name, args, env) : (isSxTruthy(sxEq(name, "marsh")) ? aserCall(name, args, env) : (isSxTruthy(contains(HTML_TAGS, name)) ? aserCall(name, args, env) : (isSxTruthy(sxOr(isSpecialForm(name), isHoForm(name))) ? aserSpecial(name, expr, env) : (isSxTruthy((isSxTruthy(envHas(env, name)) && isMacro(envGet(env, name)))) ? aser(expandMacro(envGet(env, name), args, env), env) : (function() {
+})() : (isSxTruthy(sxEq(name, "lake")) ? aserCall(name, args, env) : (isSxTruthy(sxEq(name, "marsh")) ? aserCall(name, args, env) : (isSxTruthy(sxEq(name, "error-boundary")) ? (function() {
+ var hasFallback = (len(args) > 1);
+ return (function() {
+ var bodyExprs = (isSxTruthy(hasFallback) ? rest(args) : args);
+ var errStr = NIL;
+ return (function() {
+ var rendered = tryCatch(function() { return join("", map(function(x) { return (function() {
+ var v = aser(x, env);
+ return (isSxTruthy(sxEq(typeOf(v), "sx-expr")) ? sxExprSource(v) : (isSxTruthy(isNil(v)) ? "" : serialize(v)));
+})(); }, bodyExprs)); }, function(err) { errStr = (String(err));
+return NIL; });
+ return (isSxTruthy(rendered) ? makeSxExpr((String("(error-boundary ") + String(rendered) + String(")"))) : makeSxExpr((String("(div :data-sx-boundary \"true\" ") + String("(div :class \"sx-render-error\" ") + String(":style \"color:red;font-size:0.875rem;padding:0.5rem;border:1px solid red;border-radius:0.25rem;margin:0.5rem 0;\" ") + String("\"Render error: ") + String(replace_(replace_(errStr, "\"", "'"), "\\", "\\\\")) + String("\"))"))));
+})();
+})();
+})() : (isSxTruthy(contains(HTML_TAGS, name)) ? aserCall(name, args, env) : (isSxTruthy(sxOr(isSpecialForm(name), isHoForm(name))) ? aserSpecial(name, expr, env) : (isSxTruthy((isSxTruthy(envHas(env, name)) && isMacro(envGet(env, name)))) ? aser(expandMacro(envGet(env, name), args, env), env) : (function() {
var f = trampoline(evalExpr(head, env));
var evaledArgs = map(function(a) { return trampoline(evalExpr(a, env)); }, args);
return (isSxTruthy((isSxTruthy(isCallable(f)) && isSxTruthy(!isSxTruthy(isLambda(f))) && isSxTruthy(!isSxTruthy(isComponent(f))) && !isSxTruthy(isIsland(f)))) ? apply(f, evaledArgs) : (isSxTruthy(isLambda(f)) ? trampoline(callLambda(f, evaledArgs, env)) : (isSxTruthy(isComponent(f)) ? aserCall((String("~") + String(componentName(f))), args, env) : (isSxTruthy(isIsland(f)) ? aserCall((String("~") + String(componentName(f))), args, env) : error((String("Not callable: ") + String(inspect(f))))))));
-})()))))))));
+})())))))))));
})());
})(); };
PRIMITIVES["aser-list"] = aserList;
@@ -4522,8 +4548,8 @@ PRIMITIVES["render-dom-portal"] = renderDomPortal;
// render-dom-error-boundary
var renderDomErrorBoundary = function(args, env, ns) { return (function() {
- var fallbackExpr = first(args);
- var bodyExprs = rest(args);
+ var fallbackExpr = (isSxTruthy((len(args) > 1)) ? first(args) : NIL);
+ var bodyExprs = (isSxTruthy((len(args) > 1)) ? rest(args) : args);
var container = domCreateElement("div", NIL);
var retryVersion = signal(0);
domSetAttr(container, "data-sx-boundary", "true");
@@ -4540,7 +4566,13 @@ return (function() {
var fallbackFn = trampoline(evalExpr(fallbackExpr, env));
var retryFn = function() { return swap_b(retryVersion, function(n) { return (n + 1); }); };
return (function() {
- var fallbackDom = (isSxTruthy(isLambda(fallbackFn)) ? renderLambdaDom(fallbackFn, [err, retryFn], env, ns) : renderToDom(apply(fallbackFn, [err, retryFn]), env, ns));
+ var fallbackDom = (isSxTruthy(isNil(fallbackFn)) ? (function() {
+ var el = domCreateElement("div", NIL);
+ domSetAttr(el, "class", "sx-render-error");
+ domSetAttr(el, "style", "color:red;font-size:0.875rem;padding:0.5rem;border:1px solid red;border-radius:0.25rem;margin:0.5rem 0;");
+ domSetTextContent(el, (String("Render error: ") + String(err)));
+ return el;
+})() : (isSxTruthy(isLambda(fallbackFn)) ? renderLambdaDom(fallbackFn, [err, retryFn], env, ns) : renderToDom(apply(fallbackFn, [err, retryFn]), env, ns)));
return domAppend(container, fallbackDom);
})();
})(); }); });
@@ -4578,7 +4610,7 @@ PRIMITIVES["parse-time"] = parseTime;
PRIMITIVES["parse-trigger-spec"] = parseTriggerSpec;
// default-trigger
- var defaultTrigger = function(tagName) { return (isSxTruthy(sxEq(tagName, "FORM")) ? [{["event"]: "submit", ["modifiers"]: {}}] : (isSxTruthy(sxOr(sxEq(tagName, "INPUT"), sxEq(tagName, "SELECT"), sxEq(tagName, "TEXTAREA"))) ? [{["event"]: "change", ["modifiers"]: {}}] : [{["event"]: "click", ["modifiers"]: {}}])); };
+ var defaultTrigger = function(tagName) { return (isSxTruthy(sxEq(tagName, "form")) ? [{["event"]: "submit", ["modifiers"]: {}}] : (isSxTruthy(sxOr(sxEq(tagName, "input"), sxEq(tagName, "select"), sxEq(tagName, "textarea"))) ? [{["event"]: "change", ["modifiers"]: {}}] : [{["event"]: "click", ["modifiers"]: {}}])); };
PRIMITIVES["default-trigger"] = defaultTrigger;
// get-verb-info
@@ -5053,11 +5085,11 @@ PRIMITIVES["handle-fetch-success"] = handleFetchSuccess;
var container = domCreateElement("div", NIL);
domAppend(container, rendered);
processOobSwaps(container, function(t, oob, s) { disposeIslandsIn(t);
-swapDomNodes(t, oob, s);
-sxHydrate(t);
-return processElements(t); });
+swapDomNodes(t, (isSxTruthy(sxEq(s, "innerHTML")) ? childrenToFragment(oob) : oob), s);
+return postSwap(t); });
return (function() {
var selectSel = domGetAttr(el, "sx-select");
+ return (function() {
var content = (isSxTruthy(selectSel) ? selectFromContainer(container, selectSel) : childrenToFragment(container));
disposeIslandsIn(target);
return withTransition(useTransition, function() { return (function() {
@@ -5065,6 +5097,7 @@ return processElements(t); });
return postSwap((isSxTruthy(sxEq(swapStyle, "outerHTML")) ? domParent(sxOr(swapResult, target)) : sxOr(swapResult, target)));
})(); });
})();
+})();
})() : NIL);
})();
})();
@@ -5078,12 +5111,20 @@ PRIMITIVES["handle-sx-response"] = handleSxResponse;
var selectSel = domGetAttr(el, "sx-select");
disposeIslandsIn(target);
return (isSxTruthy(selectSel) ? (function() {
- var html = selectHtmlFromDoc(doc, selectSel);
+ var container = domCreateElement("div", NIL);
+ domSetInnerHtml(container, domBodyInnerHtml(doc));
+ processOobSwaps(container, function(t, oob, s) { disposeIslandsIn(t);
+swapDomNodes(t, oob, s);
+return postSwap(t); });
+ hoistHeadElements(container);
+ return (function() {
+ var html = selectFromContainer(container, selectSel);
return withTransition(useTransition, function() { return (function() {
- var swapRoot = swapHtmlString(target, html, swapStyle);
+ var swapRoot = swapDomNodes(target, html, swapStyle);
logInfo((String("swap-root: ") + String((isSxTruthy(swapRoot) ? domTagName(swapRoot) : "nil")) + String(" target: ") + String(domTagName(target))));
return postSwap(sxOr(swapRoot, target));
})(); });
+})();
})() : (function() {
var container = domCreateElement("div", NIL);
domSetInnerHtml(container, domBodyInnerHtml(doc));
@@ -5119,7 +5160,10 @@ PRIMITIVES["handle-retry"] = handleRetry;
return forEach(function(trigger) { return (function() {
var kind = classifyTrigger(trigger);
var mods = get(trigger, "modifiers");
- return (isSxTruthy(sxEq(kind, "poll")) ? setInterval_(function() { return executeRequest(el, NIL, NIL); }, get(mods, "interval")) : (isSxTruthy(sxEq(kind, "intersect")) ? observeIntersection(el, function() { return executeRequest(el, NIL, NIL); }, false, get(mods, "delay")) : (isSxTruthy(sxEq(kind, "load")) ? setTimeout_(function() { return executeRequest(el, NIL, NIL); }, sxOr(get(mods, "delay"), 0)) : (isSxTruthy(sxEq(kind, "revealed")) ? observeIntersection(el, function() { return executeRequest(el, NIL, NIL); }, true, get(mods, "delay")) : (isSxTruthy(sxEq(kind, "event")) ? bindEvent(el, get(trigger, "event"), mods, verbInfo) : NIL)))));
+ return (isSxTruthy(sxEq(kind, "poll")) ? (function() {
+ var intervalId = NIL;
+ return (intervalId = setInterval_(function() { return (isSxTruthy(hostGet(el, "isConnected")) ? executeRequest(el, NIL, NIL) : (clearInterval_(intervalId), logInfo("poll stopped: element removed"))); }, get(mods, "interval")));
+})() : (isSxTruthy(sxEq(kind, "intersect")) ? observeIntersection(el, function() { return executeRequest(el, NIL, NIL); }, false, get(mods, "delay")) : (isSxTruthy(sxEq(kind, "load")) ? setTimeout_(function() { return executeRequest(el, NIL, NIL); }, sxOr(get(mods, "delay"), 0)) : (isSxTruthy(sxEq(kind, "revealed")) ? observeIntersection(el, function() { return executeRequest(el, NIL, NIL); }, true, get(mods, "delay")) : (isSxTruthy(sxEq(kind, "event")) ? bindEvent(el, get(trigger, "event"), mods, verbInfo) : NIL)))));
})(); }, triggers);
})(); };
PRIMITIVES["bind-triggers"] = bindTriggers;
@@ -5421,7 +5465,32 @@ PRIMITIVES["offline-aware-mutation"] = offlineAwareMutation;
PRIMITIVES["current-page-layout"] = currentPageLayout;
// swap-rendered-content
- var swapRenderedContent = function(target, rendered, pathname) { return (disposeIslandsIn(target), domSetTextContent(target, ""), domAppend(target, rendered), hoistHeadElementsFull(target), processElements(target), sxHydrateElements(target), sxHydrateIslands(target), runPostRenderHooks(), domDispatch(target, "sx:clientRoute", {["pathname"]: pathname}), logInfo((String("sx:route client ") + String(pathname)))); };
+ var swapRenderedContent = function(target, rendered, pathname) { return (function() {
+ var container = domCreateElement("div", NIL);
+ domAppend(container, rendered);
+ processOobSwaps(container, function(t, oob, s) { disposeIslandsIn(t);
+swapDomNodes(t, (isSxTruthy(sxEq(s, "innerHTML")) ? childrenToFragment(oob) : oob), s);
+return postSwap(t); });
+ return (function() {
+ var targetId = domGetAttr(target, "id");
+ return (function() {
+ var inner = (isSxTruthy(targetId) ? domQuery(container, (String("#") + String(targetId))) : NIL);
+ return (function() {
+ var content = (isSxTruthy(inner) ? childrenToFragment(inner) : childrenToFragment(container));
+ disposeIslandsIn(target);
+ domSetTextContent(target, "");
+ domAppend(target, content);
+ hoistHeadElementsFull(target);
+ processElements(target);
+ sxHydrateElements(target);
+ sxHydrateIslands(target);
+ runPostRenderHooks();
+ domDispatch(target, "sx:clientRoute", {["pathname"]: pathname});
+ return logInfo((String("sx:route client ") + String(pathname)));
+})();
+})();
+})();
+})(); };
PRIMITIVES["swap-rendered-content"] = swapRenderedContent;
// resolve-route-target
diff --git a/shared/static/wasm/sx-platform-2.js b/shared/static/wasm/sx-platform-2.js
index 9cf89bdb..eb94a850 100644
--- a/shared/static/wasm/sx-platform-2.js
+++ b/shared/static/wasm/sx-platform-2.js
@@ -166,6 +166,22 @@
document.cookie = args[0] + "=" + encodeURIComponent(args[1] || "") + ";path=/;max-age=31536000;SameSite=Lax";
});
+ // IntersectionObserver — native JS to avoid bytecode callback issues
+ K.registerNative("observe-intersection", function(args) {
+ var el = args[0], callback = args[1], once = args[2], delay = args[3];
+ var obs = new IntersectionObserver(function(entries) {
+ for (var i = 0; i < entries.length; i++) {
+ if (entries[i].isIntersecting) {
+ var d = (delay && delay !== null) ? delay : 0;
+ setTimeout(function() { K.callFn(callback, []); }, d);
+ if (once) obs.unobserve(el);
+ }
+ }
+ });
+ obs.observe(el);
+ return obs;
+ });
+
// ================================================================
// Load SX web libraries and adapters
// ================================================================
@@ -398,68 +414,6 @@
"children:", islands[j].children.length);
}
console.log("[sx] boot done");
-
- // sx-on: inline event handlers — bind from JS because the WASM
- // CSS selector [sx-on\:] doesn't match. Uses MutationObserver to
- // also catch elements added after boot (e.g. from swaps).
- function _bindSxOn(root) {
- var all = (root || document).querySelectorAll('*');
- for (var k = 0; k < all.length; k++) {
- var el = all[k];
- if (el._sxOnBound) continue;
- var attrs = el.attributes;
- var hasSxOn = false;
- for (var a = 0; a < attrs.length; a++) {
- var aname = attrs[a].name;
- if (aname.indexOf('sx-on:') === 0) {
- hasSxOn = true;
- var evtName = aname.slice(6);
- // HTML lowercases attrs: afterSwap → afterswap.
- // Engine dispatches camelCase: sx:afterSwap.
- // Listen for both forms.
- var evtName2 = null;
- if (evtName.indexOf('after') === 0 || evtName.indexOf('before') === 0) {
- evtName2 = 'sx:' + evtName; // lowercase form
- // Also try camelCase form
- var camel = evtName.replace(/swap|request|settle/gi, function(m) {
- return m.charAt(0).toUpperCase() + m.slice(1);
- });
- evtName = 'sx:' + camel;
- }
- (function(el2, evt, evt2, code) {
- var handler = function(e) {
- try { new Function('event', code).call(el2, e); }
- catch(err) { console.warn('[sx] sx-on:' + evt + ' error:', err); }
- };
- el2.addEventListener(evt, handler);
- if (evt2) el2.addEventListener(evt2, handler);
- })(el, evtName, evtName2, attrs[a].value);
- }
- }
- if (hasSxOn) el._sxOnBound = true;
- }
- }
- _bindSxOn(document);
- // Re-bind after swaps
- document.addEventListener('sx:afterSwap', function(e) {
- if (e.target) _bindSxOn(e.target);
- });
-
- // Global keyboard shortcut dispatch — WASM host-callbacks on
- // document/body don't fire, so handle from:body keyboard
- // triggers in JS and call execute-request via the SX engine.
- document.addEventListener("keyup", function(e) {
- if (e.target && e.target.matches && e.target.matches("input,textarea,select")) return;
- var sel = '[sx-trigger*="key==\'' + e.key + '\'"]';
- var els = document.querySelectorAll(sel);
- for (var i = 0; i < els.length; i++) {
- var el = els[i];
- if (!el.id) el.id = "_sx_kbd_" + Math.random().toString(36).slice(2);
- try {
- K.eval('(execute-request (dom-query-by-id "' + el.id + '") nil nil)');
- } catch(err) { console.warn("[sx] keyboard dispatch error:", err); }
- }
- });
}
}
};
diff --git a/shared/static/wasm/sx-platform.js b/shared/static/wasm/sx-platform.js
new file mode 100644
index 00000000..eb94a850
--- /dev/null
+++ b/shared/static/wasm/sx-platform.js
@@ -0,0 +1,452 @@
+/**
+ * sx-platform.js — Browser platform layer for the SX WASM kernel.
+ *
+ * Registers the 8 FFI host primitives and loads web adapter .sx files.
+ * This is the only JS needed beyond the WASM kernel itself.
+ *
+ * Usage:
+ *
+ *
+ *
+ * Or for js_of_ocaml mode:
+ *
+ *
+ */
+
+(function() {
+ "use strict";
+
+ function boot(K) {
+
+ // ================================================================
+ // 8 FFI Host Primitives
+ // ================================================================
+
+ K.registerNative("host-global", function(args) {
+ var name = args[0];
+ if (typeof globalThis !== "undefined" && name in globalThis) return globalThis[name];
+ if (typeof window !== "undefined" && name in window) return window[name];
+ return null;
+ });
+
+ K.registerNative("host-get", function(args) {
+ var obj = args[0], prop = args[1];
+ if (obj == null) return null;
+ var v = obj[prop];
+ return v === undefined ? null : v;
+ });
+
+ K.registerNative("host-set!", function(args) {
+ var obj = args[0], prop = args[1], val = args[2];
+ if (obj != null) obj[prop] = val;
+ });
+
+ K.registerNative("host-call", function(args) {
+ var obj = args[0], method = args[1];
+ var callArgs = [];
+ for (var i = 2; i < args.length; i++) callArgs.push(args[i]);
+ if (obj == null) {
+ // Global function call
+ var fn = typeof globalThis !== "undefined" ? globalThis[method] : window[method];
+ if (typeof fn === "function") return fn.apply(null, callArgs);
+ return null;
+ }
+ if (typeof obj[method] === "function") {
+ try { return obj[method].apply(obj, callArgs); }
+ catch(e) { console.error("[sx] host-call error:", e); return null; }
+ }
+ return null;
+ });
+
+ K.registerNative("host-new", function(args) {
+ var name = args[0];
+ var cArgs = args.slice(1);
+ var Ctor = typeof globalThis !== "undefined" ? globalThis[name] : window[name];
+ if (typeof Ctor !== "function") return null;
+ switch (cArgs.length) {
+ case 0: return new Ctor();
+ case 1: return new Ctor(cArgs[0]);
+ case 2: return new Ctor(cArgs[0], cArgs[1]);
+ case 3: return new Ctor(cArgs[0], cArgs[1], cArgs[2]);
+ default: return new Ctor(cArgs[0], cArgs[1], cArgs[2], cArgs[3]);
+ }
+ });
+
+ K.registerNative("host-callback", function(args) {
+ var fn = args[0];
+ // Native JS function — pass through
+ if (typeof fn === "function") return fn;
+ // SX callable (has __sx_handle) — wrap as JS function
+ if (fn && fn.__sx_handle !== undefined) {
+ return function() {
+ var a = Array.prototype.slice.call(arguments);
+ return K.callFn(fn, a);
+ };
+ }
+ return function() {};
+ });
+
+ K.registerNative("host-typeof", function(args) {
+ var obj = args[0];
+ if (obj == null) return "nil";
+ if (obj instanceof Element) return "element";
+ if (obj instanceof Text) return "text";
+ if (obj instanceof DocumentFragment) return "fragment";
+ if (obj instanceof Document) return "document";
+ if (obj instanceof Event) return "event";
+ if (obj instanceof Promise) return "promise";
+ if (obj instanceof AbortController) return "abort-controller";
+ return typeof obj;
+ });
+
+ K.registerNative("host-await", function(args) {
+ var promise = args[0], callback = args[1];
+ if (promise && typeof promise.then === "function") {
+ var cb;
+ if (typeof callback === "function") cb = callback;
+ else if (callback && callback.__sx_handle !== undefined)
+ cb = function(v) { return K.callFn(callback, [v]); };
+ else cb = function() {};
+ promise.then(cb);
+ }
+ });
+
+ // ================================================================
+ // Constants expected by .sx files
+ // ================================================================
+
+ K.eval('(define SX_VERSION "wasm-1.0")');
+ K.eval('(define SX_ENGINE "ocaml-vm-wasm")');
+ K.eval('(define parse sx-parse)');
+ K.eval('(define serialize sx-serialize)');
+
+ // ================================================================
+ // DOM query helpers used by boot.sx / orchestration.sx
+ // (These are JS-native in the transpiled bundle; here via FFI.)
+ // ================================================================
+
+ K.registerNative("query-sx-scripts", function(args) {
+ var root = (args[0] && args[0] !== null) ? args[0] : document;
+ if (typeof root.querySelectorAll !== "function") root = document;
+ return Array.prototype.slice.call(root.querySelectorAll('script[type="text/sx"]'));
+ });
+
+ K.registerNative("query-page-scripts", function(args) {
+ return Array.prototype.slice.call(document.querySelectorAll('script[type="text/sx-pages"]'));
+ });
+
+ K.registerNative("query-component-scripts", function(args) {
+ var root = (args[0] && args[0] !== null) ? args[0] : document;
+ if (typeof root.querySelectorAll !== "function") root = document;
+ return Array.prototype.slice.call(root.querySelectorAll('script[type="text/sx"][data-components]'));
+ });
+
+ // localStorage
+ K.registerNative("local-storage-get", function(args) {
+ try { var v = localStorage.getItem(args[0]); return v === null ? null : v; }
+ catch(e) { return null; }
+ });
+ K.registerNative("local-storage-set", function(args) {
+ try { localStorage.setItem(args[0], args[1]); } catch(e) {}
+ });
+ K.registerNative("local-storage-remove", function(args) {
+ try { localStorage.removeItem(args[0]); } catch(e) {}
+ });
+
+ // log-info/log-warn defined in browser.sx; log-error as native fallback
+ K.registerNative("log-error", function(args) { console.error.apply(console, ["[sx]"].concat(args)); });
+
+ // Cookie access (browser-side)
+ K.registerNative("get-cookie", function(args) {
+ var name = args[0];
+ var match = document.cookie.match(new RegExp('(?:^|; )' + name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '=([^;]*)'));
+ return match ? decodeURIComponent(match[1]) : null;
+ });
+ K.registerNative("set-cookie", function(args) {
+ document.cookie = args[0] + "=" + encodeURIComponent(args[1] || "") + ";path=/;max-age=31536000;SameSite=Lax";
+ });
+
+ // IntersectionObserver — native JS to avoid bytecode callback issues
+ K.registerNative("observe-intersection", function(args) {
+ var el = args[0], callback = args[1], once = args[2], delay = args[3];
+ var obs = new IntersectionObserver(function(entries) {
+ for (var i = 0; i < entries.length; i++) {
+ if (entries[i].isIntersecting) {
+ var d = (delay && delay !== null) ? delay : 0;
+ setTimeout(function() { K.callFn(callback, []); }, d);
+ if (once) obs.unobserve(el);
+ }
+ }
+ });
+ obs.observe(el);
+ return obs;
+ });
+
+ // ================================================================
+ // Load SX web libraries and adapters
+ // ================================================================
+
+ // Load order follows dependency graph:
+ // 1. Core spec files (parser, render, primitives already compiled into WASM kernel)
+ // 2. Spec modules: signals, deps, router, page-helpers
+ // 3. Bytecode compiler + VM (for JIT in browser)
+ // 4. Web libraries: dom.sx, browser.sx (built on 8 FFI primitives)
+ // 5. Web adapters: adapter-html, adapter-sx, adapter-dom
+ // 6. Web framework: engine, orchestration, boot
+
+ var _baseUrl = "";
+
+ // Detect base URL and cache-bust params from current script tag.
+ // _cacheBust comes from the script's own ?v= query string (used for .sx source fallback).
+ // _sxbcCacheBust comes from data-sxbc-hash attribute — a separate content hash
+ // covering all .sxbc files so each file gets its own correct cache buster.
+ var _cacheBust = "";
+ var _sxbcCacheBust = "";
+ (function() {
+ if (typeof document !== "undefined") {
+ var scripts = document.getElementsByTagName("script");
+ for (var i = scripts.length - 1; i >= 0; i--) {
+ var src = scripts[i].src || "";
+ if (src.indexOf("sx-platform") !== -1) {
+ _baseUrl = src.substring(0, src.lastIndexOf("/") + 1);
+ var qi = src.indexOf("?");
+ if (qi !== -1) _cacheBust = src.substring(qi);
+ var sxbcHash = scripts[i].getAttribute("data-sxbc-hash");
+ if (sxbcHash) _sxbcCacheBust = "?v=" + sxbcHash;
+ break;
+ }
+ }
+ }
+ })();
+
+ /**
+ * Deserialize type-tagged JSON constant back to JS value for loadModule.
+ */
+ function deserializeConstant(c) {
+ if (!c || !c.t) return null;
+ switch (c.t) {
+ case 's': return c.v;
+ case 'n': return c.v;
+ case 'b': return c.v;
+ case 'nil': return null;
+ case 'sym': return { _type: 'symbol', name: c.v };
+ case 'kw': return { _type: 'keyword', name: c.v };
+ case 'list': return { _type: 'list', items: (c.v || []).map(deserializeConstant) };
+ case 'code': return {
+ _type: 'dict',
+ bytecode: { _type: 'list', items: c.v.bytecode },
+ constants: { _type: 'list', items: (c.v.constants || []).map(deserializeConstant) },
+ arity: c.v.arity || 0,
+ 'upvalue-count': c.v['upvalue-count'] || 0,
+ locals: c.v.locals || 0,
+ };
+ case 'dict': {
+ var d = { _type: 'dict' };
+ for (var k in c.v) d[k] = deserializeConstant(c.v[k]);
+ return d;
+ }
+ default: return null;
+ }
+ }
+
+ /**
+ * Try loading a pre-compiled .sxbc bytecode module (SX text format).
+ * Returns true on success, null on failure (caller falls back to .sx source).
+ */
+ function loadBytecodeFile(path) {
+ var sxbcPath = path.replace(/\.sx$/, '.sxbc');
+ var url = _baseUrl + sxbcPath + _sxbcCacheBust;
+ try {
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", url, false);
+ xhr.send();
+ if (xhr.status !== 200) return null;
+
+ window.__sxbcText = xhr.responseText;
+ var result = K.eval('(load-sxbc (first (parse (host-global "__sxbcText"))))');
+ delete window.__sxbcText;
+ if (typeof result === 'string' && result.indexOf('Error') === 0) {
+ console.warn("[sx-platform] bytecode FAIL " + path + ":", result);
+ return null;
+ }
+ return true;
+ } catch(e) {
+ delete window.__sxbcText;
+ return null;
+ }
+ }
+
+ /**
+ * Load an .sx file synchronously via XHR (boot-time only).
+ * Returns the number of expressions loaded, or an error string.
+ */
+ function loadSxFile(path) {
+ var url = _baseUrl + path + _cacheBust;
+ try {
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", url, false); // synchronous
+ xhr.send();
+ if (xhr.status === 200) {
+ var result = K.load(xhr.responseText);
+ if (typeof result === "string" && result.indexOf("Error") === 0) {
+ console.error("[sx-platform] FAIL " + path + ":", result);
+ return 0;
+ }
+ console.log("[sx-platform] ok " + path + " (" + result + " exprs)");
+ return result;
+ } else {
+ console.error("[sx] Failed to fetch " + path + ": HTTP " + xhr.status);
+ return null;
+ }
+ } catch(e) {
+ console.error("[sx] Failed to load " + path + ":", e);
+ return null;
+ }
+ }
+
+ /**
+ * Load all web adapter .sx files in dependency order.
+ * Tries pre-compiled bytecode first, falls back to source.
+ */
+ function loadWebStack() {
+ var files = [
+ // Spec modules
+ "sx/render.sx",
+ "sx/core-signals.sx",
+ "sx/signals.sx",
+ "sx/deps.sx",
+ "sx/router.sx",
+ "sx/page-helpers.sx",
+ // Freeze scope (signal persistence) + highlight (syntax coloring)
+ "sx/freeze.sx",
+ "sx/highlight.sx",
+ // Bytecode compiler + VM
+ "sx/bytecode.sx",
+ "sx/compiler.sx",
+ "sx/vm.sx",
+ // Web libraries (use 8 FFI primitives)
+ "sx/dom.sx",
+ "sx/browser.sx",
+ // Web adapters
+ "sx/adapter-html.sx",
+ "sx/adapter-sx.sx",
+ "sx/adapter-dom.sx",
+ // Boot helpers (platform functions in pure SX)
+ "sx/boot-helpers.sx",
+ "sx/hypersx.sx",
+ // Test harness (for inline test runners)
+ "sx/harness.sx",
+ "sx/harness-reactive.sx",
+ "sx/harness-web.sx",
+ // Web framework
+ "sx/engine.sx",
+ "sx/orchestration.sx",
+ "sx/boot.sx",
+ ];
+
+ var loaded = 0, bcCount = 0, srcCount = 0;
+ if (K.beginModuleLoad) K.beginModuleLoad();
+ for (var i = 0; i < files.length; i++) {
+ var r = loadBytecodeFile(files[i]);
+ if (r) { bcCount++; continue; }
+ // Bytecode not available — end batch, load source, restart batch
+ if (K.endModuleLoad) K.endModuleLoad();
+ r = loadSxFile(files[i]);
+ if (typeof r === "number") { loaded += r; srcCount++; }
+ if (K.beginModuleLoad) K.beginModuleLoad();
+ }
+ if (K.endModuleLoad) K.endModuleLoad();
+ console.log("[sx-platform] Loaded " + files.length + " files (" + bcCount + " bytecode, " + srcCount + " source, " + loaded + " exprs)");
+ return loaded;
+ }
+
+ // ================================================================
+ // Compatibility shim — expose Sx global matching current JS API
+ // ================================================================
+
+ globalThis.Sx = {
+ VERSION: "wasm-1.0",
+ parse: function(src) { return K.parse(src); },
+ eval: function(src) { return K.eval(src); },
+ load: function(src) { return K.load(src); },
+ renderToHtml: function(expr) { return K.renderToHtml(expr); },
+ callFn: function(fn, args) { return K.callFn(fn, args); },
+ engine: function() { return K.engine(); },
+ // Boot entry point (called by auto-init or manually)
+ init: function() {
+ if (typeof K.eval === "function") {
+ // Check boot-init exists
+ // Step through boot manually
+ console.log("[sx] init-css-tracking...");
+ K.eval("(init-css-tracking)");
+ console.log("[sx] process-page-scripts...");
+ K.eval("(process-page-scripts)");
+ console.log("[sx] routes after pages:", K.eval("(len _page-routes)"));
+ console.log("[sx] process-sx-scripts...");
+ K.eval("(process-sx-scripts nil)");
+ console.log("[sx] sx-hydrate-elements...");
+ K.eval("(sx-hydrate-elements nil)");
+ console.log("[sx] sx-hydrate-islands...");
+ K.eval("(sx-hydrate-islands nil)");
+ console.log("[sx] process-elements...");
+ K.eval("(process-elements nil)");
+ // Debug islands
+ console.log("[sx] ~home/stepper defined?", K.eval("(type-of ~home/stepper)"));
+ console.log("[sx] ~layouts/header defined?", K.eval("(type-of ~layouts/header)"));
+ // Island count (JS-side, avoids VM overhead)
+ console.log("[sx] manual island query:", document.querySelectorAll("[data-sx-island]").length);
+ // Try hydrating again
+ console.log("[sx] retry hydrate-islands...");
+ K.eval("(sx-hydrate-islands nil)");
+ // Check if links are boosted
+ var links = document.querySelectorAll("a[href]");
+ var boosted = 0;
+ for (var i = 0; i < links.length; i++) {
+ if (links[i]._sxBoundboost) boosted++;
+ }
+ console.log("[sx] boosted links:", boosted, "/", links.length);
+ // Check island state
+ var islands = document.querySelectorAll("[data-sx-island]");
+ console.log("[sx] islands:", islands.length);
+ for (var j = 0; j < islands.length; j++) {
+ console.log("[sx] island:", islands[j].getAttribute("data-sx-island"),
+ "hydrated:", !!islands[j]._sxBoundislandhydrated || !!islands[j]["_sxBound" + "island-hydrated"],
+ "children:", islands[j].children.length);
+ }
+ console.log("[sx] boot done");
+ }
+ }
+ };
+
+ // ================================================================
+ // Auto-init: load web stack and boot on DOMContentLoaded
+ // ================================================================
+
+ if (typeof document !== "undefined") {
+ var _doInit = function() {
+ loadWebStack();
+ Sx.init();
+ // Enable JIT after all boot code has run
+ setTimeout(function() { K.eval('(enable-jit!)'); }, 0);
+ };
+
+ if (document.readyState === "loading") {
+ document.addEventListener("DOMContentLoaded", _doInit);
+ } else {
+ _doInit();
+ }
+ }
+
+ } // end boot
+
+ // SxKernel is available synchronously (js_of_ocaml) or after async
+ // WASM init. Poll briefly to handle both cases.
+ var K = globalThis.SxKernel;
+ if (K) { boot(K); return; }
+ var tries = 0;
+ var poll = setInterval(function() {
+ K = globalThis.SxKernel;
+ if (K) { clearInterval(poll); boot(K); }
+ else if (++tries > 100) { clearInterval(poll); console.error("[sx-platform] SxKernel not found after 5s"); }
+ }, 50);
+})();
diff --git a/shared/static/wasm/sx/adapter-sx.sx b/shared/static/wasm/sx/adapter-sx.sx
index 9382231f..9e4aece1 100644
--- a/shared/static/wasm/sx/adapter-sx.sx
+++ b/shared/static/wasm/sx/adapter-sx.sx
@@ -437,17 +437,9 @@
(let
((f (trampoline (eval-expr (first args) env)))
(coll (trampoline (eval-expr (nth args 1) env))))
- (map
- (fn
- (item)
- (if
- (lambda? f)
- (let
- ((local (env-merge (lambda-closure f) env)))
- (env-bind! local (first (lambda-params f)) item)
- (aser (lambda-body f) local))
- (cek-call f (list item))))
- coll))
+ (let
+ ((results (map (fn (item) (if (lambda? f) (let ((local (env-extend (lambda-closure f)))) (env-bind! local (first (lambda-params f)) item) (aser (lambda-body f) local)) (cek-call f (list item)))) coll)))
+ (aser-fragment results env)))
(= name "map-indexed")
(let
((f (trampoline (eval-expr (first args) env)))
diff --git a/shared/static/wasm/sx/adapter-sx.sxbc b/shared/static/wasm/sx/adapter-sx.sxbc
index 59f77473..2f305e77 100644
--- a/shared/static/wasm/sx/adapter-sx.sxbc
+++ b/shared/static/wasm/sx/adapter-sx.sxbc
@@ -1,3 +1,3 @@
-(sxbc 1 "0ae6608c929d5217"
+(sxbc 1 "4e34c093941bf2ca"
(code
- :constants ("render-to-sx" {:upvalue-count 0 :arity 2 :constants ("aser" "=" "type-of" "sx-expr" "sx-expr-source" "string" "serialize") :bytecode (20 0 0 16 0 16 1 48 2 17 2 16 2 52 2 0 1 1 3 0 52 1 0 2 33 10 0 20 4 0 16 2 49 1 32 27 0 16 2 52 2 0 1 1 5 0 52 1 0 2 33 5 0 16 2 32 6 0 16 2 52 6 0 1 50)} "aser" {:upvalue-count 0 :arity 2 :constants ("set-render-active!" "type-of" "number" "=" "string" "boolean" "nil" "symbol" "symbol-name" "env-has?" "env-get" "primitive?" "get-primitive" "true" "false" "error" "str" "Undefined symbol: " "keyword" "keyword-name" "list" "empty?" "aser-list" "spread" "scope-emit!" "element-attrs" "spread-attrs" "spread?") :bytecode (20 0 0 3 48 1 5 16 0 52 1 0 1 6 1 2 0 52 3 0 2 33 6 0 5 16 0 32 16 1 6 1 4 0 52 3 0 2 33 6 0 5 16 0 32 255 0 6 1 5 0 52 3 0 2 33 6 0 5 16 0 32 238 0 6 1 6 0 52 3 0 2 33 5 0 5 2 32 222 0 6 1 7 0 52 3 0 2 33 116 0 5 20 8 0 16 0 48 1 17 3 20 9 0 16 1 16 3 48 2 33 12 0 20 10 0 16 1 16 3 48 2 32 79 0 16 3 52 11 0 1 33 9 0 16 3 52 12 0 1 32 61 0 16 3 1 13 0 52 3 0 2 33 4 0 3 32 45 0 16 3 1 14 0 52 3 0 2 33 4 0 4 32 29 0 16 3 1 6 0 52 3 0 2 33 4 0 2 32 13 0 1 17 0 16 3 52 16 0 2 52 15 0 1 32 95 0 6 1 18 0 52 3 0 2 33 11 0 5 20 19 0 16 0 48 1 32 73 0 6 1 20 0 52 3 0 2 33 29 0 5 16 0 52 21 0 1 33 7 0 52 20 0 0 32 9 0 20 22 0 16 0 16 1 48 2 32 33 0 6 1 23 0 52 3 0 2 33 19 0 5 1 25 0 16 0 52 26 0 1 52 24 0 2 5 2 32 3 0 5 16 0 17 2 16 2 52 27 0 1 33 18 0 1 25 0 16 2 52 26 0 1 52 24 0 2 5 2 32 2 0 16 2 50)} "aser-list" {:upvalue-count 0 :arity 2 :constants ("first" "rest" "not" "=" "type-of" "symbol" "map" {:upvalue-count 1 :arity 1 :constants ("aser") :bytecode (20 0 0 16 0 18 0 49 2 50)} "symbol-name" "<>" "aser-fragment" "raw!" "aser-call" "starts-with?" "~" "env-has?" "env-get" "expand-components?" "macro?" "aser" "expand-macro" "component?" "island?" "component-affinity" "server" "client" "aser-expand-component" "lake" "marsh" "error-boundary" ">" "len" 1 "try-catch" {:upvalue-count 2 :arity 0 :constants ("join" "" "map" {:upvalue-count 1 :arity 1 :constants ("aser" "=" "type-of" "sx-expr" "sx-expr-source" "nil?" "" "serialize") :bytecode (20 0 0 16 0 18 0 48 2 17 1 16 1 52 2 0 1 1 3 0 52 1 0 2 33 10 0 20 4 0 16 1 49 1 32 21 0 16 1 52 5 0 1 33 6 0 1 6 0 32 6 0 16 1 52 7 0 1 50)}) :bytecode (1 1 0 51 3 0 0 0 18 1 52 2 0 2 52 0 0 2 50)} {:upvalue-count 1 :arity 1 :constants ("str") :bytecode (16 0 52 0 0 1 19 0 5 2 50)} "make-sx-expr" "str" "(error-boundary " ")" "(div :data-sx-boundary \"true\" " "(div :class \"sx-render-error\" " ":style \"color:red;font-size:0.875rem;padding:0.5rem;border:1px solid red;border-radius:0.25rem;margin:0.5rem 0;\" " "\"Render error: " "replace" "\"" "'" "\\" "\\\\" "\"))" "contains?" "HTML_TAGS" "special-form?" "ho-form?" "aser-special" "trampoline" "eval-expr" {:upvalue-count 1 :arity 1 :constants ("trampoline" "eval-expr") :bytecode (20 0 0 20 1 0 16 0 18 0 48 2 49 1 50)} "callable?" "lambda?" "apply" "call-lambda" "component-name" "error" "Not callable: " "inspect") :bytecode (16 0 52 0 0 1 17 2 16 0 52 1 0 1 17 3 16 2 52 4 0 1 1 5 0 52 3 0 2 52 2 0 1 33 14 0 51 7 0 1 1 16 0 52 6 0 2 32 20 3 20 8 0 16 2 48 1 17 4 16 4 1 9 0 52 3 0 2 33 12 0 20 10 0 16 3 16 1 49 2 32 243 2 16 4 1 11 0 52 3 0 2 33 15 0 20 12 0 1 11 0 16 3 16 1 49 3 32 216 2 16 4 1 14 0 52 13 0 2 33 196 0 20 15 0 16 1 16 4 48 2 33 12 0 20 16 0 16 1 16 4 48 2 32 1 0 2 17 5 20 15 0 16 1 1 17 0 48 2 33 8 0 20 17 0 48 0 32 1 0 4 17 6 16 5 6 33 7 0 5 16 5 52 18 0 1 33 21 0 20 19 0 20 20 0 16 5 16 3 16 1 48 3 16 1 49 2 32 105 0 16 5 6 33 71 0 5 16 5 52 21 0 1 6 33 60 0 5 16 5 52 22 0 1 52 2 0 1 6 33 45 0 5 16 6 6 34 15 0 5 20 23 0 16 5 48 1 1 24 0 52 3 0 2 6 33 19 0 5 20 23 0 16 5 48 1 1 25 0 52 3 0 2 52 2 0 1 33 14 0 20 26 0 16 5 16 3 16 1 49 3 32 11 0 20 12 0 16 4 16 3 16 1 49 3 32 8 2 16 4 1 27 0 52 3 0 2 33 14 0 20 12 0 16 4 16 3 16 1 49 3 32 238 1 16 4 1 28 0 52 3 0 2 33 14 0 20 12 0 16 4 16 3 16 1 49 3 32 212 1 16 4 1 29 0 52 3 0 2 33 128 0 16 3 52 31 0 1 1 32 0 52 30 0 2 17 5 16 5 33 9 0 16 3 52 1 0 1 32 2 0 16 3 17 6 2 17 7 51 34 0 1 1 1 6 51 35 0 1 7 52 33 0 2 17 8 16 8 33 20 0 20 36 0 1 38 0 16 8 1 39 0 52 37 0 3 49 1 32 46 0 20 36 0 1 40 0 1 41 0 1 42 0 1 43 0 16 7 1 45 0 1 46 0 52 44 0 3 1 47 0 1 48 0 52 44 0 3 1 49 0 52 37 0 6 49 1 32 72 1 20 51 0 16 4 52 50 0 2 33 14 0 20 12 0 16 4 16 3 16 1 49 3 32 46 1 20 52 0 16 4 48 1 6 34 8 0 5 20 53 0 16 4 48 1 33 14 0 20 54 0 16 4 16 0 16 1 49 3 32 10 1 20 15 0 16 1 16 4 48 2 6 33 14 0 5 20 16 0 16 1 16 4 48 2 52 18 0 1 33 28 0 20 19 0 20 20 0 20 16 0 16 1 16 4 48 2 16 3 16 1 48 3 16 1 49 2 32 208 0 20 55 0 20 56 0 16 2 16 1 48 2 48 1 17 5 51 57 0 1 1 16 3 52 6 0 2 17 6 20 58 0 16 5 48 1 6 33 41 0 5 16 5 52 59 0 1 52 2 0 1 6 33 26 0 5 16 5 52 21 0 1 52 2 0 1 6 33 11 0 5 16 5 52 22 0 1 52 2 0 1 33 11 0 16 5 16 6 52 60 0 2 32 113 0 16 5 52 59 0 1 33 19 0 20 55 0 20 61 0 16 5 16 6 16 1 48 3 49 1 32 85 0 16 5 52 21 0 1 33 25 0 20 12 0 1 14 0 16 5 52 62 0 1 52 37 0 2 16 3 16 1 49 3 32 51 0 16 5 52 22 0 1 33 25 0 20 12 0 1 14 0 16 5 52 62 0 1 52 37 0 2 16 3 16 1 49 3 32 17 0 1 64 0 16 5 52 65 0 1 52 37 0 2 52 63 0 1 50)} "aser-reserialize" {:upvalue-count 0 :arity 1 :constants ("not" "=" "type-of" "list" "serialize" "empty?" "()" "first" "symbol" "symbol-name" "rest" 0 "for-each" {:upvalue-count 4 :arity 1 :constants ("inc" "=" "type-of" "string" "<" "len" "not" "contains?" " " "starts-with?" "class" "id" "sx-" "data-" "style" "href" "src" "type" "name" "value" "placeholder" "action" "method" "target" "role" "for" "on" "append!" "str" ":" "serialize" "nth" "aser-reserialize") :bytecode (18 0 33 15 0 4 19 0 5 18 1 52 0 0 1 19 1 32 116 1 16 0 52 2 0 1 1 3 0 52 1 0 2 6 33 17 1 5 18 1 52 0 0 1 18 2 52 5 0 1 52 4 0 2 6 33 252 0 5 16 0 1 8 0 52 7 0 2 52 6 0 1 6 33 234 0 5 16 0 1 10 0 52 9 0 2 6 34 220 0 5 16 0 1 11 0 52 9 0 2 6 34 206 0 5 16 0 1 12 0 52 9 0 2 6 34 192 0 5 16 0 1 13 0 52 9 0 2 6 34 178 0 5 16 0 1 14 0 52 9 0 2 6 34 164 0 5 16 0 1 15 0 52 9 0 2 6 34 150 0 5 16 0 1 16 0 52 9 0 2 6 34 136 0 5 16 0 1 17 0 52 9 0 2 6 34 122 0 5 16 0 1 18 0 52 9 0 2 6 34 108 0 5 16 0 1 19 0 52 9 0 2 6 34 94 0 5 16 0 1 20 0 52 9 0 2 6 34 80 0 5 16 0 1 21 0 52 9 0 2 6 34 66 0 5 16 0 1 22 0 52 9 0 2 6 34 52 0 5 16 0 1 23 0 52 9 0 2 6 34 38 0 5 16 0 1 24 0 52 9 0 2 6 34 24 0 5 16 0 1 25 0 52 9 0 2 6 34 10 0 5 16 0 1 26 0 52 9 0 2 33 56 0 20 27 0 18 3 1 29 0 16 0 52 28 0 2 48 2 5 20 27 0 18 3 18 2 18 1 52 0 0 1 52 31 0 2 52 30 0 1 48 2 5 3 19 0 5 18 1 52 0 0 1 19 1 32 23 0 20 27 0 18 3 20 32 0 16 0 48 1 48 2 5 18 1 52 0 0 1 19 1 50)} "str" "(" "join" " " ")") :bytecode (16 0 52 2 0 1 1 3 0 52 1 0 2 52 0 0 1 33 9 0 16 0 52 4 0 1 32 122 0 16 0 52 5 0 1 33 6 0 1 6 0 32 107 0 16 0 52 7 0 1 17 1 16 1 52 2 0 1 1 8 0 52 1 0 2 52 0 0 1 33 9 0 16 0 52 4 0 1 32 70 0 20 9 0 16 1 48 1 17 2 16 2 52 3 0 1 17 3 16 0 52 10 0 1 17 4 4 17 5 1 11 0 17 6 51 13 0 1 5 1 6 1 4 1 3 16 4 52 12 0 2 5 1 15 0 1 17 0 16 3 52 16 0 2 1 18 0 52 14 0 3 50)} "aser-fragment" {:upvalue-count 0 :arity 2 :constants ("list" "for-each" {:upvalue-count 2 :arity 1 :constants ("aser" "nil?" "=" "type-of" "sx-expr" "append!" "sx-expr-source" "list" "for-each" {:upvalue-count 1 :arity 1 :constants ("not" "nil?" "=" "type-of" "sx-expr" "append!" "sx-expr-source" "aser-reserialize") :bytecode (16 0 52 1 0 1 52 0 0 1 33 50 0 16 0 52 3 0 1 1 4 0 52 2 0 2 33 17 0 20 5 0 18 0 20 6 0 16 0 48 1 49 2 32 14 0 20 5 0 18 0 20 7 0 16 0 48 1 49 2 32 1 0 2 50)} "serialize") :bytecode (20 0 0 16 0 18 0 48 2 17 1 16 1 52 1 0 1 33 4 0 2 32 76 0 16 1 52 3 0 1 1 4 0 52 2 0 2 33 17 0 20 5 0 18 1 20 6 0 16 1 48 1 49 2 32 43 0 16 1 52 3 0 1 1 7 0 52 2 0 2 33 14 0 51 9 0 0 1 16 1 52 8 0 2 32 13 0 20 5 0 18 1 16 1 52 10 0 1 49 2 50)} "empty?" "" "=" "len" 1 "make-sx-expr" "first" "str" "(<> " "join" " " ")") :bytecode (52 0 0 0 17 2 51 2 0 1 1 1 2 16 0 52 1 0 2 5 16 2 52 3 0 1 33 6 0 1 4 0 32 54 0 16 2 52 6 0 1 1 7 0 52 5 0 2 33 14 0 20 8 0 16 2 52 9 0 1 49 1 32 24 0 20 8 0 1 11 0 1 13 0 16 2 52 12 0 2 1 14 0 52 10 0 3 49 1 50)} "aser-call" {:upvalue-count 0 :arity 3 :constants ("list" 0 "scope-push!" "element-attrs" "for-each" {:upvalue-count 6 :arity 1 :constants ("inc" "=" "type-of" "keyword" "<" "len" "aser" "nth" "not" "nil?" "append!" "str" ":" "keyword-name" "sx-expr" "sx-expr-source" "serialize" "list" "for-each" {:upvalue-count 1 :arity 1 :constants ("not" "nil?" "=" "type-of" "sx-expr" "append!" "sx-expr-source" "serialize") :bytecode (16 0 52 1 0 1 52 0 0 1 33 49 0 16 0 52 3 0 1 1 4 0 52 2 0 2 33 17 0 20 5 0 18 0 20 6 0 16 0 48 1 49 2 32 13 0 20 5 0 18 0 16 0 52 7 0 1 49 2 32 1 0 2 50)}) :bytecode (18 0 33 15 0 4 19 0 5 18 1 52 0 0 1 19 1 32 16 1 16 0 52 2 0 1 1 3 0 52 1 0 2 6 33 17 0 5 18 1 52 0 0 1 18 2 52 5 0 1 52 4 0 2 33 122 0 20 6 0 18 2 18 1 52 0 0 1 52 7 0 2 18 3 48 2 17 1 16 1 52 9 0 1 52 8 0 1 33 71 0 20 10 0 18 4 1 12 0 20 13 0 16 0 48 1 52 11 0 2 48 2 5 16 1 52 2 0 1 1 14 0 52 1 0 2 33 17 0 20 10 0 18 4 20 15 0 16 1 48 1 48 2 32 13 0 20 10 0 18 4 16 1 52 16 0 1 48 2 32 1 0 2 5 3 19 0 5 18 1 52 0 0 1 19 1 32 113 0 20 6 0 16 0 18 3 48 2 17 1 16 1 52 9 0 1 52 8 0 1 33 79 0 16 1 52 2 0 1 1 14 0 52 1 0 2 33 17 0 20 10 0 18 5 20 15 0 16 1 48 1 48 2 32 43 0 16 1 52 2 0 1 1 17 0 52 1 0 2 33 14 0 51 19 0 0 5 16 1 52 18 0 2 32 13 0 20 10 0 18 5 16 1 52 16 0 1 48 2 32 1 0 2 5 18 1 52 0 0 1 19 1 50)} {:upvalue-count 1 :arity 1 :constants ("for-each" {:upvalue-count 2 :arity 1 :constants ("dict-get" "append!" "str" ":" "serialize") :bytecode (18 0 16 0 52 0 0 2 17 1 20 1 0 18 1 1 3 0 16 0 52 2 0 2 48 2 5 20 1 0 18 1 16 1 52 4 0 1 49 2 50)} "keys") :bytecode (51 1 0 1 0 0 0 16 0 52 2 0 1 52 0 0 2 50)} "scope-peek" "scope-pop!" "concat" "make-sx-expr" "str" "(" "join" " " ")") :bytecode (52 0 0 0 17 3 52 0 0 0 17 4 4 17 5 1 1 0 17 6 1 3 0 2 52 2 0 2 5 51 5 0 1 5 1 6 1 1 1 2 1 3 1 4 16 1 52 4 0 2 5 51 6 0 1 3 1 3 0 52 7 0 1 52 4 0 2 5 1 3 0 52 8 0 1 5 16 0 52 0 0 1 16 3 16 4 52 9 0 3 17 7 20 10 0 1 12 0 1 14 0 16 7 52 13 0 2 1 15 0 52 11 0 3 49 1 50)} "aser-expand-component" {:upvalue-count 0 :arity 3 :constants ("component-params" "env-merge" "component-closure" 0 "list" "for-each" {:upvalue-count 1 :arity 1 :constants ("env-bind!") :bytecode (20 0 0 18 0 16 0 2 49 3 50)} {:upvalue-count 6 :arity 1 :constants ("inc" "=" "type-of" "keyword" "<" "len" "env-bind!" "keyword-name" "aser" "nth" "append!") :bytecode (18 0 33 15 0 4 19 0 5 18 1 52 0 0 1 19 1 32 104 0 16 0 52 2 0 1 1 3 0 52 1 0 2 6 33 17 0 5 18 1 52 0 0 1 18 2 52 5 0 1 52 4 0 2 33 49 0 20 6 0 18 3 20 7 0 16 0 48 1 20 8 0 18 2 18 1 52 0 0 1 52 9 0 2 18 4 48 2 48 3 5 3 19 0 5 18 1 52 0 0 1 19 1 32 18 0 20 10 0 18 5 16 0 48 2 5 18 1 52 0 0 1 19 1 50)} "component-has-children" "map" {:upvalue-count 1 :arity 1 :constants ("aser") :bytecode (20 0 0 16 0 18 0 49 2 50)} "env-bind!" "children" "=" "len" 1 "first" "aser" "component-body") :bytecode (16 0 52 0 0 1 17 3 20 1 0 16 2 16 0 52 2 0 1 48 2 17 4 1 3 0 17 5 4 17 6 52 4 0 0 17 7 51 6 0 1 4 16 3 52 5 0 2 5 51 7 0 1 6 1 5 1 1 1 4 1 2 1 7 16 1 52 5 0 2 5 20 8 0 16 0 48 1 33 53 0 51 10 0 1 2 16 7 52 9 0 2 17 8 20 11 0 16 4 1 12 0 16 8 52 14 0 1 1 15 0 52 13 0 2 33 9 0 16 8 52 16 0 1 32 2 0 16 8 48 3 32 1 0 2 5 20 17 0 16 0 52 18 0 1 16 4 49 2 50)} "SPECIAL_FORM_NAMES" "list" "if" "when" "cond" "case" "and" "or" "let" "let*" "lambda" "fn" "define" "defcomp" "defmacro" "defstyle" "defhandler" "defpage" "defquery" "defaction" "defrelation" "begin" "do" "quote" "quasiquote" "->" "set!" "letrec" "dynamic-wind" "defisland" "deftype" "defeffect" "scope" "provide" "context" "emit!" "emitted" "HO_FORM_NAMES" "map" "map-indexed" "filter" "reduce" "some" "every?" "for-each" "special-form?" {:upvalue-count 0 :arity 1 :constants ("contains?" "SPECIAL_FORM_NAMES") :bytecode (20 1 0 16 0 52 0 0 2 50)} "ho-form?" {:upvalue-count 0 :arity 1 :constants ("contains?" "HO_FORM_NAMES") :bytecode (20 1 0 16 0 52 0 0 2 50)} "aser-special" {:upvalue-count 0 :arity 3 :constants ("rest" "=" "if" "trampoline" "eval-expr" "first" "aser" "nth" 1 ">" "len" 2 "when" "not" "for-each" {:upvalue-count 2 :arity 1 :constants ("aser") :bytecode (20 0 0 16 0 18 1 48 2 19 0 50)} "cond" "eval-cond" "case" "eval-case-aser" "let" "let*" "process-bindings" "begin" "do" "and" "some" {:upvalue-count 2 :arity 1 :constants ("trampoline" "eval-expr" "not") :bytecode (20 0 0 20 1 0 16 0 18 1 48 2 48 1 19 0 5 18 0 52 2 0 1 50)} "or" {:upvalue-count 2 :arity 1 :constants ("trampoline" "eval-expr") :bytecode (20 0 0 20 1 0 16 0 18 1 48 2 48 1 19 0 5 18 0 50)} "map" {:upvalue-count 2 :arity 1 :constants ("lambda?" "env-merge" "lambda-closure" "env-bind!" "first" "lambda-params" "aser" "lambda-body" "cek-call" "list") :bytecode (18 0 52 0 0 1 33 51 0 20 1 0 18 0 52 2 0 1 18 1 48 2 17 1 20 3 0 16 1 18 0 52 5 0 1 52 4 0 1 16 0 48 3 5 20 6 0 18 0 52 7 0 1 16 1 49 2 32 13 0 20 8 0 18 0 16 0 52 9 0 1 49 2 50)} "map-indexed" {:upvalue-count 2 :arity 2 :constants ("lambda?" "env-merge" "lambda-closure" "env-bind!" "first" "lambda-params" "nth" 1 "aser" "lambda-body" "cek-call" "list") :bytecode (18 0 52 0 0 1 33 74 0 20 1 0 18 0 52 2 0 1 18 1 48 2 17 2 20 3 0 16 2 18 0 52 5 0 1 52 4 0 1 16 0 48 3 5 20 3 0 16 2 18 0 52 5 0 1 1 7 0 52 6 0 2 16 1 48 3 5 20 8 0 18 0 52 9 0 1 16 2 49 2 32 15 0 20 10 0 18 0 16 0 16 1 52 11 0 2 49 2 50)} "list" {:upvalue-count 3 :arity 1 :constants ("lambda?" "env-merge" "lambda-closure" "env-bind!" "first" "lambda-params" "append!" "aser" "lambda-body" "cek-call" "list") :bytecode (18 0 52 0 0 1 33 58 0 20 1 0 18 0 52 2 0 1 18 1 48 2 17 1 20 3 0 16 1 18 0 52 5 0 1 52 4 0 1 16 0 48 3 5 20 6 0 18 2 20 7 0 18 0 52 8 0 1 16 1 48 2 49 2 32 13 0 20 9 0 18 0 16 0 52 10 0 1 49 2 50)} "empty?" "defisland" "serialize" "define" "defcomp" "defmacro" "defstyle" "defhandler" "defpage" "defquery" "defaction" "defrelation" "deftype" "defeffect" "scope" ">=" "type-of" "keyword" "keyword-name" "value" "slice" "scope-push!" "scope-pop!" "provide" "context" "scope-peek" "nil?" "emit!" "scope-emit!" "emitted") :bytecode (16 1 52 0 0 1 17 3 16 0 1 2 0 52 1 0 2 33 79 0 20 3 0 20 4 0 16 3 52 5 0 1 16 2 48 2 48 1 33 19 0 20 6 0 16 3 1 8 0 52 7 0 2 16 2 49 2 32 36 0 16 3 52 10 0 1 1 11 0 52 9 0 2 33 19 0 20 6 0 16 3 1 11 0 52 7 0 2 16 2 49 2 32 1 0 2 32 34 5 16 0 1 12 0 52 1 0 2 33 55 0 20 3 0 20 4 0 16 3 52 5 0 1 16 2 48 2 48 1 52 13 0 1 33 4 0 2 32 23 0 2 17 4 51 15 0 1 4 1 2 16 3 52 0 0 1 52 14 0 2 5 16 4 32 223 4 16 0 1 16 0 52 1 0 2 33 32 0 20 17 0 16 3 16 2 48 2 17 4 16 4 33 12 0 20 6 0 16 4 16 2 49 2 32 1 0 2 32 179 4 16 0 1 18 0 52 1 0 2 33 42 0 20 3 0 20 4 0 16 3 52 5 0 1 16 2 48 2 48 1 17 4 16 3 52 0 0 1 17 5 20 19 0 16 4 16 5 16 2 49 3 32 125 4 16 0 1 20 0 52 1 0 2 6 34 10 0 5 16 0 1 21 0 52 1 0 2 33 41 0 20 22 0 16 3 52 5 0 1 16 2 48 2 17 4 2 17 5 51 15 0 1 5 1 4 16 3 52 0 0 1 52 14 0 2 5 16 5 32 58 4 16 0 1 23 0 52 1 0 2 6 34 10 0 5 16 0 1 24 0 52 1 0 2 33 22 0 2 17 4 51 15 0 1 4 1 2 16 3 52 14 0 2 5 16 4 32 10 4 16 0 1 25 0 52 1 0 2 33 22 0 3 17 4 51 27 0 1 4 1 2 16 3 52 26 0 2 5 16 4 32 232 3 16 0 1 28 0 52 1 0 2 33 22 0 4 17 4 51 29 0 1 4 1 2 16 3 52 26 0 2 5 16 4 32 198 3 16 0 1 30 0 52 1 0 2 33 59 0 20 3 0 20 4 0 16 3 52 5 0 1 16 2 48 2 48 1 17 4 20 3 0 20 4 0 16 3 1 8 0 52 7 0 2 16 2 48 2 48 1 17 5 51 31 0 1 4 1 2 16 5 52 30 0 2 32 127 3 16 0 1 32 0 52 1 0 2 33 59 0 20 3 0 20 4 0 16 3 52 5 0 1 16 2 48 2 48 1 17 4 20 3 0 20 4 0 16 3 1 8 0 52 7 0 2 16 2 48 2 48 1 17 5 51 33 0 1 4 1 2 16 5 52 32 0 2 32 56 3 16 0 1 14 0 52 1 0 2 33 83 0 20 3 0 20 4 0 16 3 52 5 0 1 16 2 48 2 48 1 17 4 20 3 0 20 4 0 16 3 1 8 0 52 7 0 2 16 2 48 2 48 1 17 5 52 34 0 0 17 6 51 35 0 1 4 1 2 1 6 16 5 52 14 0 2 5 16 6 52 36 0 1 33 4 0 2 32 2 0 16 6 32 217 2 16 0 1 37 0 52 1 0 2 33 24 0 20 3 0 20 4 0 16 1 16 2 48 2 48 1 5 16 1 52 38 0 1 32 181 2 16 0 1 39 0 52 1 0 2 6 34 136 0 5 16 0 1 40 0 52 1 0 2 6 34 122 0 5 16 0 1 41 0 52 1 0 2 6 34 108 0 5 16 0 1 42 0 52 1 0 2 6 34 94 0 5 16 0 1 43 0 52 1 0 2 6 34 80 0 5 16 0 1 44 0 52 1 0 2 6 34 66 0 5 16 0 1 45 0 52 1 0 2 6 34 52 0 5 16 0 1 46 0 52 1 0 2 6 34 38 0 5 16 0 1 47 0 52 1 0 2 6 34 24 0 5 16 0 1 48 0 52 1 0 2 6 34 10 0 5 16 0 1 49 0 52 1 0 2 33 19 0 20 3 0 20 4 0 16 1 16 2 48 2 48 1 5 2 32 10 2 16 0 1 50 0 52 1 0 2 33 176 0 20 3 0 20 4 0 16 3 52 5 0 1 16 2 48 2 48 1 17 4 16 3 52 0 0 1 17 5 2 17 6 2 17 7 16 5 52 10 0 1 1 11 0 52 51 0 2 6 33 41 0 5 16 5 52 5 0 1 52 52 0 1 1 53 0 52 1 0 2 6 33 19 0 5 20 54 0 16 5 52 5 0 1 48 1 1 55 0 52 1 0 2 33 38 0 20 3 0 20 4 0 16 5 1 8 0 52 7 0 2 16 2 48 2 48 1 17 6 5 16 5 1 11 0 52 56 0 2 17 7 32 4 0 16 5 17 7 5 16 4 16 6 52 57 0 2 5 2 17 8 51 15 0 1 8 1 2 16 7 52 14 0 2 5 16 4 52 58 0 1 5 16 8 32 78 1 16 0 1 59 0 52 1 0 2 33 88 0 20 3 0 20 4 0 16 3 52 5 0 1 16 2 48 2 48 1 17 4 20 3 0 20 4 0 16 3 1 8 0 52 7 0 2 16 2 48 2 48 1 17 5 2 17 6 16 4 16 5 52 57 0 2 5 51 15 0 1 6 1 2 16 3 1 11 0 52 56 0 2 52 14 0 2 5 16 4 52 58 0 1 5 16 6 32 234 0 16 0 1 60 0 52 1 0 2 33 90 0 20 3 0 20 4 0 16 3 52 5 0 1 16 2 48 2 48 1 17 4 16 3 52 10 0 1 1 11 0 52 51 0 2 33 24 0 20 3 0 20 4 0 16 3 1 8 0 52 7 0 2 16 2 48 2 48 1 32 1 0 2 17 5 16 4 52 61 0 1 17 6 16 6 52 62 0 1 33 5 0 16 5 32 2 0 16 6 32 132 0 16 0 1 63 0 52 1 0 2 33 56 0 20 3 0 20 4 0 16 3 52 5 0 1 16 2 48 2 48 1 17 4 20 3 0 20 4 0 16 3 1 8 0 52 7 0 2 16 2 48 2 48 1 17 5 16 4 16 5 52 64 0 2 5 2 32 64 0 16 0 1 65 0 52 1 0 2 33 38 0 20 3 0 20 4 0 16 3 52 5 0 1 16 2 48 2 48 1 17 4 16 4 52 61 0 1 6 34 5 0 5 52 34 0 0 32 14 0 20 3 0 20 4 0 16 1 16 2 48 2 49 1 50)} "eval-case-aser" {:upvalue-count 0 :arity 3 :constants ("<" "len" 2 "first" "nth" 1 "=" "type-of" "keyword" "keyword-name" "else" "symbol" "symbol-name" ":else" "aser" "trampoline" "eval-expr" "eval-case-aser" "slice") :bytecode (16 1 52 1 0 1 1 2 0 52 0 0 2 33 4 0 2 32 175 0 16 1 52 3 0 1 17 3 16 1 1 5 0 52 4 0 2 17 4 16 3 52 7 0 1 1 8 0 52 6 0 2 6 33 15 0 5 20 9 0 16 3 48 1 1 10 0 52 6 0 2 6 34 52 0 5 16 3 52 7 0 1 1 11 0 52 6 0 2 6 33 34 0 5 20 12 0 16 3 48 1 1 13 0 52 6 0 2 6 34 15 0 5 20 12 0 16 3 48 1 1 10 0 52 6 0 2 33 12 0 20 14 0 16 4 16 2 49 2 32 53 0 16 0 20 15 0 20 16 0 16 3 16 2 48 2 48 1 52 6 0 2 33 12 0 20 14 0 16 4 16 2 49 2 32 18 0 20 17 0 16 0 16 1 1 2 0 52 18 0 2 16 2 49 3 50)}) :bytecode (51 1 0 128 0 0 5 51 3 0 128 2 0 5 51 5 0 128 4 0 5 51 7 0 128 6 0 5 51 9 0 128 8 0 5 51 11 0 128 10 0 5 51 13 0 128 12 0 5 1 16 0 1 17 0 1 18 0 1 19 0 1 20 0 1 21 0 1 22 0 1 23 0 1 24 0 1 25 0 1 26 0 1 27 0 1 28 0 1 29 0 1 30 0 1 31 0 1 32 0 1 33 0 1 34 0 1 35 0 1 36 0 1 37 0 1 38 0 1 39 0 1 40 0 1 41 0 1 42 0 1 43 0 1 44 0 1 45 0 1 46 0 1 47 0 1 48 0 1 49 0 1 50 0 52 15 0 35 128 14 0 5 1 52 0 1 53 0 1 54 0 1 55 0 1 56 0 1 57 0 1 58 0 52 15 0 7 128 51 0 5 51 60 0 128 59 0 5 51 62 0 128 61 0 5 51 64 0 128 63 0 5 51 66 0 128 65 0 50)))
+ :constants ("render-to-sx" {:upvalue-count 0 :arity 2 :constants ("aser" "=" "type-of" "sx-expr" "sx-expr-source" "string" "serialize") :bytecode (20 0 0 16 0 16 1 48 2 17 2 16 2 52 2 0 1 1 3 0 52 1 0 2 33 10 0 20 4 0 16 2 49 1 32 27 0 16 2 52 2 0 1 1 5 0 52 1 0 2 33 5 0 16 2 32 6 0 16 2 52 6 0 1 50)} "aser" {:upvalue-count 0 :arity 2 :constants ("set-render-active!" "type-of" "number" "=" "string" "boolean" "nil" "symbol" "symbol-name" "env-has?" "env-get" "primitive?" "get-primitive" "true" "false" "error" "str" "Undefined symbol: " "keyword" "keyword-name" "list" "empty?" "aser-list" "spread" "scope-emit!" "element-attrs" "spread-attrs" "spread?") :bytecode (20 0 0 3 48 1 5 16 0 52 1 0 1 6 1 2 0 52 3 0 2 33 6 0 5 16 0 32 16 1 6 1 4 0 52 3 0 2 33 6 0 5 16 0 32 255 0 6 1 5 0 52 3 0 2 33 6 0 5 16 0 32 238 0 6 1 6 0 52 3 0 2 33 5 0 5 2 32 222 0 6 1 7 0 52 3 0 2 33 116 0 5 20 8 0 16 0 48 1 17 3 20 9 0 16 1 16 3 48 2 33 12 0 20 10 0 16 1 16 3 48 2 32 79 0 16 3 52 11 0 1 33 9 0 16 3 52 12 0 1 32 61 0 16 3 1 13 0 52 3 0 2 33 4 0 3 32 45 0 16 3 1 14 0 52 3 0 2 33 4 0 4 32 29 0 16 3 1 6 0 52 3 0 2 33 4 0 2 32 13 0 1 17 0 16 3 52 16 0 2 52 15 0 1 32 95 0 6 1 18 0 52 3 0 2 33 11 0 5 20 19 0 16 0 48 1 32 73 0 6 1 20 0 52 3 0 2 33 29 0 5 16 0 52 21 0 1 33 7 0 52 20 0 0 32 9 0 20 22 0 16 0 16 1 48 2 32 33 0 6 1 23 0 52 3 0 2 33 19 0 5 1 25 0 16 0 52 26 0 1 52 24 0 2 5 2 32 3 0 5 16 0 17 2 16 2 52 27 0 1 33 18 0 1 25 0 16 2 52 26 0 1 52 24 0 2 5 2 32 2 0 16 2 50)} "aser-list" {:upvalue-count 0 :arity 2 :constants ("first" "rest" "not" "=" "type-of" "symbol" "map" {:upvalue-count 1 :arity 1 :constants ("aser") :bytecode (20 0 0 16 0 18 0 49 2 50)} "symbol-name" "<>" "aser-fragment" "raw!" "aser-call" "starts-with?" "~" "env-has?" "env-get" "expand-components?" "macro?" "aser" "expand-macro" "component?" "island?" "component-affinity" "server" "client" "aser-expand-component" "lake" "marsh" "error-boundary" ">" "len" 1 "try-catch" {:upvalue-count 2 :arity 0 :constants ("join" "" "map" {:upvalue-count 1 :arity 1 :constants ("aser" "=" "type-of" "sx-expr" "sx-expr-source" "nil?" "" "serialize") :bytecode (20 0 0 16 0 18 0 48 2 17 1 16 1 52 2 0 1 1 3 0 52 1 0 2 33 10 0 20 4 0 16 1 49 1 32 21 0 16 1 52 5 0 1 33 6 0 1 6 0 32 6 0 16 1 52 7 0 1 50)}) :bytecode (1 1 0 51 3 0 0 0 18 1 52 2 0 2 52 0 0 2 50)} {:upvalue-count 1 :arity 1 :constants ("str") :bytecode (16 0 52 0 0 1 19 0 5 2 50)} "make-sx-expr" "str" "(error-boundary " ")" "(div :data-sx-boundary \"true\" " "(div :class \"sx-render-error\" " ":style \"color:red;font-size:0.875rem;padding:0.5rem;border:1px solid red;border-radius:0.25rem;margin:0.5rem 0;\" " "\"Render error: " "replace" "\"" "'" "\\" "\\\\" "\"))" "contains?" "HTML_TAGS" "special-form?" "ho-form?" "aser-special" "trampoline" "eval-expr" {:upvalue-count 1 :arity 1 :constants ("trampoline" "eval-expr") :bytecode (20 0 0 20 1 0 16 0 18 0 48 2 49 1 50)} "callable?" "lambda?" "apply" "call-lambda" "component-name" "error" "Not callable: " "inspect") :bytecode (16 0 52 0 0 1 17 2 16 0 52 1 0 1 17 3 16 2 52 4 0 1 1 5 0 52 3 0 2 52 2 0 1 33 14 0 51 7 0 1 1 16 0 52 6 0 2 32 20 3 20 8 0 16 2 48 1 17 4 16 4 1 9 0 52 3 0 2 33 12 0 20 10 0 16 3 16 1 49 2 32 243 2 16 4 1 11 0 52 3 0 2 33 15 0 20 12 0 1 11 0 16 3 16 1 49 3 32 216 2 16 4 1 14 0 52 13 0 2 33 196 0 20 15 0 16 1 16 4 48 2 33 12 0 20 16 0 16 1 16 4 48 2 32 1 0 2 17 5 20 15 0 16 1 1 17 0 48 2 33 8 0 20 17 0 48 0 32 1 0 4 17 6 16 5 6 33 7 0 5 16 5 52 18 0 1 33 21 0 20 19 0 20 20 0 16 5 16 3 16 1 48 3 16 1 49 2 32 105 0 16 5 6 33 71 0 5 16 5 52 21 0 1 6 33 60 0 5 16 5 52 22 0 1 52 2 0 1 6 33 45 0 5 16 6 6 34 15 0 5 20 23 0 16 5 48 1 1 24 0 52 3 0 2 6 33 19 0 5 20 23 0 16 5 48 1 1 25 0 52 3 0 2 52 2 0 1 33 14 0 20 26 0 16 5 16 3 16 1 49 3 32 11 0 20 12 0 16 4 16 3 16 1 49 3 32 8 2 16 4 1 27 0 52 3 0 2 33 14 0 20 12 0 16 4 16 3 16 1 49 3 32 238 1 16 4 1 28 0 52 3 0 2 33 14 0 20 12 0 16 4 16 3 16 1 49 3 32 212 1 16 4 1 29 0 52 3 0 2 33 128 0 16 3 52 31 0 1 1 32 0 52 30 0 2 17 5 16 5 33 9 0 16 3 52 1 0 1 32 2 0 16 3 17 6 2 17 7 51 34 0 1 1 1 6 51 35 0 1 7 52 33 0 2 17 8 16 8 33 20 0 20 36 0 1 38 0 16 8 1 39 0 52 37 0 3 49 1 32 46 0 20 36 0 1 40 0 1 41 0 1 42 0 1 43 0 16 7 1 45 0 1 46 0 52 44 0 3 1 47 0 1 48 0 52 44 0 3 1 49 0 52 37 0 6 49 1 32 72 1 20 51 0 16 4 52 50 0 2 33 14 0 20 12 0 16 4 16 3 16 1 49 3 32 46 1 20 52 0 16 4 48 1 6 34 8 0 5 20 53 0 16 4 48 1 33 14 0 20 54 0 16 4 16 0 16 1 49 3 32 10 1 20 15 0 16 1 16 4 48 2 6 33 14 0 5 20 16 0 16 1 16 4 48 2 52 18 0 1 33 28 0 20 19 0 20 20 0 20 16 0 16 1 16 4 48 2 16 3 16 1 48 3 16 1 49 2 32 208 0 20 55 0 20 56 0 16 2 16 1 48 2 48 1 17 5 51 57 0 1 1 16 3 52 6 0 2 17 6 20 58 0 16 5 48 1 6 33 41 0 5 16 5 52 59 0 1 52 2 0 1 6 33 26 0 5 16 5 52 21 0 1 52 2 0 1 6 33 11 0 5 16 5 52 22 0 1 52 2 0 1 33 11 0 16 5 16 6 52 60 0 2 32 113 0 16 5 52 59 0 1 33 19 0 20 55 0 20 61 0 16 5 16 6 16 1 48 3 49 1 32 85 0 16 5 52 21 0 1 33 25 0 20 12 0 1 14 0 16 5 52 62 0 1 52 37 0 2 16 3 16 1 49 3 32 51 0 16 5 52 22 0 1 33 25 0 20 12 0 1 14 0 16 5 52 62 0 1 52 37 0 2 16 3 16 1 49 3 32 17 0 1 64 0 16 5 52 65 0 1 52 37 0 2 52 63 0 1 50)} "aser-reserialize" {:upvalue-count 0 :arity 1 :constants ("not" "=" "type-of" "list" "serialize" "empty?" "()" "first" "symbol" "symbol-name" "rest" 0 "for-each" {:upvalue-count 4 :arity 1 :constants ("inc" "=" "type-of" "string" "<" "len" "not" "contains?" " " "starts-with?" "class" "id" "sx-" "data-" "style" "href" "src" "type" "name" "value" "placeholder" "action" "method" "target" "role" "for" "on" "append!" "str" ":" "serialize" "nth" "aser-reserialize") :bytecode (18 0 33 15 0 4 19 0 5 18 1 52 0 0 1 19 1 32 116 1 16 0 52 2 0 1 1 3 0 52 1 0 2 6 33 17 1 5 18 1 52 0 0 1 18 2 52 5 0 1 52 4 0 2 6 33 252 0 5 16 0 1 8 0 52 7 0 2 52 6 0 1 6 33 234 0 5 16 0 1 10 0 52 9 0 2 6 34 220 0 5 16 0 1 11 0 52 9 0 2 6 34 206 0 5 16 0 1 12 0 52 9 0 2 6 34 192 0 5 16 0 1 13 0 52 9 0 2 6 34 178 0 5 16 0 1 14 0 52 9 0 2 6 34 164 0 5 16 0 1 15 0 52 9 0 2 6 34 150 0 5 16 0 1 16 0 52 9 0 2 6 34 136 0 5 16 0 1 17 0 52 9 0 2 6 34 122 0 5 16 0 1 18 0 52 9 0 2 6 34 108 0 5 16 0 1 19 0 52 9 0 2 6 34 94 0 5 16 0 1 20 0 52 9 0 2 6 34 80 0 5 16 0 1 21 0 52 9 0 2 6 34 66 0 5 16 0 1 22 0 52 9 0 2 6 34 52 0 5 16 0 1 23 0 52 9 0 2 6 34 38 0 5 16 0 1 24 0 52 9 0 2 6 34 24 0 5 16 0 1 25 0 52 9 0 2 6 34 10 0 5 16 0 1 26 0 52 9 0 2 33 56 0 20 27 0 18 3 1 29 0 16 0 52 28 0 2 48 2 5 20 27 0 18 3 18 2 18 1 52 0 0 1 52 31 0 2 52 30 0 1 48 2 5 3 19 0 5 18 1 52 0 0 1 19 1 32 23 0 20 27 0 18 3 20 32 0 16 0 48 1 48 2 5 18 1 52 0 0 1 19 1 50)} "str" "(" "join" " " ")") :bytecode (16 0 52 2 0 1 1 3 0 52 1 0 2 52 0 0 1 33 9 0 16 0 52 4 0 1 32 122 0 16 0 52 5 0 1 33 6 0 1 6 0 32 107 0 16 0 52 7 0 1 17 1 16 1 52 2 0 1 1 8 0 52 1 0 2 52 0 0 1 33 9 0 16 0 52 4 0 1 32 70 0 20 9 0 16 1 48 1 17 2 16 2 52 3 0 1 17 3 16 0 52 10 0 1 17 4 4 17 5 1 11 0 17 6 51 13 0 1 5 1 6 1 4 1 3 16 4 52 12 0 2 5 1 15 0 1 17 0 16 3 52 16 0 2 1 18 0 52 14 0 3 50)} "aser-fragment" {:upvalue-count 0 :arity 2 :constants ("list" "for-each" {:upvalue-count 2 :arity 1 :constants ("aser" "nil?" "=" "type-of" "sx-expr" "append!" "sx-expr-source" "list" "for-each" {:upvalue-count 1 :arity 1 :constants ("not" "nil?" "=" "type-of" "sx-expr" "append!" "sx-expr-source" "aser-reserialize") :bytecode (16 0 52 1 0 1 52 0 0 1 33 50 0 16 0 52 3 0 1 1 4 0 52 2 0 2 33 17 0 20 5 0 18 0 20 6 0 16 0 48 1 49 2 32 14 0 20 5 0 18 0 20 7 0 16 0 48 1 49 2 32 1 0 2 50)} "serialize") :bytecode (20 0 0 16 0 18 0 48 2 17 1 16 1 52 1 0 1 33 4 0 2 32 76 0 16 1 52 3 0 1 1 4 0 52 2 0 2 33 17 0 20 5 0 18 1 20 6 0 16 1 48 1 49 2 32 43 0 16 1 52 3 0 1 1 7 0 52 2 0 2 33 14 0 51 9 0 0 1 16 1 52 8 0 2 32 13 0 20 5 0 18 1 16 1 52 10 0 1 49 2 50)} "empty?" "" "=" "len" 1 "make-sx-expr" "first" "str" "(<> " "join" " " ")") :bytecode (52 0 0 0 17 2 51 2 0 1 1 1 2 16 0 52 1 0 2 5 16 2 52 3 0 1 33 6 0 1 4 0 32 54 0 16 2 52 6 0 1 1 7 0 52 5 0 2 33 14 0 20 8 0 16 2 52 9 0 1 49 1 32 24 0 20 8 0 1 11 0 1 13 0 16 2 52 12 0 2 1 14 0 52 10 0 3 49 1 50)} "aser-call" {:upvalue-count 0 :arity 3 :constants ("list" 0 "scope-push!" "element-attrs" "for-each" {:upvalue-count 6 :arity 1 :constants ("inc" "=" "type-of" "keyword" "<" "len" "aser" "nth" "not" "nil?" "append!" "str" ":" "keyword-name" "sx-expr" "sx-expr-source" "serialize" "list" "for-each" {:upvalue-count 1 :arity 1 :constants ("not" "nil?" "=" "type-of" "sx-expr" "append!" "sx-expr-source" "serialize") :bytecode (16 0 52 1 0 1 52 0 0 1 33 49 0 16 0 52 3 0 1 1 4 0 52 2 0 2 33 17 0 20 5 0 18 0 20 6 0 16 0 48 1 49 2 32 13 0 20 5 0 18 0 16 0 52 7 0 1 49 2 32 1 0 2 50)}) :bytecode (18 0 33 15 0 4 19 0 5 18 1 52 0 0 1 19 1 32 16 1 16 0 52 2 0 1 1 3 0 52 1 0 2 6 33 17 0 5 18 1 52 0 0 1 18 2 52 5 0 1 52 4 0 2 33 122 0 20 6 0 18 2 18 1 52 0 0 1 52 7 0 2 18 3 48 2 17 1 16 1 52 9 0 1 52 8 0 1 33 71 0 20 10 0 18 4 1 12 0 20 13 0 16 0 48 1 52 11 0 2 48 2 5 16 1 52 2 0 1 1 14 0 52 1 0 2 33 17 0 20 10 0 18 4 20 15 0 16 1 48 1 48 2 32 13 0 20 10 0 18 4 16 1 52 16 0 1 48 2 32 1 0 2 5 3 19 0 5 18 1 52 0 0 1 19 1 32 113 0 20 6 0 16 0 18 3 48 2 17 1 16 1 52 9 0 1 52 8 0 1 33 79 0 16 1 52 2 0 1 1 14 0 52 1 0 2 33 17 0 20 10 0 18 5 20 15 0 16 1 48 1 48 2 32 43 0 16 1 52 2 0 1 1 17 0 52 1 0 2 33 14 0 51 19 0 0 5 16 1 52 18 0 2 32 13 0 20 10 0 18 5 16 1 52 16 0 1 48 2 32 1 0 2 5 18 1 52 0 0 1 19 1 50)} {:upvalue-count 1 :arity 1 :constants ("for-each" {:upvalue-count 2 :arity 1 :constants ("dict-get" "append!" "str" ":" "serialize") :bytecode (18 0 16 0 52 0 0 2 17 1 20 1 0 18 1 1 3 0 16 0 52 2 0 2 48 2 5 20 1 0 18 1 16 1 52 4 0 1 49 2 50)} "keys") :bytecode (51 1 0 1 0 0 0 16 0 52 2 0 1 52 0 0 2 50)} "scope-peek" "scope-pop!" "concat" "make-sx-expr" "str" "(" "join" " " ")") :bytecode (52 0 0 0 17 3 52 0 0 0 17 4 4 17 5 1 1 0 17 6 1 3 0 2 52 2 0 2 5 51 5 0 1 5 1 6 1 1 1 2 1 3 1 4 16 1 52 4 0 2 5 51 6 0 1 3 1 3 0 52 7 0 1 52 4 0 2 5 1 3 0 52 8 0 1 5 16 0 52 0 0 1 16 3 16 4 52 9 0 3 17 7 20 10 0 1 12 0 1 14 0 16 7 52 13 0 2 1 15 0 52 11 0 3 49 1 50)} "aser-expand-component" {:upvalue-count 0 :arity 3 :constants ("component-params" "env-merge" "component-closure" 0 "list" "for-each" {:upvalue-count 1 :arity 1 :constants ("env-bind!") :bytecode (20 0 0 18 0 16 0 2 49 3 50)} {:upvalue-count 6 :arity 1 :constants ("inc" "=" "type-of" "keyword" "<" "len" "env-bind!" "keyword-name" "aser" "nth" "append!") :bytecode (18 0 33 15 0 4 19 0 5 18 1 52 0 0 1 19 1 32 104 0 16 0 52 2 0 1 1 3 0 52 1 0 2 6 33 17 0 5 18 1 52 0 0 1 18 2 52 5 0 1 52 4 0 2 33 49 0 20 6 0 18 3 20 7 0 16 0 48 1 20 8 0 18 2 18 1 52 0 0 1 52 9 0 2 18 4 48 2 48 3 5 3 19 0 5 18 1 52 0 0 1 19 1 32 18 0 20 10 0 18 5 16 0 48 2 5 18 1 52 0 0 1 19 1 50)} "component-has-children" "map" {:upvalue-count 1 :arity 1 :constants ("aser") :bytecode (20 0 0 16 0 18 0 49 2 50)} "env-bind!" "children" "=" "len" 1 "first" "aser" "component-body") :bytecode (16 0 52 0 0 1 17 3 20 1 0 16 2 16 0 52 2 0 1 48 2 17 4 1 3 0 17 5 4 17 6 52 4 0 0 17 7 51 6 0 1 4 16 3 52 5 0 2 5 51 7 0 1 6 1 5 1 1 1 4 1 2 1 7 16 1 52 5 0 2 5 20 8 0 16 0 48 1 33 53 0 51 10 0 1 2 16 7 52 9 0 2 17 8 20 11 0 16 4 1 12 0 16 8 52 14 0 1 1 15 0 52 13 0 2 33 9 0 16 8 52 16 0 1 32 2 0 16 8 48 3 32 1 0 2 5 20 17 0 16 0 52 18 0 1 16 4 49 2 50)} "SPECIAL_FORM_NAMES" "list" "if" "when" "cond" "case" "and" "or" "let" "let*" "lambda" "fn" "define" "defcomp" "defmacro" "defstyle" "defhandler" "defpage" "defquery" "defaction" "defrelation" "begin" "do" "quote" "quasiquote" "->" "set!" "letrec" "dynamic-wind" "defisland" "deftype" "defeffect" "scope" "provide" "context" "emit!" "emitted" "HO_FORM_NAMES" "map" "map-indexed" "filter" "reduce" "some" "every?" "for-each" "special-form?" {:upvalue-count 0 :arity 1 :constants ("contains?" "SPECIAL_FORM_NAMES") :bytecode (20 1 0 16 0 52 0 0 2 50)} "ho-form?" {:upvalue-count 0 :arity 1 :constants ("contains?" "HO_FORM_NAMES") :bytecode (20 1 0 16 0 52 0 0 2 50)} "aser-special" {:upvalue-count 0 :arity 3 :constants ("rest" "=" "if" "trampoline" "eval-expr" "first" "aser" "nth" 1 ">" "len" 2 "when" "not" "for-each" {:upvalue-count 2 :arity 1 :constants ("aser") :bytecode (20 0 0 16 0 18 1 48 2 19 0 50)} "cond" "eval-cond" "case" "eval-case-aser" "let" "let*" "process-bindings" "begin" "do" "and" "some" {:upvalue-count 2 :arity 1 :constants ("trampoline" "eval-expr" "not") :bytecode (20 0 0 20 1 0 16 0 18 1 48 2 48 1 19 0 5 18 0 52 2 0 1 50)} "or" {:upvalue-count 2 :arity 1 :constants ("trampoline" "eval-expr") :bytecode (20 0 0 20 1 0 16 0 18 1 48 2 48 1 19 0 5 18 0 50)} "map" {:upvalue-count 1 :arity 1 :constants ("lambda?" "env-extend" "lambda-closure" "env-bind!" "first" "lambda-params" "aser" "lambda-body" "cek-call" "list") :bytecode (18 0 52 0 0 1 33 49 0 20 1 0 18 0 52 2 0 1 48 1 17 1 20 3 0 16 1 18 0 52 5 0 1 52 4 0 1 16 0 48 3 5 20 6 0 18 0 52 7 0 1 16 1 49 2 32 13 0 20 8 0 18 0 16 0 52 9 0 1 49 2 50)} "aser-fragment" "map-indexed" {:upvalue-count 2 :arity 2 :constants ("lambda?" "env-merge" "lambda-closure" "env-bind!" "first" "lambda-params" "nth" 1 "aser" "lambda-body" "cek-call" "list") :bytecode (18 0 52 0 0 1 33 74 0 20 1 0 18 0 52 2 0 1 18 1 48 2 17 2 20 3 0 16 2 18 0 52 5 0 1 52 4 0 1 16 0 48 3 5 20 3 0 16 2 18 0 52 5 0 1 1 7 0 52 6 0 2 16 1 48 3 5 20 8 0 18 0 52 9 0 1 16 2 49 2 32 15 0 20 10 0 18 0 16 0 16 1 52 11 0 2 49 2 50)} "list" {:upvalue-count 3 :arity 1 :constants ("lambda?" "env-merge" "lambda-closure" "env-bind!" "first" "lambda-params" "append!" "aser" "lambda-body" "cek-call" "list") :bytecode (18 0 52 0 0 1 33 58 0 20 1 0 18 0 52 2 0 1 18 1 48 2 17 1 20 3 0 16 1 18 0 52 5 0 1 52 4 0 1 16 0 48 3 5 20 6 0 18 2 20 7 0 18 0 52 8 0 1 16 1 48 2 49 2 32 13 0 20 9 0 18 0 16 0 52 10 0 1 49 2 50)} "empty?" "defisland" "serialize" "define" "defcomp" "defmacro" "defstyle" "defhandler" "defpage" "defquery" "defaction" "defrelation" "deftype" "defeffect" "scope" ">=" "type-of" "keyword" "keyword-name" "value" "slice" "scope-push!" "scope-pop!" "provide" "context" "scope-peek" "nil?" "emit!" "scope-emit!" "emitted") :bytecode (16 1 52 0 0 1 17 3 16 0 1 2 0 52 1 0 2 33 79 0 20 3 0 20 4 0 16 3 52 5 0 1 16 2 48 2 48 1 33 19 0 20 6 0 16 3 1 8 0 52 7 0 2 16 2 49 2 32 36 0 16 3 52 10 0 1 1 11 0 52 9 0 2 33 19 0 20 6 0 16 3 1 11 0 52 7 0 2 16 2 49 2 32 1 0 2 32 43 5 16 0 1 12 0 52 1 0 2 33 55 0 20 3 0 20 4 0 16 3 52 5 0 1 16 2 48 2 48 1 52 13 0 1 33 4 0 2 32 23 0 2 17 4 51 15 0 1 4 1 2 16 3 52 0 0 1 52 14 0 2 5 16 4 32 232 4 16 0 1 16 0 52 1 0 2 33 32 0 20 17 0 16 3 16 2 48 2 17 4 16 4 33 12 0 20 6 0 16 4 16 2 49 2 32 1 0 2 32 188 4 16 0 1 18 0 52 1 0 2 33 42 0 20 3 0 20 4 0 16 3 52 5 0 1 16 2 48 2 48 1 17 4 16 3 52 0 0 1 17 5 20 19 0 16 4 16 5 16 2 49 3 32 134 4 16 0 1 20 0 52 1 0 2 6 34 10 0 5 16 0 1 21 0 52 1 0 2 33 41 0 20 22 0 16 3 52 5 0 1 16 2 48 2 17 4 2 17 5 51 15 0 1 5 1 4 16 3 52 0 0 1 52 14 0 2 5 16 5 32 67 4 16 0 1 23 0 52 1 0 2 6 34 10 0 5 16 0 1 24 0 52 1 0 2 33 22 0 2 17 4 51 15 0 1 4 1 2 16 3 52 14 0 2 5 16 4 32 19 4 16 0 1 25 0 52 1 0 2 33 22 0 3 17 4 51 27 0 1 4 1 2 16 3 52 26 0 2 5 16 4 32 241 3 16 0 1 28 0 52 1 0 2 33 22 0 4 17 4 51 29 0 1 4 1 2 16 3 52 26 0 2 5 16 4 32 207 3 16 0 1 30 0 52 1 0 2 33 68 0 20 3 0 20 4 0 16 3 52 5 0 1 16 2 48 2 48 1 17 4 20 3 0 20 4 0 16 3 1 8 0 52 7 0 2 16 2 48 2 48 1 17 5 51 31 0 1 4 16 5 52 30 0 2 17 6 20 32 0 16 6 16 2 49 2 32 127 3 16 0 1 33 0 52 1 0 2 33 59 0 20 3 0 20 4 0 16 3 52 5 0 1 16 2 48 2 48 1 17 4 20 3 0 20 4 0 16 3 1 8 0 52 7 0 2 16 2 48 2 48 1 17 5 51 34 0 1 4 1 2 16 5 52 33 0 2 32 56 3 16 0 1 14 0 52 1 0 2 33 83 0 20 3 0 20 4 0 16 3 52 5 0 1 16 2 48 2 48 1 17 4 20 3 0 20 4 0 16 3 1 8 0 52 7 0 2 16 2 48 2 48 1 17 5 52 35 0 0 17 6 51 36 0 1 4 1 2 1 6 16 5 52 14 0 2 5 16 6 52 37 0 1 33 4 0 2 32 2 0 16 6 32 217 2 16 0 1 38 0 52 1 0 2 33 24 0 20 3 0 20 4 0 16 1 16 2 48 2 48 1 5 16 1 52 39 0 1 32 181 2 16 0 1 40 0 52 1 0 2 6 34 136 0 5 16 0 1 41 0 52 1 0 2 6 34 122 0 5 16 0 1 42 0 52 1 0 2 6 34 108 0 5 16 0 1 43 0 52 1 0 2 6 34 94 0 5 16 0 1 44 0 52 1 0 2 6 34 80 0 5 16 0 1 45 0 52 1 0 2 6 34 66 0 5 16 0 1 46 0 52 1 0 2 6 34 52 0 5 16 0 1 47 0 52 1 0 2 6 34 38 0 5 16 0 1 48 0 52 1 0 2 6 34 24 0 5 16 0 1 49 0 52 1 0 2 6 34 10 0 5 16 0 1 50 0 52 1 0 2 33 19 0 20 3 0 20 4 0 16 1 16 2 48 2 48 1 5 2 32 10 2 16 0 1 51 0 52 1 0 2 33 176 0 20 3 0 20 4 0 16 3 52 5 0 1 16 2 48 2 48 1 17 4 16 3 52 0 0 1 17 5 2 17 6 2 17 7 16 5 52 10 0 1 1 11 0 52 52 0 2 6 33 41 0 5 16 5 52 5 0 1 52 53 0 1 1 54 0 52 1 0 2 6 33 19 0 5 20 55 0 16 5 52 5 0 1 48 1 1 56 0 52 1 0 2 33 38 0 20 3 0 20 4 0 16 5 1 8 0 52 7 0 2 16 2 48 2 48 1 17 6 5 16 5 1 11 0 52 57 0 2 17 7 32 4 0 16 5 17 7 5 16 4 16 6 52 58 0 2 5 2 17 8 51 15 0 1 8 1 2 16 7 52 14 0 2 5 16 4 52 59 0 1 5 16 8 32 78 1 16 0 1 60 0 52 1 0 2 33 88 0 20 3 0 20 4 0 16 3 52 5 0 1 16 2 48 2 48 1 17 4 20 3 0 20 4 0 16 3 1 8 0 52 7 0 2 16 2 48 2 48 1 17 5 2 17 6 16 4 16 5 52 58 0 2 5 51 15 0 1 6 1 2 16 3 1 11 0 52 57 0 2 52 14 0 2 5 16 4 52 59 0 1 5 16 6 32 234 0 16 0 1 61 0 52 1 0 2 33 90 0 20 3 0 20 4 0 16 3 52 5 0 1 16 2 48 2 48 1 17 4 16 3 52 10 0 1 1 11 0 52 52 0 2 33 24 0 20 3 0 20 4 0 16 3 1 8 0 52 7 0 2 16 2 48 2 48 1 32 1 0 2 17 5 16 4 52 62 0 1 17 6 16 6 52 63 0 1 33 5 0 16 5 32 2 0 16 6 32 132 0 16 0 1 64 0 52 1 0 2 33 56 0 20 3 0 20 4 0 16 3 52 5 0 1 16 2 48 2 48 1 17 4 20 3 0 20 4 0 16 3 1 8 0 52 7 0 2 16 2 48 2 48 1 17 5 16 4 16 5 52 65 0 2 5 2 32 64 0 16 0 1 66 0 52 1 0 2 33 38 0 20 3 0 20 4 0 16 3 52 5 0 1 16 2 48 2 48 1 17 4 16 4 52 62 0 1 6 34 5 0 5 52 35 0 0 32 14 0 20 3 0 20 4 0 16 1 16 2 48 2 49 1 50)} "eval-case-aser" {:upvalue-count 0 :arity 3 :constants ("<" "len" 2 "first" "nth" 1 "=" "type-of" "keyword" "keyword-name" "else" "symbol" "symbol-name" ":else" "aser" "trampoline" "eval-expr" "eval-case-aser" "slice") :bytecode (16 1 52 1 0 1 1 2 0 52 0 0 2 33 4 0 2 32 175 0 16 1 52 3 0 1 17 3 16 1 1 5 0 52 4 0 2 17 4 16 3 52 7 0 1 1 8 0 52 6 0 2 6 33 15 0 5 20 9 0 16 3 48 1 1 10 0 52 6 0 2 6 34 52 0 5 16 3 52 7 0 1 1 11 0 52 6 0 2 6 33 34 0 5 20 12 0 16 3 48 1 1 13 0 52 6 0 2 6 34 15 0 5 20 12 0 16 3 48 1 1 10 0 52 6 0 2 33 12 0 20 14 0 16 4 16 2 49 2 32 53 0 16 0 20 15 0 20 16 0 16 3 16 2 48 2 48 1 52 6 0 2 33 12 0 20 14 0 16 4 16 2 49 2 32 18 0 20 17 0 16 0 16 1 1 2 0 52 18 0 2 16 2 49 3 50)}) :bytecode (51 1 0 128 0 0 5 51 3 0 128 2 0 5 51 5 0 128 4 0 5 51 7 0 128 6 0 5 51 9 0 128 8 0 5 51 11 0 128 10 0 5 51 13 0 128 12 0 5 1 16 0 1 17 0 1 18 0 1 19 0 1 20 0 1 21 0 1 22 0 1 23 0 1 24 0 1 25 0 1 26 0 1 27 0 1 28 0 1 29 0 1 30 0 1 31 0 1 32 0 1 33 0 1 34 0 1 35 0 1 36 0 1 37 0 1 38 0 1 39 0 1 40 0 1 41 0 1 42 0 1 43 0 1 44 0 1 45 0 1 46 0 1 47 0 1 48 0 1 49 0 1 50 0 52 15 0 35 128 14 0 5 1 52 0 1 53 0 1 54 0 1 55 0 1 56 0 1 57 0 1 58 0 52 15 0 7 128 51 0 5 51 60 0 128 59 0 5 51 62 0 128 61 0 5 51 64 0 128 63 0 5 51 66 0 128 65 0 50)))
diff --git a/shared/static/wasm/sx/boot-helpers.sx b/shared/static/wasm/sx/boot-helpers.sx
index 211f5a1d..5f363fc1 100644
--- a/shared/static/wasm/sx/boot-helpers.sx
+++ b/shared/static/wasm/sx/boot-helpers.sx
@@ -430,17 +430,6 @@
(host-callback thunk))
(thunk))))
-(define
- observe-intersection
- (fn
- (el callback once? delay)
- (let
- ((cb (host-callback (fn (entries) (for-each (fn (entry) (when (host-get entry "isIntersecting") (if delay (set-timeout (fn () (callback entry)) delay) (callback entry)) (when once? (host-call observer "unobserve" el)))) (host-call entries "forEach" (host-callback (fn (e) e))))))))
- (let
- ((observer (host-new "IntersectionObserver" (host-callback (fn (entries) (let ((arr-len (host-get entries "length"))) (let loop ((i 0)) (when (< i arr-len) (let ((entry (host-call entries "item" i))) (when (and entry (host-get entry "isIntersecting")) (if delay (set-timeout (fn () (callback entry)) delay) (callback entry)) (when once? (host-call observer "unobserve" el)))) (loop (+ i 1))))))))))
- (host-call observer "observe" el)
- observer))))
-
(define
event-source-connect
(fn
diff --git a/shared/static/wasm/sx/boot-helpers.sxbc b/shared/static/wasm/sx/boot-helpers.sxbc
index 9b057f5d..342b8350 100644
--- a/shared/static/wasm/sx/boot-helpers.sxbc
+++ b/shared/static/wasm/sx/boot-helpers.sxbc
@@ -1,3 +1,3 @@
-(sxbc 1 "caa7735c3363e49c"
+(sxbc 1 "8ad6c485fd444ef9"
(code
- :constants ("_sx-bound-prefix" "_sxBound" "mark-processed!" {:upvalue-count 0 :arity 2 :constants ("host-set!" "str" "_sx-bound-prefix") :bytecode (20 0 0 16 0 20 2 0 16 1 52 1 0 2 3 49 3 50)} "is-processed?" {:upvalue-count 0 :arity 2 :constants ("host-get" "str" "_sx-bound-prefix") :bytecode (20 0 0 16 0 20 2 0 16 1 52 1 0 2 48 2 17 2 16 2 33 4 0 3 32 1 0 4 50)} "clear-processed!" {:upvalue-count 0 :arity 2 :constants ("host-set!" "str" "_sx-bound-prefix") :bytecode (20 0 0 16 0 20 2 0 16 1 52 1 0 2 2 49 3 50)} "callable?" {:upvalue-count 0 :arity 1 :constants ("type-of" "=" "lambda" "native-fn" "continuation") :bytecode (16 0 52 0 0 1 17 1 16 1 1 2 0 52 1 0 2 6 34 24 0 5 16 1 1 3 0 52 1 0 2 6 34 10 0 5 16 1 1 4 0 52 1 0 2 50)} "to-kebab" {:upvalue-count 0 :arity 1 :constants ("Convert camelCase to kebab-case." "list" 0 {:upvalue-count 3 :arity 1 :constants ("<" "len" "nth" ">=" "A" "<=" "Z" ">" 0 "append!" "-" "lower" "+" 1) :bytecode (16 0 18 0 52 1 0 1 52 0 0 2 33 105 0 18 0 16 0 52 2 0 2 17 1 16 1 1 4 0 52 3 0 2 6 33 10 0 5 16 1 1 6 0 52 5 0 2 33 43 0 16 0 1 8 0 52 7 0 2 33 13 0 20 9 0 18 1 1 10 0 48 2 32 1 0 2 5 20 9 0 18 1 16 1 52 11 0 1 48 2 32 9 0 20 9 0 18 1 16 1 48 2 5 18 2 16 0 1 13 0 52 12 0 2 49 1 32 1 0 2 50)} "join" "") :bytecode (1 0 0 5 52 1 0 0 17 1 1 2 0 17 2 2 17 3 51 3 0 1 0 1 1 1 3 17 3 16 3 1 2 0 48 1 5 1 5 0 16 1 52 4 0 2 50)} "sx-load-components" {:upvalue-count 0 :arity 1 :constants ("Parse and evaluate component definitions from text." ">" "len" 0 "sx-parse" "for-each" {:upvalue-count 0 :arity 1 :constants ("cek-eval") :bytecode (20 0 0 16 0 49 1 50)}) :bytecode (1 0 0 5 16 0 6 33 14 0 5 16 0 52 2 0 1 1 3 0 52 1 0 2 33 21 0 20 4 0 16 0 48 1 17 1 51 6 0 16 1 52 5 0 2 32 1 0 2 50)} "call-expr" {:upvalue-count 0 :arity 2 :constants ("Parse and evaluate an SX expression string." "sx-parse" "not" "empty?" "cek-eval" "first") :bytecode (1 0 0 5 20 1 0 16 0 48 1 17 2 16 2 52 3 0 1 52 2 0 1 33 14 0 20 4 0 16 2 52 5 0 1 49 1 32 1 0 2 50)} "base-env" {:upvalue-count 0 :arity 0 :constants ("Return the current global environment." "global-env") :bytecode (1 0 0 5 20 1 0 49 0 50)} "get-render-env" {:upvalue-count 0 :arity 1 :constants ("Get the rendering environment (global env, optionally merged with extra)." "base-env" "not" "nil?" "env-merge") :bytecode (1 0 0 5 20 1 0 48 0 17 1 16 0 6 33 11 0 5 16 0 52 3 0 1 52 2 0 1 33 12 0 20 4 0 16 1 16 0 49 2 32 2 0 16 1 50)} "merge-envs" {:upvalue-count 0 :arity 2 :constants ("Merge two environments." "env-merge" "global-env") :bytecode (1 0 0 5 16 0 6 33 3 0 5 16 1 33 12 0 20 1 0 16 0 16 1 49 2 32 19 0 16 0 6 34 13 0 5 16 1 6 34 6 0 5 20 2 0 49 0 50)} "sx-render-with-env" {:upvalue-count 0 :arity 2 :constants ("Parse SX source and render to DOM fragment." "host-global" "document" "host-call" "createDocumentFragment" "sx-parse" "for-each" {:upvalue-count 2 :arity 1 :constants ("render-to-html" ">" "len" 0 "host-call" "createElement" "template" "host-set!" "innerHTML" "appendChild" "host-get" "content") :bytecode (20 0 0 16 0 48 1 17 1 16 1 6 33 14 0 5 16 1 52 2 0 1 1 3 0 52 1 0 2 33 51 0 20 4 0 18 0 1 5 0 1 6 0 48 3 17 2 20 7 0 16 2 1 8 0 16 1 48 3 5 20 4 0 18 1 1 9 0 20 10 0 16 2 1 11 0 48 2 49 3 32 1 0 2 50)}) :bytecode (1 0 0 5 20 1 0 1 2 0 48 1 17 2 20 3 0 16 2 1 4 0 48 2 17 3 20 5 0 16 0 48 1 17 4 51 7 0 1 2 1 3 16 4 52 6 0 2 5 16 3 50)} "parse-env-attr" {:upvalue-count 0 :arity 1 :constants ("Parse data-sx-env attribute (JSON key-value pairs).") :bytecode (1 0 0 5 2 50)} "store-env-attr" {:upvalue-count 0 :arity 3 :constants () :bytecode (2 50)} "resolve-mount-target" {:upvalue-count 0 :arity 1 :constants ("Resolve a CSS selector string to a DOM element." "string?" "dom-query") :bytecode (1 0 0 5 16 0 52 1 0 1 33 10 0 20 2 0 16 0 49 1 32 2 0 16 0 50)} "remove-head-element" {:upvalue-count 0 :arity 1 :constants ("Remove a element matching selector." "dom-query" "dom-remove") :bytecode (1 0 0 5 20 1 0 16 0 48 1 17 1 16 1 33 10 0 20 2 0 16 1 49 1 32 1 0 2 50)} "set-sx-comp-cookie" {:upvalue-count 0 :arity 1 :constants ("set-cookie" "sx-components") :bytecode (1 1 0 16 0 52 0 0 2 50)} "clear-sx-comp-cookie" {:upvalue-count 0 :arity 0 :constants ("set-cookie" "sx-components" "") :bytecode (1 1 0 1 2 0 52 0 0 2 50)} "log-parse-error" {:upvalue-count 0 :arity 3 :constants ("log-error" "str" "Parse error in " ": ") :bytecode (20 0 0 1 2 0 16 0 1 3 0 16 2 52 1 0 4 49 1 50)} "loaded-component-names" {:upvalue-count 0 :arity 0 :constants ("dom-query-all" "dom-body" "script[data-components]" "list" "for-each" {:upvalue-count 1 :arity 1 :constants ("dom-get-attr" "data-components" "" ">" "len" 0 "for-each" {:upvalue-count 1 :arity 1 :constants (">" "len" "trim" 0 "append!") :bytecode (16 0 52 2 0 1 52 1 0 1 1 3 0 52 0 0 2 33 16 0 20 4 0 18 0 16 0 52 2 0 1 49 2 32 1 0 2 50)} "split" ",") :bytecode (20 0 0 16 0 1 1 0 48 2 6 34 4 0 5 1 2 0 17 1 16 1 52 4 0 1 1 5 0 52 3 0 2 33 21 0 51 7 0 0 0 16 1 1 9 0 52 8 0 2 52 6 0 2 32 1 0 2 50)}) :bytecode (20 0 0 20 1 0 48 0 1 2 0 48 2 17 0 52 3 0 0 17 1 51 5 0 1 1 16 0 52 4 0 2 5 16 1 50)} "csrf-token" {:upvalue-count 0 :arity 0 :constants ("dom-query" "meta[name=\"csrf-token\"]" "dom-get-attr" "content") :bytecode (20 0 0 1 1 0 48 1 17 0 16 0 33 13 0 20 2 0 16 0 1 3 0 49 2 32 1 0 2 50)} "validate-for-request" {:upvalue-count 0 :arity 1 :constants () :bytecode (3 50)} "build-request-body" {:upvalue-count 0 :arity 3 :constants ("upper" "=" "GET" "HEAD" "dom-tag-name" "" "FORM" "host-new" "FormData" "URLSearchParams" "host-call" "toString" "dict" "url" ">" "len" 0 "str" "contains?" "?" "&" "body" "content-type" "dom-get-attr" "enctype" "application/x-www-form-urlencoded" "multipart/form-data") :bytecode (16 1 52 0 0 1 17 3 16 3 1 2 0 52 1 0 2 6 34 10 0 5 16 3 1 3 0 52 1 0 2 33 167 0 16 0 6 33 27 0 5 20 4 0 16 0 48 1 6 34 4 0 5 1 5 0 52 0 0 1 1 6 0 52 1 0 2 33 111 0 20 7 0 1 8 0 16 0 48 2 17 4 20 7 0 1 9 0 16 4 48 2 17 5 20 10 0 16 5 1 11 0 48 2 17 6 1 13 0 16 6 6 33 14 0 5 16 6 52 15 0 1 1 16 0 52 14 0 2 33 32 0 16 2 16 2 1 19 0 52 18 0 2 33 6 0 1 20 0 32 3 0 1 19 0 16 6 52 17 0 3 32 2 0 16 2 1 21 0 2 1 22 0 2 52 12 0 6 32 17 0 1 13 0 16 2 1 21 0 2 1 22 0 2 52 12 0 6 32 173 0 16 0 6 33 27 0 5 20 4 0 16 0 48 1 6 34 4 0 5 1 5 0 52 0 0 1 1 6 0 52 1 0 2 33 120 0 20 23 0 16 0 1 24 0 48 2 6 34 4 0 5 1 25 0 17 4 16 4 1 26 0 52 1 0 2 33 33 0 20 7 0 1 8 0 16 0 48 2 17 5 1 13 0 16 2 1 21 0 16 5 1 22 0 2 52 12 0 6 32 52 0 20 7 0 1 8 0 16 0 48 2 17 5 20 7 0 1 9 0 16 5 48 2 17 6 1 13 0 16 2 1 21 0 20 10 0 16 6 1 11 0 48 2 1 22 0 1 25 0 52 12 0 6 32 17 0 1 13 0 16 2 1 21 0 2 1 22 0 2 52 12 0 6 50)} "abort-previous-target" {:upvalue-count 0 :arity 1 :constants () :bytecode (2 50)} "abort-previous" "track-controller" {:upvalue-count 0 :arity 2 :constants () :bytecode (2 50)} "track-controller-target" "new-abort-controller" {:upvalue-count 0 :arity 0 :constants ("host-new" "AbortController") :bytecode (20 0 0 1 1 0 49 1 50)} "abort-signal" {:upvalue-count 0 :arity 1 :constants ("host-get" "signal") :bytecode (20 0 0 16 0 1 1 0 49 2 50)} "apply-optimistic" "revert-optimistic" "dom-has-attr?" {:upvalue-count 0 :arity 2 :constants ("host-call" "hasAttribute") :bytecode (20 0 0 16 0 1 1 0 16 1 49 3 50)} "show-indicator" {:upvalue-count 0 :arity 1 :constants ("dom-get-attr" "sx-indicator" "dom-query" "dom-remove-class" "hidden" "dom-add-class" "sx-indicator-visible") :bytecode (20 0 0 16 0 1 1 0 48 2 17 1 16 1 33 42 0 20 2 0 16 1 48 1 17 2 16 2 33 24 0 20 3 0 16 2 1 4 0 48 2 5 20 5 0 16 2 1 6 0 48 2 32 1 0 2 32 1 0 2 5 16 1 50)} "disable-elements" {:upvalue-count 0 :arity 1 :constants ("dom-get-attr" "sx-disabled-elt" "dom-query-all" "dom-body" "for-each" {:upvalue-count 0 :arity 1 :constants ("dom-set-attr" "disabled" "") :bytecode (20 0 0 16 0 1 1 0 1 2 0 49 3 50)} "list") :bytecode (20 0 0 16 0 1 1 0 48 2 17 1 16 1 33 29 0 20 2 0 20 3 0 48 0 16 1 48 2 17 2 51 5 0 16 2 52 4 0 2 5 16 2 32 4 0 52 6 0 0 50)} "clear-loading-state" {:upvalue-count 0 :arity 3 :constants ("dom-remove-class" "sx-request" "dom-remove-attr" "aria-busy" "dom-query" "dom-add-class" "hidden" "sx-indicator-visible" "for-each" {:upvalue-count 0 :arity 1 :constants ("dom-remove-attr" "disabled") :bytecode (20 0 0 16 0 1 1 0 49 2 50)}) :bytecode (20 0 0 16 0 1 1 0 48 2 5 20 2 0 16 0 1 3 0 48 2 5 16 1 33 42 0 20 4 0 16 1 48 1 17 3 16 3 33 24 0 20 5 0 16 3 1 6 0 48 2 5 20 0 0 16 3 1 7 0 48 2 32 1 0 2 32 1 0 2 5 16 2 33 12 0 51 9 0 16 2 52 8 0 2 32 1 0 2 50)} "abort-error?" {:upvalue-count 0 :arity 1 :constants ("=" "host-get" "name" "AbortError") :bytecode (20 1 0 16 0 1 2 0 48 2 1 3 0 52 0 0 2 50)} "promise-catch" {:upvalue-count 0 :arity 2 :constants ("host-callback" "host-call" "catch") :bytecode (20 0 0 16 1 48 1 17 2 20 1 0 16 0 1 2 0 16 2 49 3 50)} "fetch-request" {:upvalue-count 0 :arity 3 :constants ("get" "url" "method" "GET" "headers" "dict" "body" "signal" "preloaded" 200 {:upvalue-count 0 :arity 1 :constants () :bytecode (2 50)} "host-new" "Headers" "Object" "for-each" {:upvalue-count 2 :arity 1 :constants ("host-call" "set" "get") :bytecode (20 0 0 18 0 1 1 0 16 0 18 1 16 0 52 2 0 2 49 4 50)} "keys" "host-set!" "promise-then" "host-call" "dom-window" "fetch" {:upvalue-count 2 :arity 1 :constants ("host-get" "ok" "status" {:upvalue-count 1 :arity 1 :constants ("host-call" "host-get" "headers" "get") :bytecode (20 0 0 20 1 0 18 0 1 2 0 48 2 1 3 0 16 0 49 3 50)} "promise-then" "host-call" "text" {:upvalue-count 4 :arity 1 :constants () :bytecode (18 0 18 1 18 2 18 3 16 0 49 4 50)}) :bytecode (20 0 0 16 0 1 1 0 48 2 17 1 20 0 0 16 0 1 2 0 48 2 17 2 51 3 0 1 0 17 3 20 4 0 20 5 0 16 0 1 6 0 48 2 51 7 0 0 0 1 1 1 2 1 3 18 1 49 3 50)}) :bytecode (16 0 1 1 0 52 0 0 2 17 3 16 0 1 2 0 52 0 0 2 6 34 4 0 5 1 3 0 17 4 16 0 1 4 0 52 0 0 2 6 34 5 0 5 52 5 0 0 17 5 16 0 1 6 0 52 0 0 2 17 6 16 0 1 7 0 52 0 0 2 17 7 16 0 1 8 0 52 0 0 2 17 8 16 8 33 16 0 16 1 3 1 9 0 51 10 0 16 8 49 4 32 139 0 20 11 0 1 12 0 48 1 17 9 20 11 0 1 13 0 48 1 17 10 51 15 0 1 9 1 5 16 5 52 16 0 1 52 14 0 2 5 20 17 0 16 10 1 2 0 16 4 48 3 5 20 17 0 16 10 1 4 0 16 9 48 3 5 16 6 33 15 0 20 17 0 16 10 1 6 0 16 6 48 3 32 1 0 2 5 16 7 33 15 0 20 17 0 16 10 1 7 0 16 7 48 3 32 1 0 2 5 20 18 0 20 19 0 20 20 0 48 0 1 21 0 16 3 16 10 48 4 51 22 0 1 1 1 2 16 2 49 3 50)} "fetch-location" {:upvalue-count 0 :arity 1 :constants ("dom-query" "[sx-boost]" "#main-panel" "browser-navigate") :bytecode (20 0 0 1 1 0 48 1 6 34 9 0 5 20 0 0 1 2 0 48 1 17 1 16 1 33 10 0 20 3 0 16 0 49 1 32 1 0 2 50)} "fetch-and-restore" {:upvalue-count 0 :arity 4 :constants ("fetch-request" "dict" "url" "method" "GET" "headers" "body" "signal" {:upvalue-count 2 :arity 4 :constants ("dom-set-inner-html" "post-swap" "host-call" "dom-window" "scrollTo" 0) :bytecode (16 0 33 39 0 20 0 0 18 0 16 3 48 2 5 20 1 0 18 0 48 1 5 20 2 0 20 3 0 48 0 1 4 0 1 5 0 18 1 49 4 32 1 0 2 50)} {:upvalue-count 0 :arity 1 :constants ("log-warn" "str" "fetch-and-restore error: ") :bytecode (20 0 0 1 2 0 16 0 52 1 0 2 49 1 50)}) :bytecode (20 0 0 1 2 0 16 1 1 3 0 1 4 0 1 5 0 16 2 1 6 0 2 1 7 0 2 52 1 0 10 51 8 0 1 0 1 3 51 9 0 49 3 50)} "fetch-preload" {:upvalue-count 0 :arity 3 :constants ("fetch-request" "dict" "url" "method" "GET" "headers" "body" "signal" {:upvalue-count 2 :arity 4 :constants ("preload-cache-set") :bytecode (16 0 33 14 0 20 0 0 18 0 18 1 16 3 49 3 32 1 0 2 50)} {:upvalue-count 0 :arity 1 :constants () :bytecode (2 50)}) :bytecode (20 0 0 1 2 0 16 0 1 3 0 1 4 0 1 5 0 16 1 1 6 0 2 1 7 0 2 52 1 0 10 51 8 0 1 2 1 0 51 9 0 49 3 50)} "fetch-streaming" {:upvalue-count 0 :arity 4 :constants ("fetch-and-restore" 0) :bytecode (20 0 0 16 0 16 1 16 2 1 1 0 49 4 50)} "dom-parse-html-document" {:upvalue-count 0 :arity 1 :constants ("host-new" "DOMParser" "host-call" "parseFromString" "text/html") :bytecode (20 0 0 1 1 0 48 1 17 1 20 2 0 16 1 1 3 0 16 0 1 4 0 49 4 50)} "dom-body-inner-html" {:upvalue-count 0 :arity 1 :constants ("host-get" "body" "innerHTML") :bytecode (20 0 0 20 0 0 16 0 1 1 0 48 2 1 2 0 49 2 50)} "create-script-clone" {:upvalue-count 0 :arity 1 :constants ("host-global" "document" "host-call" "createElement" "script" "host-get" "attributes" {:upvalue-count 3 :arity 1 :constants ("<" "host-get" "length" "host-call" "item" "setAttribute" "name" "value" "+" 1) :bytecode (16 0 20 1 0 18 0 1 2 0 48 2 52 0 0 2 33 61 0 20 3 0 18 0 1 4 0 16 0 48 3 17 1 20 3 0 18 1 1 5 0 20 1 0 16 1 1 6 0 48 2 20 1 0 16 1 1 7 0 48 2 48 4 5 18 2 16 0 1 9 0 52 8 0 2 49 1 32 1 0 2 50)} 0 "host-set!" "textContent") :bytecode (20 0 0 1 1 0 48 1 17 1 20 2 0 16 1 1 3 0 1 4 0 48 3 17 2 20 5 0 16 0 1 6 0 48 2 17 3 2 17 4 51 7 0 1 3 1 2 1 4 17 4 16 4 1 8 0 48 1 5 20 9 0 16 2 1 10 0 20 5 0 16 0 1 10 0 48 2 48 3 5 16 2 50)} "cross-origin?" {:upvalue-count 0 :arity 1 :constants ("starts-with?" "http://" "https://" "not" "browser-location-origin") :bytecode (16 0 1 1 0 52 0 0 2 6 34 10 0 5 16 0 1 2 0 52 0 0 2 33 18 0 16 0 20 4 0 48 0 52 0 0 2 52 3 0 1 32 1 0 4 50)} "browser-scroll-to" {:upvalue-count 0 :arity 2 :constants ("host-call" "dom-window" "scrollTo") :bytecode (20 0 0 20 1 0 48 0 1 2 0 16 0 16 1 49 4 50)} "with-transition" {:upvalue-count 0 :arity 2 :constants ("host-get" "host-global" "document" "startViewTransition" "host-call" "host-callback") :bytecode (16 0 6 33 17 0 5 20 0 0 20 1 0 1 2 0 48 1 1 3 0 48 2 33 26 0 20 4 0 20 1 0 1 2 0 48 1 1 3 0 20 5 0 16 1 48 1 49 3 32 4 0 16 1 49 0 50)} "observe-intersection" {:upvalue-count 0 :arity 4 :constants ("host-callback" {:upvalue-count 4 :arity 1 :constants ("for-each" {:upvalue-count 4 :arity 1 :constants ("host-get" "isIntersecting" "set-timeout" {:upvalue-count 2 :arity 0 :constants () :bytecode (18 0 18 1 49 1 50)} "host-call" "observer" "unobserve") :bytecode (20 0 0 16 0 1 1 0 48 2 33 54 0 18 0 33 17 0 20 2 0 51 3 0 0 1 1 0 18 0 48 2 32 6 0 18 1 16 0 48 1 5 18 2 33 16 0 20 4 0 20 5 0 1 6 0 18 3 49 3 32 1 0 2 32 1 0 2 50)} "host-call" "forEach" "host-callback" {:upvalue-count 0 :arity 1 :constants () :bytecode (16 0 50)}) :bytecode (51 1 0 0 0 0 1 0 2 0 3 20 2 0 16 0 1 3 0 20 4 0 51 5 0 48 1 48 3 52 0 0 2 50)} "host-new" "IntersectionObserver" {:upvalue-count 5 :arity 1 :constants ("host-get" "length" {:upvalue-count 8 :arity 1 :constants ("<" "host-call" "item" "host-get" "isIntersecting" "set-timeout" {:upvalue-count 2 :arity 0 :constants () :bytecode (18 0 18 1 49 1 50)} "unobserve" "+" 1) :bytecode (16 0 18 0 52 0 0 2 33 105 0 20 1 0 18 1 1 2 0 16 0 48 3 17 1 16 1 6 33 11 0 5 20 3 0 16 1 1 4 0 48 2 33 53 0 18 2 33 17 0 20 5 0 51 6 0 0 3 1 1 18 2 48 2 32 6 0 18 3 16 1 48 1 5 18 4 33 15 0 20 1 0 18 5 1 7 0 18 6 48 3 32 1 0 2 32 1 0 2 5 18 7 16 0 1 9 0 52 8 0 2 49 1 32 1 0 2 50)} 0) :bytecode (20 0 0 16 0 1 1 0 48 2 17 1 2 17 2 51 2 0 1 1 1 0 0 0 0 1 0 2 0 3 0 4 1 2 17 2 16 2 1 3 0 49 1 50)} "host-call" "observe") :bytecode (20 0 0 51 1 0 1 3 1 1 1 2 1 0 48 1 17 4 20 2 0 1 3 0 20 0 0 51 4 0 1 3 1 1 1 2 1 5 1 0 48 1 48 2 17 5 20 5 0 16 5 1 6 0 16 0 48 3 5 16 5 50)} "event-source-connect" {:upvalue-count 0 :arity 2 :constants ("host-new" "EventSource" "host-set!" "_sxElement") :bytecode (20 0 0 1 1 0 16 0 48 2 17 2 20 2 0 16 2 1 3 0 16 1 48 3 5 16 2 50)} "event-source-listen" {:upvalue-count 0 :arity 3 :constants ("host-call" "addEventListener" "host-callback" {:upvalue-count 1 :arity 1 :constants () :bytecode (18 0 16 0 49 1 50)}) :bytecode (20 0 0 16 0 1 1 0 16 1 20 2 0 51 3 0 1 2 48 1 49 4 50)} "bind-boost-link" {:upvalue-count 0 :arity 2 :constants ("dom-listen" "click" {:upvalue-count 2 :arity 1 :constants ("not" "event-modifier-key?" "prevent-default" "dom-has-attr?" "sx-get" "dom-set-attr" "sx-push-url" "true" "execute-request") :bytecode (20 1 0 16 0 48 1 52 0 0 1 33 89 0 20 2 0 16 0 48 1 5 20 3 0 18 0 1 4 0 48 2 52 0 0 1 33 15 0 20 5 0 18 0 1 4 0 18 1 48 3 32 1 0 2 5 20 3 0 18 0 1 6 0 48 2 52 0 0 1 33 16 0 20 5 0 18 0 1 6 0 1 7 0 48 3 32 1 0 2 5 20 8 0 18 0 2 2 49 3 32 1 0 2 50)}) :bytecode (20 0 0 16 0 1 1 0 51 2 0 1 0 1 1 49 3 50)} "bind-boost-form" {:upvalue-count 0 :arity 3 :constants ("dom-listen" "submit" {:upvalue-count 1 :arity 1 :constants ("prevent-default" "execute-request") :bytecode (20 0 0 16 0 48 1 5 20 1 0 18 0 2 2 49 3 50)}) :bytecode (20 0 0 16 0 1 1 0 51 2 0 1 0 49 3 50)} "bind-client-route-click" {:upvalue-count 0 :arity 3 :constants ("dom-listen" "click" {:upvalue-count 2 :arity 1 :constants ("not" "event-modifier-key?" "prevent-default" "dom-query" "[sx-boost]" "dom-get-attr" "sx-boost" "=" "true" "#sx-content" "try-client-route" "url-pathname" "browser-push-state" "" "browser-scroll-to" 0 "log-info" "str" "sx:route server fetch " "dom-set-attr" "sx-get" "sx-target" "sx-select" "sx-push-url" "execute-request") :bytecode (20 1 0 16 0 48 1 52 0 0 1 33 197 0 20 2 0 16 0 48 1 5 20 3 0 1 4 0 48 1 17 1 16 1 33 46 0 20 5 0 16 1 1 6 0 48 2 17 3 16 3 6 33 14 0 5 16 3 1 8 0 52 7 0 2 52 0 0 1 33 5 0 16 3 32 3 0 1 9 0 32 3 0 1 9 0 17 2 20 10 0 20 11 0 18 0 48 1 16 2 48 2 33 26 0 20 12 0 2 1 13 0 18 0 48 3 5 20 14 0 1 15 0 1 15 0 49 2 32 77 0 20 16 0 1 18 0 18 0 52 17 0 2 48 1 5 20 19 0 18 1 1 20 0 18 0 48 3 5 20 19 0 18 1 1 21 0 16 2 48 3 5 20 19 0 18 1 1 22 0 16 2 48 3 5 20 19 0 18 1 1 23 0 1 8 0 48 3 5 20 24 0 18 1 2 2 49 3 32 1 0 2 50)}) :bytecode (20 0 0 16 0 1 1 0 51 2 0 1 1 1 0 49 3 50)} "sw-post-message" "try-parse-json" {:upvalue-count 0 :arity 1 :constants ("json-parse") :bytecode (20 0 0 16 0 49 1 50)} "strip-component-scripts" {:upvalue-count 0 :arity 1 :constants ("\n\n\n" "html"))
- (h4 :class "font-semibold mt-4 mb-2" "Boot chicken-and-egg")
+ (h4 (~tw :tokens "font-semibold mt-4 mb-2") "Boot chicken-and-egg")
(p (code "boot.sx") " orchestrates the boot sequence but is itself web framework code. Solution: thin native boot shim (~30 lines) in " (code "sx-platform.js") ":")
(~docs/code :src (highlight "SxPlatform.boot = function(evaluator) {\n // 1. Evaluate web framework .sx libraries\n var libs = document.querySelectorAll('script[type=\"text/sx-lib\"]');\n for (var i = 0; i < libs.length; i++) {\n evaluator.evalSource(libs[i].textContent);\n }\n // 2. Call boot-init (defined in boot.sx)\n evaluator.callFunction('boot-init');\n};" "javascript"))
- (h4 :class "font-semibold mt-4 mb-2" "Performance")
+ (h4 (~tw :tokens "font-semibold mt-4 mb-2") "Performance")
(p "Parsing + evaluating ~5,000 lines of web framework " (code ".sx") " at startup takes ~10\u201350ms. After " (code "define") ", functions are Lambda objects dispatched identically to compiled functions. " (strong "Zero ongoing performance difference.")))
@@ -229,22 +229,22 @@
(~docs/section :title "Phase 3: Wire Up Rust/WASM" :id "phase-3"
(p (strong "Goal:") " Rust evaluator calls " (code "sx-platform.js") " via wasm-bindgen imports. Handle table bridges DOM references.")
- (h4 :class "font-semibold mt-4 mb-2" "Handle table (JS-side)")
+ (h4 (~tw :tokens "font-semibold mt-4 mb-2") "Handle table (JS-side)")
(~docs/code :src (highlight "// In sx-wasm-shim.js\nconst handles = [null]; // index 0 = null handle\nfunction allocHandle(obj) { handles.push(obj); return handles.length - 1; }\nfunction getHandle(id) { return handles[id]; }\nfunction freeHandle(id) { handles[id] = null; }" "javascript"))
(p "DOM nodes are JS objects. The handle table maps " (code "u32") " IDs to JS objects. Rust stores " (code "Value::Handle(u32)") " and passes the " (code "u32") " to imported JS functions.")
- (h4 :class "font-semibold mt-4 mb-2" "Value::Handle in Rust")
+ (h4 (~tw :tokens "font-semibold mt-4 mb-2") "Value::Handle in Rust")
(~docs/code :src (highlight "// In platform.rs\npub enum Value {\n // ... existing variants ...\n Handle(u32), // opaque reference to JS-side object\n}" "rust"))
- (h4 :class "font-semibold mt-4 mb-2" "WASM imports from platform")
+ (h4 (~tw :tokens "font-semibold mt-4 mb-2") "WASM imports from platform")
(~docs/code :src (highlight "#[wasm_bindgen(module = \"/sx-platform-wasm.js\")]\nextern \"C\" {\n fn platform_create_element(tag: &str) -> u32;\n fn platform_create_text_node(text: &str) -> u32;\n fn platform_set_attr(handle: u32, name: &str, value: &str);\n fn platform_append_child(parent: u32, child: u32);\n fn platform_add_event_listener(handle: u32, event: &str, callback_id: u32);\n // ... ~50 DOM primitives\n}" "rust"))
- (h4 :class "font-semibold mt-4 mb-2" "Callback table for events")
+ (h4 (~tw :tokens "font-semibold mt-4 mb-2") "Callback table for events")
(p "When Rust creates an event handler (a Lambda), it stores it in a callback table and gets a " (code "u32") " ID. JS " (code "addEventListener") " wraps it: when the event fires, JS calls into WASM with the callback ID. Rust looks up the Lambda and evaluates it.")
- (h4 :class "font-semibold mt-4 mb-2" "sx-wasm-shim.js")
+ (h4 (~tw :tokens "font-semibold mt-4 mb-2") "sx-wasm-shim.js")
(p "Thin glue (~100 lines):")
- (ul :class "list-disc list-inside space-y-1 mt-2"
+ (ul (~tw :tokens "list-disc list-inside space-y-1 mt-2")
(li "Instantiate WASM module")
(li "Wire handle table")
(li "Delegate all platform calls to " (code "sx-platform.js"))
@@ -258,8 +258,8 @@
(~docs/section :title "Phase 4: Web Framework Loading" :id "phase-4"
(p (strong "Goal:") " Both JS and WASM evaluators load the same web framework " (code ".sx") " files at runtime.")
- (h4 :class "font-semibold mt-4 mb-2" "Boot sequence (identical for both evaluators)")
- (ol :class "list-decimal list-inside space-y-2 mt-2"
+ (h4 (~tw :tokens "font-semibold mt-4 mb-2") "Boot sequence (identical for both evaluators)")
+ (ol (~tw :tokens "list-decimal list-inside space-y-2 mt-2")
(li "Load " (code "sx-platform.js") " + evaluator (" (code "sx-evaluator.js") " or " (code "sx-wasm-shim.js") ")")
(li "Platform registers primitives with evaluator")
(li "Platform boot shim evaluates " (code "" "html"))
(p "The client parses the SX, renders to DOM, and replaces the suspense placeholder's children."))
(div
- (h4 :class "font-semibold text-stone-700" "4. Concurrent IO")
+ (h4 (~tw :tokens "font-semibold text-stone-700") "4. Concurrent IO")
(p "Data evaluation and header construction run in parallel. " (code "asyncio.wait(FIRST_COMPLETED)") " yields resolution chunks in whatever order IO completes — no artificial sequencing."))))
(~docs/subsection :title "Continuation foundation"
(p "Delimited continuations (" (code "reset") "/" (code "shift") ") are implemented in the Python evaluator (async_eval.py lines 586-624) and available as special forms. Phase 6 uses the simpler pattern of concurrent IO + completion-order streaming, but the continuation machinery is in place for Phase 7's more sophisticated evaluation-level suspension."))
(~docs/subsection :title "Files"
- (ul :class "list-disc pl-5 text-stone-700 space-y-1 font-mono text-sm"
+ (ul (~tw :tokens "list-disc pl-5 text-stone-700 space-y-1 font-mono text-sm")
(li "shared/sx/templates/pages.sx — ~shared:pages/suspense component definition")
(li "shared/sx/types.py — PageDef.stream, PageDef.fallback_expr fields")
(li "shared/sx/evaluator.py — defpage :stream/:fallback parsing")
@@ -392,16 +392,16 @@
(li "sx/sxc/pages/helpers.py — streaming-demo-data page helper")))
(~docs/subsection :title "Demonstration"
- (p "The " (a :href "/sx/(geography.(isomorphism.streaming))" :class "text-violet-700 underline" "streaming demo page") " exercises the full pipeline:")
- (ol :class "list-decimal pl-5 text-stone-700 space-y-1"
- (li "Navigate to " (a :href "/sx/(geography.(isomorphism.streaming))" :class "text-violet-700 underline" "/sx/(geography.(isomorphism.streaming))"))
+ (p "The " (a :href "/sx/(geography.(isomorphism.streaming))" (~tw :tokens "text-violet-700 underline") "streaming demo page") " exercises the full pipeline:")
+ (ol (~tw :tokens "list-decimal pl-5 text-stone-700 space-y-1")
+ (li "Navigate to " (a :href "/sx/(geography.(isomorphism.streaming))" (~tw :tokens "text-violet-700 underline") "/sx/(geography.(isomorphism.streaming))"))
(li "The page skeleton appears " (strong "instantly") " — animated loading skeletons fill the content area")
(li "After ~1.5 seconds, the real content replaces the skeletons (streamed from server)")
(li "Open the Network tab — observe " (code "Transfer-Encoding: chunked") " on the document response")
(li "The document response shows multiple chunks arriving over time: shell first, then resolution scripts")))
(~docs/subsection :title "What to verify"
- (ul :class "list-disc pl-5 text-stone-700 space-y-1"
+ (ul (~tw :tokens "list-disc pl-5 text-stone-700 space-y-1")
(li (strong "Instant shell: ") "The page HTML arrives immediately — no waiting for the 1.5s data fetch")
(li (strong "Suspense placeholders: ") "The " (code "~shared:pages/suspense") " component renders a " (code "data-suspense") " wrapper with animated fallback content")
(li (strong "Resolution: ") "The " (code "__sxResolve()") " inline script replaces the placeholder with real rendered content")
@@ -415,24 +415,24 @@
(~docs/section :title "Phase 7: Full Isomorphism" :id "phase-7"
- (div :class "rounded border border-green-200 bg-green-50 p-4 mb-4"
- (div :class "flex items-center gap-2 mb-2"
- (span :class "inline-block px-2 py-0.5 rounded text-xs font-bold bg-green-600 text-white uppercase" "Complete"))
- (p :class "text-green-900 font-medium" "What it enables")
- (p :class "text-green-800" "Same SX code runs on either side. Runtime chooses optimal split via affinity annotations and render plans. Client data cache managed via invalidation headers and server-driven updates. Cross-host isomorphism verified by 61 automated tests."))
+ (div (~tw :tokens "rounded border border-green-200 bg-green-50 p-4 mb-4")
+ (div (~tw :tokens "flex items-center gap-2 mb-2")
+ (span (~tw :tokens "inline-block px-2 py-0.5 rounded text-xs font-bold bg-green-600 text-white uppercase") "Complete"))
+ (p (~tw :tokens "text-green-900 font-medium") "What it enables")
+ (p (~tw :tokens "text-green-800") "Same SX code runs on either side. Runtime chooses optimal split via affinity annotations and render plans. Client data cache managed via invalidation headers and server-driven updates. Cross-host isomorphism verified by 61 automated tests."))
(~docs/subsection :title "7a. Affinity Annotations & Render Target"
- (div :class "rounded border border-green-300 bg-green-50 p-3 mb-4"
- (div :class "flex items-center gap-2 mb-1"
- (span :class "inline-block px-2 py-0.5 rounded text-xs font-bold bg-green-600 text-white uppercase" "Complete"))
- (p :class "text-green-800 text-sm" "Components declare where they prefer to render. The spec combines affinity with IO analysis to produce a per-component render target decision."))
+ (div (~tw :tokens "rounded border border-green-300 bg-green-50 p-3 mb-4")
+ (div (~tw :tokens "flex items-center gap-2 mb-1")
+ (span (~tw :tokens "inline-block px-2 py-0.5 rounded text-xs font-bold bg-green-600 text-white uppercase") "Complete"))
+ (p (~tw :tokens "text-green-800 text-sm") "Components declare where they prefer to render. The spec combines affinity with IO analysis to produce a per-component render target decision."))
(p "Affinity annotations let component authors express rendering preferences:")
(~docs/code :src (highlight "(defcomp ~plans/isomorphic/product-grid (&key products)\n :affinity :client ;; interactive, prefer client rendering\n (div ...))\n\n(defcomp ~plans/isomorphic/auth-menu (&key user)\n :affinity :server ;; auth-sensitive, always server\n (div ...))\n\n(defcomp ~plans/isomorphic/card (&key title)\n ;; no annotation = :affinity :auto (default)\n ;; runtime decides from IO analysis\n (div ...))" "lisp"))
(p "The " (code "render-target") " function in deps.sx combines affinity with IO analysis:")
- (ul :class "list-disc pl-5 text-stone-700 space-y-1"
+ (ul (~tw :tokens "list-disc pl-5 text-stone-700 space-y-1")
(li (code ":affinity :server") " → always " (code "\"server\"") " (auth-sensitive, secrets, heavy IO)")
(li (code ":affinity :client") " → always " (code "\"client\"") " (interactive, IO proxied)")
(li (code ":affinity :auto") " (default) → " (code "\"server\"") " if IO-dependent, " (code "\"client\"") " if pure"))
@@ -440,7 +440,7 @@
(p "The server's partial evaluator (" (code "_aser") ") uses " (code "render_target") " instead of the previous " (code "is_pure") " check. Components with " (code ":affinity :client") " are serialized for client rendering even if they call IO primitives — the IO proxy (Phase 5) handles the calls.")
(~docs/subsection :title "Files"
- (ul :class "list-disc pl-5 text-stone-700 space-y-1 font-mono text-sm"
+ (ul (~tw :tokens "list-disc pl-5 text-stone-700 space-y-1 font-mono text-sm")
(li "shared/sx/ref/eval.sx — defcomp annotation parsing, defcomp-kwarg helper")
(li "shared/sx/ref/deps.sx — render-target function, platform interface")
(li "shared/sx/types.py — Component.affinity field, render_target property")
@@ -452,7 +452,7 @@
(li "shared/sx/ref/test-deps.sx — 6 new render-target tests")))
(~docs/subsection :title "Verification"
- (ul :class "list-disc pl-5 text-stone-700 space-y-1"
+ (ul (~tw :tokens "list-disc pl-5 text-stone-700 space-y-1")
(li "269 spec tests pass (10 new: 4 eval + 6 deps)")
(li "79 Python unit tests pass")
(li "Bootstrapped to both hosts (sx_ref.py + sx-browser.js)")
@@ -460,10 +460,10 @@
(~docs/subsection :title "7b. Runtime Boundary Optimizer"
- (div :class "rounded border border-green-300 bg-green-50 p-3 mb-4"
- (div :class "flex items-center gap-2 mb-1"
- (span :class "inline-block px-2 py-0.5 rounded text-xs font-bold bg-green-600 text-white uppercase" "Complete"))
- (p :class "text-green-800 text-sm" "Per-page render plans computed at registration time. Each page knows exactly which components render server-side vs client-side, cached on PageDef."))
+ (div (~tw :tokens "rounded border border-green-300 bg-green-50 p-3 mb-4")
+ (div (~tw :tokens "flex items-center gap-2 mb-1")
+ (span (~tw :tokens "inline-block px-2 py-0.5 rounded text-xs font-bold bg-green-600 text-white uppercase") "Complete"))
+ (p (~tw :tokens "text-green-800 text-sm") "Per-page render plans computed at registration time. Each page knows exactly which components render server-side vs client-side, cached on PageDef."))
(p "Given component tree + IO dependency graph + affinity annotations, decide per-component: server-expand, client-render, or stream. Planning step cached at registration, recomputed on component change.")
@@ -471,7 +471,7 @@
(~docs/code :src (highlight "(page-render-plan page-source env io-names)\n;; Returns:\n;; {:components {~plans/content-addressed-components/name \"server\"|\"client\" ...}\n;; :server (list of server-expanded names)\n;; :client (list of client-rendered names)\n;; :io-deps (IO primitives needed by server components)}" "lisp"))
(~docs/subsection :title "Integration Points"
- (ul :class "list-disc pl-5 text-stone-700 space-y-1"
+ (ul (~tw :tokens "list-disc pl-5 text-stone-700 space-y-1")
(li (code "shared/sx/ref/deps.sx") " — " (code "page-render-plan") " spec function")
(li (code "shared/sx/deps.py") " — Python wrapper, dispatches to bootstrapped code")
(li (code "shared/sx/pages.py") " — " (code "compute_page_render_plans()") " called at mount time, caches on PageDef")
@@ -479,17 +479,17 @@
(li (code "shared/sx/types.py") " — " (code "PageDef.render_plan") " field")))
(~docs/subsection :title "Verification"
- (ul :class "list-disc pl-5 text-stone-700 space-y-1"
+ (ul (~tw :tokens "list-disc pl-5 text-stone-700 space-y-1")
(li "5 new spec tests (page-render-plan suite)")
(li "Render plans visible on " (a :href "/sx/(geography.(isomorphism.affinity))" "affinity demo page"))
(li "Client page registry includes :render-plan for each page"))))
(~docs/subsection :title "7c. Cache Invalidation & Optimistic Data Updates"
- (div :class "rounded border border-green-300 bg-green-50 p-3 mb-4"
- (div :class "flex items-center gap-2 mb-1"
- (span :class "inline-block px-2 py-0.5 rounded text-xs font-bold bg-green-600 text-white uppercase" "Complete"))
- (p :class "text-green-800 text-sm" "Client data cache management, optimistic predicted mutations with snapshot rollback, and server-driven cache updates."))
+ (div (~tw :tokens "rounded border border-green-300 bg-green-50 p-3 mb-4")
+ (div (~tw :tokens "flex items-center gap-2 mb-1")
+ (span (~tw :tokens "inline-block px-2 py-0.5 rounded text-xs font-bold bg-green-600 text-white uppercase") "Complete"))
+ (p (~tw :tokens "text-green-800 text-sm") "Client data cache management, optimistic predicted mutations with snapshot rollback, and server-driven cache updates."))
(p "The client-side page data cache (30-second TTL) now supports cache invalidation, server-driven updates, and optimistic mutations. The client predicts the result of a mutation, immediately re-renders with the predicted data, and confirms or reverts when the server responds.")
@@ -497,12 +497,12 @@
(p "Component authors can declare cache invalidation on elements that trigger mutations:")
(~docs/code :src (highlight ";; Clear specific page's cache after successful action\n(form :sx-post \"/cart/remove\"\n :sx-cache-invalidate \"cart-page\"\n ...)\n\n;; Clear ALL page caches after action\n(button :sx-post \"/admin/reset\"\n :sx-cache-invalidate \"*\")" "lisp"))
(p "The server can also control client cache via response headers:")
- (ul :class "list-disc pl-5 text-stone-700 space-y-1"
+ (ul (~tw :tokens "list-disc pl-5 text-stone-700 space-y-1")
(li (code "SX-Cache-Invalidate: page-name") " — clear cache for a page")
(li (code "SX-Cache-Update: page-name") " — replace cache with the response body (SX-format data)")))
(~docs/subsection :title "Optimistic Mutations"
- (ul :class "list-disc pl-5 text-stone-700 space-y-1"
+ (ul (~tw :tokens "list-disc pl-5 text-stone-700 space-y-1")
(li (strong "optimistic-cache-update") " — applies a mutator function to cached data, saves a snapshot for rollback")
(li (strong "optimistic-cache-revert") " — restores the pre-mutation snapshot if the server rejects")
(li (strong "optimistic-cache-confirm") " — discards the snapshot after server confirmation")
@@ -510,33 +510,33 @@
(li (strong "/sx/action/") " — server endpoint for processing mutations (POST, returns SX wire format)")))
(~docs/subsection :title "Files"
- (ul :class "list-disc pl-5 text-stone-700 space-y-1 font-mono text-sm"
+ (ul (~tw :tokens "list-disc pl-5 text-stone-700 space-y-1 font-mono text-sm")
(li "shared/sx/ref/orchestration.sx — cache management + optimistic cache functions + submit-mutation spec")
(li "shared/sx/ref/engine.sx — SX-Cache-Invalidate, SX-Cache-Update response headers")
(li "shared/sx/pages.py — mount_action_endpoint for /sx/action/")
(li "sx/sx/optimistic-demo.sx — live demo component")))
(~docs/subsection :title "Verification"
- (ul :class "list-disc pl-5 text-stone-700 space-y-1"
- (li "Live demo at " (a :href "/sx/(geography.(isomorphism.optimistic))" :class "text-violet-600 hover:underline" "/sx/(geography.(isomorphism.optimistic))"))
+ (ul (~tw :tokens "list-disc pl-5 text-stone-700 space-y-1")
+ (li "Live demo at " (a :href "/sx/(geography.(isomorphism.optimistic))" (~tw :tokens "text-violet-600 hover:underline") "/sx/(geography.(isomorphism.optimistic))"))
(li "Console log: " (code "sx:optimistic confirmed") " / " (code "sx:optimistic reverted")))))
(~docs/subsection :title "7d. Offline Data Layer"
- (div :class "rounded border border-green-300 bg-green-50 p-3 mb-4"
- (div :class "flex items-center gap-2 mb-1"
- (span :class "inline-block px-2 py-0.5 rounded text-xs font-bold bg-green-600 text-white uppercase" "Complete"))
- (p :class "text-green-800 text-sm" "Service Worker with IndexedDB caching, connectivity tracking, and offline mutation queue with replay on reconnect."))
+ (div (~tw :tokens "rounded border border-green-300 bg-green-50 p-3 mb-4")
+ (div (~tw :tokens "flex items-center gap-2 mb-1")
+ (span (~tw :tokens "inline-block px-2 py-0.5 rounded text-xs font-bold bg-green-600 text-white uppercase") "Complete"))
+ (p (~tw :tokens "text-green-800 text-sm") "Service Worker with IndexedDB caching, connectivity tracking, and offline mutation queue with replay on reconnect."))
(p "A Service Worker registered at " (code "/sx-sw.js") " provides three-tier caching, plus an offline mutation queue that builds on Phase 7c's optimistic updates:")
- (ul :class "list-disc pl-5 text-stone-700 space-y-1"
+ (ul (~tw :tokens "list-disc pl-5 text-stone-700 space-y-1")
(li (strong "/sx/data/* ") "— network-first with IndexedDB fallback. Page data cached on fetch, served from IndexedDB when offline.")
(li (strong "/sx/io/* ") "— network-first with IndexedDB fallback. IO proxy responses cached the same way.")
(li (strong "/static/* ") "— stale-while-revalidate via Cache API. Serves cached assets immediately, updates in background.")
(li (strong "Offline mutations") " — " (code "offline-aware-mutation") " routes to " (code "submit-mutation") " when online, " (code "offline-queue-mutation") " when offline. " (code "offline-sync") " replays the queue on reconnect."))
(~docs/subsection :title "How It Works"
- (ol :class "list-decimal list-inside text-stone-700 space-y-2"
+ (ol (~tw :tokens "list-decimal list-inside text-stone-700 space-y-2")
(li "On boot, " (code "sx-browser.js") " registers the SW at " (code "/sx-sw.js") " (root scope)")
(li "SW intercepts fetch events and routes by URL pattern")
(li "For data/IO: try network first, on failure serve from IndexedDB")
@@ -545,51 +545,51 @@
(li "Offline mutations queue locally, replay on reconnect via " (code "offline-sync"))))
(~docs/subsection :title "Files"
- (ul :class "list-disc pl-5 text-stone-700 space-y-1 font-mono text-sm"
+ (ul (~tw :tokens "list-disc pl-5 text-stone-700 space-y-1 font-mono text-sm")
(li "shared/static/scripts/sx-sw.js — Service Worker (network-first + stale-while-revalidate)")
(li "shared/sx/ref/orchestration.sx — offline queue, sync, connectivity tracking, sw-post-message")
(li "shared/sx/pages.py — mount_service_worker() serves SW at /sx-sw.js")
(li "sx/sx/offline-demo.sx — live demo component")))
(~docs/subsection :title "Verification"
- (ul :class "list-disc pl-5 text-stone-700 space-y-1"
- (li "Live demo at " (a :href "/sx/(geography.(isomorphism.offline))" :class "text-violet-600 hover:underline" "/sx/(geography.(isomorphism.offline))"))
+ (ul (~tw :tokens "list-disc pl-5 text-stone-700 space-y-1")
+ (li "Live demo at " (a :href "/sx/(geography.(isomorphism.offline))" (~tw :tokens "text-violet-600 hover:underline") "/sx/(geography.(isomorphism.offline))"))
(li "Test with DevTools Network → Offline mode")
(li "Console log: " (code "sx:offline queued") ", " (code "sx:offline syncing") ", " (code "sx:offline synced")))))
(~docs/subsection :title "7e. Isomorphic Testing"
- (div :class "rounded border border-green-300 bg-green-50 p-3 mb-4"
- (div :class "flex items-center gap-2 mb-1"
- (span :class "inline-block px-2 py-0.5 rounded text-xs font-bold bg-green-600 text-white uppercase" "Complete"))
- (p :class "text-green-800 text-sm" "Cross-host test suite: same SX expressions evaluated on Python (sx_ref.py) and JS (sx-browser.js via Node.js), HTML output compared."))
+ (div (~tw :tokens "rounded border border-green-300 bg-green-50 p-3 mb-4")
+ (div (~tw :tokens "flex items-center gap-2 mb-1")
+ (span (~tw :tokens "inline-block px-2 py-0.5 rounded text-xs font-bold bg-green-600 text-white uppercase") "Complete"))
+ (p (~tw :tokens "text-green-800 text-sm") "Cross-host test suite: same SX expressions evaluated on Python (sx_ref.py) and JS (sx-browser.js via Node.js), HTML output compared."))
(p "61 isomorphic tests verify that Python and JS produce identical results:")
- (ul :class "list-disc pl-5 text-stone-700 space-y-1"
+ (ul (~tw :tokens "list-disc pl-5 text-stone-700 space-y-1")
(li "37 eval tests: arithmetic, comparison, strings, collections, logic, let/lambda, higher-order, dict, keywords, cond/case")
(li "24 render tests: elements, attributes, nesting, void elements, boolean attrs, conditionals, map, components, affinity, HTML escaping"))
(~docs/subsection :title "Files"
- (ul :class "list-disc pl-5 text-stone-700 space-y-1 font-mono text-sm"
+ (ul (~tw :tokens "list-disc pl-5 text-stone-700 space-y-1 font-mono text-sm")
(li "shared/sx/tests/test_isomorphic.py — cross-host test suite")
(li "Run: " (code "python3 -m pytest shared/sx/tests/test_isomorphic.py -q")))))
(~docs/subsection :title "7f. Universal Page Descriptor"
- (div :class "rounded border border-green-300 bg-green-50 p-3 mb-4"
- (div :class "flex items-center gap-2 mb-1"
- (span :class "inline-block px-2 py-0.5 rounded text-xs font-bold bg-green-600 text-white uppercase" "Complete"))
- (p :class "text-green-800 text-sm" "defpage is portable: same descriptor executes on server (execute_page) and client (tryClientRoute)."))
+ (div (~tw :tokens "rounded border border-green-300 bg-green-50 p-3 mb-4")
+ (div (~tw :tokens "flex items-center gap-2 mb-1")
+ (span (~tw :tokens "inline-block px-2 py-0.5 rounded text-xs font-bold bg-green-600 text-white uppercase") "Complete"))
+ (p (~tw :tokens "text-green-800 text-sm") "defpage is portable: same descriptor executes on server (execute_page) and client (tryClientRoute)."))
(p "The defpage descriptor is universal — the same definition works on both hosts:")
- (ul :class "list-disc pl-5 text-stone-700 space-y-1"
+ (ul (~tw :tokens "list-disc pl-5 text-stone-700 space-y-1")
(li (strong "Server: ") (code "execute_page()") " evaluates :data and :content slots, expands server components via " (code "_aser") ", returns SX wire format")
(li (strong "Client: ") (code "try-client-route") " matches route, evaluates content SX, renders to DOM. Data pages fetch via " (code "/sx/data/") ", IO proxied via " (code "/sx/io/"))
(li (strong "Render plan: ") "each page's " (code ":render-plan") " is included in the client page registry, showing which components render where")
(li (strong "Console visibility: ") "client logs " (code "sx:route plan pagename — N server, M client") " on each navigation")))
- (div :class "rounded border border-amber-200 bg-amber-50 p-3 mt-2"
- (p :class "text-amber-800 text-sm" (strong "Depends on: ") "All previous phases.")))
+ (div (~tw :tokens "rounded border border-amber-200 bg-amber-50 p-3 mt-2")
+ (p (~tw :tokens "text-amber-800 text-sm") (strong "Depends on: ") "All previous phases.")))
;; -----------------------------------------------------------------------
;; Cross-Cutting Concerns
@@ -598,7 +598,7 @@
(~docs/section :title "Cross-Cutting Concerns" :id "cross-cutting"
(~docs/subsection :title "Error Reporting"
- (ul :class "list-disc pl-5 text-stone-700 space-y-1"
+ (ul (~tw :tokens "list-disc pl-5 text-stone-700 space-y-1")
(li "Phase 1: \"Unknown component\" includes which page expected it and what bundle was sent")
(li "Phase 2: Server logs which components expanded server-side vs sent to client")
(li "Phase 3: Client route failures include unmatched path and available routes")
@@ -606,7 +606,7 @@
(li "Source location tracking in parser → propagate through eval → include in error messages")))
(~docs/subsection :title "Backward Compatibility"
- (ul :class "list-disc pl-5 text-stone-700 space-y-1"
+ (ul (~tw :tokens "list-disc pl-5 text-stone-700 space-y-1")
(li "Pages without annotations behave as today")
(li "SX-Request / SX-Components / SX-Css header protocol continues")
(li "Existing .sx files require no changes")
@@ -621,61 +621,61 @@
;; -----------------------------------------------------------------------
(~docs/section :title "Critical Files" :id "critical-files"
- (div :class "overflow-x-auto rounded border border-stone-200"
- (table :class "w-full text-left text-sm"
- (thead (tr :class "border-b border-stone-200 bg-stone-100"
- (th :class "px-3 py-2 font-medium text-stone-600" "File")
- (th :class "px-3 py-2 font-medium text-stone-600" "Role")
- (th :class "px-3 py-2 font-medium text-stone-600" "Phases")))
+ (div (~tw :tokens "overflow-x-auto rounded border border-stone-200")
+ (table (~tw :tokens "w-full text-left text-sm")
+ (thead (tr (~tw :tokens "border-b border-stone-200 bg-stone-100")
+ (th (~tw :tokens "px-3 py-2 font-medium text-stone-600") "File")
+ (th (~tw :tokens "px-3 py-2 font-medium text-stone-600") "Role")
+ (th (~tw :tokens "px-3 py-2 font-medium text-stone-600") "Phases")))
(tbody
- (tr :class "border-b border-stone-100"
- (td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/async_eval.py")
- (td :class "px-3 py-2 text-stone-700" "Core evaluator, _aser, server/client boundary")
- (td :class "px-3 py-2 text-stone-600" "2, 5"))
- (tr :class "border-b border-stone-100"
- (td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/helpers.py")
- (td :class "px-3 py-2 text-stone-700" "sx_page(), sx_response(), output pipeline")
- (td :class "px-3 py-2 text-stone-600" "1, 3"))
- (tr :class "border-b border-stone-100"
- (td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/jinja_bridge.py")
- (td :class "px-3 py-2 text-stone-700" "_COMPONENT_ENV, component registry")
- (td :class "px-3 py-2 text-stone-600" "1, 2"))
- (tr :class "border-b border-stone-100"
- (td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/pages.py")
- (td :class "px-3 py-2 text-stone-700" "defpage, execute_page(), page lifecycle")
- (td :class "px-3 py-2 text-stone-600" "2, 3"))
- (tr :class "border-b border-stone-100"
- (td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/ref/boot.sx")
- (td :class "px-3 py-2 text-stone-700" "Client boot, component caching")
- (td :class "px-3 py-2 text-stone-600" "1, 3, 4"))
- (tr :class "border-b border-stone-100"
- (td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/ref/orchestration.sx")
- (td :class "px-3 py-2 text-stone-700" "Client fetch/swap/morph")
- (td :class "px-3 py-2 text-stone-600" "3, 4"))
- (tr :class "border-b border-stone-100"
- (td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/ref/eval.sx")
- (td :class "px-3 py-2 text-stone-700" "Evaluator spec")
- (td :class "px-3 py-2 text-stone-600" "4"))
- (tr :class "border-b border-stone-100"
- (td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/ref/engine.sx")
- (td :class "px-3 py-2 text-stone-700" "Morph, swaps, triggers")
- (td :class "px-3 py-2 text-stone-600" "3"))
- (tr :class "border-b border-stone-100"
- (td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/deps.py")
- (td :class "px-3 py-2 text-stone-700" "Dependency analysis (new)")
- (td :class "px-3 py-2 text-stone-600" "1, 2"))
- (tr :class "border-b border-stone-100"
- (td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/ref/router.sx")
- (td :class "px-3 py-2 text-stone-700" "Client-side routing (new)")
- (td :class "px-3 py-2 text-stone-600" "3"))
- (tr :class "border-b border-stone-100"
- (td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/ref/io-bridge.sx")
- (td :class "px-3 py-2 text-stone-700" "Client IO primitives (new)")
- (td :class "px-3 py-2 text-stone-600" "4"))
- (tr :class "border-b border-stone-100"
- (td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/ref/suspense.sx")
- (td :class "px-3 py-2 text-stone-700" "Streaming/suspension (new)")
- (td :class "px-3 py-2 text-stone-600" "5"))))))))
+ (tr (~tw :tokens "border-b border-stone-100")
+ (td (~tw :tokens "px-3 py-2 font-mono text-sm text-violet-700") "shared/sx/async_eval.py")
+ (td (~tw :tokens "px-3 py-2 text-stone-700") "Core evaluator, _aser, server/client boundary")
+ (td (~tw :tokens "px-3 py-2 text-stone-600") "2, 5"))
+ (tr (~tw :tokens "border-b border-stone-100")
+ (td (~tw :tokens "px-3 py-2 font-mono text-sm text-violet-700") "shared/sx/helpers.py")
+ (td (~tw :tokens "px-3 py-2 text-stone-700") "sx_page(), sx_response(), output pipeline")
+ (td (~tw :tokens "px-3 py-2 text-stone-600") "1, 3"))
+ (tr (~tw :tokens "border-b border-stone-100")
+ (td (~tw :tokens "px-3 py-2 font-mono text-sm text-violet-700") "shared/sx/jinja_bridge.py")
+ (td (~tw :tokens "px-3 py-2 text-stone-700") "_COMPONENT_ENV, component registry")
+ (td (~tw :tokens "px-3 py-2 text-stone-600") "1, 2"))
+ (tr (~tw :tokens "border-b border-stone-100")
+ (td (~tw :tokens "px-3 py-2 font-mono text-sm text-violet-700") "shared/sx/pages.py")
+ (td (~tw :tokens "px-3 py-2 text-stone-700") "defpage, execute_page(), page lifecycle")
+ (td (~tw :tokens "px-3 py-2 text-stone-600") "2, 3"))
+ (tr (~tw :tokens "border-b border-stone-100")
+ (td (~tw :tokens "px-3 py-2 font-mono text-sm text-violet-700") "shared/sx/ref/boot.sx")
+ (td (~tw :tokens "px-3 py-2 text-stone-700") "Client boot, component caching")
+ (td (~tw :tokens "px-3 py-2 text-stone-600") "1, 3, 4"))
+ (tr (~tw :tokens "border-b border-stone-100")
+ (td (~tw :tokens "px-3 py-2 font-mono text-sm text-violet-700") "shared/sx/ref/orchestration.sx")
+ (td (~tw :tokens "px-3 py-2 text-stone-700") "Client fetch/swap/morph")
+ (td (~tw :tokens "px-3 py-2 text-stone-600") "3, 4"))
+ (tr (~tw :tokens "border-b border-stone-100")
+ (td (~tw :tokens "px-3 py-2 font-mono text-sm text-violet-700") "shared/sx/ref/eval.sx")
+ (td (~tw :tokens "px-3 py-2 text-stone-700") "Evaluator spec")
+ (td (~tw :tokens "px-3 py-2 text-stone-600") "4"))
+ (tr (~tw :tokens "border-b border-stone-100")
+ (td (~tw :tokens "px-3 py-2 font-mono text-sm text-violet-700") "shared/sx/ref/engine.sx")
+ (td (~tw :tokens "px-3 py-2 text-stone-700") "Morph, swaps, triggers")
+ (td (~tw :tokens "px-3 py-2 text-stone-600") "3"))
+ (tr (~tw :tokens "border-b border-stone-100")
+ (td (~tw :tokens "px-3 py-2 font-mono text-sm text-violet-700") "shared/sx/deps.py")
+ (td (~tw :tokens "px-3 py-2 text-stone-700") "Dependency analysis (new)")
+ (td (~tw :tokens "px-3 py-2 text-stone-600") "1, 2"))
+ (tr (~tw :tokens "border-b border-stone-100")
+ (td (~tw :tokens "px-3 py-2 font-mono text-sm text-violet-700") "shared/sx/ref/router.sx")
+ (td (~tw :tokens "px-3 py-2 text-stone-700") "Client-side routing (new)")
+ (td (~tw :tokens "px-3 py-2 text-stone-600") "3"))
+ (tr (~tw :tokens "border-b border-stone-100")
+ (td (~tw :tokens "px-3 py-2 font-mono text-sm text-violet-700") "shared/sx/ref/io-bridge.sx")
+ (td (~tw :tokens "px-3 py-2 text-stone-700") "Client IO primitives (new)")
+ (td (~tw :tokens "px-3 py-2 text-stone-600") "4"))
+ (tr (~tw :tokens "border-b border-stone-100")
+ (td (~tw :tokens "px-3 py-2 font-mono text-sm text-violet-700") "shared/sx/ref/suspense.sx")
+ (td (~tw :tokens "px-3 py-2 text-stone-700") "Streaming/suspension (new)")
+ (td (~tw :tokens "px-3 py-2 text-stone-600") "5"))))))))
;; ---------------------------------------------------------------------------
;; SX CI Pipeline
diff --git a/sx/sx/plans/js-bootstrapper.sx b/sx/sx/plans/js-bootstrapper.sx
index 81553ea6..b35fdd74 100644
--- a/sx/sx/plans/js-bootstrapper.sx
+++ b/sx/sx/plans/js-bootstrapper.sx
@@ -74,63 +74,63 @@
(p "The JS bootstrapper has more moving parts than the Python one because "
"JavaScript is the " (em "client") " host. The browser runtime includes "
"things Python never needs:")
- (div :class "overflow-x-auto rounded border border-stone-200 my-4"
- (table :class "w-full text-sm"
- (thead :class "bg-stone-50"
+ (div (~tw :tokens "overflow-x-auto rounded border border-stone-200 my-4")
+ (table (~tw :tokens "w-full text-sm")
+ (thead (~tw :tokens "bg-stone-50")
(tr
- (th :class "px-4 py-2 text-left font-semibold text-stone-700" "Spec Module")
- (th :class "px-4 py-2 text-left font-semibold text-stone-700" "Purpose")
- (th :class "px-4 py-2 text-left font-semibold text-stone-700" "Python?")
- (th :class "px-4 py-2 text-left font-semibold text-stone-700" "Browser?")))
+ (th (~tw :tokens "px-4 py-2 text-left font-semibold text-stone-700") "Spec Module")
+ (th (~tw :tokens "px-4 py-2 text-left font-semibold text-stone-700") "Purpose")
+ (th (~tw :tokens "px-4 py-2 text-left font-semibold text-stone-700") "Python?")
+ (th (~tw :tokens "px-4 py-2 text-left font-semibold text-stone-700") "Browser?")))
(tbody
- (tr :class "border-t border-stone-100"
- (td :class "px-4 py-2 font-mono" "eval.sx")
- (td :class "px-4 py-2" "Core evaluator, special forms, TCO")
- (td :class "px-4 py-2 text-center" "Yes") (td :class "px-4 py-2 text-center" "Yes"))
- (tr :class "border-t border-stone-100"
- (td :class "px-4 py-2 font-mono" "render.sx")
- (td :class "px-4 py-2" "Tag registry, void elements, boolean attrs")
- (td :class "px-4 py-2 text-center" "Yes") (td :class "px-4 py-2 text-center" "Yes"))
- (tr :class "border-t border-stone-100"
- (td :class "px-4 py-2 font-mono" "parser.sx")
- (td :class "px-4 py-2" "Tokenizer, parser, serializer")
- (td :class "px-4 py-2 text-center" "—") (td :class "px-4 py-2 text-center" "Yes"))
- (tr :class "border-t border-stone-100"
- (td :class "px-4 py-2 font-mono" "adapter-html.sx")
- (td :class "px-4 py-2" "Render to HTML string")
- (td :class "px-4 py-2 text-center" "Yes") (td :class "px-4 py-2 text-center" "Yes"))
- (tr :class "border-t border-stone-100"
- (td :class "px-4 py-2 font-mono" "adapter-sx.sx")
- (td :class "px-4 py-2" "Serialize to SX wire format")
- (td :class "px-4 py-2 text-center" "Yes") (td :class "px-4 py-2 text-center" "Yes"))
- (tr :class "border-t border-stone-100 bg-blue-50"
- (td :class "px-4 py-2 font-mono" "adapter-dom.sx")
- (td :class "px-4 py-2" "Render to live DOM nodes")
- (td :class "px-4 py-2 text-center" "—") (td :class "px-4 py-2 text-center" "Yes"))
- (tr :class "border-t border-stone-100 bg-blue-50"
- (td :class "px-4 py-2 font-mono" "engine.sx")
- (td :class "px-4 py-2" "Fetch, swap, trigger, history")
- (td :class "px-4 py-2 text-center" "—") (td :class "px-4 py-2 text-center" "Yes"))
- (tr :class "border-t border-stone-100 bg-blue-50"
- (td :class "px-4 py-2 font-mono" "orchestration.sx")
- (td :class "px-4 py-2" "Element scanning, attribute processing")
- (td :class "px-4 py-2 text-center" "—") (td :class "px-4 py-2 text-center" "Yes"))
- (tr :class "border-t border-stone-100 bg-blue-50"
- (td :class "px-4 py-2 font-mono" "boot.sx")
- (td :class "px-4 py-2" "Script processing, mount, hydration")
- (td :class "px-4 py-2 text-center" "—") (td :class "px-4 py-2 text-center" "Yes"))
- (tr :class "border-t border-stone-100"
- (td :class "px-4 py-2 font-mono" "signals.sx")
- (td :class "px-4 py-2" "Reactive signal runtime")
- (td :class "px-4 py-2 text-center" "Yes") (td :class "px-4 py-2 text-center" "Yes"))
- (tr :class "border-t border-stone-100"
- (td :class "px-4 py-2 font-mono" "deps.sx")
- (td :class "px-4 py-2" "Component dependency analysis")
- (td :class "px-4 py-2 text-center" "Yes") (td :class "px-4 py-2 text-center" "Yes"))
- (tr :class "border-t border-stone-100"
- (td :class "px-4 py-2 font-mono" "router.sx")
- (td :class "px-4 py-2" "Client-side route matching")
- (td :class "px-4 py-2 text-center" "Yes") (td :class "px-4 py-2 text-center" "Yes")))))
+ (tr (~tw :tokens "border-t border-stone-100")
+ (td (~tw :tokens "px-4 py-2 font-mono") "eval.sx")
+ (td (~tw :tokens "px-4 py-2") "Core evaluator, special forms, TCO")
+ (td (~tw :tokens "px-4 py-2 text-center") "Yes") (td (~tw :tokens "px-4 py-2 text-center") "Yes"))
+ (tr (~tw :tokens "border-t border-stone-100")
+ (td (~tw :tokens "px-4 py-2 font-mono") "render.sx")
+ (td (~tw :tokens "px-4 py-2") "Tag registry, void elements, boolean attrs")
+ (td (~tw :tokens "px-4 py-2 text-center") "Yes") (td (~tw :tokens "px-4 py-2 text-center") "Yes"))
+ (tr (~tw :tokens "border-t border-stone-100")
+ (td (~tw :tokens "px-4 py-2 font-mono") "parser.sx")
+ (td (~tw :tokens "px-4 py-2") "Tokenizer, parser, serializer")
+ (td (~tw :tokens "px-4 py-2 text-center") "—") (td (~tw :tokens "px-4 py-2 text-center") "Yes"))
+ (tr (~tw :tokens "border-t border-stone-100")
+ (td (~tw :tokens "px-4 py-2 font-mono") "adapter-html.sx")
+ (td (~tw :tokens "px-4 py-2") "Render to HTML string")
+ (td (~tw :tokens "px-4 py-2 text-center") "Yes") (td (~tw :tokens "px-4 py-2 text-center") "Yes"))
+ (tr (~tw :tokens "border-t border-stone-100")
+ (td (~tw :tokens "px-4 py-2 font-mono") "adapter-sx.sx")
+ (td (~tw :tokens "px-4 py-2") "Serialize to SX wire format")
+ (td (~tw :tokens "px-4 py-2 text-center") "Yes") (td (~tw :tokens "px-4 py-2 text-center") "Yes"))
+ (tr (~tw :tokens "border-t border-stone-100 bg-blue-50")
+ (td (~tw :tokens "px-4 py-2 font-mono") "adapter-dom.sx")
+ (td (~tw :tokens "px-4 py-2") "Render to live DOM nodes")
+ (td (~tw :tokens "px-4 py-2 text-center") "—") (td (~tw :tokens "px-4 py-2 text-center") "Yes"))
+ (tr (~tw :tokens "border-t border-stone-100 bg-blue-50")
+ (td (~tw :tokens "px-4 py-2 font-mono") "engine.sx")
+ (td (~tw :tokens "px-4 py-2") "Fetch, swap, trigger, history")
+ (td (~tw :tokens "px-4 py-2 text-center") "—") (td (~tw :tokens "px-4 py-2 text-center") "Yes"))
+ (tr (~tw :tokens "border-t border-stone-100 bg-blue-50")
+ (td (~tw :tokens "px-4 py-2 font-mono") "orchestration.sx")
+ (td (~tw :tokens "px-4 py-2") "Element scanning, attribute processing")
+ (td (~tw :tokens "px-4 py-2 text-center") "—") (td (~tw :tokens "px-4 py-2 text-center") "Yes"))
+ (tr (~tw :tokens "border-t border-stone-100 bg-blue-50")
+ (td (~tw :tokens "px-4 py-2 font-mono") "boot.sx")
+ (td (~tw :tokens "px-4 py-2") "Script processing, mount, hydration")
+ (td (~tw :tokens "px-4 py-2 text-center") "—") (td (~tw :tokens "px-4 py-2 text-center") "Yes"))
+ (tr (~tw :tokens "border-t border-stone-100")
+ (td (~tw :tokens "px-4 py-2 font-mono") "signals.sx")
+ (td (~tw :tokens "px-4 py-2") "Reactive signal runtime")
+ (td (~tw :tokens "px-4 py-2 text-center") "Yes") (td (~tw :tokens "px-4 py-2 text-center") "Yes"))
+ (tr (~tw :tokens "border-t border-stone-100")
+ (td (~tw :tokens "px-4 py-2 font-mono") "deps.sx")
+ (td (~tw :tokens "px-4 py-2") "Component dependency analysis")
+ (td (~tw :tokens "px-4 py-2 text-center") "Yes") (td (~tw :tokens "px-4 py-2 text-center") "Yes"))
+ (tr (~tw :tokens "border-t border-stone-100")
+ (td (~tw :tokens "px-4 py-2 font-mono") "router.sx")
+ (td (~tw :tokens "px-4 py-2") "Client-side route matching")
+ (td (~tw :tokens "px-4 py-2 text-center") "Yes") (td (~tw :tokens "px-4 py-2 text-center") "Yes")))))
(p "Blue rows are browser-only modules. " (code "js.sx") " must handle all of them. "
"The platform interface is also larger: DOM primitives (" (code "dom-create-element")
", " (code "dom-append") ", " (code "dom-set-attr") ", ...), "
@@ -147,78 +147,78 @@
(~docs/subsection :title "Name Mangling"
(p "SX uses kebab-case. JavaScript uses camelCase.")
- (div :class "overflow-x-auto rounded border border-stone-200 my-4"
- (table :class "w-full text-sm"
- (thead :class "bg-stone-50"
+ (div (~tw :tokens "overflow-x-auto rounded border border-stone-200 my-4")
+ (table (~tw :tokens "w-full text-sm")
+ (thead (~tw :tokens "bg-stone-50")
(tr
- (th :class "px-4 py-2 text-left font-semibold text-stone-700" "SX")
- (th :class "px-4 py-2 text-left font-semibold text-stone-700" "JavaScript")
- (th :class "px-4 py-2 text-left font-semibold text-stone-700" "Rule")))
+ (th (~tw :tokens "px-4 py-2 text-left font-semibold text-stone-700") "SX")
+ (th (~tw :tokens "px-4 py-2 text-left font-semibold text-stone-700") "JavaScript")
+ (th (~tw :tokens "px-4 py-2 text-left font-semibold text-stone-700") "Rule")))
(tbody
- (tr :class "border-t border-stone-100"
- (td :class "px-4 py-2 font-mono" "eval-expr")
- (td :class "px-4 py-2 font-mono" "evalExpr")
- (td :class "px-4 py-2" "kebab → camelCase"))
- (tr :class "border-t border-stone-100"
- (td :class "px-4 py-2 font-mono" "nil?")
- (td :class "px-4 py-2 font-mono" "isNil")
- (td :class "px-4 py-2" "predicate → is-prefix"))
- (tr :class "border-t border-stone-100"
- (td :class "px-4 py-2 font-mono" "empty?")
- (td :class "px-4 py-2 font-mono" "isEmpty")
- (td :class "px-4 py-2" "? → is-prefix (general)"))
- (tr :class "border-t border-stone-100"
- (td :class "px-4 py-2 font-mono" "set!")
- (td :class "px-4 py-2 font-mono" "—")
- (td :class "px-4 py-2" "assignment (no rename)"))
- (tr :class "border-t border-stone-100"
- (td :class "px-4 py-2 font-mono" "dom-create-element")
- (td :class "px-4 py-2 font-mono" "domCreateElement")
- (td :class "px-4 py-2" "platform function rename"))
- (tr :class "border-t border-stone-100"
- (td :class "px-4 py-2 font-mono" "delete")
- (td :class "px-4 py-2 font-mono" "delete_")
- (td :class "px-4 py-2" "JS reserved word escape"))))))
+ (tr (~tw :tokens "border-t border-stone-100")
+ (td (~tw :tokens "px-4 py-2 font-mono") "eval-expr")
+ (td (~tw :tokens "px-4 py-2 font-mono") "evalExpr")
+ (td (~tw :tokens "px-4 py-2") "kebab → camelCase"))
+ (tr (~tw :tokens "border-t border-stone-100")
+ (td (~tw :tokens "px-4 py-2 font-mono") "nil?")
+ (td (~tw :tokens "px-4 py-2 font-mono") "isNil")
+ (td (~tw :tokens "px-4 py-2") "predicate → is-prefix"))
+ (tr (~tw :tokens "border-t border-stone-100")
+ (td (~tw :tokens "px-4 py-2 font-mono") "empty?")
+ (td (~tw :tokens "px-4 py-2 font-mono") "isEmpty")
+ (td (~tw :tokens "px-4 py-2") "? → is-prefix (general)"))
+ (tr (~tw :tokens "border-t border-stone-100")
+ (td (~tw :tokens "px-4 py-2 font-mono") "set!")
+ (td (~tw :tokens "px-4 py-2 font-mono") "—")
+ (td (~tw :tokens "px-4 py-2") "assignment (no rename)"))
+ (tr (~tw :tokens "border-t border-stone-100")
+ (td (~tw :tokens "px-4 py-2 font-mono") "dom-create-element")
+ (td (~tw :tokens "px-4 py-2 font-mono") "domCreateElement")
+ (td (~tw :tokens "px-4 py-2") "platform function rename"))
+ (tr (~tw :tokens "border-t border-stone-100")
+ (td (~tw :tokens "px-4 py-2 font-mono") "delete")
+ (td (~tw :tokens "px-4 py-2 font-mono") "delete_")
+ (td (~tw :tokens "px-4 py-2") "JS reserved word escape"))))))
(~docs/subsection :title "Special Forms → JavaScript"
- (div :class "overflow-x-auto rounded border border-stone-200 my-4"
- (table :class "w-full text-sm"
- (thead :class "bg-stone-50"
+ (div (~tw :tokens "overflow-x-auto rounded border border-stone-200 my-4")
+ (table (~tw :tokens "w-full text-sm")
+ (thead (~tw :tokens "bg-stone-50")
(tr
- (th :class "px-4 py-2 text-left font-semibold text-stone-700" "SX")
- (th :class "px-4 py-2 text-left font-semibold text-stone-700" "JavaScript")))
+ (th (~tw :tokens "px-4 py-2 text-left font-semibold text-stone-700") "SX")
+ (th (~tw :tokens "px-4 py-2 text-left font-semibold text-stone-700") "JavaScript")))
(tbody
- (tr :class "border-t border-stone-100"
- (td :class "px-4 py-2 font-mono" "(if c t e)")
- (td :class "px-4 py-2 font-mono" "(sxTruthy(c) ? t : e)"))
- (tr :class "border-t border-stone-100"
- (td :class "px-4 py-2 font-mono" "(when c body)")
- (td :class "px-4 py-2 font-mono" "(sxTruthy(c) ? body : NIL)"))
- (tr :class "border-t border-stone-100"
- (td :class "px-4 py-2 font-mono" "(let ((a 1)) body)")
- (td :class "px-4 py-2 font-mono" "(function(a) { return body; })(1)"))
- (tr :class "border-t border-stone-100"
- (td :class "px-4 py-2 font-mono" "(fn (x) body)")
- (td :class "px-4 py-2 font-mono" "function(x) { return body; }"))
- (tr :class "border-t border-stone-100"
- (td :class "px-4 py-2 font-mono" "(define name val)")
- (td :class "px-4 py-2 font-mono" "var name = val;"))
- (tr :class "border-t border-stone-100"
- (td :class "px-4 py-2 font-mono" "(and a b c)")
- (td :class "px-4 py-2 font-mono" "(sxTruthy(a) ? (sxTruthy(b) ? c : b) : a)"))
- (tr :class "border-t border-stone-100"
- (td :class "px-4 py-2 font-mono" "(case x \"a\" 1 ...)")
- (td :class "px-4 py-2 font-mono" "sxCase(x, [[\"a\", () => 1], ...])"))
- (tr :class "border-t border-stone-100"
- (td :class "px-4 py-2 font-mono" "(str a b c)")
- (td :class "px-4 py-2 font-mono" "sxStr(a, b, c)"))
- (tr :class "border-t border-stone-100"
- (td :class "px-4 py-2 font-mono" "&rest args")
- (td :class "px-4 py-2 font-mono" "...args (rest params)"))))))
+ (tr (~tw :tokens "border-t border-stone-100")
+ (td (~tw :tokens "px-4 py-2 font-mono") "(if c t e)")
+ (td (~tw :tokens "px-4 py-2 font-mono") "(sxTruthy(c) ? t : e)"))
+ (tr (~tw :tokens "border-t border-stone-100")
+ (td (~tw :tokens "px-4 py-2 font-mono") "(when c body)")
+ (td (~tw :tokens "px-4 py-2 font-mono") "(sxTruthy(c) ? body : NIL)"))
+ (tr (~tw :tokens "border-t border-stone-100")
+ (td (~tw :tokens "px-4 py-2 font-mono") "(let ((a 1)) body)")
+ (td (~tw :tokens "px-4 py-2 font-mono") "(function(a) { return body; })(1)"))
+ (tr (~tw :tokens "border-t border-stone-100")
+ (td (~tw :tokens "px-4 py-2 font-mono") "(fn (x) body)")
+ (td (~tw :tokens "px-4 py-2 font-mono") "function(x) { return body; }"))
+ (tr (~tw :tokens "border-t border-stone-100")
+ (td (~tw :tokens "px-4 py-2 font-mono") "(define name val)")
+ (td (~tw :tokens "px-4 py-2 font-mono") "var name = val;"))
+ (tr (~tw :tokens "border-t border-stone-100")
+ (td (~tw :tokens "px-4 py-2 font-mono") "(and a b c)")
+ (td (~tw :tokens "px-4 py-2 font-mono") "(sxTruthy(a) ? (sxTruthy(b) ? c : b) : a)"))
+ (tr (~tw :tokens "border-t border-stone-100")
+ (td (~tw :tokens "px-4 py-2 font-mono") "(case x \"a\" 1 ...)")
+ (td (~tw :tokens "px-4 py-2 font-mono") "sxCase(x, [[\"a\", () => 1], ...])"))
+ (tr (~tw :tokens "border-t border-stone-100")
+ (td (~tw :tokens "px-4 py-2 font-mono") "(str a b c)")
+ (td (~tw :tokens "px-4 py-2 font-mono") "sxStr(a, b, c)"))
+ (tr (~tw :tokens "border-t border-stone-100")
+ (td (~tw :tokens "px-4 py-2 font-mono") "&rest args")
+ (td (~tw :tokens "px-4 py-2 font-mono") "...args (rest params)"))))))
(~docs/subsection :title "JavaScript Advantages"
(p "JavaScript is easier to target than Python in two key ways:")
- (ul :class "list-disc pl-6 space-y-2 text-stone-700"
+ (ul (~tw :tokens "list-disc pl-6 space-y-2 text-stone-700")
(li (strong "No mutation problem. ")
"JavaScript closures capture by reference, not by value. "
(code "set!") " from a nested function Just Works — no cell variable "
@@ -237,7 +237,7 @@
"definitions — it fetches data, resolves conditionals, expands components, "
"and produces a complete DOM description as an SX tree. Currently this tree "
"is either:")
- (ul :class "list-disc pl-6 space-y-1 text-stone-700"
+ (ul (~tw :tokens "list-disc pl-6 space-y-1 text-stone-700")
(li "Rendered to HTML server-side (" (code "render-to-html") ")")
(li "Serialized as SX wire format for the client to render (" (code "aser") ")"))
(p "A third option: " (strong "compile it to JavaScript") ". "
@@ -279,7 +279,7 @@ _0.appendChild(_2);" "javascript")))
(~docs/subsection :title "Why Not Just Use HTML?"
(p "HTML already does this — " (code "innerHTML") " parses and builds DOM. "
"Why compile to JS instead?")
- (ul :class "list-disc pl-6 space-y-2 text-stone-700"
+ (ul (~tw :tokens "list-disc pl-6 space-y-2 text-stone-700")
(li (strong "Event handlers. ")
"HTML can't express " (code ":on-click") " or " (code ":sx-get")
" — those need JavaScript. The compiled JS can wire up event "
@@ -304,7 +304,7 @@ _0.appendChild(_2);" "javascript")))
(~docs/subsection :title "Hybrid Mode"
(p "Not every page is fully static. Some parts are server-rendered, "
"some are interactive. " (code "js.sx") " handles this with a hybrid approach:")
- (ul :class "list-disc pl-6 space-y-2 text-stone-700"
+ (ul (~tw :tokens "list-disc pl-6 space-y-2 text-stone-700")
(li (strong "Static subtrees") " → compiled to DOM construction code (no runtime)")
(li (strong "Reactive islands") " → compiled with signal creation + subscriptions "
"(needs signal runtime, ~2KB)")
@@ -322,45 +322,45 @@ _0.appendChild(_2);" "javascript")))
(~docs/section :title "The Bootstrap Chain" :id "chain"
(p "With both " (code "py.sx") " and " (code "js.sx") ", the full picture:")
- (div :class "overflow-x-auto rounded border border-stone-200 my-4"
- (table :class "w-full text-sm"
- (thead :class "bg-stone-50"
+ (div (~tw :tokens "overflow-x-auto rounded border border-stone-200 my-4")
+ (table (~tw :tokens "w-full text-sm")
+ (thead (~tw :tokens "bg-stone-50")
(tr
- (th :class "px-4 py-2 text-left font-semibold text-stone-700" "Translator")
- (th :class "px-4 py-2 text-left font-semibold text-stone-700" "Written in")
- (th :class "px-4 py-2 text-left font-semibold text-stone-700" "Outputs")
- (th :class "px-4 py-2 text-left font-semibold text-stone-700" "Replaces")))
+ (th (~tw :tokens "px-4 py-2 text-left font-semibold text-stone-700") "Translator")
+ (th (~tw :tokens "px-4 py-2 text-left font-semibold text-stone-700") "Written in")
+ (th (~tw :tokens "px-4 py-2 text-left font-semibold text-stone-700") "Outputs")
+ (th (~tw :tokens "px-4 py-2 text-left font-semibold text-stone-700") "Replaces")))
(tbody
- (tr :class "border-t border-stone-100"
- (td :class "px-4 py-2 font-mono" "z3.sx")
- (td :class "px-4 py-2" "SX")
- (td :class "px-4 py-2" "SMT-LIB")
- (td :class "px-4 py-2 text-stone-400 italic" "(none — new capability)"))
- (tr :class "border-t border-stone-100"
- (td :class "px-4 py-2 font-mono" "prove.sx")
- (td :class "px-4 py-2" "SX")
- (td :class "px-4 py-2" "Constraint proofs")
- (td :class "px-4 py-2 text-stone-400 italic" "(none — new capability)"))
- (tr :class "border-t border-stone-100 bg-violet-50"
- (td :class "px-4 py-2 font-mono text-violet-700" "py.sx")
- (td :class "px-4 py-2" "SX")
- (td :class "px-4 py-2" "Python")
- (td :class "px-4 py-2 font-mono" "bootstrap_py.py"))
- (tr :class "border-t border-stone-100 bg-blue-50"
- (td :class "px-4 py-2 font-mono text-blue-700" "js.sx")
- (td :class "px-4 py-2" "SX")
- (td :class "px-4 py-2" "JavaScript")
- (td :class "px-4 py-2 font-mono" "bootstrap_js.py"))
- (tr :class "border-t border-stone-100"
- (td :class "px-4 py-2 font-mono text-stone-400" "go.sx")
- (td :class "px-4 py-2" "SX")
- (td :class "px-4 py-2" "Go")
- (td :class "px-4 py-2 text-stone-400 italic" "(future host)"))
- (tr :class "border-t border-stone-100"
- (td :class "px-4 py-2 font-mono text-stone-400" "rs.sx")
- (td :class "px-4 py-2" "SX")
- (td :class "px-4 py-2" "Rust")
- (td :class "px-4 py-2 text-stone-400 italic" "(future host)")))))
+ (tr (~tw :tokens "border-t border-stone-100")
+ (td (~tw :tokens "px-4 py-2 font-mono") "z3.sx")
+ (td (~tw :tokens "px-4 py-2") "SX")
+ (td (~tw :tokens "px-4 py-2") "SMT-LIB")
+ (td (~tw :tokens "px-4 py-2 text-stone-400 italic") "(none — new capability)"))
+ (tr (~tw :tokens "border-t border-stone-100")
+ (td (~tw :tokens "px-4 py-2 font-mono") "prove.sx")
+ (td (~tw :tokens "px-4 py-2") "SX")
+ (td (~tw :tokens "px-4 py-2") "Constraint proofs")
+ (td (~tw :tokens "px-4 py-2 text-stone-400 italic") "(none — new capability)"))
+ (tr (~tw :tokens "border-t border-stone-100 bg-violet-50")
+ (td (~tw :tokens "px-4 py-2 font-mono text-violet-700") "py.sx")
+ (td (~tw :tokens "px-4 py-2") "SX")
+ (td (~tw :tokens "px-4 py-2") "Python")
+ (td (~tw :tokens "px-4 py-2 font-mono") "bootstrap_py.py"))
+ (tr (~tw :tokens "border-t border-stone-100 bg-blue-50")
+ (td (~tw :tokens "px-4 py-2 font-mono text-blue-700") "js.sx")
+ (td (~tw :tokens "px-4 py-2") "SX")
+ (td (~tw :tokens "px-4 py-2") "JavaScript")
+ (td (~tw :tokens "px-4 py-2 font-mono") "bootstrap_js.py"))
+ (tr (~tw :tokens "border-t border-stone-100")
+ (td (~tw :tokens "px-4 py-2 font-mono text-stone-400") "go.sx")
+ (td (~tw :tokens "px-4 py-2") "SX")
+ (td (~tw :tokens "px-4 py-2") "Go")
+ (td (~tw :tokens "px-4 py-2 text-stone-400 italic") "(future host)"))
+ (tr (~tw :tokens "border-t border-stone-100")
+ (td (~tw :tokens "px-4 py-2 font-mono text-stone-400") "rs.sx")
+ (td (~tw :tokens "px-4 py-2") "SX")
+ (td (~tw :tokens "px-4 py-2") "Rust")
+ (td (~tw :tokens "px-4 py-2 text-stone-400 italic") "(future host)")))))
(p "Every translator is an SX program. The only Python left is the platform "
"interface (types, DOM primitives, runtime support functions) and the thin "
"runner script that loads " (code "py.sx") " or " (code "js.sx")
@@ -374,7 +374,7 @@ _0.appendChild(_2);" "javascript")))
(~docs/subsection :title "Phase 1: Expression Translator"
(p "Core SX-to-JavaScript expression translation.")
- (ul :class "list-disc pl-6 space-y-1 text-stone-700"
+ (ul (~tw :tokens "list-disc pl-6 space-y-1 text-stone-700")
(li (code "js-mangle") " — SX name → JavaScript identifier (RENAMES + kebab→camelCase)")
(li (code "js-literal") " — atoms: numbers, strings, booleans, nil, symbols, keywords")
(li (code "js-expr") " — recursive expression translator")
@@ -388,7 +388,7 @@ _0.appendChild(_2);" "javascript")))
(~docs/subsection :title "Phase 2: Statement Translator"
(p "Top-level and function body statement emission.")
- (ul :class "list-disc pl-6 space-y-1 text-stone-700"
+ (ul (~tw :tokens "list-disc pl-6 space-y-1 text-stone-700")
(li (code "js-statement") " — emit as JavaScript statement")
(li (code "define") " → " (code "var name = expr;"))
(li (code "set!") " → direct assignment (closures capture by reference)")
@@ -398,7 +398,7 @@ _0.appendChild(_2);" "javascript")))
(~docs/subsection :title "Phase 3: Spec Bootstrapper"
(p "Process spec files identically to " (code "bootstrap_js.py") ".")
- (ul :class "list-disc pl-6 space-y-1 text-stone-700"
+ (ul (~tw :tokens "list-disc pl-6 space-y-1 text-stone-700")
(li (code "js-extract-defines") " — parse .sx source, collect top-level defines")
(li (code "js-translate-file") " — translate a list of define expressions")
(li "Adapter selection: parser, html, sx, dom, engine, orchestration, boot")
@@ -407,7 +407,7 @@ _0.appendChild(_2);" "javascript")))
(~docs/subsection :title "Phase 4: Component Compiler"
(p "Ahead-of-time compilation of evaluated SX trees to JavaScript.")
- (ul :class "list-disc pl-6 space-y-1 text-stone-700"
+ (ul (~tw :tokens "list-disc pl-6 space-y-1 text-stone-700")
(li (code "js-compile-element") " — emit " (code "createElement") " + attribute setting")
(li (code "js-compile-text") " — emit " (code "textContent") " or " (code "createTextNode"))
(li (code "js-compile-component") " — inline-expand or emit component call")
@@ -431,42 +431,42 @@ python test_js_compile.py # renders both, diffs DOM" "bash")))
;; -----------------------------------------------------------------------
(~docs/section :title "Comparison with py.sx" :id "comparison"
- (div :class "overflow-x-auto rounded border border-stone-200 my-4"
- (table :class "w-full text-sm"
- (thead :class "bg-stone-50"
+ (div (~tw :tokens "overflow-x-auto rounded border border-stone-200 my-4")
+ (table (~tw :tokens "w-full text-sm")
+ (thead (~tw :tokens "bg-stone-50")
(tr
- (th :class "px-4 py-2 text-left font-semibold text-stone-700" "Concern")
- (th :class "px-4 py-2 text-left font-semibold text-stone-700" (code "py.sx"))
- (th :class "px-4 py-2 text-left font-semibold text-stone-700" (code "js.sx"))))
+ (th (~tw :tokens "px-4 py-2 text-left font-semibold text-stone-700") "Concern")
+ (th (~tw :tokens "px-4 py-2 text-left font-semibold text-stone-700") (code "py.sx"))
+ (th (~tw :tokens "px-4 py-2 text-left font-semibold text-stone-700") (code "js.sx"))))
(tbody
- (tr :class "border-t border-stone-100"
- (td :class "px-4 py-2" "Naming convention")
- (td :class "px-4 py-2" "snake_case")
- (td :class "px-4 py-2" "camelCase"))
- (tr :class "border-t border-stone-100"
- (td :class "px-4 py-2" "Closures & mutation")
- (td :class "px-4 py-2" "Cell variable hack")
- (td :class "px-4 py-2" "Direct (reference capture)"))
- (tr :class "border-t border-stone-100"
- (td :class "px-4 py-2" "Spec modules")
- (td :class "px-4 py-2" "eval, render, html, sx, deps, signals")
- (td :class "px-4 py-2" "All 12 modules"))
- (tr :class "border-t border-stone-100"
- (td :class "px-4 py-2" "Platform interface")
- (td :class "px-4 py-2" "~300 lines")
- (td :class "px-4 py-2" "~1500 lines (DOM, browser APIs)"))
- (tr :class "border-t border-stone-100"
- (td :class "px-4 py-2" "RENAMES table")
- (td :class "px-4 py-2" "~200 entries")
- (td :class "px-4 py-2" "~350 entries"))
- (tr :class "border-t border-stone-100"
- (td :class "px-4 py-2" "Component compilation")
- (td :class "px-4 py-2 text-stone-400" "N/A")
- (td :class "px-4 py-2" "Ahead-of-time DOM compiler"))
- (tr :class "border-t border-stone-100"
- (td :class "px-4 py-2" "Estimated size")
- (td :class "px-4 py-2" "~800-1000 lines")
- (td :class "px-4 py-2" "~1200-1500 lines"))))))
+ (tr (~tw :tokens "border-t border-stone-100")
+ (td (~tw :tokens "px-4 py-2") "Naming convention")
+ (td (~tw :tokens "px-4 py-2") "snake_case")
+ (td (~tw :tokens "px-4 py-2") "camelCase"))
+ (tr (~tw :tokens "border-t border-stone-100")
+ (td (~tw :tokens "px-4 py-2") "Closures & mutation")
+ (td (~tw :tokens "px-4 py-2") "Cell variable hack")
+ (td (~tw :tokens "px-4 py-2") "Direct (reference capture)"))
+ (tr (~tw :tokens "border-t border-stone-100")
+ (td (~tw :tokens "px-4 py-2") "Spec modules")
+ (td (~tw :tokens "px-4 py-2") "eval, render, html, sx, deps, signals")
+ (td (~tw :tokens "px-4 py-2") "All 12 modules"))
+ (tr (~tw :tokens "border-t border-stone-100")
+ (td (~tw :tokens "px-4 py-2") "Platform interface")
+ (td (~tw :tokens "px-4 py-2") "~300 lines")
+ (td (~tw :tokens "px-4 py-2") "~1500 lines (DOM, browser APIs)"))
+ (tr (~tw :tokens "border-t border-stone-100")
+ (td (~tw :tokens "px-4 py-2") "RENAMES table")
+ (td (~tw :tokens "px-4 py-2") "~200 entries")
+ (td (~tw :tokens "px-4 py-2") "~350 entries"))
+ (tr (~tw :tokens "border-t border-stone-100")
+ (td (~tw :tokens "px-4 py-2") "Component compilation")
+ (td (~tw :tokens "px-4 py-2 text-stone-400") "N/A")
+ (td (~tw :tokens "px-4 py-2") "Ahead-of-time DOM compiler"))
+ (tr (~tw :tokens "border-t border-stone-100")
+ (td (~tw :tokens "px-4 py-2") "Estimated size")
+ (td (~tw :tokens "px-4 py-2") "~800-1000 lines")
+ (td (~tw :tokens "px-4 py-2") "~1200-1500 lines"))))))
;; -----------------------------------------------------------------------
;; Implications
@@ -486,7 +486,7 @@ python test_js_compile.py # renders both, diffs DOM" "bash")))
(~docs/subsection :title "Progressive Enhancement Layers"
(p "The component compiler naturally supports progressive enhancement:")
- (ol :class "list-decimal pl-6 space-y-1 text-stone-700"
+ (ol (~tw :tokens "list-decimal pl-6 space-y-1 text-stone-700")
(li (strong "HTML") " — server renders to HTML string. No JS needed. Works everywhere.")
(li (strong "Compiled JS") " — server compiles to DOM construction code. "
"Event handlers work. No SX runtime. Kilobytes, not megabytes.")
@@ -500,7 +500,7 @@ python test_js_compile.py # renders both, diffs DOM" "bash")))
(~docs/subsection :title "The Bootstrap Completion"
(p "With " (code "py.sx") " and " (code "js.sx") " both written in SX:")
- (ul :class "list-disc pl-6 space-y-2 text-stone-700"
+ (ul (~tw :tokens "list-disc pl-6 space-y-2 text-stone-700")
(li "The " (em "spec") " defines SX semantics (" (code "eval.sx") ", " (code "render.sx") ", ...)")
(li "The " (em "translators") " convert the spec to host languages (" (code "py.sx") ", " (code "js.sx") ")")
(li "The " (em "prover") " verifies the spec's properties (" (code "z3.sx") ", " (code "prove.sx") ")")
diff --git a/sx/sx/plans/live-streaming.sx b/sx/sx/plans/live-streaming.sx
index c247f679..169023e1 100644
--- a/sx/sx/plans/live-streaming.sx
+++ b/sx/sx/plans/live-streaming.sx
@@ -20,7 +20,7 @@
(~docs/subsection :title "Transport Hierarchy"
(p "Three tiers, progressively more capable:")
- (ol :class "list-decimal list-inside space-y-2 text-stone-700 text-sm"
+ (ol (~tw :tokens "list-decimal list-inside space-y-2 text-stone-700 text-sm")
(li (strong "Chunked streaming") " (done) — single HTTP response, each suspense resolves once. "
"Best for: initial page load with slow IO.")
(li (strong "SSE") " — persistent one-way connection, server pushes resolve events. "
@@ -44,7 +44,7 @@
(~docs/subsection :title "Shared Resolution Mechanism"
(p "All three transports use the same client-side resolution:")
- (ul :class "list-disc list-inside space-y-1 text-stone-600 text-sm"
+ (ul (~tw :tokens "list-disc list-inside space-y-1 text-stone-600 text-sm")
(li (code "Sx.resolveSuspense(id, sxSource)") " — already exists, parses SX and renders to DOM")
(li "SSE: " (code "EventSource") " → " (code "onmessage") " → " (code "resolveSuspense()"))
(li "WS: " (code "WebSocket") " → " (code "onmessage") " → " (code "resolveSuspense()"))
@@ -54,7 +54,7 @@
(~docs/section :title "Implementation" :id "implementation"
(~docs/subsection :title "Phase 1: SSE Infrastructure"
- (ol :class "list-decimal list-inside space-y-2 text-stone-700 text-sm"
+ (ol (~tw :tokens "list-decimal list-inside space-y-2 text-stone-700 text-sm")
(li "Add " (code "~live") " component to " (code "shared/sx/templates/") " — renders child suspense placeholders, "
"emits " (code "data-sx-live") " attribute with SSE endpoint URL")
(li "Add " (code "sx-live.js") " client module — on boot, finds " (code "[data-sx-live]") " elements, "
@@ -63,48 +63,48 @@
(li "Add " (code "sse_stream()") " Quart helper — returns async generator Response with correct headers")))
(~docs/subsection :title "Phase 2: Defpage Integration"
- (ol :class "list-decimal list-inside space-y-2 text-stone-700 text-sm"
+ (ol (~tw :tokens "list-decimal list-inside space-y-2 text-stone-700 text-sm")
(li "New " (code ":live") " defpage slot — declares SSE endpoint + suspense bindings")
(li "Auto-mount SSE endpoint alongside the page route")
(li "Component defs sent as first SSE event on connection open")
(li "Automatic reconnection with exponential backoff")))
(~docs/subsection :title "Phase 3: WebSocket"
- (ol :class "list-decimal list-inside space-y-2 text-stone-700 text-sm"
+ (ol (~tw :tokens "list-decimal list-inside space-y-2 text-stone-700 text-sm")
(li "Add " (code "~ws") " component — bidirectional channel with send/receive")
(li "Add " (code "sx-ws.js") " client module — WebSocket management, message routing")
(li "Server-side: Quart WebSocket handlers that receive and broadcast SX events")
(li "Client-side: " (code "sx-send") " primitive for sending SX expressions to server")))
(~docs/subsection :title "Phase 4: Spec & Boundary"
- (ol :class "list-decimal list-inside space-y-2 text-stone-700 text-sm"
+ (ol (~tw :tokens "list-decimal list-inside space-y-2 text-stone-700 text-sm")
(li "Spec " (code "~live") " and " (code "~ws") " in " (code "render.sx") " (how they render in each mode)")
(li "Add SSE/WS IO primitives to " (code "boundary.sx"))
(li "Bootstrap SSE/WS connection management into " (code "sx-ref.js"))
(li "Spec-level tests for resolve, reconnection, and message routing"))))
(~docs/section :title "Files" :id "files"
- (table :class "w-full text-left border-collapse"
+ (table (~tw :tokens "w-full text-left border-collapse")
(thead
- (tr :class "border-b border-stone-200"
- (th :class "px-3 py-2 font-medium text-stone-600" "File")
- (th :class "px-3 py-2 font-medium text-stone-600" "Purpose")))
+ (tr (~tw :tokens "border-b border-stone-200")
+ (th (~tw :tokens "px-3 py-2 font-medium text-stone-600") "File")
+ (th (~tw :tokens "px-3 py-2 font-medium text-stone-600") "Purpose")))
(tbody
- (tr :class "border-b border-stone-100"
- (td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/templates/live.sx")
- (td :class "px-3 py-2 text-stone-700" "~live component definition"))
- (tr :class "border-b border-stone-100"
- (td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/static/scripts/sx-live.js")
- (td :class "px-3 py-2 text-stone-700" "SSE client — EventSource → resolveSuspense"))
- (tr :class "border-b border-stone-100"
- (td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/sse.py")
- (td :class "px-3 py-2 text-stone-700" "SSE helpers — event formatting, stream response"))
- (tr :class "border-b border-stone-100"
- (td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/static/scripts/sx-ws.js")
- (td :class "px-3 py-2 text-stone-700" "WebSocket client — bidirectional SX channel"))
- (tr :class "border-b border-stone-100"
- (td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/ref/render.sx")
- (td :class "px-3 py-2 text-stone-700" "Spec: ~live and ~ws rendering in all modes"))
- (tr :class "border-b border-stone-100"
- (td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/ref/boundary.sx")
- (td :class "px-3 py-2 text-stone-700" "SSE/WS IO primitive declarations")))))))
+ (tr (~tw :tokens "border-b border-stone-100")
+ (td (~tw :tokens "px-3 py-2 font-mono text-sm text-violet-700") "shared/sx/templates/live.sx")
+ (td (~tw :tokens "px-3 py-2 text-stone-700") "~live component definition"))
+ (tr (~tw :tokens "border-b border-stone-100")
+ (td (~tw :tokens "px-3 py-2 font-mono text-sm text-violet-700") "shared/static/scripts/sx-live.js")
+ (td (~tw :tokens "px-3 py-2 text-stone-700") "SSE client — EventSource → resolveSuspense"))
+ (tr (~tw :tokens "border-b border-stone-100")
+ (td (~tw :tokens "px-3 py-2 font-mono text-sm text-violet-700") "shared/sx/sse.py")
+ (td (~tw :tokens "px-3 py-2 text-stone-700") "SSE helpers — event formatting, stream response"))
+ (tr (~tw :tokens "border-b border-stone-100")
+ (td (~tw :tokens "px-3 py-2 font-mono text-sm text-violet-700") "shared/static/scripts/sx-ws.js")
+ (td (~tw :tokens "px-3 py-2 text-stone-700") "WebSocket client — bidirectional SX channel"))
+ (tr (~tw :tokens "border-b border-stone-100")
+ (td (~tw :tokens "px-3 py-2 font-mono text-sm text-violet-700") "shared/sx/ref/render.sx")
+ (td (~tw :tokens "px-3 py-2 text-stone-700") "Spec: ~live and ~ws rendering in all modes"))
+ (tr (~tw :tokens "border-b border-stone-100")
+ (td (~tw :tokens "px-3 py-2 font-mono text-sm text-violet-700") "shared/sx/ref/boundary.sx")
+ (td (~tw :tokens "px-3 py-2 text-stone-700") "SSE/WS IO primitive declarations")))))))
diff --git a/sx/sx/plans/mother-language.sx b/sx/sx/plans/mother-language.sx
index 740c476f..266deddc 100644
--- a/sx/sx/plans/mother-language.sx
+++ b/sx/sx/plans/mother-language.sx
@@ -5,7 +5,7 @@
(defcomp ~plans/mother-language/plan-mother-language-content ()
(~docs/page :title "Mother Language"
- (p :class "text-stone-500 text-sm italic mb-8"
+ (p (~tw :tokens "text-stone-500 text-sm italic mb-8")
"The ideal language for evaluating the SX core spec is SX itself. "
"The path: OCaml as the initial substrate (closest existing language to what CEK is), "
"Koka as an alternative (compile-time linearity), ultimately a self-hosting SX compiler "
@@ -18,7 +18,7 @@
(~docs/section :title "The Argument" :id "argument"
- (h4 :class "font-semibold mt-4 mb-2" "What the evaluator actually does")
+ (h4 (~tw :tokens "font-semibold mt-4 mb-2") "What the evaluator actually does")
(p "The CEK machine is a " (code "state \u2192 state") " loop over sum types. "
"Each step pattern-matches on the Control register, consults the Environment, "
"and transforms the Kontinuation. Every SX expression, every component render, "
@@ -27,10 +27,10 @@
"with minimal allocation. That means: algebraic types, pattern matching, "
"persistent data structures, and a native effect system.")
- (h4 :class "font-semibold mt-6 mb-2" "Why multiple hosts is the wrong goal")
+ (h4 (~tw :tokens "font-semibold mt-6 mb-2") "Why multiple hosts is the wrong goal")
(p "The current architecture bootstraps the spec to Python, JavaScript, and Rust. "
"Each host has impedance mismatches:")
- (ul :class "list-disc list-inside space-y-1 mt-2"
+ (ul (~tw :tokens "list-disc list-inside space-y-1 mt-2")
(li (strong "Python") " \u2014 slow. Tree-walk overhead is 100\u20131000x vs native. "
"The async adapter is complex because Python's async model is cooperative, not effect-based.")
(li (strong "JavaScript") " \u2014 no sum types, prototype-based dispatch, GC pauses unpredictable. "
@@ -40,7 +40,7 @@
(p "Each host makes the evaluator work, but none make it " (em "natural") ". "
"The translation is structure-" (em "creating") ", not structure-" (em "preserving") ".")
- (h4 :class "font-semibold mt-6 mb-2" "The Mother Language is SX")
+ (h4 (~tw :tokens "font-semibold mt-6 mb-2") "The Mother Language is SX")
(p "The spec defines the semantics. The CEK machine is the most explicit form of those semantics. "
"The ideal \"language\" is one that maps 1:1 onto CEK transitions and compiles them to "
"optimal machine code. That language is SX itself \u2014 compiled, not interpreted.")
@@ -58,49 +58,49 @@
(p "OCaml is the closest existing language to what the CEK machine is. "
"The translation from " (code "cek.sx") " to OCaml is nearly mechanical.")
- (h4 :class "font-semibold mt-4 mb-2" "Natural mapping")
- (div :class "overflow-x-auto rounded border border-stone-200 mb-4"
- (table :class "w-full text-left text-sm"
- (thead (tr :class "border-b border-stone-200 bg-stone-100"
- (th :class "px-3 py-2 font-medium text-stone-600" "SX concept")
- (th :class "px-3 py-2 font-medium text-stone-600" "OCaml primitive")
- (th :class "px-3 py-2 font-medium text-stone-600" "Notes")))
+ (h4 (~tw :tokens "font-semibold mt-4 mb-2") "Natural mapping")
+ (div (~tw :tokens "overflow-x-auto rounded border border-stone-200 mb-4")
+ (table (~tw :tokens "w-full text-left text-sm")
+ (thead (tr (~tw :tokens "border-b border-stone-200 bg-stone-100")
+ (th (~tw :tokens "px-3 py-2 font-medium text-stone-600") "SX concept")
+ (th (~tw :tokens "px-3 py-2 font-medium text-stone-600") "OCaml primitive")
+ (th (~tw :tokens "px-3 py-2 font-medium text-stone-600") "Notes")))
(tbody
- (tr :class "border-b border-stone-100"
- (td :class "px-3 py-2 text-stone-700" "Value (Nil | Num | Str | List | ...)")
- (td :class "px-3 py-2 text-stone-700" "Algebraic variant type")
- (td :class "px-3 py-2 text-stone-600" "Direct mapping, no boxing"))
- (tr :class "border-b border-stone-100"
- (td :class "px-3 py-2 text-stone-700" "Frame (IfFrame | ArgFrame | MapFrame | ...)")
- (td :class "px-3 py-2 text-stone-700" "Algebraic variant type")
- (td :class "px-3 py-2 text-stone-600" "20+ variants, pattern match dispatch"))
- (tr :class "border-b border-stone-100"
- (td :class "px-3 py-2 text-stone-700" "Environment (persistent map)")
- (td :class "px-3 py-2 text-stone-700" (code "Map.S"))
- (td :class "px-3 py-2 text-stone-600" "Built-in balanced tree, structural sharing"))
- (tr :class "border-b border-stone-100"
- (td :class "px-3 py-2 text-stone-700" "Continuation (list of frames)")
- (td :class "px-3 py-2 text-stone-700" "Immutable list")
- (td :class "px-3 py-2 text-stone-600" "cons/match, O(1) push/pop"))
- (tr :class "border-b border-stone-100"
- (td :class "px-3 py-2 text-stone-700" "cek-step (pattern match on C)")
- (td :class "px-3 py-2 text-stone-700" (code "match") " expression")
- (td :class "px-3 py-2 text-stone-600" "Compiles to jump table"))
- (tr :class "border-b border-stone-100"
- (td :class "px-3 py-2 text-stone-700" "shift/reset")
- (td :class "px-3 py-2 text-stone-700" (code "perform") " / " (code "continue"))
- (td :class "px-3 py-2 text-stone-600" "Native in OCaml 5"))
- (tr :class "border-b border-stone-100"
- (td :class "px-3 py-2 text-stone-700" "Concurrent CEK (fibers)")
- (td :class "px-3 py-2 text-stone-700" "Domains + effect handlers")
- (td :class "px-3 py-2 text-stone-600" "One fiber per CEK machine"))
+ (tr (~tw :tokens "border-b border-stone-100")
+ (td (~tw :tokens "px-3 py-2 text-stone-700") "Value (Nil | Num | Str | List | ...)")
+ (td (~tw :tokens "px-3 py-2 text-stone-700") "Algebraic variant type")
+ (td (~tw :tokens "px-3 py-2 text-stone-600") "Direct mapping, no boxing"))
+ (tr (~tw :tokens "border-b border-stone-100")
+ (td (~tw :tokens "px-3 py-2 text-stone-700") "Frame (IfFrame | ArgFrame | MapFrame | ...)")
+ (td (~tw :tokens "px-3 py-2 text-stone-700") "Algebraic variant type")
+ (td (~tw :tokens "px-3 py-2 text-stone-600") "20+ variants, pattern match dispatch"))
+ (tr (~tw :tokens "border-b border-stone-100")
+ (td (~tw :tokens "px-3 py-2 text-stone-700") "Environment (persistent map)")
+ (td (~tw :tokens "px-3 py-2 text-stone-700") (code "Map.S"))
+ (td (~tw :tokens "px-3 py-2 text-stone-600") "Built-in balanced tree, structural sharing"))
+ (tr (~tw :tokens "border-b border-stone-100")
+ (td (~tw :tokens "px-3 py-2 text-stone-700") "Continuation (list of frames)")
+ (td (~tw :tokens "px-3 py-2 text-stone-700") "Immutable list")
+ (td (~tw :tokens "px-3 py-2 text-stone-600") "cons/match, O(1) push/pop"))
+ (tr (~tw :tokens "border-b border-stone-100")
+ (td (~tw :tokens "px-3 py-2 text-stone-700") "cek-step (pattern match on C)")
+ (td (~tw :tokens "px-3 py-2 text-stone-700") (code "match") " expression")
+ (td (~tw :tokens "px-3 py-2 text-stone-600") "Compiles to jump table"))
+ (tr (~tw :tokens "border-b border-stone-100")
+ (td (~tw :tokens "px-3 py-2 text-stone-700") "shift/reset")
+ (td (~tw :tokens "px-3 py-2 text-stone-700") (code "perform") " / " (code "continue"))
+ (td (~tw :tokens "px-3 py-2 text-stone-600") "Native in OCaml 5"))
+ (tr (~tw :tokens "border-b border-stone-100")
+ (td (~tw :tokens "px-3 py-2 text-stone-700") "Concurrent CEK (fibers)")
+ (td (~tw :tokens "px-3 py-2 text-stone-700") "Domains + effect handlers")
+ (td (~tw :tokens "px-3 py-2 text-stone-600") "One fiber per CEK machine"))
(tr
- (td :class "px-3 py-2 text-stone-700" "Linear continuations")
- (td :class "px-3 py-2 text-stone-700" "One-shot continuations (default)")
- (td :class "px-3 py-2 text-stone-600" "Runtime-enforced, not compile-time")))))
+ (td (~tw :tokens "px-3 py-2 text-stone-700") "Linear continuations")
+ (td (~tw :tokens "px-3 py-2 text-stone-700") "One-shot continuations (default)")
+ (td (~tw :tokens "px-3 py-2 text-stone-600") "Runtime-enforced, not compile-time")))))
- (h4 :class "font-semibold mt-4 mb-2" "Compilation targets")
- (ul :class "list-disc list-inside space-y-1 mt-2"
+ (h4 (~tw :tokens "font-semibold mt-4 mb-2") "Compilation targets")
+ (ul (~tw :tokens "list-disc list-inside space-y-1 mt-2")
(li (strong "Native") " \u2014 OCaml's native compiler produces fast binaries, small footprint. "
"Embed in Python via C ABI (ctypes/cffi). Embed in Node via N-API.")
(li (strong "WASM") " \u2014 " (code "wasm_of_ocaml") " is mature (used by Facebook's Flow/Reason). "
@@ -108,7 +108,7 @@
(li (strong "JavaScript") " \u2014 " (code "js_of_ocaml") " for legacy browser targets. "
"Falls back to JS when WASM isn't available."))
- (h4 :class "font-semibold mt-4 mb-2" "What OCaml replaces")
+ (h4 (~tw :tokens "font-semibold mt-4 mb-2") "What OCaml replaces")
(p "The Haskell and Rust evaluator implementations become unnecessary. "
"OCaml covers both server (native) and client (WASM) from one codebase. "
"The sx-haskell and sx-rust work proved the spec is host-independent \u2014 "
@@ -128,8 +128,8 @@
(p "Koka (Daan Leijen, MSR) addresses OCaml's one weakness: "
(strong "compile-time linearity") ".")
- (h4 :class "font-semibold mt-4 mb-2" "Where Koka wins")
- (ul :class "list-disc list-inside space-y-2 mt-2"
+ (h4 (~tw :tokens "font-semibold mt-4 mb-2") "Where Koka wins")
+ (ul (~tw :tokens "list-disc list-inside space-y-2 mt-2")
(li (strong "Perceus reference counting") " \u2014 the compiler tracks which values are used linearly. "
"Linear values are mutated in-place (zero allocation). "
"Non-linear values use reference counting (no GC at all).")
@@ -140,8 +140,8 @@
"The type system prevents invoking a linear continuation twice. "
"No runtime check needed."))
- (h4 :class "font-semibold mt-4 mb-2" "Where Koka is weaker")
- (ul :class "list-disc list-inside space-y-2 mt-2"
+ (h4 (~tw :tokens "font-semibold mt-4 mb-2") "Where Koka is weaker")
+ (ul (~tw :tokens "list-disc list-inside space-y-2 mt-2")
(li (strong "Maturity") " \u2014 research language. Smaller ecosystem, fewer FFI bindings, "
"less battle-tested than OCaml.")
(li (strong "WASM backend") " \u2014 compiles to C \u2192 WASM (via Emscripten). "
@@ -162,38 +162,38 @@
(p "The end state: SX compiles itself. No intermediate language, no general-purpose host.")
- (h4 :class "font-semibold mt-4 mb-2" "Phase 1: OCaml bootstrapper")
+ (h4 (~tw :tokens "font-semibold mt-4 mb-2") "Phase 1: OCaml bootstrapper")
(p "Write " (code "bootstrap_ml.py") " \u2014 reads " (code "cek.sx") " + " (code "frames.sx")
" + " (code "primitives.sx") " + " (code "eval.sx") ", emits OCaml source. "
"Same pattern as the existing Rust/Python/JS bootstrappers.")
(p "The OCaml output is a standalone module:")
(~docs/code :src (highlight "type value =\n | Nil | Bool of bool | Num of float | Str of string\n | Sym of string | Kw of string\n | List of value list | Dict of (value * value) list\n | Lambda of params * value list * env\n | Component of string * params * value list * env\n | Handle of int (* opaque FFI reference *)\n\ntype frame =\n | IfFrame of value list * value list * env\n | ArgFrame of value list * value list * env\n | MapFrame of value * value list * value list * env\n | ReactiveResetFrame of value\n | DerefFrame of value\n (* ... 20+ frame types from frames.sx *)\n\ntype kont = frame list\ntype state = value * env * kont\n\nlet step ((ctrl, env, kont) : state) : state =\n match ctrl with\n | Lit v -> continue_val v kont\n | Var name -> continue_val (Env.find name env) kont\n | App (f, args) -> (f, env, ArgFrame(args, [], env) :: kont)\n | ..." "ocaml"))
- (h4 :class "font-semibold mt-6 mb-2" "Phase 2: Native + WASM builds")
+ (h4 (~tw :tokens "font-semibold mt-6 mb-2") "Phase 2: Native + WASM builds")
(p "Compile the OCaml output to:")
- (ul :class "list-disc list-inside space-y-1 mt-2"
+ (ul (~tw :tokens "list-disc list-inside space-y-1 mt-2")
(li (code "sx_core.so") " / " (code "sx_core.dylib") " \u2014 native shared library, C ABI")
(li (code "sx_core.wasm") " \u2014 via " (code "wasm_of_ocaml") " for browser")
(li (code "sx_core.js") " \u2014 via " (code "js_of_ocaml") " as JS fallback"))
(p "Python web framework calls " (code "sx_core.so") " via cffi. "
"Browser loads " (code "sx_core.wasm") " via " (code "sx-platform.js") ".")
- (h4 :class "font-semibold mt-6 mb-2" "Phase 3: SX evaluates web framework")
+ (h4 (~tw :tokens "font-semibold mt-6 mb-2") "Phase 3: SX evaluates web framework")
(p "The compiled core evaluator loads web framework " (code ".sx") " at runtime "
"(signals, engine, orchestration, boot). Same as the "
(a :href "/sx/(etc.(plan.isolated-evaluator))" "Isolated Evaluator") " plan, "
"but the evaluator is compiled OCaml/WASM instead of bootstrapped JS.")
- (h4 :class "font-semibold mt-6 mb-2" "Phase 4: SX linearity checking")
+ (h4 (~tw :tokens "font-semibold mt-6 mb-2") "Phase 4: SX linearity checking")
(p "Extend " (code "types.sx") " with quantity annotations:")
(~docs/code :src (highlight ";; Quantity annotations on types\n(define-type (Signal a) :quantity :affine) ;; use at most once per scope\n(define-type (Channel a) :quantity :linear) ;; must be consumed exactly once\n\n;; Effect declarations with linearity\n(define-io-primitive \"send-message\"\n :params (channel message)\n :quantity :linear\n :effects [io]\n :doc \"Must be handled exactly once.\")\n\n;; The type checker (specced in .sx, compiled to OCaml) validates\n;; linearity at component registration time. Runtime enforcement\n;; by OCaml's one-shot continuations is the safety net." "lisp"))
(p "The type checker runs at spec-validation time. The compiled evaluator "
"executes already-verified code. SX's type system provides the linearity "
"guarantees, not the host language.")
- (h4 :class "font-semibold mt-6 mb-2" "Phase 5: Self-hosting compiler")
+ (h4 (~tw :tokens "font-semibold mt-6 mb-2") "Phase 5: Self-hosting compiler")
(p "Write the compiler itself in SX:")
- (ul :class "list-disc list-inside space-y-1 mt-2"
+ (ul (~tw :tokens "list-disc list-inside space-y-1 mt-2")
(li "Spec the CEK-to-native code generation in " (code ".sx") " files")
(li "The Phase 2 OCaml evaluator compiles the compiler spec")
(li "The compiled compiler can then compile itself")
@@ -215,46 +215,46 @@
(p "OCaml 5's concurrency model maps directly onto the "
(a :href "/sx/(etc.(plan.foundations))" "Foundations") " plan's concurrent CEK spec.")
- (h4 :class "font-semibold mt-4 mb-2" "Mapping")
- (div :class "overflow-x-auto rounded border border-stone-200 mb-4"
- (table :class "w-full text-left text-sm"
- (thead (tr :class "border-b border-stone-200 bg-stone-100"
- (th :class "px-3 py-2 font-medium text-stone-600" "SX primitive")
- (th :class "px-3 py-2 font-medium text-stone-600" "OCaml 5")
- (th :class "px-3 py-2 font-medium text-stone-600" "Characteristic")))
+ (h4 (~tw :tokens "font-semibold mt-4 mb-2") "Mapping")
+ (div (~tw :tokens "overflow-x-auto rounded border border-stone-200 mb-4")
+ (table (~tw :tokens "w-full text-left text-sm")
+ (thead (tr (~tw :tokens "border-b border-stone-200 bg-stone-100")
+ (th (~tw :tokens "px-3 py-2 font-medium text-stone-600") "SX primitive")
+ (th (~tw :tokens "px-3 py-2 font-medium text-stone-600") "OCaml 5")
+ (th (~tw :tokens "px-3 py-2 font-medium text-stone-600") "Characteristic")))
(tbody
- (tr :class "border-b border-stone-100"
- (td :class "px-3 py-2 text-stone-700" (code "spawn"))
- (td :class "px-3 py-2 text-stone-700" "Fiber via " (code "perform Spawn"))
- (td :class "px-3 py-2 text-stone-600" "Lightweight, scheduled by effect handler"))
- (tr :class "border-b border-stone-100"
- (td :class "px-3 py-2 text-stone-700" (code "channel"))
- (td :class "px-3 py-2 text-stone-700" (code "Eio.Stream"))
- (td :class "px-3 py-2 text-stone-600" "Typed, bounded, backpressure"))
- (tr :class "border-b border-stone-100"
- (td :class "px-3 py-2 text-stone-700" (code "yield!"))
- (td :class "px-3 py-2 text-stone-700" (code "perform Yield"))
- (td :class "px-3 py-2 text-stone-600" "Cooperative, zero-cost"))
- (tr :class "border-b border-stone-100"
- (td :class "px-3 py-2 text-stone-700" (code "select"))
- (td :class "px-3 py-2 text-stone-700" (code "Eio.Fiber.any"))
- (td :class "px-3 py-2 text-stone-600" "First-to-complete"))
- (tr :class "border-b border-stone-100"
- (td :class "px-3 py-2 text-stone-700" (code "fork-join"))
- (td :class "px-3 py-2 text-stone-700" (code "Eio.Fiber.all"))
- (td :class "px-3 py-2 text-stone-600" "Structured concurrency"))
+ (tr (~tw :tokens "border-b border-stone-100")
+ (td (~tw :tokens "px-3 py-2 text-stone-700") (code "spawn"))
+ (td (~tw :tokens "px-3 py-2 text-stone-700") "Fiber via " (code "perform Spawn"))
+ (td (~tw :tokens "px-3 py-2 text-stone-600") "Lightweight, scheduled by effect handler"))
+ (tr (~tw :tokens "border-b border-stone-100")
+ (td (~tw :tokens "px-3 py-2 text-stone-700") (code "channel"))
+ (td (~tw :tokens "px-3 py-2 text-stone-700") (code "Eio.Stream"))
+ (td (~tw :tokens "px-3 py-2 text-stone-600") "Typed, bounded, backpressure"))
+ (tr (~tw :tokens "border-b border-stone-100")
+ (td (~tw :tokens "px-3 py-2 text-stone-700") (code "yield!"))
+ (td (~tw :tokens "px-3 py-2 text-stone-700") (code "perform Yield"))
+ (td (~tw :tokens "px-3 py-2 text-stone-600") "Cooperative, zero-cost"))
+ (tr (~tw :tokens "border-b border-stone-100")
+ (td (~tw :tokens "px-3 py-2 text-stone-700") (code "select"))
+ (td (~tw :tokens "px-3 py-2 text-stone-700") (code "Eio.Fiber.any"))
+ (td (~tw :tokens "px-3 py-2 text-stone-600") "First-to-complete"))
+ (tr (~tw :tokens "border-b border-stone-100")
+ (td (~tw :tokens "px-3 py-2 text-stone-700") (code "fork-join"))
+ (td (~tw :tokens "px-3 py-2 text-stone-700") (code "Eio.Fiber.all"))
+ (td (~tw :tokens "px-3 py-2 text-stone-600") "Structured concurrency"))
(tr
- (td :class "px-3 py-2 text-stone-700" "DAG scheduler")
- (td :class "px-3 py-2 text-stone-700" "Domains + fiber pool")
- (td :class "px-3 py-2 text-stone-600" "True parallelism across cores")))))
+ (td (~tw :tokens "px-3 py-2 text-stone-700") "DAG scheduler")
+ (td (~tw :tokens "px-3 py-2 text-stone-700") "Domains + fiber pool")
+ (td (~tw :tokens "px-3 py-2 text-stone-600") "True parallelism across cores")))))
(p "Each concurrent CEK machine is a fiber. The scheduler is an effect handler. "
"This isn't simulating concurrency \u2014 it's using native concurrency whose mechanism " (em "is") " effects.")
- (h4 :class "font-semibold mt-4 mb-2" "The Art DAG connection")
+ (h4 (~tw :tokens "font-semibold mt-4 mb-2") "The Art DAG connection")
(p "Art DAG's 3-phase execution (analyze \u2192 plan \u2192 execute) maps onto "
"concurrent CEK + OCaml domains:")
- (ul :class "list-disc list-inside space-y-1 mt-2"
+ (ul (~tw :tokens "list-disc list-inside space-y-1 mt-2")
(li "Analyze: single CEK machine walks the DAG graph (one fiber)")
(li "Plan: resolve dependencies, topological sort (pure computation)")
(li "Execute: spawn one fiber per independent node, fan out to domains (true parallelism)")
@@ -269,9 +269,9 @@
(p "The linearity axis from foundations. Two enforcement layers:")
- (h4 :class "font-semibold mt-4 mb-2" "Layer 1: SX type system (primary)")
+ (h4 (~tw :tokens "font-semibold mt-4 mb-2") "Layer 1: SX type system (primary)")
(p "Quantity annotations in " (code "types.sx") " checked at spec-validation time:")
- (ul :class "list-disc list-inside space-y-1 mt-2"
+ (ul (~tw :tokens "list-disc list-inside space-y-1 mt-2")
(li (code ":linear") " (1) \u2014 must be used exactly once")
(li (code ":affine") " (\u22641) \u2014 may be used at most once (can drop)")
(li (code ":unrestricted") " (\u03c9) \u2014 may be used any number of times"))
@@ -279,16 +279,16 @@
"A channel is consumed. A resource handle is closed. "
"The type checker proves this before the evaluator ever runs.")
- (h4 :class "font-semibold mt-4 mb-2" "Layer 2: Host runtime (safety net)")
+ (h4 (~tw :tokens "font-semibold mt-4 mb-2") "Layer 2: Host runtime (safety net)")
(p "OCaml 5's one-shot continuations enforce linearity at runtime. "
"A continuation can only be " (code "continue") "'d once \u2014 second invocation raises an exception. "
"This catches any bugs in the type checker itself.")
(p "If Koka replaces OCaml: compile-time enforcement replaces runtime enforcement. "
"The safety net becomes a proof. Same semantics, stronger guarantees.")
- (h4 :class "font-semibold mt-4 mb-2" "Decision point")
+ (h4 (~tw :tokens "font-semibold mt-4 mb-2") "Decision point")
(p "When Step 5 (Linear Effects) of the foundations plan is reached:")
- (ul :class "list-disc list-inside space-y-2 mt-2"
+ (ul (~tw :tokens "list-disc list-inside space-y-2 mt-2")
(li "If SX's type checker can enforce linearity reliably \u2192 "
(strong "stay on OCaml") ". Runtime one-shot is sufficient.")
(li "If linearity bugs keep slipping through \u2192 "
@@ -309,7 +309,7 @@
"SX is not an interpreted scripting language with a nice spec. "
"It's a compiled language whose compiler also runs in the browser.")
- (h4 :class "font-semibold mt-4 mb-2" "JIT in the browser")
+ (h4 (~tw :tokens "font-semibold mt-4 mb-2") "JIT in the browser")
(p "The server sends SX (component definitions, page content). "
"The client receives it and " (strong "compiles to WASM and executes") ". "
"Not interprets. Not dispatches bytecodes. Compiles.")
@@ -322,7 +322,7 @@
"And content-addressing means compiled artifacts are cacheable by CID \u2014 "
"compile once, store forever.")
- (h4 :class "font-semibold mt-4 mb-2" "The compilation tiers")
+ (h4 (~tw :tokens "font-semibold mt-4 mb-2") "The compilation tiers")
(~docs/code :src (highlight "Tier 0: .sx source \u2192 tree-walking CEK (correct, slow \u2014 current)\nTier 1: .sx source \u2192 bytecodes \u2192 dispatch loop (correct, fast)\nTier 2: .sx source \u2192 WASM functions \u2192 execute (correct, fastest)\nTier 3: .sx source \u2192 native machine code (ahead-of-time, maximum)" "text"))
@@ -333,7 +333,7 @@
"Tier 3 is AOT \u2014 the entire app precompiled. "
"All tiers use the same spec, same platform layer, same platform primitives.")
- (h4 :class "font-semibold mt-4 mb-2" "Server-side precompilation")
+ (h4 (~tw :tokens "font-semibold mt-4 mb-2") "Server-side precompilation")
(p "The server can compile too. Instead of sending SX source for the client to JIT, "
"send precompiled WASM:")
@@ -342,13 +342,13 @@
(p "Option B skips parsing and compilation entirely. The client instantiates "
"the WASM module and calls it. The server did all the work.")
- (h4 :class "font-semibold mt-4 mb-2" "Content-addressed compilation cache")
+ (h4 (~tw :tokens "font-semibold mt-4 mb-2") "Content-addressed compilation cache")
(p "Every " (code ".sx") " expression has a CID. Every compiled artifact has a CID. "
"The mapping is deterministic \u2014 the compiler is a pure function:")
(~docs/code :src (highlight "source CID \u2192 compiled WASM CID\nbafyrei... \u2192 bafyrei...\n\nThis mapping is cacheable everywhere:\n\u2022 Browser cache \u2014 first visitor compiles, second visitor gets cached WASM\n\u2022 CDN \u2014 compiled artifacts served at the edge\n\u2022 IPFS \u2014 content-addressed by definition, globally deduplicated\n\u2022 Local disk \u2014 offline apps work from cached compiled components" "text"))
- (h4 :class "font-semibold mt-4 mb-2" "Entire apps as machine code")
+ (h4 (~tw :tokens "font-semibold mt-4 mb-2") "Entire apps as machine code")
(p "The entire application can be ahead-of-time compiled to a WASM binary. "
"Component definitions, page layouts, event handlers, signal computations \u2014 "
"all compiled to native WASM functions. The \"app\" is a " (code ".wasm") " file. "
@@ -357,7 +357,7 @@
"user-generated SX, REPL input, " (code "eval") "'d strings. "
"And even those get JIT'd on first use and cached by CID.")
- (h4 :class "font-semibold mt-4 mb-2" "The architecture")
+ (h4 (~tw :tokens "font-semibold mt-4 mb-2") "The architecture")
(~docs/code :src (highlight "sx-platform.js \u2190 DOM, fetch, timers (the real world)\n \u2191 calls\nsx-compiler.wasm \u2190 the SX compiler (itself compiled to WASM)\n \u2191 compiles\n.sx source \u2190 received from server / cache / inline\n \u2193 emits\nnative WASM functions \u2190 cached by CID, instantiated on demand\n \u2193 executes\nactual DOM mutations via platform primitives" "text"))
@@ -377,48 +377,48 @@
"WASM + the platform layer means compiled SX code has "
(strong "zero ambient capabilities") " \u2014 every capability is explicitly granted.")
- (h4 :class "font-semibold mt-4 mb-2" "Five defence layers")
+ (h4 (~tw :tokens "font-semibold mt-4 mb-2") "Five defence layers")
- (div :class "overflow-x-auto rounded border border-stone-200 mb-4"
- (table :class "w-full text-left text-sm"
- (thead (tr :class "border-b border-stone-200 bg-stone-100"
- (th :class "px-3 py-2 font-medium text-stone-600" "Layer")
- (th :class "px-3 py-2 font-medium text-stone-600" "Enforced by")
- (th :class "px-3 py-2 font-medium text-stone-600" "What it prevents")))
+ (div (~tw :tokens "overflow-x-auto rounded border border-stone-200 mb-4")
+ (table (~tw :tokens "w-full text-left text-sm")
+ (thead (tr (~tw :tokens "border-b border-stone-200 bg-stone-100")
+ (th (~tw :tokens "px-3 py-2 font-medium text-stone-600") "Layer")
+ (th (~tw :tokens "px-3 py-2 font-medium text-stone-600") "Enforced by")
+ (th (~tw :tokens "px-3 py-2 font-medium text-stone-600") "What it prevents")))
(tbody
- (tr :class "border-b border-stone-100"
- (td :class "px-3 py-2 text-stone-700" "1. WASM sandbox")
- (td :class "px-3 py-2 text-stone-700" "Browser")
- (td :class "px-3 py-2 text-stone-600" "Memory isolation, no system calls, no DOM access except via explicit imports. Validated before execution."))
- (tr :class "border-b border-stone-100"
- (td :class "px-3 py-2 text-stone-700" "2. Platform capabilities")
- (td :class "px-3 py-2 text-stone-700" (code "sx-platform.js"))
- (td :class "px-3 py-2 text-stone-600" "Compiled code can only call functions you register. No fetch? Can't fetch. No localStorage? Can't read storage. The platform is a capability system."))
- (tr :class "border-b border-stone-100"
- (td :class "px-3 py-2 text-stone-700" "3. Content-addressed verification")
- (td :class "px-3 py-2 text-stone-700" "CID determinism")
- (td :class "px-3 py-2 text-stone-600" "Compiler is deterministic: same source \u2192 same CID. Client can re-compile and verify. Tampered WASM produces wrong CID \u2192 reject."))
- (tr :class "border-b border-stone-100"
- (td :class "px-3 py-2 text-stone-700" "4. Per-component attenuation")
- (td :class "px-3 py-2 text-stone-700" "Platform scoping")
- (td :class "px-3 py-2 text-stone-600" "Different components get different capability subsets. User-generated content gets a locked-down platform \u2014 can render DOM but can't fetch or listen to events."))
+ (tr (~tw :tokens "border-b border-stone-100")
+ (td (~tw :tokens "px-3 py-2 text-stone-700") "1. WASM sandbox")
+ (td (~tw :tokens "px-3 py-2 text-stone-700") "Browser")
+ (td (~tw :tokens "px-3 py-2 text-stone-600") "Memory isolation, no system calls, no DOM access except via explicit imports. Validated before execution."))
+ (tr (~tw :tokens "border-b border-stone-100")
+ (td (~tw :tokens "px-3 py-2 text-stone-700") "2. Platform capabilities")
+ (td (~tw :tokens "px-3 py-2 text-stone-700") (code "sx-platform.js"))
+ (td (~tw :tokens "px-3 py-2 text-stone-600") "Compiled code can only call functions you register. No fetch? Can't fetch. No localStorage? Can't read storage. The platform is a capability system."))
+ (tr (~tw :tokens "border-b border-stone-100")
+ (td (~tw :tokens "px-3 py-2 text-stone-700") "3. Content-addressed verification")
+ (td (~tw :tokens "px-3 py-2 text-stone-700") "CID determinism")
+ (td (~tw :tokens "px-3 py-2 text-stone-600") "Compiler is deterministic: same source \u2192 same CID. Client can re-compile and verify. Tampered WASM produces wrong CID \u2192 reject."))
+ (tr (~tw :tokens "border-b border-stone-100")
+ (td (~tw :tokens "px-3 py-2 text-stone-700") "4. Per-component attenuation")
+ (td (~tw :tokens "px-3 py-2 text-stone-700") "Platform scoping")
+ (td (~tw :tokens "px-3 py-2 text-stone-600") "Different components get different capability subsets. User-generated content gets a locked-down platform \u2014 can render DOM but can't fetch or listen to events."))
(tr
- (td :class "px-3 py-2 text-stone-700" "5. Source-first fallback")
- (td :class "px-3 py-2 text-stone-700" "Client compiler")
- (td :class "px-3 py-2 text-stone-600" "Don't trust precompiled WASM? Compile from source locally. The client has the compiler. Precompilation is an optimisation, not a trust requirement.")))))
+ (td (~tw :tokens "px-3 py-2 text-stone-700") "5. Source-first fallback")
+ (td (~tw :tokens "px-3 py-2 text-stone-700") "Client compiler")
+ (td (~tw :tokens "px-3 py-2 text-stone-600") "Don't trust precompiled WASM? Compile from source locally. The client has the compiler. Precompilation is an optimisation, not a trust requirement.")))))
- (h4 :class "font-semibold mt-4 mb-2" "Content-addressed tamper detection")
+ (h4 (~tw :tokens "font-semibold mt-4 mb-2") "Content-addressed tamper detection")
(p "The server sends both SX source and precompiled WASM CID. The client can verify:")
(~docs/code :src (highlight ";; Server sends:\nContent-Type: application/wasm\nX-Sx-Source-Cid: bafyrei..source\nX-Sx-Compiled-Cid: bafyrei..compiled\n\n;; Client verifies (optional, configurable):\n1. Hash the WASM binary \u2192 matches X-Sx-Compiled-Cid?\n2. Compile source locally \u2192 produces same compiled CID?\n3. Check manifest of pinned CIDs \u2192 CID is expected?\n\n;; Any mismatch = tampered = reject" "text"))
- (h4 :class "font-semibold mt-4 mb-2" "Capability attenuation per component")
+ (h4 (~tw :tokens "font-semibold mt-4 mb-2") "Capability attenuation per component")
(p "The platform scopes capabilities per evaluator instance. "
"App shell gets full access. Third-party or user-generated content gets the minimum:")
(~docs/code :src (highlight "// Full capabilities for the app shell\nplatform.registerAll(appShellCompiler);\n\n// Restricted for user-generated content\nplatform.registerSubset(userContentCompiler, {\n allow: [\"dom-create-element\", \"dom-set-attr\", \"dom-append\",\n \"dom-create-text-node\", \"dom-set-text\"],\n deny: [\"fetch\", \"localStorage\", \"dom-listen\",\n \"dom-set-inner-html\", \"eval\"]\n});\n\n// The restricted compiler's WASM module literally doesn't\n// have imports for the denied functions. Not just blocked\n// at runtime \u2014 absent from the binary." "javascript"))
- (h4 :class "font-semibold mt-4 mb-2" "Component manifests")
+ (h4 (~tw :tokens "font-semibold mt-4 mb-2") "Component manifests")
(p "The app ships with a manifest of expected CIDs for its core components. "
"Like subresource integrity (SRI) but for compiled code:")
@@ -442,7 +442,7 @@
"and to DOM on the client. Compiled WASM doesn't change this. "
"It makes the client side faster without affecting what crawlers see.")
- (h4 :class "font-semibold mt-4 mb-2" "The rendering pipeline")
+ (h4 (~tw :tokens "font-semibold mt-4 mb-2") "The rendering pipeline")
(~docs/code :src (highlight "Crawler visits:\n GET /page\n \u2192 Server compiles SX (native OCaml)\n \u2192 render-to-html (adapter-html.sx)\n \u2192 Full static HTML with semantic markup\n \u2192 Google indexes it\n\nUser first visit:\n GET /page\n \u2192 Server renders HTML (same as crawler)\n \u2192 Browser displays immediately (no JS needed)\n \u2192 Client loads sx-compiler.wasm + sx-platform.js\n \u2192 Hydrates: attaches event handlers, activates islands\n \u2192 Page is interactive\n\nUser navigates (SPA):\n sx-get /next-page\n \u2192 Server sends SX wire format (aser)\n \u2192 Client compiles + renders via WASM\n \u2192 Morph engine patches the DOM" "text"))
@@ -453,15 +453,15 @@
"makes hydration and SPA navigation faster, but the initial HTML "
"is always server-rendered.")
- (h4 :class "font-semibold mt-4 mb-2" "What crawlers see")
- (ul :class "list-disc list-inside space-y-1 mt-2"
+ (h4 (~tw :tokens "font-semibold mt-4 mb-2") "What crawlers see")
+ (ul (~tw :tokens "list-disc list-inside space-y-1 mt-2")
(li "Fully rendered HTML \u2014 no \"loading...\" skeleton, no JS-dependent content")
(li "Semantic markup \u2014 " (code "