diff --git a/hosts/javascript/bootstrap.py b/hosts/javascript/bootstrap.py
index 770df04e..1525f28f 100644
--- a/hosts/javascript/bootstrap.py
+++ b/hosts/javascript/bootstrap.py
@@ -99,6 +99,8 @@ def compile_ref_to_js(
spec_mod_set.add(sm)
if "dom" in adapter_set and "signals" in SPEC_MODULES:
spec_mod_set.add("signals")
+ if "signals-web" in SPEC_MODULES:
+ spec_mod_set.add("signals-web")
if "boot" in adapter_set:
spec_mod_set.add("router")
spec_mod_set.add("deps")
diff --git a/hosts/javascript/manifest.py b/hosts/javascript/manifest.py
new file mode 100644
index 00000000..384ae49b
--- /dev/null
+++ b/hosts/javascript/manifest.py
@@ -0,0 +1,95 @@
+#!/usr/bin/env python3
+"""Output JS build manifest as structured text for the MCP server."""
+from __future__ import annotations
+
+import json
+import os
+import re
+import sys
+
+_HERE = os.path.dirname(os.path.abspath(__file__))
+_PROJECT = os.path.abspath(os.path.join(_HERE, "..", ".."))
+if _PROJECT not in sys.path:
+ sys.path.insert(0, _PROJECT)
+
+from hosts.javascript.platform import (
+ ADAPTER_FILES, ADAPTER_DEPS, SPEC_MODULES, SPEC_MODULE_ORDER,
+ PRIMITIVES_JS_MODULES, _ALL_JS_MODULES, EXTENSION_NAMES,
+)
+
+
+def extract_primitives(js_code: str) -> list[str]:
+ """Extract PRIMITIVES["name"] registrations from JS code."""
+ return sorted(set(re.findall(r'PRIMITIVES\["([^"]+)"\]', js_code)))
+
+
+def main():
+ # Core spec files (always included)
+ core_files = [
+ "evaluator.sx (frames + eval + CEK)",
+ "freeze.sx (serializable state)",
+ "content.sx (content-addressed computation)",
+ "render.sx (core renderer)",
+ "web-forms.sx (defstyle, deftype, defeffect)",
+ ]
+
+ # Adapters
+ adapter_lines = []
+ for name, (filename, label) in sorted(ADAPTER_FILES.items()):
+ deps = ADAPTER_DEPS.get(name, [])
+ dep_str = f" (deps: {', '.join(deps)})" if deps else ""
+ adapter_lines.append(f" {name:18s} {filename:22s} {label}{dep_str}")
+
+ # Spec modules
+ module_lines = []
+ for name in SPEC_MODULE_ORDER:
+ if name in SPEC_MODULES:
+ filename, label = SPEC_MODULES[name]
+ module_lines.append(f" {name:18s} {filename:22s} {label}")
+
+ # Extensions
+ ext_lines = [f" {name}" for name in sorted(EXTENSION_NAMES)]
+
+ # Primitive modules
+ prim_lines = []
+ for mod_name in sorted(_ALL_JS_MODULES):
+ if mod_name in PRIMITIVES_JS_MODULES:
+ prims = extract_primitives(PRIMITIVES_JS_MODULES[mod_name])
+ prim_lines.append(f" {mod_name} ({len(prims)}): {', '.join(prims)}")
+
+ # Current build file
+ build_path = os.path.join(_PROJECT, "shared", "static", "scripts", "sx-browser.js")
+ build_info = ""
+ if os.path.exists(build_path):
+ size = os.path.getsize(build_path)
+ mtime = os.path.getmtime(build_path)
+ from datetime import datetime
+ ts = datetime.fromtimestamp(mtime).strftime("%Y-%m-%d %H:%M:%S")
+ # Count PRIMITIVES in actual build
+ with open(build_path) as f:
+ content = f.read()
+ actual_prims = extract_primitives(content)
+ build_info = f"\nCurrent build: {size:,} bytes, {ts}, {len(actual_prims)} primitives registered"
+
+ print(f"""JS Build Manifest
+=================
+{build_info}
+
+Core files (always included):
+ {chr(10).join(' ' + f for f in core_files)}
+
+Adapters ({len(ADAPTER_FILES)}):
+{chr(10).join(adapter_lines)}
+
+Spec modules ({len(SPEC_MODULES)}, order: {' → '.join(SPEC_MODULE_ORDER)}):
+{chr(10).join(module_lines)}
+
+Extensions ({len(EXTENSION_NAMES)}):
+{chr(10).join(ext_lines)}
+
+Primitive modules ({len(_ALL_JS_MODULES)}):
+{chr(10).join(prim_lines)}""")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/hosts/javascript/platform.py b/hosts/javascript/platform.py
index e943121a..67cfd5b4 100644
--- a/hosts/javascript/platform.py
+++ b/hosts/javascript/platform.py
@@ -61,6 +61,7 @@ SPEC_MODULES = {
"deps": ("deps.sx", "deps (component dependency analysis)"),
"router": ("router.sx", "router (client-side route matching)"),
"signals": ("signals.sx", "signals (reactive signal runtime)"),
+ "signals-web": ("web-signals.sx", "signals-web (stores, events, resources)"),
"page-helpers": ("page-helpers.sx", "page-helpers (pure data transformation helpers)"),
"types": ("types.sx", "types (gradual type system)"),
"vm": ("vm.sx", "vm (bytecode virtual machine)"),
@@ -68,7 +69,7 @@ SPEC_MODULES = {
# Note: frames and cek are now part of evaluator.sx (always loaded as core)
# Explicit ordering for spec modules with dependencies.
-SPEC_MODULE_ORDER = ["deps", "page-helpers", "router", "signals", "types", "vm"]
+SPEC_MODULE_ORDER = ["deps", "page-helpers", "router", "signals", "signals-web", "types", "vm"]
EXTENSION_NAMES = {"continuations"}
@@ -2665,6 +2666,17 @@ PLATFORM_ORCHESTRATION_JS = """
: catchFn;
try { return t(); } catch (e) { return c(e); }
}
+ function cekTry(thunkFn, handlerFn) {
+ try {
+ var result = _wrapSxFn(thunkFn)();
+ if (!handlerFn || handlerFn === NIL) return [SYM("ok"), result];
+ return result;
+ } catch (e) {
+ var msg = (e && e.message) ? e.message : String(e);
+ if (handlerFn && handlerFn !== NIL) return _wrapSxFn(handlerFn)(msg);
+ return [SYM("error"), msg];
+ }
+ }
function errorMessage(e) {
return e && e.message ? e.message : String(e);
}
@@ -3077,7 +3089,7 @@ PLATFORM_BOOT_JS = """
}
function getRenderEnv(extraEnv) {
- return extraEnv ? merge(componentEnv, extraEnv) : componentEnv;
+ return extraEnv ? merge(componentEnv, PRIMITIVES, extraEnv) : merge(componentEnv, PRIMITIVES);
}
function mergeEnvs(base, newEnv) {
@@ -3287,7 +3299,18 @@ def fixups_js(has_html, has_sx, has_dom, has_signals=False, has_deps=False, has_
try { localStorage.removeItem(key); } catch (e) {}
return NIL;
};
- if (typeof sxParse === "function") PRIMITIVES["sx-parse"] = sxParse;''']
+ if (typeof sxParse === "function") PRIMITIVES["sx-parse"] = sxParse;
+ PRIMITIVES["cek-try"] = function(thunkFn, handlerFn) {
+ try {
+ var result = _wrapSxFn(thunkFn)();
+ if (!handlerFn || handlerFn === NIL) return [SYM("ok"), result];
+ return result;
+ } catch (e) {
+ var msg = (e && e.message) ? e.message : String(e);
+ if (handlerFn && handlerFn !== NIL) return _wrapSxFn(handlerFn)(msg);
+ return [SYM("error"), msg];
+ }
+ };''']
if has_deps:
lines.append('''
// Platform deps functions (native JS, not transpiled — need explicit registration)
diff --git a/hosts/javascript/transpiler.sx b/hosts/javascript/transpiler.sx
index 785ea6b5..4b1d35e9 100644
--- a/hosts/javascript/transpiler.sx
+++ b/hosts/javascript/transpiler.sx
@@ -506,6 +506,7 @@
"to-kebab" "toKebab"
"log-info" "logInfo"
"log-warn" "logWarn"
+ "cek-try" "cekTry"
"log-parse-error" "logParseError"
"_page-routes" "_pageRoutes"
"process-page-scripts" "processPageScripts"
diff --git a/hosts/ocaml/bin/mcp_tree.ml b/hosts/ocaml/bin/mcp_tree.ml
index 29410877..f91a92ce 100644
--- a/hosts/ocaml/bin/mcp_tree.ml
+++ b/hosts/ocaml/bin/mcp_tree.ml
@@ -177,6 +177,27 @@ let setup_env () =
| [f; List l] | [f; ListRef { contents = l }] ->
List (List.mapi (fun i x -> Sx_ref.cek_call f (List [Number (float_of_int i); x])) l)
| _ -> List []);
+ (* Native list-replace — bypasses CEK map-indexed callback chain for deep tree edits *)
+ bind "list-replace" (fun args -> match args with
+ | [List l; Number idx; v] ->
+ let i = int_of_float idx in
+ List (List.mapi (fun j x -> if j = i then v else x) l)
+ | [ListRef { contents = l }; Number idx; v] ->
+ let i = int_of_float idx in
+ List (List.mapi (fun j x -> if j = i then v else x) l)
+ | _ -> Nil);
+ (* Native navigate — bypasses CEK reduce callback chain for deep path reads *)
+ bind "navigate" (fun args -> match args with
+ | [tree; List path] | [tree; ListRef { contents = path }] ->
+ let nodes = match tree with List _ | ListRef _ -> tree | _ -> List [tree] in
+ List.fold_left (fun current idx ->
+ match current, idx with
+ | (List l | ListRef { contents = l }), Number n ->
+ let i = int_of_float n in
+ if i >= 0 && i < List.length l then List.nth l i else Nil
+ | _ -> Nil
+ ) nodes path
+ | _ -> Nil);
bind "trim" (fun args -> match args with
| [String s] -> String (String.trim s) | _ -> String "");
bind "split" (fun args -> match args with
@@ -683,38 +704,93 @@ let rec handle_tool name args =
Filename.dirname spec_dir
in
let spec = args |> member "spec" |> to_string_option in
- let spec_arg = match spec with Some s -> " " ^ s | None -> "" in
- let cmd = Printf.sprintf "cd %s/tests/playwright && npx playwright test%s --reporter=line 2>&1" project_dir spec_arg in
- let ic = Unix.open_process_in cmd in
- let lines = ref [] in
- (try while true do lines := input_line ic :: !lines done with End_of_file -> ());
- ignore (Unix.close_process_in ic);
- let all_lines = List.rev !lines in
- let fails = List.filter (fun l -> let t = String.trim l in
- String.length t > 1 && (t.[0] = '\xE2' (* ✘ *) || (String.length t > 4 && String.sub t 0 4 = "FAIL"))) all_lines in
- let summary = List.find_opt (fun l ->
- try let _ = Str.search_forward (Str.regexp "passed\\|failed") l 0 in true
- with Not_found -> false) (List.rev all_lines) in
- let result = match summary with
- | Some s ->
- if fails = [] then s
- else s ^ "\n\nFailures:\n" ^ String.concat "\n" fails
- | None ->
- let last_n = List.filteri (fun i _ -> i >= List.length all_lines - 10) all_lines in
- String.concat "\n" last_n
+ let mode = args |> member "mode" |> to_string_option in
+ let url = args |> member "url" |> to_string_option in
+ let selector = args |> member "selector" |> to_string_option in
+ let expr = args |> member "expr" |> to_string_option in
+ let actions = args |> member "actions" |> to_string_option in
+ (* Determine whether to run specs or the inspector *)
+ let use_inspector = match mode with
+ | Some m when m <> "run" -> true
+ | _ -> spec = None && mode <> None
in
- text_result result
+ if not use_inspector then begin
+ (* Original spec runner *)
+ let spec_arg = match spec with Some s -> " " ^ s | None -> "" in
+ let cmd = Printf.sprintf "cd %s/tests/playwright && npx playwright test%s --reporter=line 2>&1" project_dir spec_arg in
+ let ic = Unix.open_process_in cmd in
+ let lines = ref [] in
+ (try while true do lines := input_line ic :: !lines done with End_of_file -> ());
+ ignore (Unix.close_process_in ic);
+ let all_lines = List.rev !lines in
+ let fails = List.filter (fun l -> let t = String.trim l in
+ String.length t > 1 && (t.[0] = '\xE2' (* ✘ *) || (String.length t > 4 && String.sub t 0 4 = "FAIL"))) all_lines in
+ let summary = List.find_opt (fun l ->
+ try let _ = Str.search_forward (Str.regexp "passed\\|failed") l 0 in true
+ with Not_found -> false) (List.rev all_lines) in
+ let result = match summary with
+ | Some s ->
+ if fails = [] then s
+ else s ^ "\n\nFailures:\n" ^ String.concat "\n" fails
+ | None ->
+ let last_n = List.filteri (fun i _ -> i >= List.length all_lines - 10) all_lines in
+ String.concat "\n" last_n
+ in
+ text_result result
+ end else begin
+ (* SX-aware inspector *)
+ let inspector_args = `Assoc (List.filter_map Fun.id [
+ (match mode with Some m -> Some ("mode", `String m) | None -> Some ("mode", `String "inspect"));
+ (match url with Some u -> Some ("url", `String u) | None -> None);
+ (match selector with Some s -> Some ("selector", `String s) | None -> None);
+ (match expr with Some e -> Some ("expr", `String e) | None -> None);
+ (match actions with Some a -> Some ("actions", `String a) | None -> None);
+ ]) in
+ let args_json = Yojson.Safe.to_string (Yojson.Safe.from_string (Yojson.Basic.to_string inspector_args)) in
+ let cmd = Printf.sprintf "cd %s && node tests/playwright/sx-inspect.js '%s' 2>&1" project_dir (String.escaped args_json) in
+ let ic = Unix.open_process_in cmd in
+ let lines = ref [] in
+ (try while true do lines := input_line ic :: !lines done with End_of_file -> ());
+ ignore (Unix.close_process_in ic);
+ let raw = String.concat "\n" (List.rev !lines) in
+ (* Try to parse as JSON and format nicely *)
+ try
+ let json = Yojson.Basic.from_string raw in
+ let pretty = Yojson.Basic.pretty_to_string json in
+ text_result pretty
+ with _ ->
+ text_result raw
+ end
| "sx_harness_eval" ->
let expr_str = args |> member "expr" |> to_string in
let mock_str = args |> member "mock" |> to_string_option in
let file = args |> member "file" |> to_string_option in
+ let setup_str = args |> member "setup" |> to_string_option in
+ let files_json = try args |> member "files" with _ -> `Null in
let e = !env in
- (* Optionally load a file's definitions *)
- (match file with
- | Some f ->
- (try load_sx_file e f
- with exn -> Printf.eprintf "[mcp] Warning: %s: %s\n%!" f (Printexc.to_string exn))
+ let warnings = ref [] in
+ (* Collect all files to load *)
+ let all_files = match files_json with
+ | `List items ->
+ List.map (fun j -> Yojson.Safe.Util.to_string j) items
+ | _ -> match file with Some f -> [f] | None -> []
+ in
+ (* Load each file *)
+ List.iter (fun f ->
+ try load_sx_file e f
+ with exn ->
+ warnings := Printf.sprintf "Warning: %s: %s" f (Printexc.to_string exn) :: !warnings
+ ) all_files;
+ (* Run setup expression if provided *)
+ (match setup_str with
+ | Some s ->
+ let setup_exprs = Sx_parser.parse_all s in
+ List.iter (fun expr ->
+ try ignore (Sx_ref.eval_expr expr (Env e))
+ with exn ->
+ warnings := Printf.sprintf "Setup error: %s" (Printexc.to_string exn) :: !warnings
+ ) setup_exprs
| None -> ());
(* Create harness with optional mock overrides *)
let mock_arg = match mock_str with
@@ -743,7 +819,10 @@ let rec handle_tool name args =
) items)
| _ -> "\n\n(no IO calls)"
in
- text_result (Printf.sprintf "Result: %s%s" (Sx_types.inspect result) log_str)
+ let warn_str = if !warnings = [] then "" else
+ "\n\nWarnings:\n" ^ String.concat "\n" (List.rev !warnings)
+ in
+ text_result (Printf.sprintf "Result: %s%s%s" (Sx_types.inspect result) log_str warn_str)
| "sx_write_file" ->
let file = args |> member "file" |> to_string in
@@ -912,6 +991,219 @@ let rec handle_tool name args =
) Nil exprs in
text_result (Sx_runtime.value_to_str result)
+ | "sx_trace" ->
+ let expr_str = args |> member "expr" |> to_string in
+ let max_steps = (try args |> member "max_steps" |> to_int with _ -> 200) in
+ let file = try Some (args |> member "file" |> to_string) with _ -> None in
+ let e = !env in
+ (match file with
+ | Some f -> (try load_sx_file e f with _ -> ())
+ | None -> ());
+ let exprs = Sx_parser.parse_all expr_str in
+ let expr = match exprs with [e] -> e | _ -> List exprs in
+ let state = ref (Sx_ref.make_cek_state expr (Env e) (List [])) in
+ let steps = Buffer.create 2048 in
+ let step_count = ref 0 in
+ let truncate s n = if String.length s > n then String.sub s 0 n ^ "..." else s in
+ (try
+ while !step_count < max_steps do
+ let s = !state in
+ (match s with
+ | CekState cs ->
+ incr step_count;
+ let n = !step_count in
+ if cs.cs_phase = "eval" then begin
+ let ctrl = cs.cs_control in
+ (match ctrl with
+ | Symbol sym_name ->
+ let resolved = (try
+ let v = Sx_ref.eval_expr ctrl cs.cs_env in
+ truncate (Sx_runtime.value_to_str v) 60
+ with _ -> "???") in
+ Buffer.add_string steps
+ (Printf.sprintf "%3d LOOKUP %s → %s\n" n sym_name resolved)
+ | List (hd :: _) ->
+ let head_str = truncate (Sx_runtime.value_to_str hd) 30 in
+ let ctrl_str = truncate (Sx_runtime.value_to_str ctrl) 80 in
+ Buffer.add_string steps
+ (Printf.sprintf "%3d CALL %s\n" n ctrl_str);
+ ignore head_str
+ | _ ->
+ Buffer.add_string steps
+ (Printf.sprintf "%3d LITERAL %s\n" n
+ (truncate (Sx_runtime.value_to_str ctrl) 60)))
+ end else begin
+ (* continue phase *)
+ let val_str = truncate (Sx_runtime.value_to_str cs.cs_value) 60 in
+ let kont = cs.cs_kont in
+ let frame_type = match kont with
+ | List (Dict d :: _) ->
+ (match Hashtbl.find_opt d "type" with
+ | Some (String s) -> s | _ -> "?")
+ | List (CekState ks :: _) ->
+ (match ks.cs_control with
+ | Dict d ->
+ (match Hashtbl.find_opt d "type" with
+ | Some (String s) -> s | _ -> "?")
+ | _ -> "?")
+ | _ -> "done" in
+ Buffer.add_string steps
+ (Printf.sprintf "%3d RETURN %s → %s\n" n val_str frame_type)
+ end;
+ (match Sx_ref.cek_terminal_p s with
+ | Bool true -> raise Exit
+ | _ -> ());
+ state := Sx_ref.cek_step s
+ | _ -> raise Exit)
+ done
+ with
+ | Exit -> ()
+ | Eval_error msg ->
+ Buffer.add_string steps (Printf.sprintf "ERROR: %s\n" msg)
+ | exn ->
+ Buffer.add_string steps (Printf.sprintf "ERROR: %s\n" (Printexc.to_string exn)));
+ let final_val = (match !state with
+ | CekState cs -> Sx_runtime.value_to_str cs.cs_value
+ | v -> Sx_runtime.value_to_str v) in
+ text_result (Printf.sprintf "Result: %s\n\nTrace (%d steps):\n%s"
+ final_val !step_count (Buffer.contents steps))
+
+ | "sx_deps" ->
+ let file = args |> member "file" |> to_string in
+ let name = try Some (args |> member "name" |> to_string) with _ -> None in
+ let dir = try args |> member "dir" |> to_string with _ ->
+ try Sys.getenv "SX_PROJECT_DIR" with Not_found ->
+ try Sys.getenv "PWD" with Not_found -> "." in
+ let tree = parse_file file in
+ (* Find the target subtree *)
+ let target = match name with
+ | Some n ->
+ (* Find the named define/defcomp/defisland *)
+ let items = match tree with List l | ListRef { contents = l } -> l | _ -> [tree] in
+ let found = List.find_opt (fun item ->
+ match item with
+ | List (Symbol head :: Symbol def_name :: _)
+ | List (Symbol head :: List (Symbol def_name :: _) :: _)
+ when (head = "define" || head = "defcomp" || head = "defisland" ||
+ head = "defmacro" || head = "deftest") ->
+ def_name = n || ("~" ^ def_name) = n || def_name = String.sub n 1 (String.length n - 1)
+ | _ -> false
+ ) items in
+ (match found with Some f -> f | None -> tree)
+ | None -> tree
+ in
+ let free_syms = call_sx "collect-free-symbols" [target] in
+ let sym_names = match free_syms with
+ | List items | ListRef { contents = items } ->
+ List.filter_map (fun v -> match v with String s -> Some s | _ -> None) items
+ | _ -> []
+ in
+ (* Resolve where each symbol is defined *)
+ let file_defines = Hashtbl.create 32 in
+ let same_file_items = match tree with List l | ListRef { contents = l } -> l | _ -> [] in
+ List.iter (fun item ->
+ match item with
+ | List (Symbol head :: Symbol def_name :: _)
+ when (head = "define" || head = "defcomp" || head = "defisland" || head = "defmacro") ->
+ Hashtbl.replace file_defines def_name true
+ | _ -> ()
+ ) same_file_items;
+ (* Check primitives *)
+ let is_prim name = try ignore (Sx_primitives.get_primitive name); true with _ -> false in
+ (* Scan directory for definitions *)
+ let all_sx_files = glob_sx_files dir in
+ let ext_defs = Hashtbl.create 64 in
+ List.iter (fun path ->
+ if path <> file then
+ try
+ let t = parse_file path in
+ let items = match t with List l | ListRef { contents = l } -> l | _ -> [] in
+ List.iter (fun item ->
+ match item with
+ | List (Symbol head :: Symbol def_name :: _)
+ when (head = "define" || head = "defcomp" || head = "defisland" || head = "defmacro") ->
+ if not (Hashtbl.mem ext_defs def_name) then
+ Hashtbl.replace ext_defs def_name (relative_path ~base:dir path)
+ | _ -> ()
+ ) items
+ with _ -> ()
+ ) all_sx_files;
+ (* Format output *)
+ let lines = List.map (fun sym ->
+ if Hashtbl.mem file_defines sym then
+ Printf.sprintf " %-30s (same file)" sym
+ else if is_prim sym then
+ Printf.sprintf " %-30s [primitive]" sym
+ else match Hashtbl.find_opt ext_defs sym with
+ | Some path -> Printf.sprintf " %-30s %s" sym path
+ | None -> Printf.sprintf " %-30s ???" sym
+ ) sym_names in
+ let header = match name with
+ | Some n -> Printf.sprintf "Dependencies of %s in %s" n file
+ | None -> Printf.sprintf "Dependencies of %s" file
+ in
+ text_result (Printf.sprintf "%s\n%d symbols referenced:\n%s"
+ header (List.length sym_names) (String.concat "\n" lines))
+
+ | "sx_build_manifest" ->
+ let target = (try args |> member "target" |> to_string with _ -> "js") in
+ (match target with
+ | "ocaml" ->
+ let e = !env in
+ (* Collect all bindings from the env *)
+ let bindings = ref [] in
+ (* Walk env chain collecting all bindings *)
+ let rec collect_bindings env acc =
+ Hashtbl.iter (fun k v ->
+ if not (Hashtbl.mem acc k) then Hashtbl.replace acc k v
+ ) env.bindings;
+ match env.parent with Some p -> collect_bindings p acc | None -> ()
+ in
+ let all = Hashtbl.create 256 in
+ collect_bindings e all;
+ Hashtbl.iter (fun k v ->
+ let kind = match v with
+ | NativeFn _ -> "native"
+ | Lambda _ -> "lambda"
+ | Component _ -> "component"
+ | Island _ -> "island"
+ | Macro _ -> "macro"
+ | _ -> "value"
+ in
+ bindings := (k, kind) :: !bindings
+ ) all;
+ let sorted = List.sort (fun (a,_) (b,_) -> String.compare a b) !bindings in
+ let by_kind = Hashtbl.create 8 in
+ List.iter (fun (name, kind) ->
+ let cur = try Hashtbl.find by_kind kind with Not_found -> [] in
+ Hashtbl.replace by_kind kind (name :: cur)
+ ) sorted;
+ let sections = Buffer.create 2048 in
+ Buffer.add_string sections "OCaml Build Manifest\n====================\n\n";
+ Buffer.add_string sections (Printf.sprintf "Total bindings: %d\n\n" (List.length sorted));
+ Buffer.add_string sections "Loaded files: parser.sx, tree-tools.sx, harness.sx\n\n";
+ List.iter (fun kind ->
+ match Hashtbl.find_opt by_kind kind with
+ | Some names ->
+ let rev_names = List.rev names in
+ Buffer.add_string sections
+ (Printf.sprintf "%s (%d):\n %s\n\n" kind (List.length rev_names)
+ (String.concat ", " rev_names))
+ | None -> ()
+ ) ["native"; "lambda"; "macro"; "component"; "island"; "value"];
+ text_result (Buffer.contents sections)
+ | _ ->
+ let project_dir = try Sys.getenv "SX_PROJECT_DIR" with Not_found ->
+ try Sys.getenv "PWD" with Not_found -> "." in
+ let cmd = Printf.sprintf "cd %s && python3 hosts/javascript/manifest.py 2>&1"
+ (Filename.quote project_dir) in
+ let ic = Unix.open_process_in cmd in
+ let buf = Buffer.create 4096 in
+ (try while true do Buffer.add_string buf (input_line ic ^ "\n") done
+ with End_of_file -> ());
+ ignore (Unix.close_process_in ic);
+ text_result (Buffer.contents buf))
+
| _ -> error_result ("Unknown tool: " ^ name)
and write_edit file result =
@@ -980,6 +1272,16 @@ let tool_definitions = `List [
[file_prop; path_prop; ("wrapper", `Assoc [("type", `String "string"); ("description", `String "Wrapper with _ placeholder")])] ["file"; "path"; "wrapper"];
tool "sx_eval" "Evaluate an SX expression. Environment has parser + tree-tools + primitives."
[("expr", `Assoc [("type", `String "string"); ("description", `String "SX expression to evaluate")])] ["expr"];
+ tool "sx_trace" "Step-through SX evaluation showing each CEK machine step (symbol lookups, function calls, returns). Useful for debugging."
+ [("expr", `Assoc [("type", `String "string"); ("description", `String "SX expression to trace")]);
+ ("file", `Assoc [("type", `String "string"); ("description", `String "Optional .sx file to load for definitions")]);
+ ("max_steps", `Assoc [("type", `String "integer"); ("description", `String "Max CEK steps to show (default: 200)")])] ["expr"];
+ tool "sx_deps" "Dependency analysis for a component or file. Shows all referenced symbols and where they're defined."
+ [file_prop;
+ ("name", `Assoc [("type", `String "string"); ("description", `String "Specific define/defcomp/defisland to analyze")]);
+ ("dir", `Assoc [("type", `String "string"); ("description", `String "Directory to search for definitions (default: project root)")])] ["file"];
+ tool "sx_build_manifest" "Show build manifest: which modules, primitives, adapters, and exports are included in a JS or OCaml build."
+ [("target", `Assoc [("type", `String "string"); ("description", `String "Build target: \"js\" (default) or \"ocaml\"")])] [];
tool "sx_find_across" "Search for a pattern across all .sx files under a directory. Returns file paths, tree paths, and summaries."
[dir_prop; ("pattern", `Assoc [("type", `String "string"); ("description", `String "Search pattern")])] ["dir"; "pattern"];
tool "sx_comp_list" "List all definitions (defcomp, defisland, defmacro, defpage, define) across .sx files in a directory."
@@ -1042,13 +1344,20 @@ let tool_definitions = `List [
[file_prop; path_prop] ["file"];
tool "sx_doc_gen" "Generate component documentation from all defcomp/defisland/defmacro signatures in a directory."
[dir_prop] ["dir"];
- tool "sx_harness_eval" "Evaluate SX in a test harness with mock IO. Returns result + IO trace. Use mock param to override default mock responses."
+ tool "sx_harness_eval" "Evaluate SX in a test harness with mock IO. Returns result + IO trace. Supports loading multiple files and setup expressions."
[("expr", `Assoc [("type", `String "string"); ("description", `String "SX expression to evaluate")]);
("mock", `Assoc [("type", `String "string"); ("description", `String "Optional mock platform overrides as SX dict, e.g. {:fetch (fn (url) {:status 200})}")]);
- ("file", `Assoc [("type", `String "string"); ("description", `String "Optional .sx file to load for definitions")])]
+ ("file", `Assoc [("type", `String "string"); ("description", `String "Optional .sx file to load for definitions")]);
+ ("files", `Assoc [("type", `String "array"); ("items", `Assoc [("type", `String "string")]); ("description", `String "Multiple .sx files to load in order")]);
+ ("setup", `Assoc [("type", `String "string"); ("description", `String "SX setup expression to run before main evaluation")])]
["expr"];
- tool "sx_playwright" "Run Playwright browser tests for the SX docs site. Optionally specify a single spec file."
- [("spec", `Assoc [("type", `String "string"); ("description", `String "Optional spec file name (e.g. demo-interactions.spec.js)")])]
+ tool "sx_playwright" "Run Playwright browser tests or inspect SX pages interactively. Modes: run (spec files), inspect (page report), diff (SSR vs hydrated), eval (JS expression), interact (action sequence), screenshot."
+ [("spec", `Assoc [("type", `String "string"); ("description", `String "Spec file to run (run mode). e.g. stepper.spec.js")]);
+ ("mode", `Assoc [("type", `String "string"); ("description", `String "Mode: run, inspect, diff, eval, interact, screenshot")]);
+ ("url", `Assoc [("type", `String "string"); ("description", `String "URL path to navigate to (default: /)")]);
+ ("selector", `Assoc [("type", `String "string"); ("description", `String "CSS selector to focus on (screenshot mode)")]);
+ ("expr", `Assoc [("type", `String "string"); ("description", `String "JS expression to evaluate (eval mode)")]);
+ ("actions", `Assoc [("type", `String "string"); ("description", `String "Semicolon-separated action sequence (interact mode). Actions: click:sel, fill:sel:val, wait:ms, text:sel, html:sel, attrs:sel, screenshot, screenshot:sel, count:sel, visible:sel")])]
[];
]
diff --git a/lib/tests/test-tree-tools.sx b/lib/tests/test-tree-tools.sx
index abaa3596..e1bf7739 100644
--- a/lib/tests/test-tree-tools.sx
+++ b/lib/tests/test-tree-tools.sx
@@ -672,4 +672,81 @@
(assert (not (nil? (get result "ok"))))
(let ((found (find-all (get result "ok") "second")))
(assert (>= (len found) 1))))))
-)
+
+ (deftest "replace-node 6-level deep preserves full tree"
+ (let ((tree (sx-parse "(outer (a (b (c (d (e x y z) f g h i j k l) m n) o p) q r) s t)")))
+ (let ((result (replace-node tree (list 0 1 1 1 1 1 1) "replaced")))
+ (assert (not (nil? (get result "ok"))))
+ (let ((new-tree (get result "ok")))
+ (assert-equal "replaced" (symbol-name (navigate new-tree (list 0 1 1 1 1 1 1))))
+ (assert-equal 1 (len new-tree))
+ (let ((d-node (navigate new-tree (list 0 1 1 1 1))))
+ (assert-equal 9 (len d-node)))
+ (assert-equal "s" (symbol-name (navigate new-tree (list 0 2))))))))
+
+
+ (deftest "collect-free-symbols: let binds exclude variables"
+ (let ((tree (sx-parse "(let ((x 10)) (+ x y))")))
+ (let ((syms (collect-free-symbols (first tree))))
+ (assert (contains? syms "+"))
+ (assert (contains? syms "y"))
+ (assert (not (contains? syms "x"))))))
+
+ (deftest "collect-free-symbols: fn params excluded"
+ (let ((tree (sx-parse "(fn (a b) (+ a b c))")))
+ (let ((syms (collect-free-symbols (first tree))))
+ (assert (contains? syms "+"))
+ (assert (contains? syms "c"))
+ (assert (not (contains? syms "a")))
+ (assert (not (contains? syms "b"))))))
+
+ (deftest "collect-free-symbols: define name excluded"
+ (let ((tree (sx-parse "(define foo (+ bar 1))")))
+ (let ((syms (collect-free-symbols (first tree))))
+ (assert (contains? syms "+"))
+ (assert (contains? syms "bar"))
+ (assert (not (contains? syms "foo"))))))
+
+ (deftest "collect-free-symbols: letrec bindings visible in body"
+ (let ((tree (sx-parse "(letrec ((f (fn (x) (g x))) (g (fn (y) y))) (f 1))")))
+ (let ((syms (collect-free-symbols (first tree))))
+ (assert (not (contains? syms "f")))
+ (assert (not (contains? syms "g")))
+ (assert (not (contains? syms "x")))
+ (assert (not (contains? syms "y"))))))
+
+ (deftest "collect-free-symbols: quote skipped"
+ (let ((tree (sx-parse "(quote (foo bar baz))")))
+ (let ((syms (collect-free-symbols (first tree))))
+ (assert (empty? syms)))))
+
+ (deftest "collect-free-symbols: special forms not reported"
+ (let ((tree (sx-parse "(if (> x 0) (+ x 1) 0)")))
+ (let ((syms (collect-free-symbols (first tree))))
+ (assert (not (contains? syms "if")))
+ (assert (contains? syms ">"))
+ (assert (contains? syms "x"))
+ (assert (contains? syms "+")))))
+
+ (deftest "collect-free-symbols: defcomp params excluded"
+ (let ((tree (sx-parse "(defcomp ~card (&key title) (div title))")))
+ (let ((syms (collect-free-symbols (first tree))))
+ (assert (contains? syms "div"))
+ (assert (not (contains? syms "title"))))))
+
+ (deftest "native list-replace works at all positions"
+ (assert-equal (list 99 2 3) (list-replace (list 1 2 3) 0 99))
+ (assert-equal (list 1 99 3) (list-replace (list 1 2 3) 1 99))
+ (assert-equal (list 1 2 99) (list-replace (list 1 2 3) 2 99)))
+
+ (deftest "native navigate deep path"
+ (let ((tree (sx-parse "(a (b (c (d (e (f target))))))")))
+ (let ((result (navigate tree (list 0 1 1 1 1 1 1))))
+ (assert-equal "target" (symbol-name result)))))
+
+ (deftest "native navigate preserves structure"
+ (let ((tree (sx-parse "(outer (inner x y z) sibling)")))
+ (assert-equal "inner" (symbol-name (first (navigate tree (list 0 1)))))
+ (assert-equal "sibling" (symbol-name (navigate tree (list 0 2))))))
+
+)
\ No newline at end of file
diff --git a/lib/tree-tools.sx b/lib/tree-tools.sx
index 61c988de..f758ca7f 100644
--- a/lib/tree-tools.sx
+++ b/lib/tree-tools.sx
@@ -1,93 +1,1317 @@
-(define path-str :effects () (fn ((path :as list)) (str "[" (join "," (map str path)) "]")))
+(define
+ path-str
+ :effects ()
+ (fn ((path :as list)) (str "[" (join "," (map str path)) "]")))
-(define annotate-tree :effects () (fn (exprs) (let ((nodes (if (list? exprs) exprs (list exprs)))) (let ((result (list))) (for-each (fn (i) (annotate-node (nth nodes i) (list i) 0 result)) (range 0 (len nodes))) (join "\n" result)))))
+(define
+ annotate-tree
+ :effects ()
+ (fn
+ (exprs)
+ (let
+ ((nodes (if (list? exprs) exprs (list exprs))))
+ (let
+ ((result (list)))
+ (for-each
+ (fn (i) (annotate-node (nth nodes i) (list i) 0 result))
+ (range 0 (len nodes)))
+ (join "\n" result)))))
-(define annotate-node :effects () (fn (node path depth result) (let ((indent (join "" (map (fn (_) " ") (range 0 depth)))) (label (path-str path))) (if (list? node) (if (empty? node) (append! result (str indent label " ()")) (let ((head (first node)) (head-str (node-display head))) (if (and (<= (len node) 4) (not (some (fn (c) (list? c)) (rest node)))) (append! result (str indent label " (" (join " " (map node-display node)) ")")) (do (append! result (str indent label " (" head-str)) (for-each (fn (i) (annotate-node (nth node i) (concat path (list i)) (+ depth 1) result)) (range 1 (len node))) (append! result (str indent " )")))))) (append! result (str indent label " " (node-display node)))))))
+(define
+ annotate-node
+ :effects ()
+ (fn
+ (node path depth result)
+ (let
+ ((indent (join "" (map (fn (_) " ") (range 0 depth))))
+ (label (path-str path)))
+ (if
+ (list? node)
+ (if
+ (empty? node)
+ (append! result (str indent label " ()"))
+ (let
+ ((head (first node)) (head-str (node-display head)))
+ (if
+ (and
+ (<= (len node) 4)
+ (not (some (fn (c) (list? c)) (rest node))))
+ (append!
+ result
+ (str
+ indent
+ label
+ " ("
+ (join " " (map node-display node))
+ ")"))
+ (do
+ (append! result (str indent label " (" head-str))
+ (for-each
+ (fn
+ (i)
+ (annotate-node
+ (nth node i)
+ (concat path (list i))
+ (+ depth 1)
+ result))
+ (range 1 (len node)))
+ (append! result (str indent " )"))))))
+ (append! result (str indent label " " (node-display node)))))))
-(define node-display :effects () (fn (node) (cond (nil? node) "nil" (= (type-of node) "symbol") (symbol-name node) (= (type-of node) "keyword") (str ":" (keyword-name node)) (= (type-of node) "string") (let ((s (if (> (len node) 40) (str (slice node 0 37) "...") node))) (str "\"" s "\"")) (= (type-of node) "number") (str node) (= (type-of node) "boolean") (if node "true" "false") (list? node) (if (empty? node) "()" (str "(" (node-display (first node)) (if (> (len node) 1) " ..." "") ")")) (= (type-of node) "dict") "{...}" :else (str node))))
+(define
+ node-display
+ :effects ()
+ (fn
+ (node)
+ (cond
+ (nil? node)
+ "nil"
+ (= (type-of node) "symbol")
+ (symbol-name node)
+ (= (type-of node) "keyword")
+ (str ":" (keyword-name node))
+ (= (type-of node) "string")
+ (let
+ ((s (if (> (len node) 40) (str (slice node 0 37) "...") node)))
+ (str "\"" s "\""))
+ (= (type-of node) "number")
+ (str node)
+ (= (type-of node) "boolean")
+ (if node "true" "false")
+ (list? node)
+ (if
+ (empty? node)
+ "()"
+ (str
+ "("
+ (node-display (first node))
+ (if (> (len node) 1) " ..." "")
+ ")"))
+ (= (type-of node) "dict")
+ "{...}"
+ :else (str node))))
-(define summarise :effects () (fn (exprs max-depth) (let ((nodes (if (list? exprs) exprs (list exprs))) (result (list))) (for-each (fn (i) (summarise-node (nth nodes i) (list i) 0 max-depth result)) (range 0 (len nodes))) (join "\n" result))))
+(define
+ summarise
+ :effects ()
+ (fn
+ (exprs max-depth)
+ (let
+ ((nodes (if (list? exprs) exprs (list exprs))) (result (list)))
+ (for-each
+ (fn (i) (summarise-node (nth nodes i) (list i) 0 max-depth result))
+ (range 0 (len nodes)))
+ (join "\n" result))))
-(define summarise-node :effects () (fn (node path depth max-depth result) (let ((indent (join "" (map (fn (_) " ") (range 0 depth)))) (label (path-str path))) (if (list? node) (if (empty? node) (append! result (str indent label " ()")) (let ((head (first node)) (head-str (node-display head))) (if (>= depth max-depth) (append! result (str indent label " (" head-str (if (> (len node) 1) (str " ... " (- (len node) 1) " children") "") ")")) (do (append! result (str indent label " (" head-str)) (for-each (fn (i) (summarise-node (nth node i) (concat path (list i)) (+ depth 1) max-depth result)) (range 1 (len node))) (append! result (str indent " )")))))) (append! result (str indent label " " (node-display node)))))))
+(define
+ summarise-node
+ :effects ()
+ (fn
+ (node path depth max-depth result)
+ (let
+ ((indent (join "" (map (fn (_) " ") (range 0 depth))))
+ (label (path-str path)))
+ (if
+ (list? node)
+ (if
+ (empty? node)
+ (append! result (str indent label " ()"))
+ (let
+ ((head (first node)) (head-str (node-display head)))
+ (if
+ (>= depth max-depth)
+ (append!
+ result
+ (str
+ indent
+ label
+ " ("
+ head-str
+ (if
+ (> (len node) 1)
+ (str " ... " (- (len node) 1) " children")
+ "")
+ ")"))
+ (do
+ (append! result (str indent label " (" head-str))
+ (for-each
+ (fn
+ (i)
+ (summarise-node
+ (nth node i)
+ (concat path (list i))
+ (+ depth 1)
+ max-depth
+ result))
+ (range 1 (len node)))
+ (append! result (str indent " )"))))))
+ (append! result (str indent label " " (node-display node)))))))
-(define read-subtree :effects () (fn (exprs path) (let ((node (navigate exprs path))) (if (nil? node) (str "Error: path " (path-str path) " not found") (annotate-tree (list node))))))
+(define
+ read-subtree
+ :effects ()
+ (fn
+ (exprs path)
+ (let
+ ((node (navigate exprs path)))
+ (if
+ (nil? node)
+ (str "Error: path " (path-str path) " not found")
+ (annotate-tree (list node))))))
-(define get-context :effects () (fn (exprs path) (let ((result (list)) (nodes (if (list? exprs) exprs (list exprs)))) (for-each (fn (depth) (let ((prefix (slice path 0 (+ depth 1))) (node (navigate nodes prefix))) (when (not (nil? node)) (let ((label (path-str prefix)) (indent (join "" (map (fn (_) " ") (range 0 depth)))) (marker (if (= (+ depth 1) (len path)) "→ " " "))) (if (list? node) (append! result (str indent marker label " " (node-summary node))) (append! result (str indent marker label " " (node-display node)))))))) (range 0 (len path))) (join "\n" result))))
+(define
+ get-context
+ :effects ()
+ (fn
+ (exprs path)
+ (let
+ ((result (list)) (nodes (if (list? exprs) exprs (list exprs))))
+ (for-each
+ (fn
+ (depth)
+ (let
+ ((prefix (slice path 0 (+ depth 1)))
+ (node (navigate nodes prefix)))
+ (when
+ (not (nil? node))
+ (let
+ ((label (path-str prefix))
+ (indent (join "" (map (fn (_) " ") (range 0 depth))))
+ (marker (if (= (+ depth 1) (len path)) "→ " " ")))
+ (if
+ (list? node)
+ (append!
+ result
+ (str indent marker label " " (node-summary node)))
+ (append!
+ result
+ (str indent marker label " " (node-display node))))))))
+ (range 0 (len path)))
+ (join "\n" result))))
-(define node-summary :effects () (fn (node) (if (or (not (list? node)) (empty? node)) (node-display node) (let ((head (node-display (first node))) (child-count (- (len node) 1))) (if (<= child-count 3) (str "(" (join " " (map node-display node)) ")") (str "(" head " " (node-display (nth node 1)) (when (> child-count 1) (str " ... +" (- child-count 1))) ")"))))))
+(define
+ node-summary
+ :effects ()
+ (fn
+ (node)
+ (if
+ (or (not (list? node)) (empty? node))
+ (node-display node)
+ (let
+ ((head (node-display (first node)))
+ (child-count (- (len node) 1)))
+ (if
+ (<= child-count 3)
+ (str "(" (join " " (map node-display node)) ")")
+ (str
+ "("
+ head
+ " "
+ (node-display (nth node 1))
+ (when (> child-count 1) (str " ... +" (- child-count 1)))
+ ")"))))))
-(define find-all :effects () (fn (exprs pattern) (let ((results (list)) (nodes (if (list? exprs) exprs (list exprs)))) (for-each (fn (i) (find-in-node (nth nodes i) (list i) pattern results)) (range 0 (len nodes))) results)))
+(define
+ find-all
+ :effects ()
+ (fn
+ (exprs pattern)
+ (let
+ ((results (list)) (nodes (if (list? exprs) exprs (list exprs))))
+ (for-each
+ (fn (i) (find-in-node (nth nodes i) (list i) pattern results))
+ (range 0 (len nodes)))
+ results)))
-(define find-in-node :effects () (fn (node path pattern results) (when (node-matches? node pattern) (append! results (list path (node-summary-short node)))) (when (list? node) (for-each (fn (i) (find-in-node (nth node i) (concat path (list i)) pattern results)) (range 0 (len node))))))
+(define
+ find-in-node
+ :effects ()
+ (fn
+ (node path pattern results)
+ (when
+ (node-matches? node pattern)
+ (append! results (list path (node-summary-short node))))
+ (when
+ (list? node)
+ (for-each
+ (fn
+ (i)
+ (find-in-node (nth node i) (concat path (list i)) pattern results))
+ (range 0 (len node))))))
-(define node-matches? :effects () (fn (node pattern) (cond (= (type-of node) "symbol") (contains? (symbol-name node) pattern) (string? node) (contains? node pattern) (and (list? node) (not (empty? node)) (= (type-of (first node)) "symbol")) (contains? (symbol-name (first node)) pattern) :else false)))
+(define
+ node-matches?
+ :effects ()
+ (fn
+ (node pattern)
+ (cond
+ (= (type-of node) "symbol")
+ (contains? (symbol-name node) pattern)
+ (string? node)
+ (contains? node pattern)
+ (and
+ (list? node)
+ (not (empty? node))
+ (= (type-of (first node)) "symbol"))
+ (contains? (symbol-name (first node)) pattern)
+ :else false)))
-(define node-summary-short :effects () (fn (node) (if (list? node) (if (empty? node) "()" (let ((head (node-display (first node)))) (if (> (len node) 3) (str "(" head " " (node-display (nth node 1)) " ...)") (str "(" (join " " (map node-display node)) ")")))) (node-display node))))
+(define
+ node-summary-short
+ :effects ()
+ (fn
+ (node)
+ (if
+ (list? node)
+ (if
+ (empty? node)
+ "()"
+ (let
+ ((head (node-display (first node))))
+ (if
+ (> (len node) 3)
+ (str "(" head " " (node-display (nth node 1)) " ...)")
+ (str "(" (join " " (map node-display node)) ")"))))
+ (node-display node))))
-(define get-siblings :effects () (fn (exprs path) (if (empty? path) "Error: root has no siblings" (let ((parent-path (slice path 0 (- (len path) 1))) (target-idx (last path)) (parent (navigate exprs parent-path))) (if (or (nil? parent) (not (list? parent))) (str "Error: parent at " (path-str parent-path) " not found or not a list") (let ((result (list))) (for-each (fn (i) (let ((child (nth parent i)) (child-path (concat parent-path (list i))) (marker (if (= i target-idx) "→ " " "))) (append! result (str marker (path-str child-path) " " (if (list? child) (node-summary child) (node-display child)))))) (range 0 (len parent))) (join "\n" result)))))))
+(define
+ get-siblings
+ :effects ()
+ (fn
+ (exprs path)
+ (if
+ (empty? path)
+ "Error: root has no siblings"
+ (let
+ ((parent-path (slice path 0 (- (len path) 1)))
+ (target-idx (last path))
+ (parent (navigate exprs parent-path)))
+ (if
+ (or (nil? parent) (not (list? parent)))
+ (str
+ "Error: parent at "
+ (path-str parent-path)
+ " not found or not a list")
+ (let
+ ((result (list)))
+ (for-each
+ (fn
+ (i)
+ (let
+ ((child (nth parent i))
+ (child-path (concat parent-path (list i)))
+ (marker (if (= i target-idx) "→ " " ")))
+ (append!
+ result
+ (str
+ marker
+ (path-str child-path)
+ " "
+ (if
+ (list? child)
+ (node-summary child)
+ (node-display child))))))
+ (range 0 (len parent)))
+ (join "\n" result)))))))
-(define validate :effects () (fn (exprs) (let ((errors (list)) (nodes (if (list? exprs) exprs (list exprs)))) (for-each (fn (i) (validate-node (nth nodes i) (list i) errors)) (range 0 (len nodes))) (if (empty? errors) "OK" (join "\n" errors)))))
+(define
+ validate
+ :effects ()
+ (fn
+ (exprs)
+ (let
+ ((errors (list)) (nodes (if (list? exprs) exprs (list exprs))))
+ (for-each
+ (fn (i) (validate-node (nth nodes i) (list i) errors))
+ (range 0 (len nodes)))
+ (if (empty? errors) "OK" (join "\n" errors)))))
-(define validate-node :effects () (fn (node path errors) (when (list? node) (when (not (empty? node)) (let ((head (first node))) (when (and (= (type-of head) "symbol") (= (symbol-name head) "letrec") (>= (len node) 2)) (let ((bindings (nth node 1))) (when (list? bindings) (for-each (fn (i) (let ((pair (nth bindings i))) (when (not (and (list? pair) (>= (len pair) 2) (= (type-of (first pair)) "symbol"))) (append! errors (str "WARNING " (path-str (concat path (list 1 i))) ": letrec binding " i " is not a (name value) pair: " (node-display pair)))))) (range 0 (len bindings)))))) (when (and (= (type-of head) "symbol") (or (= (symbol-name head) "defisland") (= (symbol-name head) "defcomp"))) (when (< (len node) 4) (append! errors (str "ERROR " (path-str path) ": " (symbol-name head) " has fewer than 3 args (name params body)")))))) (for-each (fn (i) (validate-node (nth node i) (concat path (list i)) errors)) (range 0 (len node))))))
+(define
+ validate-node
+ :effects ()
+ (fn
+ (node path errors)
+ (when
+ (list? node)
+ (when
+ (not (empty? node))
+ (let
+ ((head (first node)))
+ (when
+ (and
+ (= (type-of head) "symbol")
+ (= (symbol-name head) "letrec")
+ (>= (len node) 2))
+ (let
+ ((bindings (nth node 1)))
+ (when
+ (list? bindings)
+ (for-each
+ (fn
+ (i)
+ (let
+ ((pair (nth bindings i)))
+ (when
+ (not
+ (and
+ (list? pair)
+ (>= (len pair) 2)
+ (= (type-of (first pair)) "symbol")))
+ (append!
+ errors
+ (str
+ "WARNING "
+ (path-str (concat path (list 1 i)))
+ ": letrec binding "
+ i
+ " is not a (name value) pair: "
+ (node-display pair))))))
+ (range 0 (len bindings))))))
+ (when
+ (and
+ (= (type-of head) "symbol")
+ (or
+ (= (symbol-name head) "defisland")
+ (= (symbol-name head) "defcomp")))
+ (when
+ (< (len node) 4)
+ (append!
+ errors
+ (str
+ "ERROR "
+ (path-str path)
+ ": "
+ (symbol-name head)
+ " has fewer than 3 args (name params body)"))))))
+ (for-each
+ (fn
+ (i)
+ (validate-node (nth node i) (concat path (list i)) errors))
+ (range 0 (len node))))))
-(define navigate :effects () (fn (exprs path) (let ((nodes (if (list? exprs) exprs (list exprs)))) (reduce (fn (current idx) (if (or (nil? current) (not (list? current)) (>= idx (len current))) nil (nth current idx))) nodes path))))
+(define
+ navigate
+ :effects ()
+ (fn
+ (exprs path)
+ (let
+ ((nodes (if (list? exprs) exprs (list exprs))))
+ (reduce
+ (fn
+ (current idx)
+ (if
+ (or
+ (nil? current)
+ (not (list? current))
+ (>= idx (len current)))
+ nil
+ (nth current idx)))
+ nodes
+ path))))
-(define resolve-named-path :effects () (fn (exprs named-path) (let ((segments (if (string? named-path) (split-path-string named-path) named-path)) (nodes (if (list? exprs) exprs (list exprs)))) (let ((result (reduce (fn (state segment) (if (nil? (get state "node")) state (let ((node (get state "node")) (path (get state "path"))) (if (not (list? node)) {:node nil :path path} (let ((idx (find-child-by-name node segment))) (if (nil? idx) {:node nil :path path} {:node (nth node idx) :path (concat path (list idx))})))))) {:node nodes :path (list)} segments))) (get result "path")))))
+(define
+ resolve-named-path
+ :effects ()
+ (fn
+ (exprs named-path)
+ (let
+ ((segments (if (string? named-path) (split-path-string named-path) named-path))
+ (nodes (if (list? exprs) exprs (list exprs))))
+ (let
+ ((result (reduce (fn (state segment) (if (nil? (get state "node")) state (let ((node (get state "node")) (path (get state "path"))) (if (not (list? node)) {:node nil :path path} (let ((idx (find-child-by-name node segment))) (if (nil? idx) {:node nil :path path} {:node (nth node idx) :path (concat path (list idx))})))))) {:node nodes :path (list)} segments)))
+ (get result "path")))))
-(define split-path-string :effects () (fn ((s :as string)) (filter (fn (x) (not (= (trim x) ""))) (split s ">"))))
+(define
+ split-path-string
+ :effects ()
+ (fn
+ ((s :as string))
+ (filter (fn (x) (not (= (trim x) ""))) (split s ">"))))
-(define find-child-by-name :effects () (fn (node name) (let ((trimmed (trim name)) (result nil)) (for-each (fn (i) (when (nil? result) (let ((child (nth node i))) (when (or (and (= (type-of child) "symbol") (= (symbol-name child) trimmed)) (and (list? child) (not (empty? child)) (= (type-of (first child)) "symbol") (= (symbol-name (first child)) trimmed))) (set! result i))))) (range 0 (len node))) result)))
+(define
+ find-child-by-name
+ :effects ()
+ (fn
+ (node name)
+ (let
+ ((trimmed (trim name)) (result nil))
+ (for-each
+ (fn
+ (i)
+ (when
+ (nil? result)
+ (let
+ ((child (nth node i)))
+ (when
+ (or
+ (and
+ (= (type-of child) "symbol")
+ (= (symbol-name child) trimmed))
+ (and
+ (list? child)
+ (not (empty? child))
+ (= (type-of (first child)) "symbol")
+ (= (symbol-name (first child)) trimmed)))
+ (set! result i)))))
+ (range 0 (len node)))
+ result)))
-(define replace-node :effects () (fn (exprs path new-source) (let ((fragment (sx-parse new-source))) (if (empty? fragment) {:error (str "Fragment parse error: empty result from: " new-source)} (let ((new-node (if (= (len fragment) 1) (first fragment) (cons (make-symbol "begin") fragment))) (result (tree-set exprs path new-node))) (if (nil? result) {:error (str "Path not found: " (path-str path))} {:ok result}))))))
+(define
+ replace-node
+ :effects ()
+ (fn
+ (exprs path new-source)
+ (let
+ ((fragment (sx-parse new-source)))
+ (if
+ (empty? fragment)
+ {:error (str "Fragment parse error: empty result from: " new-source)}
+ (let
+ ((new-node (if (= (len fragment) 1) (first fragment) (cons (make-symbol "begin") fragment)))
+ (result (tree-set exprs path new-node)))
+ (if (nil? result) {:error (str "Path not found: " (path-str path))} {:ok result}))))))
-(define insert-child :effects () (fn (exprs path index new-source) (let ((fragment (sx-parse new-source))) (if (empty? fragment) {:error (str "Failed to parse new source: " new-source)} (let ((parent (navigate exprs path))) (if (or (nil? parent) (not (list? parent))) {:error (str "Parent at " (path-str path) " not found or not a list")} (if (or (< index 0) (> index (len parent))) {:error (str "Index " index " out of range for parent with " (len parent) " children")} (let ((new-parent (concat (slice parent 0 index) fragment (slice parent index))) (result (tree-set exprs path new-parent))) (if (nil? result) {:error (str "Failed to set node at path " (path-str path))} {:ok result})))))))))
+(define
+ insert-child
+ :effects ()
+ (fn
+ (exprs path index new-source)
+ (let
+ ((fragment (sx-parse new-source)))
+ (if
+ (empty? fragment)
+ {:error (str "Failed to parse new source: " new-source)}
+ (let
+ ((parent (navigate exprs path)))
+ (if
+ (or (nil? parent) (not (list? parent)))
+ {:error (str "Parent at " (path-str path) " not found or not a list")}
+ (if
+ (or (< index 0) (> index (len parent)))
+ {:error (str "Index " index " out of range for parent with " (len parent) " children")}
+ (let
+ ((new-parent (concat (slice parent 0 index) fragment (slice parent index)))
+ (result (tree-set exprs path new-parent)))
+ (if (nil? result) {:error (str "Failed to set node at path " (path-str path))} {:ok result})))))))))
-(define delete-node :effects () (fn (exprs path) (if (empty? path) {:error "Cannot delete root"} (let ((parent-path (slice path 0 (- (len path) 1))) (child-idx (last path)) (parent (navigate exprs parent-path))) (if (or (nil? parent) (not (list? parent))) {:error (str "Parent not found or not a list: " (path-str parent-path))} (if (or (< child-idx 0) (>= child-idx (len parent))) {:error (str "Index " child-idx " out of range")} (let ((new-parent (list-remove parent child-idx)) (result (tree-set exprs parent-path new-parent))) (if (nil? result) {:error "Failed to update tree"} {:ok result}))))))))
+(define
+ delete-node
+ :effects ()
+ (fn
+ (exprs path)
+ (if
+ (empty? path)
+ {:error "Cannot delete root"}
+ (let
+ ((parent-path (slice path 0 (- (len path) 1)))
+ (child-idx (last path))
+ (parent (navigate exprs parent-path)))
+ (if
+ (or (nil? parent) (not (list? parent)))
+ {:error (str "Parent not found or not a list: " (path-str parent-path))}
+ (if
+ (or (< child-idx 0) (>= child-idx (len parent)))
+ {:error (str "Index " child-idx " out of range")}
+ (let
+ ((new-parent (list-remove parent child-idx))
+ (result (tree-set exprs parent-path new-parent)))
+ (if (nil? result) {:error "Failed to update tree"} {:ok result}))))))))
-(define wrap-node :effects () (fn (exprs path wrapper-source) (let ((fragment (sx-parse wrapper-source))) (if (empty? fragment) {:error (str "Wrapper parse error: empty result from: " wrapper-source)} (let ((wrapper (first fragment)) (target (navigate exprs path))) (if (nil? target) {:error (str "Path not found: " (path-str path))} (let ((filled (replace-placeholder wrapper target))) (if (nil? filled) {:error "Wrapper must contain _ as placeholder for the wrapped node"} (let ((result (tree-set exprs path filled))) (if (nil? result) {:error "Failed to update tree"} {:ok result}))))))))))
+(define
+ wrap-node
+ :effects ()
+ (fn
+ (exprs path wrapper-source)
+ (let
+ ((fragment (sx-parse wrapper-source)))
+ (if
+ (empty? fragment)
+ {:error (str "Wrapper parse error: empty result from: " wrapper-source)}
+ (let
+ ((wrapper (first fragment)) (target (navigate exprs path)))
+ (if
+ (nil? target)
+ {:error (str "Path not found: " (path-str path))}
+ (let
+ ((filled (replace-placeholder wrapper target)))
+ (if
+ (nil? filled)
+ {:error "Wrapper must contain _ as placeholder for the wrapped node"}
+ (let
+ ((result (tree-set exprs path filled)))
+ (if (nil? result) {:error "Failed to update tree"} {:ok result}))))))))))
-(define replace-placeholder :effects () (fn (node replacement) (cond (and (= (type-of node) "symbol") (= (symbol-name node) "_")) replacement (list? node) (let ((found false) (result (map (fn (child) (if found child (if (and (= (type-of child) "symbol") (= (symbol-name child) "_")) (do (set! found true) replacement) (if (list? child) (let ((sub (replace-placeholder child replacement))) (if (nil? sub) child (do (set! found true) sub))) child)))) node))) (if found result nil)) :else nil)))
+(define
+ replace-placeholder
+ :effects ()
+ (fn
+ (node replacement)
+ (cond
+ (and (= (type-of node) "symbol") (= (symbol-name node) "_"))
+ replacement
+ (list? node)
+ (let
+ ((found false)
+ (result
+ (map
+ (fn
+ (child)
+ (if
+ found
+ child
+ (if
+ (and
+ (= (type-of child) "symbol")
+ (= (symbol-name child) "_"))
+ (do (set! found true) replacement)
+ (if
+ (list? child)
+ (let
+ ((sub (replace-placeholder child replacement)))
+ (if (nil? sub) child (do (set! found true) sub)))
+ child))))
+ node)))
+ (if found result nil))
+ :else nil)))
-(define tree-set :effects () (fn (exprs path new-node) (let ((nodes (if (list? exprs) exprs (list exprs)))) (if (empty? path) (if (list? new-node) new-node (list new-node)) (tree-set-inner nodes path 0 new-node)))))
+(define
+ tree-set
+ :effects ()
+ (fn
+ (exprs path new-node)
+ (let
+ ((nodes (if (list? exprs) exprs (list exprs))))
+ (if
+ (empty? path)
+ (if (list? new-node) new-node (list new-node))
+ (tree-set-inner nodes path 0 new-node)))))
-(define tree-set-inner :effects () (fn (node path depth new-node) (if (not (list? node)) nil (let ((idx (nth path depth))) (if (or (< idx 0) (>= idx (len node))) nil (if (= depth (- (len path) 1)) (list-replace node idx new-node) (let ((child (nth node idx)) (new-child (tree-set-inner child path (+ depth 1) new-node))) (if (nil? new-child) nil (list-replace node idx new-child)))))))))
+(define
+ tree-set-inner
+ :effects ()
+ (fn
+ (node path depth new-node)
+ (if
+ (not (list? node))
+ nil
+ (let
+ ((idx (nth path depth)))
+ (if
+ (or (< idx 0) (>= idx (len node)))
+ nil
+ (if
+ (= depth (- (len path) 1))
+ (list-replace node idx new-node)
+ (let
+ ((child (nth node idx))
+ (new-child (tree-set-inner child path (+ depth 1) new-node)))
+ (if (nil? new-child) nil (list-replace node idx new-child)))))))))
-(define list-replace :effects () (fn (lst idx val) (map-indexed (fn (i item) (if (= i idx) val item)) lst)))
+(define
+ list-replace
+ :effects ()
+ (fn
+ (lst idx val)
+ (map-indexed (fn (i item) (if (= i idx) val item)) lst)))
-(define list-insert :effects () (fn (lst idx val) (concat (slice lst 0 idx) (list val) (slice lst idx))))
+(define
+ list-insert
+ :effects ()
+ (fn (lst idx val) (concat (slice lst 0 idx) (list val) (slice lst idx))))
-(define list-remove :effects () (fn (lst idx) (concat (slice lst 0 idx) (slice lst (+ idx 1)))))
+(define
+ list-remove
+ :effects ()
+ (fn (lst idx) (concat (slice lst 0 idx) (slice lst (+ idx 1)))))
-(define tree-diff :effects () (fn (exprs-a exprs-b) (let ((nodes-a (if (list? exprs-a) exprs-a (list exprs-a))) (nodes-b (if (list? exprs-b) exprs-b (list exprs-b))) (results (list))) (diff-children nodes-a nodes-b (list) results) (if (empty? results) "No differences" (join "\n" results)))))
+(define
+ tree-diff
+ :effects ()
+ (fn
+ (exprs-a exprs-b)
+ (let
+ ((nodes-a (if (list? exprs-a) exprs-a (list exprs-a)))
+ (nodes-b (if (list? exprs-b) exprs-b (list exprs-b)))
+ (results (list)))
+ (diff-children nodes-a nodes-b (list) results)
+ (if (empty? results) "No differences" (join "\n" results)))))
-(define diff-children :effects () (fn (list-a list-b path results) (let ((len-a (len list-a)) (len-b (len list-b)) (min-len (if (< len-a len-b) len-a len-b))) (for-each (fn (i) (diff-node (nth list-a i) (nth list-b i) (concat path (list i)) results)) (range 0 min-len)) (when (> len-b min-len) (for-each (fn (i) (append! results (str "ADDED " (path-str (concat path (list i))) " " (node-summary-short (nth list-b i))))) (range min-len len-b))) (when (> len-a min-len) (for-each (fn (i) (append! results (str "REMOVED " (path-str (concat path (list i))) " " (node-summary-short (nth list-a i))))) (range min-len len-a))))))
+(define
+ diff-children
+ :effects ()
+ (fn
+ (list-a list-b path results)
+ (let
+ ((len-a (len list-a))
+ (len-b (len list-b))
+ (min-len (if (< len-a len-b) len-a len-b)))
+ (for-each
+ (fn
+ (i)
+ (diff-node
+ (nth list-a i)
+ (nth list-b i)
+ (concat path (list i))
+ results))
+ (range 0 min-len))
+ (when
+ (> len-b min-len)
+ (for-each
+ (fn
+ (i)
+ (append!
+ results
+ (str
+ "ADDED "
+ (path-str (concat path (list i)))
+ " "
+ (node-summary-short (nth list-b i)))))
+ (range min-len len-b)))
+ (when
+ (> len-a min-len)
+ (for-each
+ (fn
+ (i)
+ (append!
+ results
+ (str
+ "REMOVED "
+ (path-str (concat path (list i)))
+ " "
+ (node-summary-short (nth list-a i)))))
+ (range min-len len-a))))))
-(define diff-node :effects () (fn (a b path results) (cond (and (list? a) (list? b)) (diff-children a b path results) (and (not (list? a)) (not (list? b))) (when (not (= (node-display a) (node-display b))) (append! results (str "CHANGED " (path-str path) " " (node-display a) " → " (node-display b)))) :else (append! results (str "CHANGED " (path-str path) " " (node-summary-short a) " → " (node-summary-short b))))))
+(define
+ diff-node
+ :effects ()
+ (fn
+ (a b path results)
+ (cond
+ (and (list? a) (list? b))
+ (diff-children a b path results)
+ (and (not (list? a)) (not (list? b)))
+ (when
+ (not (= (node-display a) (node-display b)))
+ (append!
+ results
+ (str
+ "CHANGED "
+ (path-str path)
+ " "
+ (node-display a)
+ " → "
+ (node-display b))))
+ :else (append!
+ results
+ (str
+ "CHANGED "
+ (path-str path)
+ " "
+ (node-summary-short a)
+ " → "
+ (node-summary-short b))))))
-(define path-prefix? :effects () (fn (prefix path) (if (> (len prefix) (len path)) false (let ((result true)) (for-each (fn (i) (when (not (= (nth prefix i) (nth path i))) (set! result false))) (range 0 (len prefix))) result))))
+(define
+ path-prefix?
+ :effects ()
+ (fn
+ (prefix path)
+ (if
+ (> (len prefix) (len path))
+ false
+ (let
+ ((result true))
+ (for-each
+ (fn
+ (i)
+ (when
+ (not (= (nth prefix i) (nth path i)))
+ (set! result false)))
+ (range 0 (len prefix)))
+ result))))
-(define path-on-match-route? :effects () (fn (path match-paths) (let ((found false)) (for-each (fn (i) (when (not found) (let ((mp (first (nth match-paths i)))) (when (or (path-prefix? path mp) (path-prefix? mp path)) (set! found true))))) (range 0 (len match-paths))) found)))
+(define
+ path-on-match-route?
+ :effects ()
+ (fn
+ (path match-paths)
+ (let
+ ((found false))
+ (for-each
+ (fn
+ (i)
+ (when
+ (not found)
+ (let
+ ((mp (first (nth match-paths i))))
+ (when
+ (or (path-prefix? path mp) (path-prefix? mp path))
+ (set! found true)))))
+ (range 0 (len match-paths)))
+ found)))
-(define annotate-focused :effects () (fn (exprs pattern) (let ((nodes (if (list? exprs) exprs (list exprs))) (match-paths (find-all nodes pattern)) (result (list))) (for-each (fn (i) (annotate-node-focused (nth nodes i) (list i) 0 match-paths result)) (range 0 (len nodes))) (join "\n" result))))
+(define
+ annotate-focused
+ :effects ()
+ (fn
+ (exprs pattern)
+ (let
+ ((nodes (if (list? exprs) exprs (list exprs)))
+ (match-paths (find-all nodes pattern))
+ (result (list)))
+ (for-each
+ (fn
+ (i)
+ (annotate-node-focused (nth nodes i) (list i) 0 match-paths result))
+ (range 0 (len nodes)))
+ (join "\n" result))))
-(define annotate-node-focused :effects () (fn (node path depth match-paths result) (let ((indent (join "" (map (fn (_) " ") (range 0 depth)))) (label (path-str path))) (if (list? node) (if (empty? node) (append! result (str indent label " ()")) (let ((head (first node)) (head-str (node-display head)) (on-route (path-on-match-route? path match-paths))) (if on-route (do (append! result (str indent label " (" head-str)) (for-each (fn (i) (annotate-node-focused (nth node i) (concat path (list i)) (+ depth 1) match-paths result)) (range 1 (len node))) (append! result (str indent " )"))) (append! result (str indent label " (" head-str (if (> (len node) 1) (str " ... " (- (len node) 1) " children") "") ")"))))) (append! result (str indent label " " (node-display node)))))))
+(define
+ annotate-node-focused
+ :effects ()
+ (fn
+ (node path depth match-paths result)
+ (let
+ ((indent (join "" (map (fn (_) " ") (range 0 depth))))
+ (label (path-str path)))
+ (if
+ (list? node)
+ (if
+ (empty? node)
+ (append! result (str indent label " ()"))
+ (let
+ ((head (first node))
+ (head-str (node-display head))
+ (on-route (path-on-match-route? path match-paths)))
+ (if
+ on-route
+ (do
+ (append! result (str indent label " (" head-str))
+ (for-each
+ (fn
+ (i)
+ (annotate-node-focused
+ (nth node i)
+ (concat path (list i))
+ (+ depth 1)
+ match-paths
+ result))
+ (range 1 (len node)))
+ (append! result (str indent " )")))
+ (append!
+ result
+ (str
+ indent
+ label
+ " ("
+ head-str
+ (if
+ (> (len node) 1)
+ (str " ... " (- (len node) 1) " children")
+ "")
+ ")")))))
+ (append! result (str indent label " " (node-display node)))))))
-(define annotate-paginated :effects () (fn (exprs offset limit) (let ((nodes (if (list? exprs) exprs (list exprs))) (all-lines (list))) (for-each (fn (i) (annotate-node (nth nodes i) (list i) 0 all-lines)) (range 0 (len nodes))) (let ((total (len all-lines)) (end (if (> (+ offset limit) total) total (+ offset limit))) (sliced (slice all-lines offset end)) (header (str ";; Lines " offset "-" end " of " total (if (< end total) " (more available)" " (complete)")))) (str header "\n" (join "\n" sliced))))))
+(define
+ annotate-paginated
+ :effects ()
+ (fn
+ (exprs offset limit)
+ (let
+ ((nodes (if (list? exprs) exprs (list exprs))) (all-lines (list)))
+ (for-each
+ (fn (i) (annotate-node (nth nodes i) (list i) 0 all-lines))
+ (range 0 (len nodes)))
+ (let
+ ((total (len all-lines))
+ (end (if (> (+ offset limit) total) total (+ offset limit)))
+ (sliced (slice all-lines offset end))
+ (header
+ (str
+ ";; Lines "
+ offset
+ "-"
+ end
+ " of "
+ total
+ (if (< end total) " (more available)" " (complete)"))))
+ (str header "\n" (join "\n" sliced))))))
-(define rename-symbol :effects () (fn (exprs old-name new-name) (let ((nodes (if (list? exprs) exprs (list exprs)))) (map (fn (node) (rename-in-node node old-name new-name)) nodes))))
+(define
+ rename-symbol
+ :effects ()
+ (fn
+ (exprs old-name new-name)
+ (let
+ ((nodes (if (list? exprs) exprs (list exprs))))
+ (map (fn (node) (rename-in-node node old-name new-name)) nodes))))
-(define rename-in-node :effects () (fn (node old-name new-name) (cond (and (= (type-of node) "symbol") (= (symbol-name node) old-name)) (make-symbol new-name) (list? node) (map (fn (child) (rename-in-node child old-name new-name)) node) :else node)))
+(define
+ rename-in-node
+ :effects ()
+ (fn
+ (node old-name new-name)
+ (cond
+ (and (= (type-of node) "symbol") (= (symbol-name node) old-name))
+ (make-symbol new-name)
+ (list? node)
+ (map (fn (child) (rename-in-node child old-name new-name)) node)
+ :else node)))
-(define count-renames :effects () (fn (exprs old-name) (let ((nodes (if (list? exprs) exprs (list exprs))) (hits (list))) (count-in-node nodes old-name hits) (len hits))))
+(define
+ count-renames
+ :effects ()
+ (fn
+ (exprs old-name)
+ (let
+ ((nodes (if (list? exprs) exprs (list exprs))) (hits (list)))
+ (count-in-node nodes old-name hits)
+ (len hits))))
-(define count-in-node :effects () (fn (node old-name hits) (cond (and (= (type-of node) "symbol") (= (symbol-name node) old-name)) (append! hits true) (list? node) (for-each (fn (child) (count-in-node child old-name hits)) node) :else nil)))
+(define
+ count-in-node
+ :effects ()
+ (fn
+ (node old-name hits)
+ (cond
+ (and (= (type-of node) "symbol") (= (symbol-name node) old-name))
+ (append! hits true)
+ (list? node)
+ (for-each (fn (child) (count-in-node child old-name hits)) node)
+ :else nil)))
-(define replace-by-pattern :effects () (fn (exprs pattern new-source) (let ((nodes (if (list? exprs) exprs (list exprs))) (matches (find-all nodes pattern))) (if (empty? matches) {:error (str "No nodes matching pattern: " pattern)} (let ((target-path (first (first matches))) (fragment (sx-parse new-source))) (if (empty? fragment) {:error (str "Failed to parse new source: " new-source)} (let ((new-node (first fragment)) (result (tree-set nodes target-path new-node))) (if (nil? result) {:error (str "Failed to set node at path " (path-str target-path))} {:ok result :path target-path}))))))))
+(define
+ replace-by-pattern
+ :effects ()
+ (fn
+ (exprs pattern new-source)
+ (let
+ ((nodes (if (list? exprs) exprs (list exprs)))
+ (matches (find-all nodes pattern)))
+ (if
+ (empty? matches)
+ {:error (str "No nodes matching pattern: " pattern)}
+ (let
+ ((target-path (first (first matches)))
+ (fragment (sx-parse new-source)))
+ (if
+ (empty? fragment)
+ {:error (str "Failed to parse new source: " new-source)}
+ (let
+ ((new-node (first fragment))
+ (result (tree-set nodes target-path new-node)))
+ (if (nil? result) {:error (str "Failed to set node at path " (path-str target-path))} {:ok result :path target-path}))))))))
-(define replace-all-by-pattern :effects () (fn (exprs pattern new-source) (let ((nodes (if (list? exprs) exprs (list exprs))) (matches (find-all nodes pattern)) (fragment (sx-parse new-source))) (if (empty? matches) {:error (str "No nodes matching pattern: " pattern)} (if (empty? fragment) {:error (str "Failed to parse new source: " new-source)} (let ((new-node (first fragment)) (current nodes) (count 0)) (for-each (fn (i) (let ((idx (- (- (len matches) 1) i)) (match (nth matches idx)) (target-path (first match)) (result (tree-set current target-path new-node))) (when (not (nil? result)) (set! current result) (set! count (+ count 1))))) (range 0 (len matches))) {:count count :ok current}))))))
+(define
+ replace-all-by-pattern
+ :effects ()
+ (fn
+ (exprs pattern new-source)
+ (let
+ ((nodes (if (list? exprs) exprs (list exprs)))
+ (matches (find-all nodes pattern))
+ (fragment (sx-parse new-source)))
+ (if
+ (empty? matches)
+ {:error (str "No nodes matching pattern: " pattern)}
+ (if
+ (empty? fragment)
+ {:error (str "Failed to parse new source: " new-source)}
+ (let
+ ((new-node (first fragment)) (current nodes) (count 0))
+ (for-each
+ (fn
+ (i)
+ (let
+ ((idx (- (- (len matches) 1) i))
+ (match (nth matches idx))
+ (target-path (first match))
+ (result (tree-set current target-path new-node)))
+ (when
+ (not (nil? result))
+ (set! current result)
+ (set! count (+ count 1)))))
+ (range 0 (len matches)))
+ {:count count :ok current}))))))
-(define insert-near-pattern :effects () (fn (exprs pattern position new-source) (let ((nodes (if (list? exprs) exprs (list exprs))) (matches (find-all nodes pattern))) (if (empty? matches) {:error (str "No nodes matching pattern: " pattern)} (let ((match-path (first (first matches))) (fragment (sx-parse new-source))) (if (empty? fragment) {:error (str "Failed to parse new source: " new-source)} (if (empty? match-path) {:error "Cannot insert near root node"} (let ((top-idx (first match-path)) (insert-idx (if (= position "after") (+ top-idx 1) top-idx)) (new-node (first fragment)) (new-tree (list-insert nodes insert-idx new-node))) {:ok new-tree :path (list insert-idx)}))))))))
+(define
+ insert-near-pattern
+ :effects ()
+ (fn
+ (exprs pattern position new-source)
+ (let
+ ((nodes (if (list? exprs) exprs (list exprs)))
+ (matches (find-all nodes pattern)))
+ (if
+ (empty? matches)
+ {:error (str "No nodes matching pattern: " pattern)}
+ (let
+ ((match-path (first (first matches)))
+ (fragment (sx-parse new-source)))
+ (if
+ (empty? fragment)
+ {:error (str "Failed to parse new source: " new-source)}
+ (if
+ (empty? match-path)
+ {:error "Cannot insert near root node"}
+ (let
+ ((top-idx (first match-path))
+ (insert-idx
+ (if (= position "after") (+ top-idx 1) top-idx))
+ (new-node (first fragment))
+ (new-tree (list-insert nodes insert-idx new-node)))
+ {:ok new-tree :path (list insert-idx)}))))))))
-(define lint-file :effects () (fn (exprs) (let ((nodes (if (list? exprs) exprs (list exprs))) (warnings (list))) (for-each (fn (i) (lint-node (nth nodes i) (list i) warnings)) (range 0 (len nodes))) warnings)))
+(define
+ lint-file
+ :effects ()
+ (fn
+ (exprs)
+ (let
+ ((nodes (if (list? exprs) exprs (list exprs))) (warnings (list)))
+ (for-each
+ (fn (i) (lint-node (nth nodes i) (list i) warnings))
+ (range 0 (len nodes)))
+ warnings)))
-(define lint-node :effects () (fn (node path warnings) (when (list? node) (when (not (empty? node)) (let ((head (first node)) (head-name (if (= (type-of head) "symbol") (symbol-name head) ""))) (when (or (= head-name "let") (= head-name "letrec")) (when (>= (len node) 2) (let ((bindings (nth node 1))) (when (and (list? bindings) (empty? bindings)) (append! warnings (str "WARN " (path-str path) ": " head-name " with empty bindings")))))) (when (or (= head-name "defcomp") (= head-name "defisland")) (when (< (len node) 4) (append! warnings (str "ERROR " (path-str path) ": " head-name " needs (name params body), has " (- (len node) 1) " args")))) (when (= head-name "define") (let ((effective-len (len (filter (fn (x) (not (= (type-of x) "keyword"))) (rest node))))) (when (< effective-len 2) (append! warnings (str "WARN " (path-str path) ": define with no body"))))) (when (or (= head-name "defcomp") (= head-name "defisland")) (when (>= (len node) 3) (let ((params (nth node 2))) (when (list? params) (let ((seen (list))) (for-each (fn (p) (when (= (type-of p) "symbol") (let ((pname (symbol-name p))) (when (and (not (= pname "&key")) (not (= pname "&rest")) (not (starts-with? pname "&"))) (when (contains? seen pname) (append! warnings (str "ERROR " (path-str path) ": duplicate param " pname))) (append! seen pname))))) params)))))) (for-each (fn (i) (lint-node (nth node i) (concat path (list i)) warnings)) (range 0 (len node))))))))
+(define
+ lint-node
+ :effects ()
+ (fn
+ (node path warnings)
+ (when
+ (list? node)
+ (when
+ (not (empty? node))
+ (let
+ ((head (first node))
+ (head-name
+ (if (= (type-of head) "symbol") (symbol-name head) "")))
+ (when
+ (or (= head-name "let") (= head-name "letrec"))
+ (when
+ (>= (len node) 2)
+ (let
+ ((bindings (nth node 1)))
+ (when
+ (and (list? bindings) (empty? bindings))
+ (append!
+ warnings
+ (str
+ "WARN "
+ (path-str path)
+ ": "
+ head-name
+ " with empty bindings"))))))
+ (when
+ (or (= head-name "defcomp") (= head-name "defisland"))
+ (when
+ (< (len node) 4)
+ (append!
+ warnings
+ (str
+ "ERROR "
+ (path-str path)
+ ": "
+ head-name
+ " needs (name params body), has "
+ (- (len node) 1)
+ " args"))))
+ (when
+ (= head-name "define")
+ (let
+ ((effective-len (len (filter (fn (x) (not (= (type-of x) "keyword"))) (rest node)))))
+ (when
+ (< effective-len 2)
+ (append!
+ warnings
+ (str "WARN " (path-str path) ": define with no body")))))
+ (when
+ (or (= head-name "defcomp") (= head-name "defisland"))
+ (when
+ (>= (len node) 3)
+ (let
+ ((params (nth node 2)))
+ (when
+ (list? params)
+ (let
+ ((seen (list)))
+ (for-each
+ (fn
+ (p)
+ (when
+ (= (type-of p) "symbol")
+ (let
+ ((pname (symbol-name p)))
+ (when
+ (and
+ (not (= pname "&key"))
+ (not (= pname "&rest"))
+ (not (starts-with? pname "&")))
+ (when
+ (contains? seen pname)
+ (append!
+ warnings
+ (str
+ "ERROR "
+ (path-str path)
+ ": duplicate param "
+ pname)))
+ (append! seen pname)))))
+ params))))))
+ (for-each
+ (fn
+ (i)
+ (lint-node (nth node i) (concat path (list i)) warnings))
+ (range 0 (len node))))))))
+
+(define
+ collect-free-symbols
+ :effects ()
+ (fn
+ (node)
+ (let
+ ((result (list))
+ (seen (dict))
+ (special-forms
+ (dict
+ "if"
+ true
+ "when"
+ true
+ "cond"
+ true
+ "case"
+ true
+ "and"
+ true
+ "or"
+ true
+ "let"
+ true
+ "let*"
+ true
+ "letrec"
+ true
+ "lambda"
+ true
+ "fn"
+ true
+ "define"
+ true
+ "defcomp"
+ true
+ "defisland"
+ true
+ "defmacro"
+ true
+ "deftest"
+ true
+ "begin"
+ true
+ "do"
+ true
+ "quote"
+ true
+ "quasiquote"
+ true
+ "set!"
+ true
+ "->"
+ true
+ "reset"
+ true
+ "shift"
+ true
+ "deref"
+ true
+ "scope"
+ true
+ "provide"
+ true
+ "context"
+ true
+ "emit!"
+ true
+ "emitted"
+ true
+ "dynamic-wind"
+ true
+ "map"
+ true
+ "filter"
+ true
+ "reduce"
+ true
+ "for-each"
+ true
+ "some"
+ true
+ "every?"
+ true
+ "map-indexed"
+ true
+ "list"
+ true
+ "dict"
+ true
+ "str"
+ true
+ "cons"
+ true
+ "concat"
+ true)))
+ (define
+ add-sym
+ (fn
+ (name)
+ (when
+ (and
+ (not (has-key? seen name))
+ (not (has-key? special-forms name))
+ (not (starts-with? name ":")))
+ (dict-set! seen name true)
+ (append! result name))))
+ (define
+ extract-binding-names
+ (fn
+ (bindings)
+ (let
+ ((names (dict)))
+ (for-each
+ (fn
+ (b)
+ (when
+ (list? b)
+ (let
+ ((name (first b)))
+ (when
+ (= (type-of name) "symbol")
+ (dict-set! names (symbol-name name) true)))))
+ bindings)
+ names)))
+ (define
+ walk
+ (fn
+ (node bound)
+ (cond
+ (nil? node)
+ nil
+ (= (type-of node) "symbol")
+ (let
+ ((name (symbol-name node)))
+ (when (not (has-key? bound name)) (add-sym name)))
+ (= (type-of node) "keyword")
+ nil
+ (not (list? node))
+ nil
+ (empty? node)
+ nil
+ :else (let
+ ((head (first node)) (args (rest node)))
+ (if
+ (not (= (type-of head) "symbol"))
+ (for-each (fn (child) (walk child bound)) node)
+ (let
+ ((hname (symbol-name head)))
+ (cond
+ (or (= hname "define") (= hname "defmacro"))
+ (when
+ (>= (len args) 2)
+ (let
+ ((body-start (if (= (type-of (nth args 1)) "keyword") 3 1)))
+ (for-each
+ (fn (child) (walk child bound))
+ (slice args body-start))))
+ (or
+ (= hname "defcomp")
+ (= hname "defisland")
+ (= hname "deftest"))
+ (when
+ (>= (len args) 2)
+ (let
+ ((params (nth args 1)) (param-names (dict)))
+ (when
+ (list? params)
+ (for-each
+ (fn
+ (p)
+ (when
+ (= (type-of p) "symbol")
+ (dict-set! param-names (symbol-name p) true)))
+ params))
+ (let
+ ((new-bound (merge bound param-names)))
+ (for-each
+ (fn (child) (walk child new-bound))
+ (slice args 2)))))
+ (or (= hname "fn") (= hname "lambda"))
+ (when
+ (>= (len args) 2)
+ (let
+ ((params (first args)) (param-names (dict)))
+ (when
+ (list? params)
+ (for-each
+ (fn
+ (p)
+ (cond
+ (= (type-of p) "symbol")
+ (dict-set! param-names (symbol-name p) true)
+ (list? p)
+ (when
+ (= (type-of (first p)) "symbol")
+ (dict-set!
+ param-names
+ (symbol-name (first p))
+ true))))
+ params))
+ (let
+ ((new-bound (merge bound param-names)))
+ (for-each
+ (fn (child) (walk child new-bound))
+ (rest args)))))
+ (or (= hname "let") (= hname "let*") (= hname "letrec"))
+ (when
+ (>= (len args) 2)
+ (let
+ ((bindings (first args))
+ (bind-names
+ (if
+ (list? (first args))
+ (extract-binding-names (first args))
+ (dict))))
+ (let
+ ((new-bound (merge bound bind-names)))
+ (when
+ (list? bindings)
+ (for-each
+ (fn
+ (b)
+ (when
+ (and (list? b) (>= (len b) 2))
+ (walk
+ (nth b 1)
+ (if (= hname "letrec") new-bound bound))))
+ bindings))
+ (for-each
+ (fn (child) (walk child new-bound))
+ (rest args)))))
+ (= hname "quote")
+ nil
+ (= hname "set!")
+ (when
+ (>= (len args) 2)
+ (when
+ (= (type-of (first args)) "symbol")
+ (let
+ ((name (symbol-name (first args))))
+ (when (not (has-key? bound name)) (add-sym name))))
+ (walk (nth args 1) bound))
+ :else (do
+ (when
+ (not (has-key? special-forms hname))
+ (when (not (has-key? bound hname)) (add-sym hname)))
+ (for-each (fn (child) (walk child bound)) args)))))))))
+ (walk node (dict))
+ result)))
diff --git a/shared/static/scripts/sx-browser.js b/shared/static/scripts/sx-browser.js
index 64ab46cf..092438e3 100644
--- a/shared/static/scripts/sx-browser.js
+++ b/shared/static/scripts/sx-browser.js
@@ -14,7 +14,7 @@
// =========================================================================
var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } });
- var SX_VERSION = "2026-03-25T00:09:53Z";
+ var SX_VERSION = "2026-03-26T11:30:03Z";
function isNil(x) { return x === NIL || x === null || x === undefined; }
function isSxTruthy(x) { return x !== false && !isNil(x); }
@@ -1376,7 +1376,8 @@ PRIMITIVES["parse-comp-params"] = parseCompParams;
var sfDefisland = function(args, env) { return (function() {
var nameSym = first(args);
var paramsRaw = nth(args, 1);
- var body = last(args);
+ var bodyExprs = slice(args, 2);
+ var body = (isSxTruthy((len(bodyExprs) == 1)) ? first(bodyExprs) : cons(makeSymbol("begin"), bodyExprs));
var compName = stripPrefix(symbolName(nameSym), "~");
var parsed = parseCompParams(paramsRaw);
var params = first(parsed);
@@ -2892,7 +2893,7 @@ PRIMITIVES["render-html-marsh"] = renderHtmlMarsh;
envBind(local, "children", makeRawHtml(join("", map(function(c) { return renderToHtml(c, env); }, children))));
}
return (function() {
- var bodyHtml = renderToHtml(componentBody(island), local);
+ var bodyHtml = cekTry(function() { return renderToHtml(componentBody(island), local); }, function(err) { return ""; });
var stateSx = serializeIslandState(kwargs);
return (String("") + String(bodyHtml) + String(""));
})();
@@ -3149,6 +3150,530 @@ PRIMITIVES["aser-special"] = aserSpecial;
PRIMITIVES["eval-case-aser"] = evalCaseAser;
+ // === Transpiled from lib/dom (DOM library) ===
+
+ // dom-document
+ var domDocument = function() { return hostGlobal("document"); };
+PRIMITIVES["dom-document"] = domDocument;
+
+ // dom-window
+ var domWindow = function() { return hostGlobal("window"); };
+PRIMITIVES["dom-window"] = domWindow;
+
+ // dom-body
+ var domBody = function() { return hostGet(domDocument(), "body"); };
+PRIMITIVES["dom-body"] = domBody;
+
+ // dom-head
+ var domHead = function() { return hostGet(domDocument(), "head"); };
+PRIMITIVES["dom-head"] = domHead;
+
+ // dom-create-element
+ var domCreateElement = function(tag) { var nsArg = Array.prototype.slice.call(arguments, 1); return (function() {
+ var ns = (isSxTruthy((isSxTruthy(nsArg) && !isSxTruthy(isEmpty(nsArg)))) ? first(nsArg) : NIL);
+ return (isSxTruthy(ns) ? hostCall(domDocument(), "createElementNS", ns, tag) : hostCall(domDocument(), "createElement", tag));
+})(); };
+PRIMITIVES["dom-create-element"] = domCreateElement;
+
+ // create-text-node
+ var createTextNode = function(s) { return hostCall(domDocument(), "createTextNode", s); };
+PRIMITIVES["create-text-node"] = createTextNode;
+
+ // create-fragment
+ var createFragment = function() { return hostCall(domDocument(), "createDocumentFragment"); };
+PRIMITIVES["create-fragment"] = createFragment;
+
+ // create-comment
+ var createComment = function(text) { return hostCall(domDocument(), "createComment", sxOr(text, "")); };
+PRIMITIVES["create-comment"] = createComment;
+
+ // dom-append
+ var domAppend = function(parent, child) { return (isSxTruthy((isSxTruthy(parent) && child)) ? hostCall(parent, "appendChild", child) : NIL); };
+PRIMITIVES["dom-append"] = domAppend;
+
+ // dom-prepend
+ var domPrepend = function(parent, child) { return (isSxTruthy((isSxTruthy(parent) && child)) ? hostCall(parent, "prepend", child) : NIL); };
+PRIMITIVES["dom-prepend"] = domPrepend;
+
+ // dom-insert-before
+ var domInsertBefore = function(parent, child, ref) { return (isSxTruthy((isSxTruthy(parent) && child)) ? hostCall(parent, "insertBefore", child, ref) : NIL); };
+PRIMITIVES["dom-insert-before"] = domInsertBefore;
+
+ // dom-insert-after
+ var domInsertAfter = function(ref, node) { "Insert node after ref in the same parent.";
+return (function() {
+ var parent = hostGet(ref, "parentNode");
+ var next = hostGet(ref, "nextSibling");
+ return (isSxTruthy(parent) ? (isSxTruthy(next) ? hostCall(parent, "insertBefore", node, next) : hostCall(parent, "appendChild", node)) : NIL);
+})(); };
+PRIMITIVES["dom-insert-after"] = domInsertAfter;
+
+ // dom-remove
+ var domRemove = function(el) { return (isSxTruthy(el) ? hostCall(el, "remove") : NIL); };
+PRIMITIVES["dom-remove"] = domRemove;
+
+ // dom-is-active-element?
+ var domIsActiveElement = function(el) { return (function() {
+ var active = hostGet(domDocument(), "activeElement");
+ return (isSxTruthy((isSxTruthy(active) && el)) ? isIdentical(el, active) : false);
+})(); };
+PRIMITIVES["dom-is-active-element?"] = domIsActiveElement;
+
+ // dom-is-input-element?
+ var domIsInputElement = function(el) { return (function() {
+ var tag = upper(sxOr(domTagName(el), ""));
+ return sxOr((tag == "INPUT"), (tag == "TEXTAREA"), (tag == "SELECT"));
+})(); };
+PRIMITIVES["dom-is-input-element?"] = domIsInputElement;
+
+ // dom-is-child-of?
+ var domIsChildOf = function(child, parent) { return (isSxTruthy(child) && isSxTruthy(parent) && hostCall(parent, "contains", child)); };
+PRIMITIVES["dom-is-child-of?"] = domIsChildOf;
+
+ // dom-attr-list
+ var domAttrList = function(el) { return (function() {
+ var attrs = hostGet(el, "attributes");
+ var result = [];
+ if (isSxTruthy(attrs)) {
+ (function() {
+ var n = hostGet(attrs, "length");
+ return (function loop(i) {
+ return (isSxTruthy((i < n)) ? ((function() {
+ var attr = hostCall(attrs, "item", i);
+ return append_b(result, [hostGet(attr, "name"), hostGet(attr, "value")]);
+})(), loop((i + 1))) : NIL);
+})(0);
+})();
+}
+ return result;
+})(); };
+PRIMITIVES["dom-attr-list"] = domAttrList;
+
+ // dom-remove-child
+ var domRemoveChild = function(parent, child) { return (isSxTruthy((isSxTruthy(parent) && child)) ? hostCall(parent, "removeChild", child) : NIL); };
+PRIMITIVES["dom-remove-child"] = domRemoveChild;
+
+ // dom-replace-child
+ var domReplaceChild = function(parent, newChild, oldChild) { return (isSxTruthy((isSxTruthy(parent) && isSxTruthy(newChild) && oldChild)) ? hostCall(parent, "replaceChild", newChild, oldChild) : NIL); };
+PRIMITIVES["dom-replace-child"] = domReplaceChild;
+
+ // dom-clone
+ var domClone = function(node, deep) { return hostCall(node, "cloneNode", (isSxTruthy(isNil(deep)) ? true : deep)); };
+PRIMITIVES["dom-clone"] = domClone;
+
+ // dom-query
+ var domQuery = function(rootOrSel) { var rest = Array.prototype.slice.call(arguments, 1); return (isSxTruthy(isEmpty(rest)) ? hostCall(domDocument(), "querySelector", rootOrSel) : hostCall(rootOrSel, "querySelector", first(rest))); };
+PRIMITIVES["dom-query"] = domQuery;
+
+ // dom-query-all
+ var domQueryAll = function(root, sel) { "Query DOM and return an SX list (not a host NodeList).";
+return (function() {
+ var nodeList = (isSxTruthy(isNil(sel)) ? hostCall(domDocument(), "querySelectorAll", root) : hostCall(root, "querySelectorAll", sel));
+ return (isSxTruthy(isNil(nodeList)) ? [] : (function() {
+ var n = hostGet(nodeList, "length");
+ var result = [];
+ (function loop(i) {
+ return (isSxTruthy((i < n)) ? (append_b(result, hostCall(nodeList, "item", i)), loop((i + 1))) : NIL);
+})(0);
+ return result;
+})());
+})(); };
+PRIMITIVES["dom-query-all"] = domQueryAll;
+
+ // dom-query-by-id
+ var domQueryById = function(id) { return hostCall(domDocument(), "getElementById", id); };
+PRIMITIVES["dom-query-by-id"] = domQueryById;
+
+ // dom-closest
+ var domClosest = function(el, sel) { return (isSxTruthy(el) ? hostCall(el, "closest", sel) : NIL); };
+PRIMITIVES["dom-closest"] = domClosest;
+
+ // dom-matches?
+ var domMatches = function(el, sel) { return (isSxTruthy((isSxTruthy(el) && hostGet(el, "matches"))) ? hostCall(el, "matches", sel) : false); };
+PRIMITIVES["dom-matches?"] = domMatches;
+
+ // dom-get-attr
+ var domGetAttr = function(el, name) { return (isSxTruthy((isSxTruthy(el) && hostGet(el, "getAttribute"))) ? (function() {
+ var v = hostCall(el, "getAttribute", name);
+ return (isSxTruthy(isNil(v)) ? NIL : v);
+})() : NIL); };
+PRIMITIVES["dom-get-attr"] = domGetAttr;
+
+ // dom-set-attr
+ var domSetAttr = function(el, name, val) { return (isSxTruthy((isSxTruthy(el) && hostGet(el, "setAttribute"))) ? hostCall(el, "setAttribute", name, val) : NIL); };
+PRIMITIVES["dom-set-attr"] = domSetAttr;
+
+ // dom-remove-attr
+ var domRemoveAttr = function(el, name) { return (isSxTruthy((isSxTruthy(el) && hostGet(el, "removeAttribute"))) ? hostCall(el, "removeAttribute", name) : NIL); };
+PRIMITIVES["dom-remove-attr"] = domRemoveAttr;
+
+ // dom-has-attr?
+ var domHasAttr = function(el, name) { return (isSxTruthy((isSxTruthy(el) && hostGet(el, "hasAttribute"))) ? hostCall(el, "hasAttribute", name) : false); };
+PRIMITIVES["dom-has-attr?"] = domHasAttr;
+
+ // dom-add-class
+ var domAddClass = function(el, cls) { return (isSxTruthy(el) ? hostCall(hostGet(el, "classList"), "add", cls) : NIL); };
+PRIMITIVES["dom-add-class"] = domAddClass;
+
+ // dom-remove-class
+ var domRemoveClass = function(el, cls) { return (isSxTruthy(el) ? hostCall(hostGet(el, "classList"), "remove", cls) : NIL); };
+PRIMITIVES["dom-remove-class"] = domRemoveClass;
+
+ // dom-has-class?
+ var domHasClass = function(el, cls) { return (isSxTruthy(el) ? hostCall(hostGet(el, "classList"), "contains", cls) : false); };
+PRIMITIVES["dom-has-class?"] = domHasClass;
+
+ // dom-text-content
+ var domTextContent = function(el) { return hostGet(el, "textContent"); };
+PRIMITIVES["dom-text-content"] = domTextContent;
+
+ // dom-set-text-content
+ var domSetTextContent = function(el, val) { return hostSet(el, "textContent", val); };
+PRIMITIVES["dom-set-text-content"] = domSetTextContent;
+
+ // dom-inner-html
+ var domInnerHtml = function(el) { return hostGet(el, "innerHTML"); };
+PRIMITIVES["dom-inner-html"] = domInnerHtml;
+
+ // dom-set-inner-html
+ var domSetInnerHtml = function(el, val) { return hostSet(el, "innerHTML", val); };
+PRIMITIVES["dom-set-inner-html"] = domSetInnerHtml;
+
+ // dom-outer-html
+ var domOuterHtml = function(el) { return hostGet(el, "outerHTML"); };
+PRIMITIVES["dom-outer-html"] = domOuterHtml;
+
+ // dom-insert-adjacent-html
+ var domInsertAdjacentHtml = function(el, position, html) { return hostCall(el, "insertAdjacentHTML", position, html); };
+PRIMITIVES["dom-insert-adjacent-html"] = domInsertAdjacentHtml;
+
+ // dom-get-style
+ var domGetStyle = function(el, prop) { return hostGet(hostGet(el, "style"), prop); };
+PRIMITIVES["dom-get-style"] = domGetStyle;
+
+ // dom-set-style
+ var domSetStyle = function(el, prop, val) { return hostCall(hostGet(el, "style"), "setProperty", prop, val); };
+PRIMITIVES["dom-set-style"] = domSetStyle;
+
+ // dom-get-prop
+ var domGetProp = function(el, name) { return hostGet(el, name); };
+PRIMITIVES["dom-get-prop"] = domGetProp;
+
+ // dom-set-prop
+ var domSetProp = function(el, name, val) { return hostSet(el, name, val); };
+PRIMITIVES["dom-set-prop"] = domSetProp;
+
+ // dom-tag-name
+ var domTagName = function(el) { return (isSxTruthy(el) ? lower(sxOr(hostGet(el, "tagName"), "")) : ""); };
+PRIMITIVES["dom-tag-name"] = domTagName;
+
+ // dom-node-type
+ var domNodeType = function(el) { return hostGet(el, "nodeType"); };
+PRIMITIVES["dom-node-type"] = domNodeType;
+
+ // dom-node-name
+ var domNodeName = function(el) { return hostGet(el, "nodeName"); };
+PRIMITIVES["dom-node-name"] = domNodeName;
+
+ // dom-id
+ var domId = function(el) { return hostGet(el, "id"); };
+PRIMITIVES["dom-id"] = domId;
+
+ // dom-parent
+ var domParent = function(el) { return hostGet(el, "parentNode"); };
+PRIMITIVES["dom-parent"] = domParent;
+
+ // dom-first-child
+ var domFirstChild = function(el) { return hostGet(el, "firstChild"); };
+PRIMITIVES["dom-first-child"] = domFirstChild;
+
+ // dom-next-sibling
+ var domNextSibling = function(el) { return hostGet(el, "nextSibling"); };
+PRIMITIVES["dom-next-sibling"] = domNextSibling;
+
+ // dom-child-list
+ var domChildList = function(el) { "Return child nodes as an SX list.";
+return (isSxTruthy(el) ? (function() {
+ var nl = hostGet(el, "childNodes");
+ var n = hostGet(nl, "length");
+ var result = [];
+ (function loop(i) {
+ return (isSxTruthy((i < n)) ? (append_b(result, hostCall(nl, "item", i)), loop((i + 1))) : NIL);
+})(0);
+ return result;
+})() : []); };
+PRIMITIVES["dom-child-list"] = domChildList;
+
+ // dom-is-fragment?
+ var domIsFragment = function(el) { return (hostGet(el, "nodeType") == 11); };
+PRIMITIVES["dom-is-fragment?"] = domIsFragment;
+
+ // dom-child-nodes
+ var domChildNodes = function(el) { "Return child nodes as an SX list.";
+return (isSxTruthy(el) ? (function() {
+ var nl = hostGet(el, "childNodes");
+ var n = hostGet(nl, "length");
+ var result = [];
+ (function loop(i) {
+ return (isSxTruthy((i < n)) ? (append_b(result, hostCall(nl, "item", i)), loop((i + 1))) : NIL);
+})(0);
+ return result;
+})() : []); };
+PRIMITIVES["dom-child-nodes"] = domChildNodes;
+
+ // dom-remove-children-after
+ var domRemoveChildrenAfter = function(marker) { "Remove all siblings after marker node.";
+return (function() {
+ var parent = domParent(marker);
+ return (isSxTruthy(parent) ? (function loop() {
+ return (function() {
+ var next = domNextSibling(marker);
+ return (isSxTruthy(next) ? (hostCall(parent, "removeChild", next), loop()) : NIL);
+})();
+})() : NIL);
+})(); };
+PRIMITIVES["dom-remove-children-after"] = domRemoveChildrenAfter;
+
+ // dom-focus
+ var domFocus = function(el) { return (isSxTruthy(el) ? hostCall(el, "focus") : NIL); };
+PRIMITIVES["dom-focus"] = domFocus;
+
+ // dom-parse-html
+ var domParseHtml = function(html) { return (function() {
+ var parser = hostNew("DOMParser");
+ var doc = hostCall(parser, "parseFromString", html, "text/html");
+ return hostGet(hostGet(doc, "body"), "childNodes");
+})(); };
+PRIMITIVES["dom-parse-html"] = domParseHtml;
+
+ // dom-listen
+ var domListen = function(el, eventName, handler) { return (function() {
+ var cb = hostCallback(handler);
+ hostCall(el, "addEventListener", eventName, cb);
+ return function() { return hostCall(el, "removeEventListener", eventName, cb); };
+})(); };
+PRIMITIVES["dom-listen"] = domListen;
+
+ // dom-add-listener
+ var domAddListener = function(el, eventName, handler) { var opts = Array.prototype.slice.call(arguments, 3); return (function() {
+ var cb = hostCallback(handler);
+ (isSxTruthy((isSxTruthy(opts) && !isSxTruthy(isEmpty(opts)))) ? hostCall(el, "addEventListener", eventName, cb, first(opts)) : hostCall(el, "addEventListener", eventName, cb));
+ return function() { return hostCall(el, "removeEventListener", eventName, cb); };
+})(); };
+PRIMITIVES["dom-add-listener"] = domAddListener;
+
+ // dom-dispatch
+ var domDispatch = function(el, eventName, detail) { return (function() {
+ var evt = hostNew("CustomEvent", eventName, {["detail"]: detail, ["bubbles"]: true});
+ return hostCall(el, "dispatchEvent", evt);
+})(); };
+PRIMITIVES["dom-dispatch"] = domDispatch;
+
+ // event-detail
+ var eventDetail = function(evt) { return hostGet(evt, "detail"); };
+PRIMITIVES["event-detail"] = eventDetail;
+
+ // prevent-default
+ var preventDefault_ = function(e) { return (isSxTruthy(e) ? hostCall(e, "preventDefault") : NIL); };
+PRIMITIVES["prevent-default"] = preventDefault_;
+
+ // stop-propagation
+ var stopPropagation_ = function(e) { return (isSxTruthy(e) ? hostCall(e, "stopPropagation") : NIL); };
+PRIMITIVES["stop-propagation"] = stopPropagation_;
+
+ // event-modifier-key?
+ var eventModifierKey_p = function(e) { return (isSxTruthy(e) && sxOr(hostGet(e, "ctrlKey"), hostGet(e, "metaKey"), hostGet(e, "shiftKey"), hostGet(e, "altKey"))); };
+PRIMITIVES["event-modifier-key?"] = eventModifierKey_p;
+
+ // element-value
+ var elementValue = function(el) { return (isSxTruthy((isSxTruthy(el) && !isSxTruthy(isNil(hostGet(el, "value"))))) ? hostGet(el, "value") : NIL); };
+PRIMITIVES["element-value"] = elementValue;
+
+ // error-message
+ var errorMessage = function(e) { return (isSxTruthy((isSxTruthy(e) && hostGet(e, "message"))) ? hostGet(e, "message") : (String(e))); };
+PRIMITIVES["error-message"] = errorMessage;
+
+ // dom-get-data
+ var domGetData = function(el, key) { return (function() {
+ var store = hostGet(el, "__sx_data");
+ return (isSxTruthy(store) ? hostGet(store, key) : NIL);
+})(); };
+PRIMITIVES["dom-get-data"] = domGetData;
+
+ // dom-set-data
+ var domSetData = function(el, key, val) { if (isSxTruthy(!isSxTruthy(hostGet(el, "__sx_data")))) {
+ hostSet(el, "__sx_data", {});
+}
+return hostSet(hostGet(el, "__sx_data"), key, val); };
+PRIMITIVES["dom-set-data"] = domSetData;
+
+ // dom-append-to-head
+ var domAppendToHead = function(el) { return (isSxTruthy(domHead()) ? hostCall(domHead(), "appendChild", el) : NIL); };
+PRIMITIVES["dom-append-to-head"] = domAppendToHead;
+
+ // set-document-title
+ var setDocumentTitle = function(title) { return hostSet(domDocument(), "title", title); };
+PRIMITIVES["set-document-title"] = setDocumentTitle;
+
+
+ // === Transpiled from lib/browser (browser API library) ===
+
+ // browser-location-href
+ var browserLocationHref = function() { return hostGet(hostGet(domWindow(), "location"), "href"); };
+PRIMITIVES["browser-location-href"] = browserLocationHref;
+
+ // browser-location-pathname
+ var browserLocationPathname = function() { return hostGet(hostGet(domWindow(), "location"), "pathname"); };
+PRIMITIVES["browser-location-pathname"] = browserLocationPathname;
+
+ // browser-location-origin
+ var browserLocationOrigin = function() { return hostGet(hostGet(domWindow(), "location"), "origin"); };
+PRIMITIVES["browser-location-origin"] = browserLocationOrigin;
+
+ // browser-same-origin?
+ var browserSameOrigin = function(url) { return startsWith(url, browserLocationOrigin()); };
+PRIMITIVES["browser-same-origin?"] = browserSameOrigin;
+
+ // url-pathname
+ var urlPathname = function(url) { return hostGet(hostNew("URL", url, browserLocationOrigin()), "pathname"); };
+PRIMITIVES["url-pathname"] = urlPathname;
+
+ // browser-push-state
+ var browserPushState = function(urlOrState) { var rest = Array.prototype.slice.call(arguments, 1); return (isSxTruthy(isEmpty(rest)) ? hostCall(hostGet(domWindow(), "history"), "pushState", NIL, "", urlOrState) : hostCall(hostGet(domWindow(), "history"), "pushState", urlOrState, first(rest), nth(rest, 1))); };
+PRIMITIVES["browser-push-state"] = browserPushState;
+
+ // browser-replace-state
+ var browserReplaceState = function(urlOrState) { var rest = Array.prototype.slice.call(arguments, 1); return (isSxTruthy(isEmpty(rest)) ? hostCall(hostGet(domWindow(), "history"), "replaceState", NIL, "", urlOrState) : hostCall(hostGet(domWindow(), "history"), "replaceState", urlOrState, first(rest), nth(rest, 1))); };
+PRIMITIVES["browser-replace-state"] = browserReplaceState;
+
+ // browser-reload
+ var browserReload = function() { return hostCall(hostGet(domWindow(), "location"), "reload"); };
+PRIMITIVES["browser-reload"] = browserReload;
+
+ // browser-navigate
+ var browserNavigate = function(url) { return hostSet(hostGet(domWindow(), "location"), "href", url); };
+PRIMITIVES["browser-navigate"] = browserNavigate;
+
+ // local-storage-get
+ var localStorageGet = function(key) { return hostCall(hostGet(domWindow(), "localStorage"), "getItem", key); };
+PRIMITIVES["local-storage-get"] = localStorageGet;
+
+ // local-storage-set
+ var localStorageSet = function(key, val) { return hostCall(hostGet(domWindow(), "localStorage"), "setItem", key, val); };
+PRIMITIVES["local-storage-set"] = localStorageSet;
+
+ // local-storage-remove
+ var localStorageRemove = function(key) { return hostCall(hostGet(domWindow(), "localStorage"), "removeItem", key); };
+PRIMITIVES["local-storage-remove"] = localStorageRemove;
+
+ // set-timeout
+ var setTimeout_ = function(fnVal, ms) { return hostCall(domWindow(), "setTimeout", hostCallback(fnVal), ms); };
+PRIMITIVES["set-timeout"] = setTimeout_;
+
+ // set-interval
+ var setInterval_ = function(fnVal, ms) { return hostCall(domWindow(), "setInterval", hostCallback(fnVal), ms); };
+PRIMITIVES["set-interval"] = setInterval_;
+
+ // clear-timeout
+ var clearTimeout_ = function(id) { return hostCall(domWindow(), "clearTimeout", id); };
+PRIMITIVES["clear-timeout"] = clearTimeout_;
+
+ // clear-interval
+ var clearInterval_ = function(id) { return hostCall(domWindow(), "clearInterval", id); };
+PRIMITIVES["clear-interval"] = clearInterval_;
+
+ // request-animation-frame
+ var requestAnimationFrame_ = function(fnVal) { return hostCall(domWindow(), "requestAnimationFrame", hostCallback(fnVal)); };
+PRIMITIVES["request-animation-frame"] = requestAnimationFrame_;
+
+ // fetch-request
+ var fetchRequest = function(url, opts) { return hostCall(domWindow(), "fetch", url, opts); };
+PRIMITIVES["fetch-request"] = fetchRequest;
+
+ // new-abort-controller
+ var newAbortController = function() { return hostNew("AbortController"); };
+PRIMITIVES["new-abort-controller"] = newAbortController;
+
+ // controller-signal
+ var controllerSignal = function(controller) { return hostGet(controller, "signal"); };
+PRIMITIVES["controller-signal"] = controllerSignal;
+
+ // controller-abort
+ var controllerAbort = function(controller) { return hostCall(controller, "abort"); };
+PRIMITIVES["controller-abort"] = controllerAbort;
+
+ // promise-then
+ var promiseThen = function(p, onResolve, onReject) { return (function() {
+ var cbResolve = hostCallback(onResolve);
+ var cbReject = (isSxTruthy(onReject) ? hostCallback(onReject) : NIL);
+ return (isSxTruthy(cbReject) ? hostCall(hostCall(p, "then", cbResolve), "catch", cbReject) : hostCall(p, "then", cbResolve));
+})(); };
+PRIMITIVES["promise-then"] = promiseThen;
+
+ // promise-resolve
+ var promiseResolve = function(val) { return hostCall(hostGlobal("Promise"), "resolve", val); };
+PRIMITIVES["promise-resolve"] = promiseResolve;
+
+ // promise-delayed
+ var promiseDelayed = function(ms, val) { return hostNew("Promise", hostCallback(function(resolve) { return setTimeout_(function() { return hostCall(resolve, "call", NIL, val); }, ms); })); };
+PRIMITIVES["promise-delayed"] = promiseDelayed;
+
+ // browser-confirm
+ var browserConfirm = function(msg) { return hostCall(domWindow(), "confirm", msg); };
+PRIMITIVES["browser-confirm"] = browserConfirm;
+
+ // browser-prompt
+ var browserPrompt = function(msg, default_) { return hostCall(domWindow(), "prompt", msg, default_); };
+PRIMITIVES["browser-prompt"] = browserPrompt;
+
+ // browser-media-matches?
+ var browserMediaMatches = function(query) { return hostGet(hostCall(domWindow(), "matchMedia", query), "matches"); };
+PRIMITIVES["browser-media-matches?"] = browserMediaMatches;
+
+ // json-parse
+ var jsonParse = function(s) { return hostCall(hostGlobal("JSON"), "parse", s); };
+PRIMITIVES["json-parse"] = jsonParse;
+
+ // log-info
+ var logInfo = function(msg) { return hostCall(hostGlobal("console"), "log", (String("[sx] ") + String(msg))); };
+PRIMITIVES["log-info"] = logInfo;
+
+ // log-warn
+ var logWarn = function(msg) { return hostCall(hostGlobal("console"), "warn", (String("[sx] ") + String(msg))); };
+PRIMITIVES["log-warn"] = logWarn;
+
+ // console-log
+ var consoleLog = function() { var args = Array.prototype.slice.call(arguments, 0); return hostCall(hostGlobal("console"), "log", join(" ", cons("[sx]", map(str, args)))); };
+PRIMITIVES["console-log"] = consoleLog;
+
+ // now-ms
+ var nowMs = function() { return hostCall(hostGlobal("Date"), "now"); };
+PRIMITIVES["now-ms"] = nowMs;
+
+ // schedule-idle
+ var scheduleIdle = function(f) { return (function() {
+ var cb = hostCallback(function(_deadline) { return f(); });
+ return (isSxTruthy(hostGet(domWindow(), "requestIdleCallback")) ? hostCall(domWindow(), "requestIdleCallback", cb) : setTimeout_(cb, 0));
+})(); };
+PRIMITIVES["schedule-idle"] = scheduleIdle;
+
+ // set-cookie
+ var setCookie = function(name, value, days) { return (function() {
+ var d = sxOr(days, 365);
+ var expires = hostCall(hostNew("Date", (hostCall(hostGlobal("Date"), "now") + (d * 86400000))), "toUTCString");
+ return hostSet(domDocument(), "cookie", (String(name) + String("=") + String(hostCall(NIL, "encodeURIComponent", value)) + String(";expires=") + String(expires) + String(";path=/;SameSite=Lax")));
+})(); };
+PRIMITIVES["set-cookie"] = setCookie;
+
+ // get-cookie
+ var getCookie = function(name) { return (function() {
+ var cookies = hostGet(domDocument(), "cookie");
+ var match = hostCall(cookies, "match", hostNew("RegExp", (String("(?:^|;\\s*)") + String(name) + String("=([^;]*)"))));
+ return (isSxTruthy(match) ? hostCall(NIL, "decodeURIComponent", hostGet(match, 1)) : NIL);
+})(); };
+PRIMITIVES["get-cookie"] = getCookie;
+
+
// === Transpiled from adapter-dom ===
// SVG_NS
@@ -3159,15 +3684,23 @@ PRIMITIVES["SVG_NS"] = SVG_NS;
var MATH_NS = "http://www.w3.org/1998/Math/MathML";
PRIMITIVES["MATH_NS"] = MATH_NS;
+ // island-scope?
+ var islandScope_p = function() { return !isSxTruthy(isNil(scopePeek("sx-island-scope"))); };
+PRIMITIVES["island-scope?"] = islandScope_p;
+
+ // contains-deref?
+ var containsDeref_p = function(expr) { return (isSxTruthy(!isSxTruthy(isList(expr))) ? false : (isSxTruthy(isEmpty(expr)) ? false : (isSxTruthy((isSxTruthy((typeOf(first(expr)) == "symbol")) && (symbolName(first(expr)) == "deref"))) ? true : some(containsDeref_p, expr)))); };
+PRIMITIVES["contains-deref?"] = containsDeref_p;
+
// dom-on
- var domOn = function(el, name, handler) { return domListen(el, name, (isSxTruthy(isLambda(handler)) ? (isSxTruthy((0 == len(lambdaParams(handler)))) ? function() { trampoline(callLambda(handler, []));
+ var domOn = function(el, name, handler) { return domListen(el, name, (isSxTruthy(isLambda(handler)) ? (isSxTruthy((0 == len(lambdaParams(handler)))) ? function(_e) { trampoline(callLambda(handler, []));
return runPostRenderHooks(); } : function(e) { trampoline(callLambda(handler, [e]));
return runPostRenderHooks(); }) : handler)); };
PRIMITIVES["dom-on"] = domOn;
// render-to-dom
var renderToDom = function(expr, env, ns) { setRenderActiveB(true);
-return (function() { var _m = typeOf(expr); if (_m == "nil") return createFragment(); if (_m == "boolean") return createFragment(); if (_m == "raw-html") return domParseHtml(rawHtmlContent(expr)); if (_m == "string") return createTextNode(expr); if (_m == "number") return createTextNode((String(expr))); if (_m == "symbol") return renderToDom(trampoline(evalExpr(expr, env)), env, ns); if (_m == "keyword") return createTextNode(keywordName(expr)); if (_m == "dom-node") return expr; if (_m == "spread") return (sxEmit("element-attrs", spreadAttrs(expr)), expr); if (_m == "dict") return createFragment(); if (_m == "list") return (isSxTruthy(isEmpty(expr)) ? createFragment() : renderDomList(expr, env, ns)); return (isSxTruthy(isSignal(expr)) ? (isSxTruthy(sxContext("sx-island-scope", NIL)) ? reactiveText(expr) : createTextNode((String(deref(expr))))) : createTextNode((String(expr)))); })(); };
+return (function() { var _m = typeOf(expr); if (_m == "nil") return createFragment(); if (_m == "boolean") return createFragment(); if (_m == "raw-html") return domParseHtml(rawHtmlContent(expr)); if (_m == "string") return createTextNode(expr); if (_m == "number") return createTextNode((String(expr))); if (_m == "symbol") return renderToDom(trampoline(evalExpr(expr, env)), env, ns); if (_m == "keyword") return createTextNode(keywordName(expr)); if (_m == "dom-node") return expr; if (_m == "spread") return ((isSxTruthy(!isSxTruthy(islandScope_p())) ? scopeEmit("element-attrs", spreadAttrs(expr)) : NIL), expr); if (_m == "dict") return (isSxTruthy(dictHas(expr, "__host_handle")) ? expr : createFragment()); if (_m == "list") return (isSxTruthy(isEmpty(expr)) ? createFragment() : renderDomList(expr, env, ns)); return (isSxTruthy(isSignal(expr)) ? (isSxTruthy(islandScope_p()) ? reactiveText(expr) : createTextNode((String(deref(expr))))) : createTextNode((String(expr)))); })(); };
PRIMITIVES["render-to-dom"] = renderToDom;
// render-dom-list
@@ -3176,13 +3709,18 @@ PRIMITIVES["render-to-dom"] = renderToDom;
return (isSxTruthy((typeOf(head) == "symbol")) ? (function() {
var name = symbolName(head);
var args = rest(expr);
- return (isSxTruthy((name == "raw!")) ? renderDomRaw(args, env) : (isSxTruthy((name == "<>")) ? renderDomFragment(args, env, ns) : (isSxTruthy((name == "lake")) ? renderDomLake(args, env, ns) : (isSxTruthy((name == "marsh")) ? renderDomMarsh(args, env, ns) : (isSxTruthy(startsWith(name, "html:")) ? renderDomElement(slice(name, 5), args, env, ns) : (isSxTruthy(isRenderDomForm(name)) ? (isSxTruthy((isSxTruthy(contains(HTML_TAGS, name)) && sxOr((isSxTruthy((len(args) > 0)) && (typeOf(first(args)) == "keyword")), ns))) ? renderDomElement(name, args, env, ns) : dispatchRenderForm(name, expr, env, ns)) : (isSxTruthy((isSxTruthy(envHas(env, name)) && isMacro(envGet(env, name)))) ? renderToDom(expandMacro(envGet(env, name), args, env), env, ns) : (isSxTruthy(contains(HTML_TAGS, name)) ? renderDomElement(name, args, env, ns) : (isSxTruthy((isSxTruthy(startsWith(name, "~")) && isSxTruthy(envHas(env, name)) && isIsland(envGet(env, name)))) ? renderDomIsland(envGet(env, name), args, env, ns) : (isSxTruthy(startsWith(name, "~")) ? (function() {
+ return (isSxTruthy((name == "raw!")) ? renderDomRaw(args, env) : (isSxTruthy((name == "<>")) ? renderDomFragment(args, env, ns) : (isSxTruthy((name == "lake")) ? renderDomLake(args, env, ns) : (isSxTruthy((name == "marsh")) ? renderDomMarsh(args, env, ns) : (isSxTruthy(startsWith(name, "html:")) ? renderDomElement(slice(name, 5), args, env, ns) : (isSxTruthy(isRenderDomForm(name)) ? (isSxTruthy((isSxTruthy(contains(HTML_TAGS, name)) && sxOr((isSxTruthy((len(args) > 0)) && (typeOf(first(args)) == "keyword")), ns))) ? renderDomElement(name, args, env, ns) : dispatchRenderForm(name, expr, env, ns)) : (isSxTruthy((isSxTruthy(envHas(env, name)) && isMacro(envGet(env, name)))) ? renderToDom(expandMacro(envGet(env, name), args, env), env, ns) : (isSxTruthy(contains(HTML_TAGS, name)) ? renderDomElement(name, args, env, ns) : (isSxTruthy((isSxTruthy(startsWith(name, "~")) && isSxTruthy(envHas(env, name)) && isIsland(envGet(env, name)))) ? (isSxTruthy(scopePeek("sx-render-markers")) ? (function() {
+ var island = envGet(env, name);
+ var marker = domCreateElement("span", NIL);
+ domSetAttr(marker, "data-sx-island", componentName(island));
+ return marker;
+})() : renderDomIsland(envGet(env, name), args, env, ns)) : (isSxTruthy(startsWith(name, "~")) ? (function() {
var comp = envGet(env, name);
return (isSxTruthy(isComponent(comp)) ? renderDomComponent(comp, args, env, ns) : renderDomUnknownComponent(name));
-})() : (isSxTruthy((isSxTruthy((indexOf_(name, "-") > 0)) && isSxTruthy((len(args) > 0)) && (typeOf(first(args)) == "keyword"))) ? renderDomElement(name, args, env, ns) : (isSxTruthy(ns) ? renderDomElement(name, args, env, ns) : (isSxTruthy((isSxTruthy((name == "deref")) && sxContext("sx-island-scope", NIL))) ? (function() {
+})() : (isSxTruthy((isSxTruthy((indexOf_(name, "-") > 0)) && isSxTruthy((len(args) > 0)) && (typeOf(first(args)) == "keyword"))) ? renderDomElement(name, args, env, ns) : (isSxTruthy(ns) ? renderDomElement(name, args, env, ns) : (isSxTruthy((isSxTruthy((name == "deref")) && islandScope_p())) ? (function() {
var sigOrVal = trampoline(evalExpr(first(args), env));
return (isSxTruthy(isSignal(sigOrVal)) ? reactiveText(sigOrVal) : createTextNode((String(deref(sigOrVal)))));
-})() : renderToDom(trampoline(evalExpr(expr, env)), env, ns))))))))))))));
+})() : (isSxTruthy((isSxTruthy(islandScope_p()) && containsDeref_p(expr))) ? reactiveText(computed(function() { return trampoline(evalExpr(expr, env)); })) : renderToDom(trampoline(evalExpr(expr, env)), env, ns)))))))))))))));
})() : (isSxTruthy(sxOr(isLambda(head), (typeOf(head) == "list"))) ? renderToDom(trampoline(evalExpr(expr, env)), env, ns) : (function() {
var frag = createFragment();
{ var _c = expr; for (var _i = 0; _i < _c.length; _i++) { var x = _c[_i]; (function() {
@@ -3216,14 +3754,14 @@ PRIMITIVES["render-dom-list"] = renderDomList;
})() : (isSxTruthy((attrName == "key")) ? (function() {
var attrVal = trampoline(evalExpr(attrExpr, env));
return domSetAttr(el, "key", (String(attrVal)));
-})() : (isSxTruthy(sxContext("sx-island-scope", NIL)) ? reactiveAttr(el, attrName, function() { return trampoline(evalExpr(attrExpr, env)); }) : (function() {
+})() : (isSxTruthy(islandScope_p()) ? reactiveAttr(el, attrName, function() { return trampoline(evalExpr(attrExpr, env)); }) : (function() {
var attrVal = trampoline(evalExpr(attrExpr, env));
return (isSxTruthy(sxOr(isNil(attrVal), (attrVal == false))) ? NIL : (isSxTruthy(contains(BOOLEAN_ATTRS, attrName)) ? (isSxTruthy(attrVal) ? domSetAttr(el, attrName, "") : NIL) : (isSxTruthy((attrVal == true)) ? domSetAttr(el, attrName, "") : domSetAttr(el, attrName, (String(attrVal))))));
})())))));
return assoc(state, "skip", true, "i", (get(state, "i") + 1));
})() : ((isSxTruthy(!isSxTruthy(contains(VOID_ELEMENTS, tag))) ? (function() {
var child = renderToDom(arg, env, newNs);
- return (isSxTruthy((isSxTruthy(isSpread(child)) && sxContext("sx-island-scope", NIL))) ? reactiveSpread(el, function() { return renderToDom(arg, env, newNs); }) : (isSxTruthy(isSpread(child)) ? NIL : domAppend(el, child)));
+ return (isSxTruthy((isSxTruthy(isSpread(child)) && islandScope_p())) ? reactiveSpread(el, function() { return renderToDom(arg, env, newNs); }) : (isSxTruthy(isSpread(child)) ? NIL : domAppend(el, child)));
})() : NIL), assoc(state, "i", (get(state, "i") + 1)))));
})(); }, {["i"]: 0, ["skip"]: false}, args);
{ var _c = sxEmitted("element-attrs"); for (var _i = 0; _i < _c.length; _i++) { var spreadDict = _c[_i]; { var _c = keys(spreadDict); for (var _i = 0; _i < _c.length; _i++) { var key = _c[_i]; (function() {
@@ -3298,7 +3836,7 @@ PRIMITIVES["render-dom-raw"] = renderDomRaw;
PRIMITIVES["render-dom-unknown-component"] = renderDomUnknownComponent;
// RENDER_DOM_FORMS
- var RENDER_DOM_FORMS = ["if", "when", "cond", "case", "let", "let*", "begin", "do", "define", "defcomp", "defisland", "defmacro", "defstyle", "map", "map-indexed", "filter", "for-each", "portal", "error-boundary", "scope", "provide"];
+ var RENDER_DOM_FORMS = ["if", "when", "cond", "case", "let", "let*", "letrec", "begin", "do", "define", "defcomp", "defisland", "defmacro", "defstyle", "map", "map-indexed", "filter", "for-each", "portal", "error-boundary", "scope", "provide", "cyst"];
PRIMITIVES["RENDER_DOM_FORMS"] = RENDER_DOM_FORMS;
// render-dom-form?
@@ -3306,7 +3844,7 @@ PRIMITIVES["RENDER_DOM_FORMS"] = RENDER_DOM_FORMS;
PRIMITIVES["render-dom-form?"] = isRenderDomForm;
// dispatch-render-form
- var dispatchRenderForm = function(name, expr, env, ns) { return (isSxTruthy((name == "if")) ? (isSxTruthy(sxContext("sx-island-scope", NIL)) ? (function() {
+ var dispatchRenderForm = function(name, expr, env, ns) { return (isSxTruthy((name == "if")) ? (isSxTruthy(islandScope_p()) ? (function() {
var marker = createComment("r-if");
var currentNodes = [];
var initialResult = NIL;
@@ -3329,7 +3867,7 @@ PRIMITIVES["render-dom-form?"] = isRenderDomForm;
})() : (function() {
var condVal = trampoline(evalExpr(nth(expr, 1), env));
return (isSxTruthy(condVal) ? renderToDom(nth(expr, 2), env, ns) : (isSxTruthy((len(expr) > 3)) ? renderToDom(nth(expr, 3), env, ns) : createFragment()));
-})()) : (isSxTruthy((name == "when")) ? (isSxTruthy(sxContext("sx-island-scope", NIL)) ? (function() {
+})()) : (isSxTruthy((name == "when")) ? (isSxTruthy(islandScope_p()) ? (function() {
var marker = createComment("r-when");
var currentNodes = [];
var initialResult = NIL;
@@ -3356,7 +3894,7 @@ PRIMITIVES["render-dom-form?"] = isRenderDomForm;
var frag = createFragment();
{ var _c = range(2, len(expr)); for (var _i = 0; _i < _c.length; _i++) { var i = _c[_i]; domAppend(frag, renderToDom(nth(expr, i), env, ns)); } }
return frag;
-})())) : (isSxTruthy((name == "cond")) ? (isSxTruthy(sxContext("sx-island-scope", NIL)) ? (function() {
+})())) : (isSxTruthy((name == "cond")) ? (isSxTruthy(islandScope_p()) ? (function() {
var marker = createComment("r-cond");
var currentNodes = [];
var initialResult = NIL;
@@ -3393,6 +3931,22 @@ PRIMITIVES["render-dom-form?"] = isRenderDomForm;
})(); } }
return frag;
})());
+})() : (isSxTruthy((name == "letrec")) ? (function() {
+ var bindings = nth(expr, 1);
+ var body = slice(expr, 2);
+ var local = envExtend(env);
+ { var _c = bindings; for (var _i = 0; _i < _c.length; _i++) { var pair = _c[_i]; (function() {
+ var pname = (isSxTruthy((typeOf(first(pair)) == "symbol")) ? symbolName(first(pair)) : (String(first(pair))));
+ return envBind(local, pname, NIL);
+})(); } }
+ { var _c = bindings; for (var _i = 0; _i < _c.length; _i++) { var pair = _c[_i]; (function() {
+ var pname = (isSxTruthy((typeOf(first(pair)) == "symbol")) ? symbolName(first(pair)) : (String(first(pair))));
+ return envSet(local, pname, trampoline(evalExpr(nth(pair, 1), local)));
+})(); } }
+ if (isSxTruthy((len(body) > 1))) {
+ { var _c = init(body); for (var _i = 0; _i < _c.length; _i++) { var e = _c[_i]; trampoline(evalExpr(e, local)); } }
+}
+ return renderToDom(last(body), local, ns);
})() : (isSxTruthy(sxOr((name == "begin"), (name == "do"))) ? (isSxTruthy((len(expr) == 2)) ? renderToDom(nth(expr, 1), env, ns) : (function() {
var frag = createFragment();
{ var _c = range(1, len(expr)); for (var _i = 0; _i < _c.length; _i++) { var i = _c[_i]; (function() {
@@ -3402,7 +3956,7 @@ PRIMITIVES["render-dom-form?"] = isRenderDomForm;
return frag;
})()) : (isSxTruthy(isDefinitionForm(name)) ? (trampoline(evalExpr(expr, env)), createFragment()) : (isSxTruthy((name == "map")) ? (function() {
var collExpr = nth(expr, 2);
- return (isSxTruthy((isSxTruthy(sxContext("sx-island-scope", NIL)) && isSxTruthy((typeOf(collExpr) == "list")) && isSxTruthy((len(collExpr) > 1)) && isSxTruthy((typeOf(first(collExpr)) == "symbol")) && (symbolName(first(collExpr)) == "deref"))) ? (function() {
+ return (isSxTruthy((isSxTruthy(islandScope_p()) && isSxTruthy((typeOf(collExpr) == "list")) && isSxTruthy((len(collExpr) > 1)) && isSxTruthy((typeOf(first(collExpr)) == "symbol")) && (symbolName(first(collExpr)) == "deref"))) ? (function() {
var f = trampoline(evalExpr(nth(expr, 1), env));
var sig = trampoline(evalExpr(nth(collExpr, 1), env));
return (isSxTruthy(isSignal(sig)) ? reactiveList(f, sig, env, ns) : (function() {
@@ -3461,7 +4015,27 @@ PRIMITIVES["render-dom-form?"] = isRenderDomForm;
{ var _c = range(3, len(expr)); for (var _i = 0; _i < _c.length; _i++) { var i = _c[_i]; domAppend(frag, renderToDom(nth(expr, i), env, ns)); } }
scopePop(provName);
return frag;
-})() : renderToDom(trampoline(evalExpr(expr, env)), env, ns)))))))))))))))); };
+})() : (isSxTruthy((name == "cyst")) ? (function() {
+ var cystKey = (isSxTruthy((isSxTruthy((len(expr) > 2)) && isSxTruthy((typeOf(nth(expr, 1)) == "keyword")) && (keywordName(nth(expr, 1)) == "key"))) ? (String(trampoline(evalExpr(nth(expr, 2), env)))) : nextCystId());
+ var cached = get(_memoCache_, cystKey);
+ return (isSxTruthy((isSxTruthy(cached) && hostGet(cached, "isConnected"))) ? cached : (function() {
+ var container = domCreateElement("div", NIL);
+ var disposers = [];
+ var bodyExprs = (isSxTruthy((isSxTruthy((len(expr) > 2)) && isSxTruthy((typeOf(nth(expr, 1)) == "keyword")) && (keywordName(nth(expr, 1)) == "key"))) ? slice(expr, 3) : slice(expr, 1));
+ domSetAttr(container, "data-sx-cyst", cystKey);
+ return (function() {
+ var bodyDom = withIslandScope(function(d) { return append_b(disposers, d); }, function() { return (function() {
+ var frag = createFragment();
+ { var _c = bodyExprs; for (var _i = 0; _i < _c.length; _i++) { var child = _c[_i]; domAppend(frag, renderToDom(child, env, ns)); } }
+ return frag;
+})(); });
+ domAppend(container, bodyDom);
+ domSetData(container, "sx-disposers", disposers);
+ _memoCache_[cystKey] = container;
+ return container;
+})();
+})());
+})() : renderToDom(trampoline(evalExpr(expr, env)), env, ns)))))))))))))))))); };
PRIMITIVES["dispatch-render-form"] = dispatchRenderForm;
// render-lambda-dom
@@ -3946,7 +4520,7 @@ PRIMITIVES["revert-optimistic"] = revertOptimistic;
PRIMITIVES["find-oob-swaps"] = findOobSwaps;
// morph-node
- var morphNode = function(oldNode, newNode) { return (isSxTruthy(sxOr(domHasAttr(oldNode, "sx-preserve"), domHasAttr(oldNode, "sx-ignore"))) ? NIL : (isSxTruthy((isSxTruthy(domHasAttr(oldNode, "data-sx-island")) && isSxTruthy(isProcessed(oldNode, "island-hydrated")) && isSxTruthy(domHasAttr(newNode, "data-sx-island")) && (domGetAttr(oldNode, "data-sx-island") == domGetAttr(newNode, "data-sx-island")))) ? morphIslandChildren(oldNode, newNode) : (isSxTruthy(sxOr(!isSxTruthy((domNodeType(oldNode) == domNodeType(newNode))), !isSxTruthy((domNodeName(oldNode) == domNodeName(newNode))))) ? domReplaceChild(domParent(oldNode), domClone(newNode), oldNode) : (isSxTruthy(sxOr((domNodeType(oldNode) == 3), (domNodeType(oldNode) == 8))) ? (isSxTruthy(!isSxTruthy((domTextContent(oldNode) == domTextContent(newNode)))) ? domSetTextContent(oldNode, domTextContent(newNode)) : NIL) : (isSxTruthy((domNodeType(oldNode) == 1)) ? ((isSxTruthy((isSxTruthy(domHasAttr(oldNode, "data-sx-island")) && isSxTruthy(domHasAttr(newNode, "data-sx-island")) && !isSxTruthy((domGetAttr(oldNode, "data-sx-island") == domGetAttr(newNode, "data-sx-island"))))) ? (disposeIslandsIn(oldNode), clearProcessed(oldNode, "island-hydrated")) : NIL), syncAttrs(oldNode, newNode), (isSxTruthy(!isSxTruthy((isSxTruthy(domIsActiveElement(oldNode)) && domIsInputElement(oldNode)))) ? morphChildren(oldNode, newNode) : NIL)) : NIL))))); };
+ var morphNode = function(oldNode, newNode) { return (isSxTruthy(sxOr(domHasAttr(oldNode, "sx-preserve"), domHasAttr(oldNode, "sx-ignore"))) ? NIL : (isSxTruthy((isSxTruthy(domHasAttr(oldNode, "data-sx-island")) && isSxTruthy(isProcessed(oldNode, "island-hydrated")) && isSxTruthy(domHasAttr(newNode, "data-sx-island")) && (domGetAttr(oldNode, "data-sx-island") == domGetAttr(newNode, "data-sx-island")))) ? morphIslandChildren(oldNode, newNode) : (isSxTruthy(sxOr(!isSxTruthy((domNodeType(oldNode) == domNodeType(newNode))), !isSxTruthy((domNodeName(oldNode) == domNodeName(newNode))))) ? domReplaceChild(domParent(oldNode), domClone(newNode), oldNode) : (isSxTruthy(sxOr((domNodeType(oldNode) == 3), (domNodeType(oldNode) == 8))) ? (isSxTruthy(!isSxTruthy((domTextContent(oldNode) == domTextContent(newNode)))) ? domSetTextContent(oldNode, domTextContent(newNode)) : NIL) : (isSxTruthy((domNodeType(oldNode) == 1)) ? ((isSxTruthy((isSxTruthy(domHasAttr(oldNode, "data-sx-island")) && isSxTruthy(domHasAttr(newNode, "data-sx-island")) && !isSxTruthy((domGetAttr(oldNode, "data-sx-island") == domGetAttr(newNode, "data-sx-island"))))) ? (disposeIsland(oldNode), disposeIslandsIn(oldNode)) : NIL), syncAttrs(oldNode, newNode), (isSxTruthy(!isSxTruthy((isSxTruthy(domIsActiveElement(oldNode)) && domIsInputElement(oldNode)))) ? morphChildren(oldNode, newNode) : NIL)) : NIL))))); };
PRIMITIVES["morph-node"] = morphNode;
// sync-attrs
@@ -4073,14 +4647,15 @@ PRIMITIVES["process-signal-updates"] = processSignalUpdates;
return morphChildren(target, wrapper);
})()); if (_m == "outerHTML") return (function() {
var parent = domParent(target);
+ var newEl = domClone(newNodes);
(isSxTruthy(domIsFragment(newNodes)) ? (function() {
var fc = domFirstChild(newNodes);
- return (isSxTruthy(fc) ? (morphNode(target, fc), (function() {
+ return (isSxTruthy(fc) ? ((newEl = domClone(fc)), domReplaceChild(parent, newEl, target), (function() {
var sib = domNextSibling(fc);
- return insertRemainingSiblings(parent, target, sib);
+ return insertRemainingSiblings(parent, newEl, sib);
})()) : domRemoveChild(parent, target));
-})() : morphNode(target, newNodes));
- return parent;
+})() : domReplaceChild(parent, newEl, target));
+ return newEl;
})(); if (_m == "afterend") return domInsertAfter(target, newNodes); if (_m == "beforeend") return domAppend(target, newNodes); if (_m == "afterbegin") return domPrepend(target, newNodes); if (_m == "beforebegin") return domInsertBefore(domParent(target), newNodes, target); if (_m == "delete") return domRemoveChild(domParent(target), target); if (_m == "none") return NIL; return (isSxTruthy(domIsFragment(newNodes)) ? morphChildren(target, newNodes) : (function() {
var wrapper = domCreateElement("div", NIL);
domAppend(wrapper, newNodes);
@@ -4297,8 +4872,10 @@ return processElements(t); });
var selectSel = domGetAttr(el, "sx-select");
var content = (isSxTruthy(selectSel) ? selectFromContainer(container, selectSel) : childrenToFragment(container));
disposeIslandsIn(target);
- return withTransition(useTransition, function() { swapDomNodes(target, content, swapStyle);
-return postSwap(target); });
+ return withTransition(useTransition, function() { return (function() {
+ var swapResult = swapDomNodes(target, content, swapStyle);
+ return postSwap(sxOr(swapResult, target));
+})(); });
})();
})() : NIL);
})();
@@ -5034,7 +5611,14 @@ PRIMITIVES["sx-hydrate-islands"] = sxHydrateIslands;
var local = envMerge(componentClosure(comp), env);
{ var _c = componentParams(comp); for (var _i = 0; _i < _c.length; _i++) { var p = _c[_i]; envBind(local, p, (isSxTruthy(dictHas(kwargs, p)) ? dictGet(kwargs, p) : NIL)); } }
return (function() {
- var bodyDom = withIslandScope(function(disposable) { return append_b(disposers, disposable); }, function() { return renderToDom(componentBody(comp), local, NIL); });
+ var bodyDom = cekTry(function() { return withIslandScope(function(disposable) { return append_b(disposers, disposable); }, function() { return renderToDom(componentBody(comp), local, NIL); }); }, function(err) { logWarn((String("hydrate-island FAILED: ") + String(compName) + String(" — ") + String(err)));
+return (function() {
+ var errorEl = domCreateElement("div", NIL);
+ domSetAttr(errorEl, "class", "sx-island-error");
+ domSetAttr(errorEl, "style", "padding:8px;margin:4px 0;border:1px solid #ef4444;border-radius:4px;background:#fef2f2;color:#b91c1c;font-family:monospace;font-size:12px;white-space:pre-wrap");
+ domSetTextContent(errorEl, (String("Island error: ") + String(compName) + String("\n") + String(err)));
+ return errorEl;
+})(); });
domSetTextContent(el, "");
domAppend(el, bodyDom);
domSetData(el, "sx-disposers", disposers);
@@ -5093,13 +5677,13 @@ PRIMITIVES["register-post-render-hook"] = registerPostRenderHook;
PRIMITIVES["run-pre-render-hooks"] = runPreRenderHooks;
// run-post-render-hooks
- var runPostRenderHooks = function() { logInfo("run-post-render-hooks:", len(_postRenderHooks_), "hooks");
-return forEach(function(hook) { logInfo(" hook type:", typeOf(hook), "callable:", isCallable(hook), "lambda:", isLambda(hook));
+ var runPostRenderHooks = function() { logInfo((String("run-post-render-hooks: ") + String(len(_postRenderHooks_)) + String(" hooks")));
+return forEach(function(hook) { logInfo((String(" hook type: ") + String(typeOf(hook)) + String(" callable: ") + String(isCallable(hook)) + String(" lambda: ") + String(isLambda(hook))));
return cekCall(hook, NIL); }, _postRenderHooks_); };
PRIMITIVES["run-post-render-hooks"] = runPostRenderHooks;
// boot-init
- var bootInit = function() { return (logInfo((String("sx-browser ") + String(SX_VERSION))), initCssTracking(), processPageScripts(), processSxScripts(NIL), sxHydrateElements(NIL), sxHydrateIslands(NIL), runPostRenderHooks(), processElements(NIL)); };
+ var bootInit = function() { return (logInfo((String("sx-browser ") + String(SX_VERSION))), initCssTracking(), processPageScripts(), processSxScripts(NIL), sxHydrateElements(NIL), sxHydrateIslands(NIL), runPostRenderHooks(), processElements(NIL), domListen(domWindow(), "popstate", function(e) { return handlePopstate(0); })); };
PRIMITIVES["boot-init"] = bootInit;
@@ -5811,7 +6395,7 @@ PRIMITIVES["reset!"] = reset_b;
// swap!
var swap_b = function(s, f) { var args = Array.prototype.slice.call(arguments, 2); return (isSxTruthy(isSignal(s)) ? (function() {
var old = signalValue(s);
- var newVal = apply(f, cons(old, args));
+ var newVal = trampoline(apply(f, cons(old, args)));
return (isSxTruthy(!isSxTruthy(isIdentical(old, newVal))) ? (signalSetValue(s, newVal), notifySubscribers(s)) : NIL);
})() : NIL); };
PRIMITIVES["swap!"] = swap_b;
@@ -5926,11 +6510,14 @@ PRIMITIVES["with-island-scope"] = withIslandScope;
// register-in-scope
var registerInScope = function(disposable) { return (function() {
- var collector = sxContext("sx-island-scope", NIL);
+ var collector = scopePeek("sx-island-scope");
return (isSxTruthy(collector) ? cekCall(collector, [disposable]) : NIL);
})(); };
PRIMITIVES["register-in-scope"] = registerInScope;
+
+ // === Transpiled from signals-web (stores, events, resources) ===
+
// with-marsh-scope
var withMarshScope = function(marshEl, bodyFn) { return (function() {
var disposers = [];
@@ -6088,6 +6675,17 @@ PRIMITIVES["resource"] = resource;
return NIL;
};
if (typeof sxParse === "function") PRIMITIVES["sx-parse"] = sxParse;
+ PRIMITIVES["cek-try"] = function(thunkFn, handlerFn) {
+ try {
+ var result = _wrapSxFn(thunkFn)();
+ if (!handlerFn || handlerFn === NIL) return [SYM("ok"), result];
+ return result;
+ } catch (e) {
+ var msg = (e && e.message) ? e.message : String(e);
+ if (handlerFn && handlerFn !== NIL) return _wrapSxFn(handlerFn)(msg);
+ return [SYM("error"), msg];
+ }
+ };
// Platform deps functions (native JS, not transpiled — need explicit registration)
PRIMITIVES["component-deps"] = componentDeps;
@@ -6961,6 +7559,17 @@ PRIMITIVES["resource"] = resource;
: catchFn;
try { return t(); } catch (e) { return c(e); }
}
+ function cekTry(thunkFn, handlerFn) {
+ try {
+ var result = _wrapSxFn(thunkFn)();
+ if (!handlerFn || handlerFn === NIL) return [SYM("ok"), result];
+ return result;
+ } catch (e) {
+ var msg = (e && e.message) ? e.message : String(e);
+ if (handlerFn && handlerFn !== NIL) return _wrapSxFn(handlerFn)(msg);
+ return [SYM("error"), msg];
+ }
+ }
function errorMessage(e) {
return e && e.message ? e.message : String(e);
}
@@ -7371,7 +7980,7 @@ PRIMITIVES["resource"] = resource;
}
function getRenderEnv(extraEnv) {
- return extraEnv ? merge(componentEnv, extraEnv) : componentEnv;
+ return extraEnv ? merge(componentEnv, PRIMITIVES, extraEnv) : merge(componentEnv, PRIMITIVES);
}
function mergeEnvs(base, newEnv) {
diff --git a/sx/sx/nav-applications.sx b/sx/sx/nav-applications.sx
new file mode 100644
index 00000000..55788e0c
--- /dev/null
+++ b/sx/sx/nav-applications.sx
@@ -0,0 +1,8 @@
+;; Navigation items for the Applications section
+
+(define protocols-nav-items (list (dict :label "Wire Format" :href "/sx/(applications.(protocol.wire-format))") (dict :label "Fragments" :href "/sx/(applications.(protocol.fragments))") (dict :label "Resolver I/O" :href "/sx/(applications.(protocol.resolver-io))") (dict :label "Internal Services" :href "/sx/(applications.(protocol.internal-services))") (dict :label "ActivityPub" :href "/sx/(applications.(protocol.activitypub))") (dict :label "Future" :href "/sx/(applications.(protocol.future))")))
+
+(define cssx-nav-items (list (dict :label "Overview" :href "/sx/(applications.(cssx))") (dict :label "Patterns" :href "/sx/(applications.(cssx.patterns))") (dict :label "Delivery" :href "/sx/(applications.(cssx.delivery))") (dict :label "Async CSS" :href "/sx/(applications.(cssx.async))") (dict :label "Live Styles" :href "/sx/(applications.(cssx.live))") (dict :label "Comparisons" :href "/sx/(applications.(cssx.comparison))") (dict :label "Philosophy" :href "/sx/(applications.(cssx.philosophy))")))
+
+(define reactive-runtime-nav-items (list (dict :label "Ref" :href "/sx/(applications.(reactive-runtime.ref))") (dict :label "Foreign FFI" :href "/sx/(applications.(reactive-runtime.foreign))") (dict :label "State Machines" :href "/sx/(applications.(reactive-runtime.machine))") (dict :label "Commands" :href "/sx/(applications.(reactive-runtime.commands))") (dict :label "Render Loop" :href "/sx/(applications.(reactive-runtime.loop))") (dict :label "Keyed Lists" :href "/sx/(applications.(reactive-runtime.keyed-lists))") (dict :label "App Shell" :href "/sx/(applications.(reactive-runtime.app-shell))")))
+
diff --git a/sx/sx/nav-etc.sx b/sx/sx/nav-etc.sx
new file mode 100644
index 00000000..69a31190
--- /dev/null
+++ b/sx/sx/nav-etc.sx
@@ -0,0 +1,8 @@
+;; Navigation items for the Etc section (essays, philosophy, plans)
+
+(define essays-nav-items (list (dict :label "Why S-Expressions" :href "/sx/(etc.(essay.why-sexps))" :summary "Why SX uses s-expressions instead of HTML templates, JSX, or any other syntax.") (dict :label "The htmx/React Hybrid" :href "/sx/(etc.(essay.htmx-react-hybrid))" :summary "How SX combines the server-driven simplicity of htmx with the component model of React.") (dict :label "On-Demand CSS" :href "/sx/(etc.(essay.on-demand-css))" :summary "How SX delivers only the CSS each page needs — server scans rendered classes, sends the delta.") (dict :label "Client Reactivity" :href "/sx/(etc.(essay.client-reactivity))" :summary "Reactive UI updates without a virtual DOM, diffing library, or build step.") (dict :label "SX Native" :href "/sx/(etc.(essay.sx-native))" :summary "Extending SX beyond the browser — native desktop and mobile rendering from the same source.") (dict :label "Tail-Call Optimization" :href "/sx/(etc.(essay.tail-call-optimization))" :summary "How SX implements proper tail calls via trampolining in a language that doesn't have them.") (dict :label "Continuations" :href "/sx/(etc.(essay.continuations))" :summary "First-class continuations in a tree-walking evaluator — theory and implementation.") (dict :label "The Reflexive Web" :href "/sx/(etc.(essay.reflexive-web))" :summary "A web where pages can inspect, modify, and extend their own rendering pipeline.") (dict :label "Server Architecture" :href "/sx/(etc.(essay.server-architecture))" :summary "How SX enforces the boundary between host and embedded language, and what it looks like across targets.") (dict :label "Separate your Own Concerns" :href "/sx/(etc.(essay.separation-of-concerns))" :summary "The web's HTML/CSS/JS split separates the framework's concerns, not your application's. Real separation is domain-specific.") (dict :label "SX and AI" :href "/sx/(etc.(essay.sx-and-ai))" :summary "Why s-expressions are the most AI-friendly representation for web interfaces.") (dict :label "There Is No Alternative" :href "/sx/(etc.(essay.no-alternative))" :summary "Every attempt to escape s-expressions leads back to s-expressions. This is not an accident.") (dict :label "sx sucks" :href "/sx/(etc.(essay.sx-sucks))" :summary "An honest accounting of everything wrong with SX and why you probably shouldn't use it.") (dict :label "Tools for Fools" :href "/sx/(etc.(essay.zero-tooling))" :summary "SX was built without a code editor. No IDE, no build tools, no linters, no bundlers. What zero-tooling web development looks like.") (dict :label "React is Hypermedia" :href "/sx/(etc.(essay.react-is-hypermedia))" :summary "A React Island is a hypermedia control. Its behavior is specified in SX.") (dict :label "The Hegelian Synthesis" :href "/sx/(etc.(essay.hegelian-synthesis))" :summary "On the dialectical resolution of the hypertext/reactive contradiction. Thesis: the server renders. Antithesis: the client reacts. Synthesis: the island in the lake.") (dict :label "The Art Chain" :href "/sx/(etc.(essay.the-art-chain))" :summary "On making, self-making, and the chain of artifacts that produces itself. Ars, techne, content addressing, and why the spec is the art.") (dict :label "The True Hypermedium" :href "/sx/(etc.(essay.self-defining-medium))" :summary "The true hypermedium must define itself with itself. On ontological uniformity, the metacircular web, and why address and content should be the same stuff.") (dict :label "Hypermedia in the Age of AI" :href "/sx/(etc.(essay.hypermedia-age-of-ai))" :summary "JSON hypermedia, MCP, and why s-expressions are the format both humans and AI agents actually need.")))
+
+(define philosophy-nav-items (list (dict :label "The SX Manifesto" :href "/sx/(etc.(philosophy.sx-manifesto))" :summary "The design principles behind SX: simplicity, self-hosting, and s-expressions all the way down.") (dict :label "Strange Loops" :href "/sx/(etc.(philosophy.godel-escher-bach))" :summary "Self-reference, and the tangled hierarchy of a language that defines itself.") (dict :label "SX and Wittgenstein" :href "/sx/(etc.(philosophy.wittgenstein))" :summary "The limits of my language are the limits of my world — Wittgenstein's philosophy and what it means for SX.") (dict :label "SX and Dennett" :href "/sx/(etc.(philosophy.dennett))" :summary "Real patterns, intentional stance, and multiple drafts — Dennett's philosophy of mind as a framework for understanding SX.") (dict :label "S-Existentialism" :href "/sx/(etc.(philosophy.existentialism))" :summary "Existence precedes essence — Sartre, Camus, and the absurd freedom of writing a Lisp for the web.") (dict :label "Platonic SX" :href "/sx/(etc.(philosophy.platonic-sx))" :summary "The allegory of the cave, the theory of Forms, and why a self-defining hypermedium participates in something Plato would have recognized.")))
+
+(define plans-nav-items (list (dict :label "Status" :href "/sx/(etc.(plan.status))" :summary "Audit of all plans — what's done, what's in progress, and what remains.") (dict :label "Reader Macros" :href "/sx/(etc.(plan.reader-macros))" :summary "Extensible parse-time transformations via # dispatch — datum comments, raw strings, and quote shorthand.") (dict :label "Reader Macro Demo" :href "/sx/(etc.(plan.reader-macro-demo))" :summary "Live demo: #z3 translates SX spec declarations to SMT-LIB verification conditions.") (dict :label "Theorem Prover" :href "/sx/(etc.(plan.theorem-prover))" :summary "prove.sx — constraint solver and property prover for SX primitives, written in SX.") (dict :label "Self-Hosting Bootstrapper" :href "/sx/(etc.(plan.self-hosting-bootstrapper))" :summary "py.sx — an SX-to-Python translator written in SX. Complete: G0 == G1, 128/128 defines match.") (dict :label "JS Bootstrapper" :href "/sx/(etc.(plan.js-bootstrapper))" :summary "js.sx — SX-to-JavaScript translator + ahead-of-time component compiler. Zero-runtime static sites.") (dict :label "SX-Activity" :href "/sx/(etc.(plan.sx-activity))" :summary "A new web built on SX — executable content, shared components, parsers, and logic on IPFS, provenance on Bitcoin, all running within your own security context.") (dict :label "Predictive Prefetching" :href "/sx/(etc.(plan.predictive-prefetch))" :summary "Prefetch missing component definitions before the user clicks — hover a link, fetch its deps, navigate client-side.") (dict :label "Content-Addressed Components" :href "/sx/(etc.(plan.content-addressed-components))" :summary "Components identified by CID, stored on IPFS, fetched from anywhere. Canonical serialization, content verification, federated sharing.") (dict :label "Environment Images" :href "/sx/(etc.(plan.environment-images))" :summary "Serialize evaluated environments as content-addressed images. Spec CID → image CID → every endpoint is fully executable and verifiable.") (dict :label "Runtime Slicing" :href "/sx/(etc.(plan.runtime-slicing))" :summary "Tier the client runtime by need: L0 hypermedia (~5KB), L1 DOM ops (~8KB), L2 islands (~15KB), L3 full eval (~44KB). Sliced by slice.sx, translated by js.sx.") (dict :label "Typed SX" :href "/sx/(etc.(plan.typed-sx))" :summary "Gradual type system with static effect checking. Optional type annotations, deftype (aliases, unions, records), and effect declarations — checked at registration time, zero runtime cost. types.sx — specced, bootstrapped, catches composition and boundary errors.") (dict :label "Nav Redesign" :href "/sx/(etc.(plan.nav-redesign))" :summary "Replace menu bars with vertical breadcrumb navigation. Logo → section → page, arrows for siblings, children below. No dropdowns, no hamburger, infinite depth.") (dict :label "Fragment Protocol" :href "/sx/(etc.(plan.fragment-protocol))" :summary "Structured sexp request/response for cross-service component transfer.") (dict :label "Glue Decoupling" :href "/sx/(etc.(plan.glue-decoupling))" :summary "Eliminate all cross-app model imports via glue service layer.") (dict :label "Social Sharing" :href "/sx/(etc.(plan.social-sharing))" :summary "OAuth-based sharing to Facebook, Instagram, Threads, Twitter/X, LinkedIn, and Mastodon.") (dict :label "SX CI Pipeline" :href "/sx/(etc.(plan.sx-ci))" :summary "Build, test, and deploy in s-expressions — CI pipelines as SX components.") (dict :label "Live Streaming" :href "/sx/(etc.(plan.live-streaming))" :summary "SSE and WebSocket transports for re-resolving suspense slots after initial page load — live data, real-time collaboration.") (dict :label "sx-web Platform" :href "/sx/(etc.(plan.sx-web-platform))" :summary "sx-web.org as online development platform — embedded Claude Code, IPFS storage, sx-activity publishing, sx-ci testing. Author, stage, test, deploy from the browser.") (dict :label "sx-forge" :href "/sx/(etc.(plan.sx-forge))" :summary "Git forge in SX — repositories, issues, pull requests, CI, permissions, and federation. Configuration as macros, diffs as components.") (dict :label "sx-swarm" :href "/sx/(etc.(plan.sx-swarm))" :summary "Container orchestration in SX — service definitions, environment macros, deploy pipelines. Replace YAML with a real language.") (dict :label "sx-proxy" :href "/sx/(etc.(plan.sx-proxy))" :summary "Reverse proxy in SX — routes, TLS, middleware chains, load balancing. Macros generate config from the same service definitions as the orchestrator.") (dict :label "Async Eval Convergence" :href "/sx/(etc.(plan.async-eval-convergence))" :summary "Eliminate hand-written evaluators — bootstrap async_eval.py from the spec via an async adapter layer. One spec, one truth, zero divergence.") (dict :label "WASM Bytecode VM" :href "/sx/(etc.(plan.wasm-bytecode-vm))" :summary "Compile SX to bytecode, run in a Rust/WASM VM. Compact wire format, no parse overhead, near-native speed, DOM via JS bindings.") (dict :label "Generative SX" :href "/sx/(etc.(plan.generative-sx))" :summary "Programs that write themselves as they run — self-compiling specs, runtime self-extension, generative testing, seed networks.") (dict :label "Art DAG on SX" :href "/sx/(etc.(plan.art-dag-sx))" :summary "SX endpoints as portals into media processing environments — recipes as programs, split execution across GPU/cache/live boundaries, streaming AV output.") (dict :label "Spec Explorer" :href "/sx/(etc.(plan.spec-explorer))" :summary "The fifth ring — SX exploring itself. Per-function cards showing source, Python/JS/Z3 translations, platform dependencies, tests, proofs, and usage examples.") (dict :label "SX Protocol" :href "/sx/(etc.(plan.sx-protocol))" :summary "S-expressions as a universal protocol for networked hypermedia — replacing URLs, HTTP verbs, query languages, and rendering with one evaluable format.") (dict :label "Scoped Effects" :href "/sx/(etc.(plan.scoped-effects))" :summary "Algebraic effects as the unified foundation — spreads, islands, lakes, signals, and context are all instances of one primitive: a named scope with downward value, upward accumulation, and a propagation mode.") (dict :label "Foundations" :href "/sx/(etc.(plan.foundations))" :summary "The computational floor — from scoped effects through algebraic effects and delimited continuations to the CEK machine. Why three registers are irreducible, and the three-axis model (depth, topology, linearity).") (dict :label "Deref as Shift" :href "/sx/(etc.(plan.cek-reactive))" :summary "Phase B: replace explicit effect wrapping with implicit continuation capture. Deref inside reactive-reset performs shift, capturing the rest of the expression as the subscriber.") (dict :label "Rust/WASM Host" :href "/sx/(etc.(plan.rust-wasm-host))" :summary "Bootstrap the SX spec to Rust, compile to WASM, replace sx-browser.js. Shared platform layer for DOM, phased rollout from parse to full parity.") (dict :label "Isolated Evaluator" :href "/sx/(etc.(plan.isolated-evaluator))" :summary "Core/application split, shared sx-platform.js, isolated JS evaluator, Rust WASM via handle table. Only language-defining spec gets bootstrapped; everything else is runtime-evaluated .sx.") (dict :label "Mother Language" :href "/sx/(etc.(plan.mother-language))" :summary "SX as its own compiler. OCaml as substrate (closest to CEK), Koka as alternative (compile-time linearity), ultimately self-hosting. One language, every target.") (dict :label "sx-web" :href "/sx/(etc.(plan.sx-web))" :summary "Federated component web. Browser nodes via WebTransport, server nodes via IPFS, content-addressed SX verified by CID. In-browser editing, testing, publishing. AI composition over the federated graph.")))
+
diff --git a/sx/sx/nav-geography.sx b/sx/sx/nav-geography.sx
new file mode 100644
index 00000000..3d22c1c2
--- /dev/null
+++ b/sx/sx/nav-geography.sx
@@ -0,0 +1,16 @@
+;; Navigation items for the Geography section
+
+(define reference-nav-items (list (dict :label "Attributes" :href "/sx/(geography.(hypermedia.(reference.attributes)))") (dict :label "Headers" :href "/sx/(geography.(hypermedia.(reference.headers)))") (dict :label "Events" :href "/sx/(geography.(hypermedia.(reference.events)))") (dict :label "JS API" :href "/sx/(geography.(hypermedia.(reference.js-api)))")))
+
+(define examples-nav-items (list (dict :label "Click to Load" :href "/sx/(geography.(hypermedia.(example.click-to-load)))") (dict :label "Form Submission" :href "/sx/(geography.(hypermedia.(example.form-submission)))") (dict :label "Polling" :href "/sx/(geography.(hypermedia.(example.polling)))") (dict :label "Delete Row" :href "/sx/(geography.(hypermedia.(example.delete-row)))") (dict :label "Inline Edit" :href "/sx/(geography.(hypermedia.(example.inline-edit)))") (dict :label "OOB Swaps" :href "/sx/(geography.(hypermedia.(example.oob-swaps)))") (dict :label "Lazy Loading" :href "/sx/(geography.(hypermedia.(example.lazy-loading)))") (dict :label "Infinite Scroll" :href "/sx/(geography.(hypermedia.(example.infinite-scroll)))") (dict :label "Progress Bar" :href "/sx/(geography.(hypermedia.(example.progress-bar)))") (dict :label "Active Search" :href "/sx/(geography.(hypermedia.(example.active-search)))") (dict :label "Inline Validation" :href "/sx/(geography.(hypermedia.(example.inline-validation)))") (dict :label "Value Select" :href "/sx/(geography.(hypermedia.(example.value-select)))") (dict :label "Reset on Submit" :href "/sx/(geography.(hypermedia.(example.reset-on-submit)))") (dict :label "Edit Row" :href "/sx/(geography.(hypermedia.(example.edit-row)))") (dict :label "Bulk Update" :href "/sx/(geography.(hypermedia.(example.bulk-update)))") (dict :label "Swap Positions" :href "/sx/(geography.(hypermedia.(example.swap-positions)))") (dict :label "Select Filter" :href "/sx/(geography.(hypermedia.(example.select-filter)))") (dict :label "Tabs" :href "/sx/(geography.(hypermedia.(example.tabs)))") (dict :label "Animations" :href "/sx/(geography.(hypermedia.(example.animations)))") (dict :label "Dialogs" :href "/sx/(geography.(hypermedia.(example.dialogs)))") (dict :label "Keyboard Shortcuts" :href "/sx/(geography.(hypermedia.(example.keyboard-shortcuts)))") (dict :label "PUT / PATCH" :href "/sx/(geography.(hypermedia.(example.put-patch)))") (dict :label "JSON Encoding" :href "/sx/(geography.(hypermedia.(example.json-encoding)))") (dict :label "Vals & Headers" :href "/sx/(geography.(hypermedia.(example.vals-and-headers)))") (dict :label "Loading States" :href "/sx/(geography.(hypermedia.(example.loading-states)))") (dict :label "Request Abort" :href "/sx/(geography.(hypermedia.(example.sync-replace)))") (dict :label "Retry" :href "/sx/(geography.(hypermedia.(example.retry)))")))
+
+(define isomorphism-nav-items (list (dict :label "Roadmap" :href "/sx/(geography.(isomorphism))") (dict :label "Bundle Analyzer" :href "/sx/(geography.(isomorphism.bundle-analyzer))") (dict :label "Routing Analyzer" :href "/sx/(geography.(isomorphism.routing-analyzer))") (dict :label "Data Test" :href "/sx/(geography.(isomorphism.data-test))") (dict :label "Async IO" :href "/sx/(geography.(isomorphism.async-io))") (dict :label "Streaming" :href "/sx/(geography.(isomorphism.streaming))") (dict :label "Affinity" :href "/sx/(geography.(isomorphism.affinity))") (dict :label "Optimistic" :href "/sx/(geography.(isomorphism.optimistic))") (dict :label "Offline" :href "/sx/(geography.(isomorphism.offline))")))
+
+(define cek-nav-items (list (dict :label "Overview" :href "/sx/(geography.(cek))" :summary "The CEK machine — explicit evaluator with Control, Environment, Kontinuation. Three registers, pure step function.") (dict :label "Demo" :href "/sx/(geography.(cek.demo))" :summary "Live islands evaluated by the CEK machine. Counter, computed chains, reactive attributes — all through explicit continuation frames.") (dict :label "Freeze / Thaw" :href "/sx/(geography.(cek.freeze))" :summary "Serialize a CEK state to s-expressions. Ship it, store it, content-address it. Thaw and resume anywhere.") (dict :label "Content Addressing" :href "/sx/(geography.(cek.content))" :summary "Hash frozen state to a CID. Same state = same address. Store, share, verify, reproduce.")))
+
+(define reactive-examples-nav-items (list {:href "/sx/(geography.(reactive.(examples.counter)))" :label "Counter"} {:href "/sx/(geography.(reactive.(examples.temperature)))" :label "Temperature"} {:href "/sx/(geography.(reactive.(examples.stopwatch)))" :label "Stopwatch"} {:href "/sx/(geography.(reactive.(examples.imperative)))" :label "Imperative"} {:href "/sx/(geography.(reactive.(examples.reactive-list)))" :label "Reactive List"} {:href "/sx/(geography.(reactive.(examples.input-binding)))" :label "Input Binding"} {:href "/sx/(geography.(reactive.(examples.portal)))" :label "Portals"} {:href "/sx/(geography.(reactive.(examples.error-boundary)))" :label "Error Boundary"} {:href "/sx/(geography.(reactive.(examples.refs)))" :label "Refs"} {:href "/sx/(geography.(reactive.(examples.dynamic-class)))" :label "Dynamic Class"} {:href "/sx/(geography.(reactive.(examples.resource)))" :label "Resource"} {:href "/sx/(geography.(reactive.(examples.transition)))" :label "Transitions"} {:href "/sx/(geography.(reactive.(examples.stores)))" :label "Stores"} {:href "/sx/(geography.(reactive.(examples.event-bridge-demo)))" :label "Event Bridge"} {:href "/sx/(geography.(reactive.(examples.defisland)))" :label "defisland"} {:href "/sx/(geography.(reactive.(examples.tests)))" :label "Tests"} {:href "/sx/(geography.(reactive.(examples.coverage)))" :label "Coverage"} {:href "/sx/(geography.(reactive.(examples.cyst)))" :label "Cyst"} {:href "/sx/(geography.(reactive.(examples.reactive-expressions)))" :label "Reactive Expressions"}))
+
+(define reactive-islands-nav-items (list (dict :label "Examples" :href "/sx/(geography.(reactive.(examples)))" :summary "Live interactive islands — click the buttons, type in the inputs." :children reactive-examples-nav-items)))
+
+(define marshes-examples-nav-items (list {:href "/sx/(geography.(marshes.hypermedia-feeds))" :label "Hypermedia Feeds State"} {:href "/sx/(geography.(marshes.server-signals))" :label "Server Writes to Signals"} {:href "/sx/(geography.(marshes.on-settle))" :label "sx-on-settle"} {:href "/sx/(geography.(marshes.signal-triggers))" :label "Signal-Bound Triggers"} {:href "/sx/(geography.(marshes.view-transform))" :label "Reactive View Transform"}))
+
diff --git a/sx/sx/nav-language.sx b/sx/sx/nav-language.sx
new file mode 100644
index 00000000..ef230532
--- /dev/null
+++ b/sx/sx/nav-language.sx
@@ -0,0 +1,26 @@
+;; Navigation items for the Language section (docs, specs, testing, bootstrappers)
+
+(define docs-nav-items (list (dict :label "Introduction" :href "/sx/(language.(doc.introduction))") (dict :label "Getting Started" :href "/sx/(language.(doc.getting-started))") (dict :label "Components" :href "/sx/(language.(doc.components))") (dict :label "Evaluator" :href "/sx/(language.(doc.evaluator))") (dict :label "Primitives" :href "/sx/(language.(doc.primitives))") (dict :label "Special Forms" :href "/sx/(language.(doc.special-forms))") (dict :label "Server Rendering" :href "/sx/(language.(doc.server-rendering))")))
+
+(define specs-nav-items (list {:href "/sx/(language.(spec.core))" :children (list {:href "/sx/(language.(spec.parser))" :label "Parser"} {:href "/sx/(language.(spec.evaluator))" :label "Evaluator"} {:href "/sx/(language.(spec.primitives))" :label "Primitives"} {:href "/sx/(language.(spec.special-forms))" :label "Special Forms"} {:href "/sx/(language.(spec.renderer))" :label "Renderer"}) :label "Core"} {:href "/sx/(language.(spec.adapters))" :children (list {:href "/sx/(language.(spec.adapter-dom))" :label "DOM Adapter"} {:href "/sx/(language.(spec.adapter-html))" :label "HTML Adapter"} {:href "/sx/(language.(spec.adapter-sx))" :label "SX Wire Adapter"} {:href "/sx/(language.(spec.adapter-async))" :label "Async Adapter"}) :label "Adapters"} {:href "/sx/(language.(spec.browser))" :children (list {:href "/sx/(language.(spec.engine))" :label "SxEngine"} {:href "/sx/(language.(spec.orchestration))" :label "Orchestration"} {:href "/sx/(language.(spec.boot))" :label "Boot"} {:href "/sx/(language.(spec.router))" :label "Router"}) :label "Browser"} {:href "/sx/(language.(spec.reactive))" :children (list {:href "/sx/(language.(spec.signals))" :label "Signals"} {:href "/sx/(language.(spec.frames))" :label "CEK Frames"} {:href "/sx/(language.(spec.cek))" :label "CEK Machine"}) :label "Reactive"} {:href "/sx/(language.(spec.host))" :children (list {:href "/sx/(language.(spec.boundary))" :label "Boundary"} {:href "/sx/(language.(spec.forms))" :label "Forms"} {:href "/sx/(language.(spec.page-helpers))" :label "Page Helpers"}) :label "Host Interface"} {:href "/sx/(language.(spec.extensions))" :children (list {:href "/sx/(language.(spec.continuations))" :label "Continuations"} {:href "/sx/(language.(spec.callcc))" :label "call/cc"} {:href "/sx/(language.(spec.types))" :label "Types"} {:href "/sx/(language.(spec.deps))" :label "Deps"}) :label "Extensions"}))
+
+(define testing-nav-items (list (dict :label "Overview" :href "/sx/(language.(test))") (dict :label "Evaluator" :href "/sx/(language.(test.eval))") (dict :label "Parser" :href "/sx/(language.(test.parser))") (dict :label "Router" :href "/sx/(language.(test.router))") (dict :label "Renderer" :href "/sx/(language.(test.render))") (dict :label "Dependencies" :href "/sx/(language.(test.deps))") (dict :label "Engine" :href "/sx/(language.(test.engine))") (dict :label "Orchestration" :href "/sx/(language.(test.orchestration))") (dict :label "Runners" :href "/sx/(language.(test.runners))")))
+
+(define bootstrappers-nav-items (list (dict :label "Overview" :href "/sx/(language.(bootstrapper))") (dict :label "JavaScript" :href "/sx/(language.(bootstrapper.javascript))") (dict :label "Python" :href "/sx/(language.(bootstrapper.python))") (dict :label "Self-Hosting (py.sx)" :href "/sx/(language.(bootstrapper.self-hosting))") (dict :label "Self-Hosting JS (js.sx)" :href "/sx/(language.(bootstrapper.self-hosting-js))") (dict :label "Page Helpers" :href "/sx/(language.(bootstrapper.page-helpers))")))
+
+(define core-spec-items (list (dict :slug "parser" :filename "parser.sx" :title "Parser" :desc "Tokenization and parsing of SX source text into AST." :prose "The parser converts SX source text into an abstract syntax tree. It tokenizes the input into atoms, strings, numbers, keywords, and delimiters, then assembles them into nested list structures. The parser is intentionally minimal — s-expressions need very little syntax to parse. Special reader macros handle quasiquote (\\`), unquote (~), splice (~@), and the quote (') shorthand. The output is a tree of plain lists, symbols, keywords, strings, and numbers that the evaluator can walk directly.") (dict :slug "evaluator" :filename "evaluator.sx" :title "Evaluator" :desc "CEK machine evaluator." :prose "The evaluator walks the AST produced by the parser and reduces it to values. It implements lexical scoping with closures, special forms (define, let, if, cond, fn, defcomp, defmacro, quasiquote, set!, do), and function application. Macros are expanded at eval time. Component definitions (defcomp) create callable component objects that participate in the rendering pipeline. The evaluator delegates rendering expressions — HTML tags, components, fragments — to whichever adapter is active, making the same source renderable to DOM nodes, HTML strings, or SX wire format.") (dict :slug "primitives" :filename "primitives.sx" :title "Primitives" :desc "All built-in pure functions and their signatures." :prose "Primitives are the built-in functions available in every SX environment. Each entry declares a name, parameter signature, and semantics. Bootstrap compilers implement these natively per target (JavaScript, Python, etc.). The registry covers arithmetic, comparison, string manipulation, list operations, dict operations, type predicates, and control flow helpers. All primitives are pure — they take values and return values with no side effects. Platform-specific operations (DOM access, HTTP, file I/O) are provided separately via platform bridge functions, not primitives.") (dict :slug "special-forms" :filename "special-forms.sx" :title "Special Forms" :desc "All special forms — syntactic constructs with custom evaluation rules." :prose "Special forms are the syntactic constructs whose arguments are NOT evaluated before dispatch. Each form has its own evaluation rules — unlike primitives, which receive pre-evaluated values. Together with primitives, special forms define the complete language surface. The registry covers control flow (if, when, cond, case, and, or), binding (let, letrec, define, set!), functions (lambda, defcomp, defmacro), sequencing (begin, do, thread-first), quoting (quote, quasiquote), continuations (reset, shift), guards (dynamic-wind), higher-order forms (map, filter, reduce), and domain-specific definitions (defstyle, defhandler, defpage, defquery, defaction).") (dict :slug "renderer" :filename "render.sx" :title "Renderer" :desc "Shared rendering registries and utilities used by all adapters." :prose "The renderer defines what is renderable and how arguments are parsed, but not the output format. It maintains registries of known HTML tags, SVG tags, void elements, and boolean attributes. It specifies how keyword arguments on elements become HTML attributes, how children are collected, and how special attributes (class, style, data-*) are handled. All three adapters (DOM, HTML, SX wire) share these definitions so they agree on what constitutes valid markup.")))
+
+(define adapter-spec-items (list (dict :slug "adapter-dom" :filename "adapter-dom.sx" :title "DOM Adapter" :desc "Renders SX expressions to live DOM nodes. Browser-only." :prose "The DOM adapter renders evaluated SX expressions into live browser DOM nodes — Elements, Text nodes, and DocumentFragments. It mirrors the HTML adapter's logic but produces DOM objects instead of strings. This is the adapter used by the browser-side SX runtime for initial mount, hydration, and dynamic updates. It handles element creation, attribute setting (including event handlers and style objects), SVG namespace handling, and fragment composition.") (dict :slug "adapter-html" :filename "adapter-html.sx" :title "HTML Adapter" :desc "Renders SX expressions to HTML strings. Server-side." :prose "The HTML adapter renders evaluated SX expressions to HTML strings. It is used server-side to produce complete HTML pages and fragments. It handles void elements (self-closing tags like
,
), boolean attributes, style serialization, class merging, and proper escaping. The output is standard HTML5 that any browser can parse.") (dict :slug "adapter-sx" :filename "adapter-sx.sx" :title "SX Wire Adapter" :desc "Serializes SX for client-side rendering. Component calls stay unexpanded." :prose "The SX wire adapter serializes expressions as SX source text for transmission to the browser, where sx.js renders them client-side. Unlike the HTML adapter, component calls (~plans/content-addressed-components/name ...) are NOT expanded — they are sent to the client as-is, allowing the browser to render them with its local component registry. HTML tags ARE serialized as s-expression source. This is the format used for SX-over-HTTP responses and the page boot payload.") (dict :slug "adapter-async" :filename "adapter-async.sx" :title "Async Adapter" :desc "Async versions of HTML and SX wire adapters for server-side rendering with I/O." :prose "The async adapter provides async-aware versions of the HTML and SX wire rendering functions. It intercepts I/O operations (database queries, service calls, fragment fetches) during evaluation, awaiting them before continuing. Entry points: async-render (HTML output with awaited I/O), async-aser (SX wire format with awaited I/O). The bootstrapper emits async def and automatic await insertion for all define-async functions. This adapter is what makes server-side SX pages work with real data.")))
+
+(define browser-spec-items (list (dict :slug "engine" :filename "engine.sx" :title "SxEngine" :desc "Pure logic for fetch, swap, history, SSE, triggers, morph, and indicators." :prose "The engine specifies the pure logic of the browser-side fetch/swap/history system. Like HTMX but native to SX. It defines trigger parsing (click, submit, intersect, poll, load, revealed), swap algorithms (innerHTML, outerHTML, morph, beforebegin, etc.), the morph/diff algorithm for patching existing DOM, history management (push-url, replace-url, popstate), out-of-band swap identification, Server-Sent Events parsing, retry logic with exponential backoff, request header building, response header processing, and optimistic UI updates. This file contains no browser API calls — all platform interaction is in orchestration.sx.") (dict :slug "orchestration" :filename "orchestration.sx" :title "Orchestration" :desc "Browser wiring that binds engine logic to DOM events, fetch, and lifecycle." :prose "Orchestration is the browser wiring layer. It binds the pure engine logic to actual browser APIs: DOM event listeners, fetch(), AbortController, setTimeout/setInterval, IntersectionObserver, history.pushState, and EventSource (SSE). It implements the full request lifecycle — from trigger through fetch through swap — including CSS tracking, response type detection (SX vs HTML), OOB swap processing, script activation, element boosting, and preload. Dependency is strictly one-way: orchestration depends on engine, never the reverse.") (dict :slug "boot" :filename "boot.sx" :title "Boot" :desc "Browser startup lifecycle: mount, hydrate, script processing." :prose "Boot handles the browser startup sequence and provides the public API for mounting SX content. On page load it: (1) initializes CSS tracking, (2) processes