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

") ", " (code "