diff --git a/shared/sx/ocaml_bridge.py b/shared/sx/ocaml_bridge.py index 2e22281a..5613fb3a 100644 --- a/shared/sx/ocaml_bridge.py +++ b/shared/sx/ocaml_bridge.py @@ -716,10 +716,12 @@ class OcamlBridge: if len(parts) < 2: raise OcamlBridgeError(f"Malformed io-request: {line}") - # Skip numeric batch ID if present + # Skip leading numeric IDs (epoch, batch ID, or both) offset = 1 - if isinstance(parts[1], (int, float)): - offset = 2 + while offset < len(parts) and isinstance(parts[offset], (int, float)): + offset += 1 + if offset >= len(parts): + raise OcamlBridgeError(f"Malformed io-request (no name after IDs): {line}") req_name = _to_str(parts[offset]) args = parts[offset + 1:] diff --git a/sx/sx/affinity-demo.sx b/sx/sx/affinity-demo.sx index 64f926c5..416c948f 100644 --- a/sx/sx/affinity-demo.sx +++ b/sx/sx/affinity-demo.sx @@ -35,7 +35,7 @@ (span :class "inline-block w-2 h-2 rounded-full bg-red-400") (span :class "text-sm font-mono text-red-600" ":affinity :auto + IO")) (p :class "text-red-800 mb-3" "Auto affinity with IO dependency — auto-detected as server-rendered.") - (~docs/code :code (highlight "(render-target name env io-names)" "lisp")))) + (~docs/code :src (highlight "(render-target name env io-names)" "lisp")))) (defcomp ~affinity-demo/aff-demo-io-client () :affinity :client @@ -44,7 +44,7 @@ (span :class "inline-block w-2 h-2 rounded-full bg-violet-400") (span :class "text-sm font-mono text-violet-600" ":affinity :client + IO")) (p :class "text-violet-800 mb-3" "Client affinity overrides IO — calls proxied to server via /sx/io/.") - (~docs/code :code (highlight "(component-affinity comp)" "lisp")))) + (~docs/code :src (highlight "(component-affinity comp)" "lisp")))) ;; --- Main page component --- @@ -61,7 +61,7 @@ ;; Syntax (~docs/section :title "Syntax" :id "syntax" (p "Add " (code ":affinity") " between the params list and the body:") - (~docs/code :code (highlight "(defcomp ~affinity-demo/my-component (&key title)\n :affinity :client ;; or :server, or omit for :auto\n (div title))" "lisp")) + (~docs/code :src (highlight "(defcomp ~affinity-demo/my-component (&key title)\n :affinity :client ;; or :server, or omit for :auto\n (div title))" "lisp")) (p "Three values:") (ul :class "list-disc pl-5 text-stone-700 space-y-1" (li (code ":auto") " (default) — runtime decides from IO dependency analysis") diff --git a/sx/sx/async-io-demo.sx b/sx/sx/async-io-demo.sx index 8b6be8a4..b0804887 100644 --- a/sx/sx/async-io-demo.sx +++ b/sx/sx/async-io-demo.sx @@ -30,17 +30,17 @@ (div :class "rounded-lg border border-stone-200 bg-white p-5 space-y-3" (h3 :class "text-sm font-medium text-stone-500 uppercase tracking-wide" "SX component definition") - (~docs/code :code + (~docs/code :src (highlight "(defcomp ~async-io-demo/card (&key title subtitle &rest children)\n (div :class \"border rounded-lg p-4 shadow-sm\"\n (h2 :class \"text-lg font-bold\" title)\n (when subtitle\n (p :class \"text-stone-500 text-sm\" subtitle))\n (div :class \"mt-3\" children)))" "lisp"))) (div :class "rounded-lg border border-stone-200 bg-white p-5 space-y-3" (h3 :class "text-sm font-medium text-stone-500 uppercase tracking-wide" "Python server code") - (~docs/code :code + (~docs/code :src (highlight "from shared.sx.pages import mount_io_endpoint\n\n# The IO proxy serves any allowed primitive:\n# GET /sx/io/highlight?_arg0=code&_arg1=lisp\nasync def io_proxy(name):\n result = await execute_io(name, args, kwargs, ctx)\n return serialize(result)" "python"))) (div :class "rounded-lg border border-stone-200 bg-white p-5 space-y-3" (h3 :class "text-sm font-medium text-stone-500 uppercase tracking-wide" "SX async rendering spec") - (~docs/code :code + (~docs/code :src (highlight ";; try-client-route reads io-deps from page registry\n(let ((io-deps (get match \"io-deps\"))\n (has-io (and io-deps (not (empty? io-deps)))))\n ;; Register IO deps as proxied primitives on demand\n (when has-io (register-io-deps io-deps))\n (if has-io\n ;; Async render: IO primitives proxied via /sx/io/\n (do\n (try-async-eval-content content-src env\n (fn (rendered)\n (when rendered\n (swap-rendered-content target rendered pathname))))\n true)\n ;; Sync render: pure components, no IO\n (let ((rendered (try-eval-content content-src env)))\n (swap-rendered-content target rendered pathname))))" "lisp")))) ;; Architecture explanation diff --git a/sx/sx/docs-content.sx b/sx/sx/docs-content.sx index ca14d941..b0d954b0 100644 --- a/sx/sx/docs-content.sx +++ b/sx/sx/docs-content.sx @@ -27,13 +27,13 @@ (~docs/section :title "Minimal example" :id "minimal" (p :class "text-stone-600" "An sx response is s-expression source code with content type text/sx:") - (~docs/code :code (highlight "(div :class \"p-4 bg-white rounded\"\n (h1 :class \"text-2xl font-bold\" \"Hello, world!\")\n (p \"This is rendered from an s-expression.\"))" "lisp")) + (~docs/code :src (highlight "(div :class \"p-4 bg-white rounded\"\n (h1 :class \"text-2xl font-bold\" \"Hello, world!\")\n (p \"This is rendered from an s-expression.\"))" "lisp")) (p :class "text-stone-600" "Add sx-get to any element to make it fetch and render sx:")) (~docs/section :title "Hypermedia attributes" :id "attrs" (p :class "text-stone-600" "Like htmx, sx adds attributes to HTML elements to trigger HTTP requests:") - (~docs/code :code (highlight "(button\n :sx-get \"/api/data\"\n :sx-target \"#result\"\n :sx-swap \"innerHTML\"\n \"Load data\")" "lisp")) + (~docs/code :src (highlight "(button\n :sx-get \"/api/data\"\n :sx-target \"#result\"\n :sx-swap \"innerHTML\"\n \"Load data\")" "lisp")) (p :class "text-stone-600" "sx-get, sx-post, sx-put, sx-delete, sx-patch — all work the same way. The response is parsed as sx and rendered into the target element.")))) @@ -42,10 +42,10 @@ (~docs/section :title "defcomp" :id "defcomp" (p :class "text-stone-600" "Components are defined with defcomp. They take keyword parameters and optional children:") - (~docs/code :code (highlight "(defcomp ~docs-content/card (&key title subtitle &rest children)\n (div :class \"border rounded p-4\"\n (h2 :class \"font-bold\" title)\n (when subtitle (p :class \"text-stone-500\" subtitle))\n (div :class \"mt-3\" children)))" "lisp")) + (~docs/code :src (highlight "(defcomp ~docs-content/card (&key title subtitle &rest children)\n (div :class \"border rounded p-4\"\n (h2 :class \"font-bold\" title)\n (when subtitle (p :class \"text-stone-500\" subtitle))\n (div :class \"mt-3\" children)))" "lisp")) (p :class "text-stone-600" "Use components with the ~ prefix:") - (~docs/code :code (highlight "(~docs-content/card :title \"My Card\" :subtitle \"A description\"\n (p \"First child\")\n (p \"Second child\"))" "lisp"))) + (~docs/code :src (highlight "(~docs-content/card :title \"My Card\" :subtitle \"A description\"\n (p \"First child\")\n (p \"Second child\"))" "lisp"))) (~docs/section :title "Component caching" :id "caching" (p :class "text-stone-600" "Component definitions are sent in a \n \n \n\n\n
\n \n\n" "html")) + (~docs/code :src (highlight "\n\n\n \n \n \n\n\n
\n \n\n" "html")) (p "This document is its own CID. Pin it to IPFS and it's a permanent, executable, verifiable application. No origin server, no CDN, no DNS. The content network is the deployment target.") @@ -168,7 +168,7 @@ (~docs/section :title "Namespaced Environments" :id "namespaces" (p "As the component library grows across services, a flat environment risks name collisions. Images provide a natural boundary for namespace scoping.") - (~docs/code :code (highlight "(sx-image\n :version 1\n :namespace \"market\"\n :spec-cids {...}\n :extends \"bafy...shared-image\" ;; inherits shared components\n :components (\n ;; market-specific components\n (defcomp ~plans/environment-images/product-card ...)\n (defcomp ~plans/environment-images/price-tag ...)\n ))" "lisp")) + (~docs/code :src (highlight "(sx-image\n :version 1\n :namespace \"market\"\n :spec-cids {...}\n :extends \"bafy...shared-image\" ;; inherits shared components\n :components (\n ;; market-specific components\n (defcomp ~plans/environment-images/product-card ...)\n (defcomp ~plans/environment-images/price-tag ...)\n ))" "lisp")) (p "Resolution: " (code "market/~plans/environment-images/product-card") " → look in market image first, then fall through to the shared image (via " (code ":extends") "). Each service produces its own image, layered on top of the shared base.") @@ -205,7 +205,7 @@ (~docs/section :title "The Verification Chain" :id "chain" (p "The full provenance chain from served page back to source:") - (~docs/code :code (highlight ";; 1. Page served with provenance headers\n;;\n;; SX-Spec: bafy...eval,bafy...render,...\n;; SX-Image: bafy...market-image\n;; SX-Page: (defpage product :path \"/products/\" ...)\n;;\n;; 2. Verify image → spec\n;;\n;; Fetch specs by CID → evaluate → build image → compare CID\n;; If match: the image was correctly produced from these specs\n;;\n;; 3. Verify page → image\n;;\n;; Deserialize image → evaluate page defn → render\n;; If output matches served HTML: the page was correctly rendered\n;;\n;; 4. Trust chain terminates at the spec\n;;\n;; The spec is self-hosting (eval.sx evaluates itself)\n;; The spec's CID is its identity\n;; No external trust anchor needed beyond the hash function" "lisp")) + (~docs/code :src (highlight ";; 1. Page served with provenance headers\n;;\n;; SX-Spec: bafy...eval,bafy...render,...\n;; SX-Image: bafy...market-image\n;; SX-Page: (defpage product :path \"/products/\" ...)\n;;\n;; 2. Verify image → spec\n;;\n;; Fetch specs by CID → evaluate → build image → compare CID\n;; If match: the image was correctly produced from these specs\n;;\n;; 3. Verify page → image\n;;\n;; Deserialize image → evaluate page defn → render\n;; If output matches served HTML: the page was correctly rendered\n;;\n;; 4. Trust chain terminates at the spec\n;;\n;; The spec is self-hosting (eval.sx evaluates itself)\n;; The spec's CID is its identity\n;; No external trust anchor needed beyond the hash function" "lisp")) (p "This is stronger than code signing. Code signing says " (em "\"this entity vouches for this binary.\"") " Content addressing says " (em "\"this binary is the deterministic output of this source.\"") " No entity needed. No certificate authority. No revocation lists. Just math.")) diff --git a/sx/sx/plans/foundations.sx b/sx/sx/plans/foundations.sx index 682e8303..dc7ea29b 100644 --- a/sx/sx/plans/foundations.sx +++ b/sx/sx/plans/foundations.sx @@ -20,7 +20,7 @@ (p "Every layer is definable in terms of the one below. " "No layer can be decomposed without the layer beneath it.") - (~docs/code :code + (~docs/code :src (str "Layer 0: CEK machine (expression + environment + continuation) \u2714 DONE\n" "Layer 1: Continuations (shift / reset \u2014 delimited capture) \u2714 DONE\n" "Layer 2: Algebraic effects (operations + handlers) \u2714 DONE\n" @@ -117,7 +117,7 @@ (p "All higher-order forms step element-by-element through the CEK machine:") - (~docs/code :code + (~docs/code :src (str ";; map pushes a MapFrame, calls f on each element via continue-with-call\n" "(map (fn (x) (* 2 (deref counter))) items)\n" "\n" @@ -138,7 +138,7 @@ (p "Delimited continuations (Felleisen 1988, Danvy & Filinski 1990) " "expose the K register as a first-class value:") - (~docs/code :code + (~docs/code :src (str ";; reset marks a point in the continuation\n" "(reset\n" " (+ 1 (shift k ;; k = \"the rest up to reset\"\n" @@ -154,7 +154,7 @@ (p "The reactive payoff. " (code "deref") " inside a " (code "reactive-reset") " boundary " "is shift/reset applied to signals:") - (~docs/code :code + (~docs/code :src (str ";; User writes:\n" "(div :class (str \"count-\" (deref counter))\n" " (str \"Value: \" (deref counter)))\n" @@ -258,7 +258,7 @@ (p "Fixed-size mutable byte arrays. A small primitive surface that unlocks " "binary protocol parsing, image headers, wire formats, and efficient CID computation:") - (~docs/code :code + (~docs/code :src (str ";; Create and write\n" "(let ((buf (make-buffer 256)))\n" " (buffer-write-u8! buf 0 #xFF)\n" @@ -317,7 +317,7 @@ "On compiled hosts (OCaml, Rust), they compile to unboxed records — no dict overhead, " "no hash lookups, no string key comparisons:") - (~docs/code :code + (~docs/code :src (str ";; Define a struct\n" "(defstruct point (x : number) (y : number))\n" "\n" @@ -396,7 +396,7 @@ "If " (code "~bank/payment-form") " calls " (code "~bank/amount-input") " calls " (code "~ui/text-field") ", all three definitions are part of the CID:") - (~docs/code :code + (~docs/code :src (str ";; Shallow CID — just this component's definition\n" "(freeze-to-cid ~bank/payment-form) ;; => bafyrei..abc\n" "\n" @@ -413,7 +413,7 @@ "Canonical SX: no comments, no redundant whitespace, deterministic key ordering in dicts, " "normalized number representation:") - (~docs/code :code + (~docs/code :src (str ";; These must produce the same CID on JS, Python, and OCaml:\n" "(canonical-sx '(div :class \"card\" (p \"hello\")))\n" ";; => \"(div :class \\\"card\\\" (p \\\"hello\\\"))\"\n" @@ -425,7 +425,7 @@ (p "The client-side verification flow:") - (~docs/code :code + (~docs/code :src (str ";; Server sends component + CID via aser wire format\n" ";; Browser receives, independently computes CID, compares\n" "\n" @@ -449,7 +449,7 @@ (p "Publishers declare expected CIDs via a well-known manifest:") - (~docs/code :code + (~docs/code :src (str ";; .well-known/sx-manifest.json\n" "{\"version\": 1,\n" " \"components\": {\n" @@ -514,7 +514,7 @@ (p "The fundamental primitive. Create a new CEK machine from a thunk, " "run it concurrently, return a signal that resolves with the result:") - (~docs/code :code + (~docs/code :src (str "(let ((result (spawn (fn () (* 6 7))))) " " ;; result is a signal \u2014 nil until the computation completes @@ -534,7 +534,7 @@ (p "The default implementation is synchronous (no actual concurrency). " "The host overrides " (code "spawn-impl") " for real concurrency:") - (~docs/code :code + (~docs/code :src (str ";; Host-provided spawn (Web Worker) " "(set! spawn-impl @@ -554,7 +554,7 @@ (p "Typed communication pipes between concurrent computations. " "Send is non-blocking. Receive suspends via " (code "shift") " until data arrives:") - (~docs/code :code + (~docs/code :src (str ";; Create a channel " "(make-channel \"results\") @@ -599,7 +599,7 @@ (p "Spawn multiple computations, collect all results. " "The spec primitive, not a library pattern:") - (~docs/code :code + (~docs/code :src (str ";; Fork: run N computations concurrently " "(let ((results (fork-join (list @@ -633,7 +633,7 @@ (li (strong "Cooperative") " \u2014 machines yield explicitly via " (code "yield!")) (li (strong "DAG-ordered") " \u2014 step machines whose inputs are ready (Art DAG)")) - (~docs/code :code + (~docs/code :src (str ";; Cooperative scheduling " "(spawn (fn () @@ -676,7 +676,7 @@ (li "Distribute: send input-CID to whichever worker has the data") (li "Verify: any machine can re-run from input-CID and check output-CID matches")) - (~docs/code :code + (~docs/code :src (str ";; Content-addressed spawn " "(let ((input-cid (freeze-to-cid \"job\"))) diff --git a/sx/sx/plans/generative-sx.sx b/sx/sx/plans/generative-sx.sx index 3e2f476f..ac7f124f 100644 --- a/sx/sx/plans/generative-sx.sx +++ b/sx/sx/plans/generative-sx.sx @@ -32,7 +32,7 @@ (~docs/subsection :title "Homoiconicity" (p "SX code is SX data. " (code "parse") " takes a string and returns a list. " (code "aser") " takes a list and returns a string. These round-trip perfectly. The program can read its own source as naturally as it reads a config file, because they're the same format.") - (~docs/code :code (highlight ";; Code is data\n(define source \"(+ 1 2)\")\n(define ast (parse source)) ;; → (list '+ 1 2)\n(define result (eval-expr ast env)) ;; → 3\n\n;; Data is code\n(define spec '(define greet (fn (name) (str \"Hello, \" name \"!\"))))\n(eval-expr spec env)\n(greet \"world\") ;; → \"Hello, world!\"" "lisp"))) + (~docs/code :src (highlight ";; Code is data\n(define source \"(+ 1 2)\")\n(define ast (parse source)) ;; → (list '+ 1 2)\n(define result (eval-expr ast env)) ;; → 3\n\n;; Data is code\n(define spec '(define greet (fn (name) (str \"Hello, \" name \"!\"))))\n(eval-expr spec env)\n(greet \"world\") ;; → \"Hello, world!\"" "lisp"))) (~docs/subsection :title "Runtime eval" (p (code "eval-expr") " is available at runtime, not just boot. " (code "data-init") " scripts already use it. Any SX string can become running code at any point in the program's execution. This is not " (code "eval()") " bolted onto a language that doesn't want it — it's the " (em "primary mechanism") " of the language.")) @@ -54,7 +54,7 @@ (~docs/section :title "The generative pattern" :id "pattern" (p "A generative SX program starts with a seed and grows by evaluating its own output.") - (~docs/code :code (highlight ";; The core loop\n(define run\n (fn (env source)\n (let ((ast (parse source))\n (result (eval-expr ast env)))\n ;; result might be:\n ;; a value → done, return it\n ;; a string → new SX source, evaluate it (grow)\n ;; a list of defs → new definitions to add to env\n ;; a dict → {source: \"...\", env-patch: {...}} (grow + configure)\n (cond\n (string? result)\n (run env result) ;; evaluate the output\n (and (dict? result) (has-key? result \"source\"))\n (let ((patched (env-merge env (get result \"env-patch\"))))\n (run patched (get result \"source\")))\n :else\n result))))" "lisp")) + (~docs/code :src (highlight ";; The core loop\n(define run\n (fn (env source)\n (let ((ast (parse source))\n (result (eval-expr ast env)))\n ;; result might be:\n ;; a value → done, return it\n ;; a string → new SX source, evaluate it (grow)\n ;; a list of defs → new definitions to add to env\n ;; a dict → {source: \"...\", env-patch: {...}} (grow + configure)\n (cond\n (string? result)\n (run env result) ;; evaluate the output\n (and (dict? result) (has-key? result \"source\"))\n (let ((patched (env-merge env (get result \"env-patch\"))))\n (run patched (get result \"source\")))\n :else\n result))))" "lisp")) (p "The program evaluates source. If the result is more source, it evaluates that too. Each iteration can extend the environment — add new functions, new macros, new primitives. The environment grows. The program becomes capable of things it couldn't do at the start.") @@ -96,29 +96,29 @@ (~docs/subsection :title "1. The spec that compiles itself" (p "Currently: " (code "bootstrap_js.py") " (Python) reads " (code "eval.sx") " (SX) and emits " (code "sx-browser.js") " (JavaScript). Three languages. Two of them are hosts, one is the spec.") (p "Generative version: " (code "eval.sx") " evaluates itself with a code-generation adapter. The evaluator walks its own AST and emits the target language. No Python bootstrapper. No JavaScript template. The spec " (em "is") " the compiler.") - (~docs/code :code (highlight ";; bootstrap.sx — the spec compiles itself\n;;\n;; Load the codegen adapter for the target\n(define emit (load-adapter target)) ;; target = \"js\" | \"py\" | \"go\" | ...\n;;\n;; Read the spec files\n(define spec-source (read-file \"eval.sx\"))\n(define spec-ast (parse spec-source))\n;;\n;; Walk the AST and emit target code\n;; The walker IS the evaluator — eval.sx evaluating eval.sx\n;; with emit instead of execute\n(define target-code\n (eval-expr spec-ast\n (env-extend codegen-env\n ;; Override define, fn, if, etc. to emit instead of execute\n (codegen-special-forms emit))))\n;;\n(write-file (str \"sx-ref.\" (target-extension target)) target-code)" "lisp")) + (~docs/code :src (highlight ";; bootstrap.sx — the spec compiles itself\n;;\n;; Load the codegen adapter for the target\n(define emit (load-adapter target)) ;; target = \"js\" | \"py\" | \"go\" | ...\n;;\n;; Read the spec files\n(define spec-source (read-file \"eval.sx\"))\n(define spec-ast (parse spec-source))\n;;\n;; Walk the AST and emit target code\n;; The walker IS the evaluator — eval.sx evaluating eval.sx\n;; with emit instead of execute\n(define target-code\n (eval-expr spec-ast\n (env-extend codegen-env\n ;; Override define, fn, if, etc. to emit instead of execute\n (codegen-special-forms emit))))\n;;\n(write-file (str \"sx-ref.\" (target-extension target)) target-code)" "lisp")) (p "This is the bootstrapper rewritten as a generative program. The spec reads itself, walks itself, and writes the output language. Adding a new target means writing a new " (code "load-adapter") " — a set of emitters for the SX special forms. The walker doesn't change. The spec doesn't change. Only the output format changes.") (p "The current bootstrappers (" (code "bootstrap_js.py") ", " (code "bootstrap_py.py") ") would become the first two adapters. Future targets (Go, Rust, WASM) are additional adapters, written in SX and bootstrapped like everything else.")) (~docs/subsection :title "2. The program that discovers its dependencies" (p (code "deps.sx") " analyzes component dependency graphs. It walks component ASTs, finds " (code "~plans/content-addressed-components/name") " references, computes transitive closures. This is the analytic mode — SX analyzing SX.") (p "The generative version: a program that discovers it needs a component, searches for its definition (local files, IPFS, a registry), loads it, evaluates it, and continues. The program grows its own capability set at runtime.") - (~docs/code :code (highlight ";; A program that discovers and loads what it needs\n(define render-page\n (fn (page-name)\n (let ((page-def (lookup-component page-name)))\n (when (nil? page-def)\n ;; Component not found locally — fetch from registry\n (let ((source (fetch-component-source page-name)))\n ;; Evaluate the definition — it joins the environment\n (eval-expr (parse source) env)\n ;; Now it exists\n (set! page-def (lookup-component page-name))))\n ;; Render with all dependencies resolved\n (render-to-html (list page-def)))))" "lisp")) + (~docs/code :src (highlight ";; A program that discovers and loads what it needs\n(define render-page\n (fn (page-name)\n (let ((page-def (lookup-component page-name)))\n (when (nil? page-def)\n ;; Component not found locally — fetch from registry\n (let ((source (fetch-component-source page-name)))\n ;; Evaluate the definition — it joins the environment\n (eval-expr (parse source) env)\n ;; Now it exists\n (set! page-def (lookup-component page-name))))\n ;; Render with all dependencies resolved\n (render-to-html (list page-def)))))" "lisp")) (p "This already happens in the browser. " (code "sx_response") " prepends missing component definitions as a " (code "data-components") " script block. The client evaluates them and they join the environment. The generative version makes this explicit: the program tells you what it needs, you give it source, it evaluates it, it grows.")) (~docs/subsection :title "3. The test suite that writes tests" (p "Given a function's signature and a set of properties (" (code "prove.sx") " already has the property language), generate test cases that verify the properties. The program reads its own function definitions, generates SX expressions that test them, and evaluates those expressions.") - (~docs/code :code (highlight ";; Given: a function and properties about it\n(define-property string-reverse-involutory\n :forall ((s string?))\n :holds (= (reverse (reverse s)) s))\n\n;; Generate: test cases from the property\n;; The program reads the property, generates test source, evals it\n(define tests (generate-tests string-reverse-involutory))\n;; tests = list of (assert (= (reverse (reverse \"hello\")) \"hello\"))\n;; (assert (= (reverse (reverse \"\")) \"\"))\n;; (assert (= (reverse (reverse \"a\")) \"a\"))\n;; ... (random strings, edge cases)\n(for-each (fn (t) (eval-expr t env)) tests)" "lisp")) + (~docs/code :src (highlight ";; Given: a function and properties about it\n(define-property string-reverse-involutory\n :forall ((s string?))\n :holds (= (reverse (reverse s)) s))\n\n;; Generate: test cases from the property\n;; The program reads the property, generates test source, evals it\n(define tests (generate-tests string-reverse-involutory))\n;; tests = list of (assert (= (reverse (reverse \"hello\")) \"hello\"))\n;; (assert (= (reverse (reverse \"\")) \"\"))\n;; (assert (= (reverse (reverse \"a\")) \"a\"))\n;; ... (random strings, edge cases)\n(for-each (fn (t) (eval-expr t env)) tests)" "lisp")) (p "The program analyzed itself (read the property), generated new SX (the test cases), and evaluated it (ran the tests). Three modes — analytic, synthetic, generative — in sequence.")) (~docs/subsection :title "4. The server that extends its own API" (p "An SX server receives a request it doesn't know how to handle. Instead of returning 404, it examines the request, generates a handler, evaluates it, and handles the request.") - (~docs/code :code (highlight ";; A route handler that generates new route handlers\n(define handle-unknown-route\n (fn (path params)\n ;; Analyze what was requested\n (let ((segments (split path \"/\"))\n (resource (nth segments 1))\n (action (nth segments 2)))\n ;; Check if a schema exists for this resource\n (let ((schema (lookup-schema resource)))\n (when schema\n ;; Generate a CRUD handler from the schema\n (let ((handler-source (generate-crud-handler resource action schema)))\n ;; Evaluate it — the handler now exists\n (eval-expr (parse handler-source) env)\n ;; Route future requests to the generated handler\n (register-route path (env-get env (str resource \"-\" action)))\n ;; Handle this request with the new handler\n ((env-get env (str resource \"-\" action)) params)))))))" "lisp")) + (~docs/code :src (highlight ";; A route handler that generates new route handlers\n(define handle-unknown-route\n (fn (path params)\n ;; Analyze what was requested\n (let ((segments (split path \"/\"))\n (resource (nth segments 1))\n (action (nth segments 2)))\n ;; Check if a schema exists for this resource\n (let ((schema (lookup-schema resource)))\n (when schema\n ;; Generate a CRUD handler from the schema\n (let ((handler-source (generate-crud-handler resource action schema)))\n ;; Evaluate it — the handler now exists\n (eval-expr (parse handler-source) env)\n ;; Route future requests to the generated handler\n (register-route path (env-get env (str resource \"-\" action)))\n ;; Handle this request with the new handler\n ((env-get env (str resource \"-\" action)) params)))))))" "lisp")) (p "This is not code generation in the Rails scaffolding sense — those generate files you then edit. This generates running code at runtime. The handler didn't exist. Now it does. The server grew.")) (~docs/subsection :title "5. The macro system that learns idioms" (p "A generative macro system that detects repeated patterns in code and synthesizes macros to capture them. The program watches itself being written and abstracts its own patterns.") - (~docs/code :code (highlight ";; The program notices this pattern appearing repeatedly:\n;; (div :class \"card\" (h2 title) (p body) children...)\n;;\n;; It generates:\n(defmacro ~card (title body &rest children)\n (div :class \"card\"\n (h2 ,title)\n (p ,body)\n ,@children))\n;;\n;; And rewrites its own source to use the new macro.\n;; This is an SX program that:\n;; 1. Analyzed its own AST (found repeated subtrees)\n;; 2. Synthesized a macro (extracted the pattern)\n;; 3. Evaluated the macro definition (extended env)\n;; 4. Rewrote its own source (used the macro)\n;; Four generative steps." "lisp")) + (~docs/code :src (highlight ";; The program notices this pattern appearing repeatedly:\n;; (div :class \"card\" (h2 title) (p body) children...)\n;;\n;; It generates:\n(defmacro ~card (title body &rest children)\n (div :class \"card\"\n (h2 ,title)\n (p ,body)\n ,@children))\n;;\n;; And rewrites its own source to use the new macro.\n;; This is an SX program that:\n;; 1. Analyzed its own AST (found repeated subtrees)\n;; 2. Synthesized a macro (extracted the pattern)\n;; 3. Evaluated the macro definition (extended env)\n;; 4. Rewrote its own source (used the macro)\n;; Four generative steps." "lisp")) (p "The connection to the Art DAG: each version of the source is content-addressed. The original (before macros) and the refactored (after macros) are both immutable nodes. The generative step is an edge in the DAG. You can always inspect what the program was before it rewrote itself."))) ;; ===================================================================== @@ -128,7 +128,7 @@ (~docs/section :title "The seed" :id "seed" (p "A generative SX program starts with a seed. The seed must contain enough to bootstrap the generative loop: a parser, an evaluator, and the " (code "run") " function. Everything else is grown.") - (~docs/code :code (highlight ";; seed.sx — the minimal generative program\n;;\n;; This file contains:\n;; - The SX parser (parse)\n;; - The SX evaluator (eval-expr)\n;; - The generative loop (run)\n;; - A source acquisition function (next-source)\n;;\n;; Everything else — primitives, rendering, networking, persistence —\n;; is loaded by the program as it discovers it needs them.\n\n(define run\n (fn (env)\n (let ((source (next-source env)))\n (when source\n (let ((result (eval-expr (parse source) env)))\n (run env))))))\n\n;; Start with a bare environment\n(run (env-extend (dict\n \"parse\" parse\n \"eval-expr\" eval-expr\n \"next-source\" initial-source-fn)))" "lisp")) + (~docs/code :src (highlight ";; seed.sx — the minimal generative program\n;;\n;; This file contains:\n;; - The SX parser (parse)\n;; - The SX evaluator (eval-expr)\n;; - The generative loop (run)\n;; - A source acquisition function (next-source)\n;;\n;; Everything else — primitives, rendering, networking, persistence —\n;; is loaded by the program as it discovers it needs them.\n\n(define run\n (fn (env)\n (let ((source (next-source env)))\n (when source\n (let ((result (eval-expr (parse source) env)))\n (run env))))))\n\n;; Start with a bare environment\n(run (env-extend (dict\n \"parse\" parse\n \"eval-expr\" eval-expr\n \"next-source\" initial-source-fn)))" "lisp")) (p "The seed is a quine that doesn't just reproduce itself — it " (em "extends") " itself. Each call to " (code "next-source") " returns new SX that the seed evaluates in its own environment. The environment grows. The seed's capabilities grow. But the seed itself never changes — it's the fixed point of the generative process.") @@ -192,7 +192,7 @@ (~docs/subsection :title "Correct quotation and splicing" (p "Quasiquote (" (code "`") "), unquote (" (code ",") "), and unquote-splicing (" (code ",@") ") must work correctly for programmatic code construction. The host needs these as first-class operations, not string concatenation.") (p "A generative program builds code by template:") - (~docs/code :code (highlight ";; The generative program builds new definitions by template\n(define gen-handler\n (fn (name params body)\n `(define ,name\n (fn ,params\n ,@body))))\n\n;; gen-handler produces an AST, not a string\n;; The AST can be inspected, transformed, hashed, then evaluated\n(eval-in (gen-handler 'greet '(name) '((str \"Hello, \" name))) env)" "lisp")) + (~docs/code :src (highlight ";; The generative program builds new definitions by template\n(define gen-handler\n (fn (name params body)\n `(define ,name\n (fn ,params\n ,@body))))\n\n;; gen-handler produces an AST, not a string\n;; The AST can be inspected, transformed, hashed, then evaluated\n(eval-in (gen-handler 'greet '(name) '((str \"Hello, \" name))) env)" "lisp")) (p "String concatenation would work — " (code "(str \"(define \" name \" ...)\")") " — but it's fragile, unstructured, and can't be inspected before evaluation. Quasiquote produces an AST. The generative program works with " (em "structure") ", not text.")) (~docs/subsection :title "Tail-call optimization" @@ -229,7 +229,7 @@ (~docs/subsection :title "with-boundary as migration point" (p "Execution migrates to where the primitives are. When the evaluator hits a " (code "with-boundary") " block requiring primitives the current host doesn't have, it serializes state (" (code "env-snapshot") "), ships the pending expression plus environment to a host that has them, and execution continues there. The block is the unit of migration, not individual primitive calls.") - (~docs/code :code (highlight "(with-boundary (media-processing encoding)\n (let ((frames (gpu-exec recipe cached-layers)))\n (encode-stream frames :codec \"h264\"\n :on-input-needed (fn (slot)\n (with-boundary (live-ingest)\n (open-feed :protocol \"webrtc\" :slot slot))))))" "lisp")) + (~docs/code :src (highlight "(with-boundary (media-processing encoding)\n (let ((frames (gpu-exec recipe cached-layers)))\n (encode-stream frames :codec \"h264\"\n :on-input-needed (fn (slot)\n (with-boundary (live-ingest)\n (open-feed :protocol \"webrtc\" :slot slot))))))" "lisp")) (p "This program starts wherever it starts. When it hits " (code "(with-boundary (media-processing encoding) ...)") ", the evaluator checks: does the current host provide " (code "gpu-exec") " and " (code "encode-stream") "? If yes, evaluate in place. If no, snapshot the environment, serialize the pending expression, and dispatch to a host that does. Inside the encoding block, " (code ":on-input-needed") " triggers a nested migration — the " (code "(with-boundary (live-ingest) ...)") " block dispatches to an ingest server that provides " (code "open-feed") ".") (p "The program doesn't know where it runs. It declares what it needs. The runtime figures out " (em "where") ".")) diff --git a/sx/sx/plans/isolated-evaluator.sx b/sx/sx/plans/isolated-evaluator.sx index 9809f5a3..e4ffa0f0 100644 --- a/sx/sx/plans/isolated-evaluator.sx +++ b/sx/sx/plans/isolated-evaluator.sx @@ -152,7 +152,7 @@ (p (strong "Goal:") " All real-world-touching JavaScript lives in one standalone module. The evaluator never directly accesses " (code "document") ", " (code "window") ", " (code "fetch") ", " (code "localStorage") ", " (code "history") ", etc.") (h4 :class "font-semibold mt-4 mb-2" "Architecture") - (~docs/code :code (highlight " \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n \u2502 sx-platform.js \u2502 \u2190 DOM, fetch, timers, storage\n \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n \u2502\n \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n \u2502 \u2502\n \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n \u2502 sx-evaluator.js \u2502 \u2502 sx-wasm-shim.js \u2502\n \u2502 (isolated JS) \u2502 \u2502 (WASM instance \u2502\n \u2502 \u2502 \u2502 + handle table) \u2502\n \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518" "text")) + (~docs/code :src (highlight " \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n \u2502 sx-platform.js \u2502 \u2190 DOM, fetch, timers, storage\n \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n \u2502\n \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n \u2502 \u2502\n \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n \u2502 sx-evaluator.js \u2502 \u2502 sx-wasm-shim.js \u2502\n \u2502 (isolated JS) \u2502 \u2502 (WASM instance \u2502\n \u2502 \u2502 \u2502 + handle table) \u2502\n \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518" "text")) (h4 :class "font-semibold mt-4 mb-2" "What moves into sx-platform.js") (p "Extracted from " (code "platform_js.py") " string constants:") @@ -208,15 +208,15 @@ (h4 :class "font-semibold mt-4 mb-2" "Core-only build") (p "The bootstrapper already supports selecting which modules to compile. A core-only build:") - (~docs/code :code (highlight "# In run_js_sx.py \u2014 core-only build\ncompile_ref_to_js(\n adapters=[\"parser\", \"html\", \"sx\", \"dom\"], # core spec + adapters\n modules=None, # no signals, engine, orchestration, boot\n extensions=None, # no continuations\n spec_modules=None # no deps, router, cek, frames, page-helpers\n)" "python")) + (~docs/code :src (highlight "# In run_js_sx.py \u2014 core-only build\ncompile_ref_to_js(\n adapters=[\"parser\", \"html\", \"sx\", \"dom\"], # core spec + adapters\n modules=None, # no signals, engine, orchestration, boot\n extensions=None, # no continuations\n spec_modules=None # no deps, router, cek, frames, page-helpers\n)" "python")) (h4 :class "font-semibold mt-4 mb-2" "Web framework loading") (p "Web framework " (code ".sx") " files ship as " (code "\n\n\n" "html")) + (~docs/code :src (highlight "\n\n\n" "html")) (h4 :class "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 :code (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")) + (~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") (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."))) @@ -230,14 +230,14 @@ (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)") - (~docs/code :code (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")) + (~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") - (~docs/code :code (highlight "// In platform.rs\npub enum Value {\n // ... existing variants ...\n Handle(u32), // opaque reference to JS-side object\n}" "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") - (~docs/code :code (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")) + (~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") (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.") @@ -268,7 +268,7 @@ (li "Page is interactive")) (h4 :class "font-semibold mt-4 mb-2" "Library dependency order") - (~docs/code :code (highlight "signals.sx \u2190 no SX deps (uses only core primitives)\ndeps.sx \u2190 no SX deps\nframes.sx \u2190 no SX deps\nrouter.sx \u2190 no SX deps\npage-helpers.sx \u2190 no SX deps\nengine.sx \u2190 uses render.sx (core), adapter-dom.sx (core)\norchestration.sx \u2190 depends on engine.sx\nboot.sx \u2190 depends on orchestration.sx" "text"))) + (~docs/code :src (highlight "signals.sx \u2190 no SX deps (uses only core primitives)\ndeps.sx \u2190 no SX deps\nframes.sx \u2190 no SX deps\nrouter.sx \u2190 no SX deps\npage-helpers.sx \u2190 no SX deps\nengine.sx \u2190 uses render.sx (core), adapter-dom.sx (core)\norchestration.sx \u2190 depends on engine.sx\nboot.sx \u2190 depends on orchestration.sx" "text"))) ;; ----------------------------------------------------------------------- diff --git a/sx/sx/plans/isomorphic.sx b/sx/sx/plans/isomorphic.sx index cc43ea1f..9e59333e 100644 --- a/sx/sx/plans/isomorphic.sx +++ b/sx/sx/plans/isomorphic.sx @@ -50,7 +50,7 @@ (div (h4 :class "font-semibold text-stone-700" "1. Transitive closure (deps.sx)") (p "9 functions that walk the component graph. The core:") - (~docs/code :code (highlight "(define (transitive-deps name env)\n (let ((key (if (starts-with? name \"~\") name\n (concat \"~\" name)))\n (seen (set-create)))\n (transitive-deps-walk key env seen)\n (set-remove seen key)))" "lisp")) + (~docs/code :src (highlight "(define (transitive-deps name env)\n (let ((key (if (starts-with? name \"~\") name\n (concat \"~\" name)))\n (seen (set-create)))\n (transitive-deps-walk key env seen)\n (set-remove seen key)))" "lisp")) (p (code "scan-refs") " walks a component body AST collecting " (code "~") " symbols. " (code "transitive-deps") " follows references recursively through the env. " (code "compute-all-deps") " batch-computes and caches deps for every component. " @@ -58,7 +58,7 @@ (div (h4 :class "font-semibold text-stone-700" "2. Page scanning") - (~docs/code :code (highlight "(define (components-needed page-source env)\n (let ((direct (scan-components-from-source page-source))\n (all-needed (set-create)))\n (for-each (fn (name) ...\n (set-add! all-needed name)\n (set-union! all-needed (component-deps comp)))\n direct)\n all-needed))" "lisp")) + (~docs/code :src (highlight "(define (components-needed page-source env)\n (let ((direct (scan-components-from-source page-source))\n (all-needed (set-create)))\n (for-each (fn (name) ...\n (set-add! all-needed name)\n (set-union! all-needed (component-deps comp)))\n direct)\n all-needed))" "lisp")) (p (code "scan-components-from-source") " finds " (code "(~plans/content-addressed-components/name") " patterns in serialized SX via regex. " (code "components-needed") " combines scanning with the cached transitive closure to produce the minimal component set for a page.")) (div @@ -110,12 +110,12 @@ (div (h4 :class "font-semibold text-stone-700" "1. IO scanning") (p (code "scan-io-refs") " walks an AST node, collecting symbol names that match an IO name set. The IO set is provided by the host from boundary declarations (all three tiers: core IO, deployment IO, page helpers).") - (~docs/code :code (highlight "(define scan-io-refs\n (fn (node io-names)\n (let ((refs (list)))\n (scan-io-refs-walk node io-names refs)\n refs)))" "lisp"))) + (~docs/code :src (highlight "(define scan-io-refs\n (fn (node io-names)\n (let ((refs (list)))\n (scan-io-refs-walk node io-names refs)\n refs)))" "lisp"))) (div (h4 :class "font-semibold text-stone-700" "2. Transitive IO closure") (p (code "transitive-io-refs") " follows component deps recursively, unioning IO refs from all reachable components and macros. Cycle-safe via seen-set.") - (~docs/code :code (highlight "(define transitive-io-refs\n (fn (name env io-names)\n ;; Walk deps, scan each body for IO refs,\n ;; union all refs transitively.\n ...))" "lisp"))) + (~docs/code :src (highlight "(define transitive-io-refs\n (fn (name env io-names)\n ;; Walk deps, scan each body for IO refs,\n ;; union all refs transitively.\n ...))" "lisp"))) (div (h4 :class "font-semibold text-stone-700" "3. Batch computation") @@ -168,13 +168,13 @@ (div (h4 :class "font-semibold text-stone-700" "1. Route matching spec (router.sx)") (p "New spec module with pure functions for Flask-style route pattern matching:") - (~docs/code :code (highlight "(define split-path-segments ;; \"/language/docs/hello\" → (\"docs\" \"hello\")\n(define parse-route-pattern ;; \"/language/docs/\" → segment descriptors\n(define match-route-segments ;; segments + pattern → params dict or nil\n(define find-matching-route ;; path + route table → first match" "lisp")) + (~docs/code :src (highlight "(define split-path-segments ;; \"/language/docs/hello\" → (\"docs\" \"hello\")\n(define parse-route-pattern ;; \"/language/docs/\" → segment descriptors\n(define match-route-segments ;; segments + pattern → params dict or nil\n(define find-matching-route ;; path + route table → first match" "lisp")) (p "No platform interface needed — uses only pure string and list primitives. Bootstrapped to both hosts via " (code "--spec-modules deps,router") ".")) (div (h4 :class "font-semibold text-stone-700" "2. Page registry") (p "Server serializes defpage metadata as SX dict literals inside " (code "" "html")) + (~docs/code :src (highlight "" "html")) (p "The client parses the SX, renders to DOM, and replaces the suspense placeholder's children.")) (div @@ -429,7 +429,7 @@ (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.")) (p "Affinity annotations let component authors express rendering preferences:") - (~docs/code :code (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")) + (~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" @@ -468,7 +468,7 @@ (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.") (p (code "page-render-plan") " in deps.sx computes per-page boundary decisions:") - (~docs/code :code (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/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" @@ -495,7 +495,7 @@ (~docs/subsection :title "Cache Invalidation" (p "Component authors can declare cache invalidation on elements that trigger mutations:") - (~docs/code :code (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")) + (~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" (li (code "SX-Cache-Invalidate: page-name") " — clear cache for a page") diff --git a/sx/sx/plans/js-bootstrapper.sx b/sx/sx/plans/js-bootstrapper.sx index 8cae6e56..81553ea6 100644 --- a/sx/sx/plans/js-bootstrapper.sx +++ b/sx/sx/plans/js-bootstrapper.sx @@ -33,7 +33,7 @@ (~docs/subsection :title "Mode 1: Spec Bootstrapper" (p "Same job as " (code "bootstrap_js.py") ". Read spec " (code ".sx") " files, " "emit " (code "sx-ref.js") ".") - (~docs/code :code (highlight ";; Translate eval.sx to JavaScript + (~docs/code :src (highlight ";; Translate eval.sx to JavaScript (js-translate-file (parse-file \"eval.sx\")) ;; → \"function evalExpr(expr, env) { ... }\" @@ -49,7 +49,7 @@ "Given a component tree that the server has already evaluated (data fetched, " "conditionals resolved, loops expanded), " (code "js.sx") " compiles the " "resulting DOM description into a JavaScript program that builds the same DOM.") - (~docs/code :code (highlight ";; Server evaluates the page (fetches data, expands components) + (~docs/code :src (highlight ";; Server evaluates the page (fetches data, expands components) ;; Result is a resolved SX tree: (div :class \"...\" (h1 \"Hello\") ...) ;; js.sx compiles that tree to standalone JS @@ -247,20 +247,20 @@ (~docs/subsection :title "What Gets Compiled" (p "A resolved SX tree like:") - (~docs/code :code (highlight "(div :class \"container\" + (~docs/code :src (highlight "(div :class \"container\" (h1 \"Hello\") (ul (map (fn (item) (li :class \"item\" (get item \"name\"))) items)))" "lisp")) (p "After server-side evaluation (with " (code "items") " = " (code "[{\"name\": \"Alice\"}, {\"name\": \"Bob\"}]") "):") - (~docs/code :code (highlight "(div :class \"container\" + (~docs/code :src (highlight "(div :class \"container\" (h1 \"Hello\") (ul (li :class \"item\" \"Alice\") (li :class \"item\" \"Bob\")))" "lisp")) (p "Compiles to:") - (~docs/code :code (highlight "var _0 = document.createElement('div'); + (~docs/code :src (highlight "var _0 = document.createElement('div'); _0.className = 'container'; var _1 = document.createElement('h1'); _1.textContent = 'Hello'; @@ -416,7 +416,7 @@ _0.appendChild(_2);" "javascript"))) (li "Runtime slicing: analyze tree → include only necessary runtime modules"))) (~docs/subsection :title "Phase 5: Verification" - (~docs/code :code (highlight "# Mode 1: spec bootstrapper parity + (~docs/code :src (highlight "# Mode 1: spec bootstrapper parity python bootstrap_js.py > sx-ref-g0.js python run_js_sx.py > sx-ref-g1.js diff sx-ref-g0.js sx-ref-g1.js # must be empty diff --git a/sx/sx/plans/live-streaming.sx b/sx/sx/plans/live-streaming.sx index 8a252160..c247f679 100644 --- a/sx/sx/plans/live-streaming.sx +++ b/sx/sx/plans/live-streaming.sx @@ -30,17 +30,17 @@ (~docs/subsection :title "SSE Protocol" (p "A " (code "~live") " component declares a persistent connection to an SSE endpoint:") - (~docs/code :code (highlight "(~live :src \"/api/stream/dashboard\"\n (~shared:pages/suspense :id \"cpu\" :fallback (span \"Loading...\"))\n (~shared:pages/suspense :id \"memory\" :fallback (span \"Loading...\"))\n (~shared:pages/suspense :id \"requests\" :fallback (span \"Loading...\")))" "lisp")) + (~docs/code :src (highlight "(~live :src \"/api/stream/dashboard\"\n (~shared:pages/suspense :id \"cpu\" :fallback (span \"Loading...\"))\n (~shared:pages/suspense :id \"memory\" :fallback (span \"Loading...\"))\n (~shared:pages/suspense :id \"requests\" :fallback (span \"Loading...\")))" "lisp")) (p "The server SSE endpoint yields SX resolve events:") - (~docs/code :code (highlight "async def dashboard_stream():\n while True:\n stats = await get_system_stats()\n yield sx_sse_event(\"cpu\", f'(~stat-badge :value \"{stats.cpu}%\")')\n yield sx_sse_event(\"memory\", f'(~stat-badge :value \"{stats.mem}%\")')\n await asyncio.sleep(1)" "python")) + (~docs/code :src (highlight "async def dashboard_stream():\n while True:\n stats = await get_system_stats()\n yield sx_sse_event(\"cpu\", f'(~stat-badge :value \"{stats.cpu}%\")')\n yield sx_sse_event(\"memory\", f'(~stat-badge :value \"{stats.mem}%\")')\n await asyncio.sleep(1)" "python")) (p "SSE wire format — each event is a suspense resolve:") - (~docs/code :code (highlight "event: sx-resolve\ndata: {\"id\": \"cpu\", \"sx\": \"(~stat-badge :value \\\"42%\\\")\"}\n\nevent: sx-resolve\ndata: {\"id\": \"memory\", \"sx\": \"(~stat-badge :value \\\"68%\\\")\"}" "text"))) + (~docs/code :src (highlight "event: sx-resolve\ndata: {\"id\": \"cpu\", \"sx\": \"(~stat-badge :value \\\"42%\\\")\"}\n\nevent: sx-resolve\ndata: {\"id\": \"memory\", \"sx\": \"(~stat-badge :value \\\"68%\\\")\"}" "text"))) (~docs/subsection :title "WebSocket Protocol" (p "A " (code "~ws") " component establishes a bidirectional channel:") - (~docs/code :code (highlight "(~ws :src \"/ws/chat\"\n :on-message handle-chat-message\n (~shared:pages/suspense :id \"messages\" :fallback (div \"Connecting...\"))\n (~shared:pages/suspense :id \"typing\" :fallback (span)))" "lisp")) + (~docs/code :src (highlight "(~ws :src \"/ws/chat\"\n :on-message handle-chat-message\n (~shared:pages/suspense :id \"messages\" :fallback (div \"Connecting...\"))\n (~shared:pages/suspense :id \"typing\" :fallback (span)))" "lisp")) (p "Client can send SX expressions back:") - (~docs/code :code (highlight ";; Client sends:\n(sx-send ws-conn '(chat-message :text \"hello\" :user \"alice\"))\n\n;; Server receives, broadcasts to all connected clients:\n;; event: sx-resolve for \"messages\" suspense" "lisp"))) + (~docs/code :src (highlight ";; Client sends:\n(sx-send ws-conn '(chat-message :text \"hello\" :user \"alice\"))\n\n;; Server receives, broadcasts to all connected clients:\n;; event: sx-resolve for \"messages\" suspense" "lisp"))) (~docs/subsection :title "Shared Resolution Mechanism" (p "All three transports use the same client-side resolution:") diff --git a/sx/sx/plans/mother-language.sx b/sx/sx/plans/mother-language.sx index d6626d80..740c476f 100644 --- a/sx/sx/plans/mother-language.sx +++ b/sx/sx/plans/mother-language.sx @@ -167,7 +167,7 @@ " + " (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 :code (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")) + (~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") (p "Compile the OCaml output to:") @@ -186,7 +186,7 @@ (h4 :class "font-semibold mt-6 mb-2" "Phase 4: SX linearity checking") (p "Extend " (code "types.sx") " with quantity annotations:") - (~docs/code :code (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")) + (~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.") @@ -200,7 +200,7 @@ (li "OCaml becomes the bootstrap language only \u2014 " "needed once to get the self-hosting loop started, then never touched again")) - (~docs/code :code (highlight ";; The bootstrap chain\n\nStep 0: Python evaluator (existing)\n \u2193 evaluates bootstrap_ml.py\nStep 1: OCaml evaluator (compiled from spec by Python)\n \u2193 evaluates compiler.sx\nStep 2: SX compiler (compiled from .sx by OCaml evaluator)\n \u2193 compiles itself\nStep 3: SX compiler (compiled by itself)\n \u2193 compiles everything\n \u2193 emits native, WASM, JS from .sx spec\n \u2193 OCaml is no longer in the chain" "text")) + (~docs/code :src (highlight ";; The bootstrap chain\n\nStep 0: Python evaluator (existing)\n \u2193 evaluates bootstrap_ml.py\nStep 1: OCaml evaluator (compiled from spec by Python)\n \u2193 evaluates compiler.sx\nStep 2: SX compiler (compiled from .sx by OCaml evaluator)\n \u2193 compiles itself\nStep 3: SX compiler (compiled by itself)\n \u2193 compiles everything\n \u2193 emits native, WASM, JS from .sx spec\n \u2193 OCaml is no longer in the chain" "text")) (p "At Step 3, the only language is SX. The compiler reads " (code ".sx") " and emits machine code. " "OCaml was the scaffolding. The scaffolding comes down.")) @@ -314,7 +314,7 @@ "The client receives it and " (strong "compiles to WASM and executes") ". " "Not interprets. Not dispatches bytecodes. Compiles.") - (~docs/code :code (highlight "Server sends: (defcomp ~card (&key title) (div :class \"card\" (h2 title)))\n\nClient does:\n 1. Parse SX source (fast \u2014 it's s-expressions)\n 2. Hash AST \u2192 CID\n 3. Cache hit? Call the already-compiled WASM function\n 4. Cache miss? Compile to WASM, cache by CID, call it\n\nStep 4 is the JIT. The compiler (itself WASM) emits a WASM\nfunction, instantiates it via WebAssembly.instantiate, caches\nthe module by CID. Next time: direct function call, zero overhead." "text")) + (~docs/code :src (highlight "Server sends: (defcomp ~card (&key title) (div :class \"card\" (h2 title)))\n\nClient does:\n 1. Parse SX source (fast \u2014 it's s-expressions)\n 2. Hash AST \u2192 CID\n 3. Cache hit? Call the already-compiled WASM function\n 4. Cache miss? Compile to WASM, cache by CID, call it\n\nStep 4 is the JIT. The compiler (itself WASM) emits a WASM\nfunction, instantiates it via WebAssembly.instantiate, caches\nthe module by CID. Next time: direct function call, zero overhead." "text")) (p "This is what V8 does with JavaScript. What LuaJIT does with Lua. " "The difference: SX's semantics are simpler (no prototype chains, no " (code "this") @@ -324,7 +324,7 @@ (h4 :class "font-semibold mt-4 mb-2" "The compilation tiers") - (~docs/code :code (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")) + (~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")) (p "Each tier is faster. Tier 1 (bytecodes) is the " (a :href "/sx/(etc.(plan.wasm-bytecode-vm))" "WASM Bytecode VM") @@ -337,7 +337,7 @@ (p "The server can compile too. Instead of sending SX source for the client to JIT, " "send precompiled WASM:") - (~docs/code :code (highlight ";; Option A: send SX source, client JIT compiles\nContent-Type: text/sx\n\n(div :class \"card\" (h2 \"hello\"))\n\n;; Option B: send precompiled WASM, client instantiates directly\nContent-Type: application/wasm\nX-Sx-Cid: bafyrei...\n\n" "text")) + (~docs/code :src (highlight ";; Option A: send SX source, client JIT compiles\nContent-Type: text/sx\n\n(div :class \"card\" (h2 \"hello\"))\n\n;; Option B: send precompiled WASM, client instantiates directly\nContent-Type: application/wasm\nX-Sx-Cid: bafyrei...\n\n" "text")) (p "Option B skips parsing and compilation entirely. The client instantiates " "the WASM module and calls it. The server did all the work.") @@ -346,7 +346,7 @@ (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 :code (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")) + (~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") (p "The entire application can be ahead-of-time compiled to a WASM binary. " @@ -359,7 +359,7 @@ (h4 :class "font-semibold mt-4 mb-2" "The architecture") - (~docs/code :code (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")) + (~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")) (p "The compiler is WASM. The code it produces is WASM. " "It's compiled code all the way down. " @@ -410,19 +410,19 @@ (h4 :class "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 :code (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")) + (~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") (p "The platform scopes capabilities per evaluator instance. " "App shell gets full access. Third-party or user-generated content gets the minimum:") - (~docs/code :code (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")) + (~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") (p "The app ships with a manifest of expected CIDs for its core components. " "Like subresource integrity (SRI) but for compiled code:") - (~docs/code :code (highlight ";; Component manifest (shipped with the app, signed)\n{\n \"~card\": \"bafyrei..abc\"\n \"~header\": \"bafyrei..def\"\n \"~nav-item\": \"bafyrei..ghi\"\n}\n\n;; On navigation: server sends component update\n;; Client compiles \u2192 checks CID against manifest\n;; Match = trusted, execute\n;; Mismatch = tampered, reject and report" "text")) + (~docs/code :src (highlight ";; Component manifest (shipped with the app, signed)\n{\n \"~card\": \"bafyrei..abc\"\n \"~header\": \"bafyrei..def\"\n \"~nav-item\": \"bafyrei..ghi\"\n}\n\n;; On navigation: server sends component update\n;; Client compiles \u2192 checks CID against manifest\n;; Match = trusted, execute\n;; Mismatch = tampered, reject and report" "text")) (p "The security model is " (em "structural") ", not bolt-on. " "WASM isolation, platform capabilities, content-addressed verification, " @@ -444,7 +444,7 @@ (h4 :class "font-semibold mt-4 mb-2" "The rendering pipeline") - (~docs/code :code (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")) + (~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")) (p "The server and client have the " (em "same compiler") " from the " (em "same spec") ". " (code "adapter-html.sx") " produces HTML strings. " @@ -465,7 +465,7 @@ (p "The server can prerender every page to static HTML, hash it, " "and cache it at the CDN edge:") - (~docs/code :code (highlight "Page source CID \u2192 Rendered HTML CID\nbafyrei..source \u2192 bafyrei..html\n\n\u2022 Crawler hits CDN \u2192 instant HTML, no server round-trip\n\u2022 Page content changes \u2192 new source CID \u2192 new HTML CID \u2192 CDN invalidated\n\u2022 Same CID = same HTML forever \u2192 infinite cache, zero revalidation" "text")) + (~docs/code :src (highlight "Page source CID \u2192 Rendered HTML CID\nbafyrei..source \u2192 bafyrei..html\n\n\u2022 Crawler hits CDN \u2192 instant HTML, no server round-trip\n\u2022 Page content changes \u2192 new source CID \u2192 new HTML CID \u2192 CDN invalidated\n\u2022 Same CID = same HTML forever \u2192 infinite cache, zero revalidation" "text")) (p "This is the same content-addressed caching as compiled WASM, " "applied to the HTML output. Both the compiled client code and " @@ -534,11 +534,11 @@ "Tag names, class strings, attribute keys \u2014 all baked into the compiled component. " "The only payload is the data that differs between instances.") - (~docs/code :code (highlight ";; HTML response: ~450 bytes\n

Hello

World

\n\n;; SX source response: ~60 bytes\n(~card :title \"Hello\" :body \"World\")\n\n;; Bytecode response: ~18 bytes\n[CALL_CID bafyrei..card] [STR \"Hello\"] [STR \"World\"]\n\n;; Gzipped bytecode: ~12 bytes" "text")) + (~docs/code :src (highlight ";; HTML response: ~450 bytes\n

Hello

World

\n\n;; SX source response: ~60 bytes\n(~card :title \"Hello\" :body \"World\")\n\n;; Bytecode response: ~18 bytes\n[CALL_CID bafyrei..card] [STR \"Hello\"] [STR \"World\"]\n\n;; Gzipped bytecode: ~12 bytes" "text")) (p "For a list of 50 cards:") - (~docs/code :code (highlight "HTML: 50\u00d7

...

...
~22 KB\nSX source: 50\u00d7 (~card :title \"...\" :body \"...\") ~3 KB\nBytecode: [CALL_CID] + 50\u00d7 [STR, STR] ~800 bytes\nGzipped bytecode: ~400 bytes" "text")) + (~docs/code :src (highlight "HTML: 50\u00d7

...

...
~22 KB\nSX source: 50\u00d7 (~card :title \"...\" :body \"...\") ~3 KB\nBytecode: [CALL_CID] + 50\u00d7 [STR, STR] ~800 bytes\nGzipped bytecode: ~400 bytes" "text")) (p "The markup structure is in the compiled component. " "The class strings are in the compiled component. " @@ -548,7 +548,7 @@ (h4 :class "font-semibold mt-4 mb-2" "Content negotiation") (p "The client advertises what it has via request headers:") - (~docs/code :code (highlight ";; Client tells server what's cached\nAccept: application/sx-bytecode, text/sx, text/html\nX-Sx-Cached-Components: bafyrei..card, bafyrei..header, bafyrei..nav\n\n;; Server picks the smallest response:\n;; - Client has ~card cached? Send bytecode (data only)\n;; - Client has compiler but not ~card? Send SX source\n;; - Client has nothing? Send HTML" "text")) + (~docs/code :src (highlight ";; Client tells server what's cached\nAccept: application/sx-bytecode, text/sx, text/html\nX-Sx-Cached-Components: bafyrei..card, bafyrei..header, bafyrei..nav\n\n;; Server picks the smallest response:\n;; - Client has ~card cached? Send bytecode (data only)\n;; - Client has compiler but not ~card? Send SX source\n;; - Client has nothing? Send HTML" "text")) (p "The server and client negotiate the optimal response format automatically. " "First visit is HTML (full progressive enhancement). " diff --git a/sx/sx/plans/nav-redesign.sx b/sx/sx/plans/nav-redesign.sx index 307ee1cb..c3d59482 100644 --- a/sx/sx/plans/nav-redesign.sx +++ b/sx/sx/plans/nav-redesign.sx @@ -19,7 +19,7 @@ (~docs/subsection :title "Structure" (p "One vertical column, centered. Each level is a row.") - (~docs/code :code (highlight ";; Home (nothing selected)\n;;\n;; [ sx ]\n;;\n;; Docs CSSX Reference Protocols Examples\n;; Essays Philosophy Specs Bootstrappers\n;; Testing Isomorphism Plans Reactive Islands\n\n\n;; Section selected (e.g. Plans)\n;;\n;; [ sx ]\n;;\n;; < Plans >\n;;\n;; Status Reader Macros Theorem Prover\n;; Self-Hosting JS Bootstrapper SX-Activity\n;; Predictive Prefetching Content-Addressed\n;; Environment Images Runtime Slicing Typed SX\n;; Fragment Protocol ...\n\n\n;; Page selected (e.g. Typed SX under Plans)\n;;\n;; [ sx ]\n;;\n;; < Plans >\n;;\n;; < Typed SX >\n;;\n;; [ page content here ]" "lisp"))) + (~docs/code :src (highlight ";; Home (nothing selected)\n;;\n;; [ sx ]\n;;\n;; Docs CSSX Reference Protocols Examples\n;; Essays Philosophy Specs Bootstrappers\n;; Testing Isomorphism Plans Reactive Islands\n\n\n;; Section selected (e.g. Plans)\n;;\n;; [ sx ]\n;;\n;; < Plans >\n;;\n;; Status Reader Macros Theorem Prover\n;; Self-Hosting JS Bootstrapper SX-Activity\n;; Predictive Prefetching Content-Addressed\n;; Environment Images Runtime Slicing Typed SX\n;; Fragment Protocol ...\n\n\n;; Page selected (e.g. Typed SX under Plans)\n;;\n;; [ sx ]\n;;\n;; < Plans >\n;;\n;; < Typed SX >\n;;\n;; [ page content here ]" "lisp"))) (~docs/subsection :title "Rules" (ol :class "list-decimal pl-5 text-stone-700 space-y-2" @@ -65,7 +65,7 @@ (~docs/subsection :title "Arrows" (p "Left and right arrows are inline with the selected item name. They navigate to the previous/next sibling in the current list. Keyboard accessible: left/right arrow keys when the row is focused.") - (~docs/code :code (highlight ";; Arrow rendering\n;;\n;; < Plans >\n;;\n;; < is a link to /plans/content-addressed-components\n;; (the previous sibling in plans-nav-items)\n;; > is a link to /plans/fragment-protocol\n;; (the next sibling)\n;; \"Plans\" is a link to /plans/ (the section index)\n;;\n;; At the edges, the arrow wraps:\n;; first item: < wraps to last\n;; last item: > wraps to first" "lisp"))) + (~docs/code :src (highlight ";; Arrow rendering\n;;\n;; < Plans >\n;;\n;; < is a link to /plans/content-addressed-components\n;; (the previous sibling in plans-nav-items)\n;; > is a link to /plans/fragment-protocol\n;; (the next sibling)\n;; \"Plans\" is a link to /plans/ (the section index)\n;;\n;; At the edges, the arrow wraps:\n;; first item: < wraps to last\n;; last item: > wraps to first" "lisp"))) (~docs/subsection :title "Transitions" (p "Selecting an item: the list fades/collapses, the selected item moves to breadcrumb position, children appear below. This is an L0 morph — the server renders the new state, the client morphs. No JS animation library needed, just CSS transitions on the morph targets.") @@ -78,11 +78,11 @@ (~docs/section :title "Data Model" :id "data" (p "The current nav data is flat — each section has its own " (code "define") ". The new model is a single tree:") - (~docs/code :code (highlight "(define sx-nav-tree\n {:label \"sx\"\n :href \"/\"\n :children (list\n {:label \"Docs\"\n :href \"/language/docs/introduction\"\n :children docs-nav-items}\n {:label \"CSSX\"\n :href \"/applications/cssx/\"\n :children cssx-nav-items}\n {:label \"Reference\"\n :href \"/reference/\"\n :children reference-nav-items}\n {:label \"Protocols\"\n :href \"/applications/protocols/wire-format\"\n :children protocols-nav-items}\n {:label \"Examples\"\n :href \"/examples/click-to-load\"\n :children examples-nav-items}\n {:label \"Essays\"\n :href \"/etc/essays/\"\n :children essays-nav-items}\n {:label \"Philosophy\"\n :href \"/etc/philosophy/sx-manifesto\"\n :children philosophy-nav-items}\n {:label \"Specs\"\n :href \"/language/specs/\"\n :children specs-nav-items}\n {:label \"Bootstrappers\"\n :href \"/language/bootstrappers/\"\n :children bootstrappers-nav-items}\n {:label \"Testing\"\n :href \"/language/testing/\"\n :children testing-nav-items}\n {:label \"Isomorphism\"\n :href \"/geography/isomorphism/\"\n :children isomorphism-nav-items}\n {:label \"Plans\"\n :href \"/etc/plans/\"\n :children plans-nav-items}\n {:label \"Reactive Islands\"\n :href \"/reactive-islands/\"\n :children reactive-islands-nav-items})})" "lisp")) + (~docs/code :src (highlight "(define sx-nav-tree\n {:label \"sx\"\n :href \"/\"\n :children (list\n {:label \"Docs\"\n :href \"/language/docs/introduction\"\n :children docs-nav-items}\n {:label \"CSSX\"\n :href \"/applications/cssx/\"\n :children cssx-nav-items}\n {:label \"Reference\"\n :href \"/reference/\"\n :children reference-nav-items}\n {:label \"Protocols\"\n :href \"/applications/protocols/wire-format\"\n :children protocols-nav-items}\n {:label \"Examples\"\n :href \"/examples/click-to-load\"\n :children examples-nav-items}\n {:label \"Essays\"\n :href \"/etc/essays/\"\n :children essays-nav-items}\n {:label \"Philosophy\"\n :href \"/etc/philosophy/sx-manifesto\"\n :children philosophy-nav-items}\n {:label \"Specs\"\n :href \"/language/specs/\"\n :children specs-nav-items}\n {:label \"Bootstrappers\"\n :href \"/language/bootstrappers/\"\n :children bootstrappers-nav-items}\n {:label \"Testing\"\n :href \"/language/testing/\"\n :children testing-nav-items}\n {:label \"Isomorphism\"\n :href \"/geography/isomorphism/\"\n :children isomorphism-nav-items}\n {:label \"Plans\"\n :href \"/etc/plans/\"\n :children plans-nav-items}\n {:label \"Reactive Islands\"\n :href \"/reactive-islands/\"\n :children reactive-islands-nav-items})})" "lisp")) (p "The existing per-section lists (" (code "docs-nav-items") ", " (code "plans-nav-items") ", etc.) remain unchanged — they just become the " (code ":children") " of tree nodes. Sub-sections that have their own sub-items can nest further:") - (~docs/code :code (highlight ";; Future: deeper nesting\n{:label \"Plans\"\n :href \"/etc/plans/\"\n :children (list\n {:label \"Status\" :href \"/etc/plans/status\"}\n {:label \"Bootstrappers\" :href \"/etc/plans/self-hosting-bootstrapper\"\n :children (list\n {:label \"py.sx\" :href \"/etc/plans/self-hosting-bootstrapper\"}\n {:label \"js.sx\" :href \"/etc/plans/js-bootstrapper\"})}\n ;; ...\n )}" "lisp")) + (~docs/code :src (highlight ";; Future: deeper nesting\n{:label \"Plans\"\n :href \"/etc/plans/\"\n :children (list\n {:label \"Status\" :href \"/etc/plans/status\"}\n {:label \"Bootstrappers\" :href \"/etc/plans/self-hosting-bootstrapper\"\n :children (list\n {:label \"py.sx\" :href \"/etc/plans/self-hosting-bootstrapper\"}\n {:label \"js.sx\" :href \"/etc/plans/js-bootstrapper\"})}\n ;; ...\n )}" "lisp")) (p "The tree depth is unlimited. The nav component recurses.")) @@ -94,19 +94,19 @@ (p "Three new components replace the entire menu bar system:") (~docs/subsection :title "~plans/nav-redesign/logo" - (~docs/code :code (highlight "(defcomp ~plans/nav-redesign/logo ()\n (a :href \"/\"\n :sx-get \"/\" :sx-target \"#main-panel\" :sx-select \"#main-panel\"\n :sx-swap \"outerHTML\" :sx-push-url \"true\"\n :class \"block text-center py-4\"\n (span :class \"text-2xl font-bold text-violet-700\" \"sx\")))" "lisp")) + (~docs/code :src (highlight "(defcomp ~plans/nav-redesign/logo ()\n (a :href \"/\"\n :sx-get \"/\" :sx-target \"#main-panel\" :sx-select \"#main-panel\"\n :sx-swap \"outerHTML\" :sx-push-url \"true\"\n :class \"block text-center py-4\"\n (span :class \"text-2xl font-bold text-violet-700\" \"sx\")))" "lisp")) (p "Always at the top. Always centered. The anchor.")) (~docs/subsection :title "~plans/nav-redesign/nav-breadcrumb" - (~docs/code :code (highlight "(defcomp ~plans/nav-redesign/nav-breadcrumb (&key path siblings level)\n ;; Renders one breadcrumb row: < Label >\n ;; path = the nav tree node for this level\n ;; siblings = list of sibling nodes (for arrow nav)\n ;; level = depth (controls text size/color)\n (let ((idx (find-index siblings path))\n (prev (nth siblings (mod (- idx 1) (len siblings))))\n (next (nth siblings (mod (+ idx 1) (len siblings)))))\n (div :class (str \"flex items-center justify-center gap-3 py-1\"\n (nav-level-classes level))\n (a :href (get prev \"href\")\n :sx-get (get prev \"href\") :sx-target \"#main-panel\"\n :sx-select \"#main-panel\" :sx-swap \"outerHTML\"\n :sx-push-url \"true\"\n :class \"text-stone-400 hover:text-violet-600\"\n :aria-label \"Previous\"\n \"<\")\n (a :href (get path \"href\")\n :sx-get (get path \"href\") :sx-target \"#main-panel\"\n :sx-select \"#main-panel\" :sx-swap \"outerHTML\"\n :sx-push-url \"true\"\n :class \"font-medium\"\n (get path \"label\"))\n (a :href (get next \"href\")\n :sx-get (get next \"href\") :sx-target \"#main-panel\"\n :sx-select \"#main-panel\" :sx-swap \"outerHTML\"\n :sx-push-url \"true\"\n :class \"text-stone-400 hover:text-violet-600\"\n :aria-label \"Next\"\n \">\"))))" "lisp")) + (~docs/code :src (highlight "(defcomp ~plans/nav-redesign/nav-breadcrumb (&key path siblings level)\n ;; Renders one breadcrumb row: < Label >\n ;; path = the nav tree node for this level\n ;; siblings = list of sibling nodes (for arrow nav)\n ;; level = depth (controls text size/color)\n (let ((idx (find-index siblings path))\n (prev (nth siblings (mod (- idx 1) (len siblings))))\n (next (nth siblings (mod (+ idx 1) (len siblings)))))\n (div :class (str \"flex items-center justify-center gap-3 py-1\"\n (nav-level-classes level))\n (a :href (get prev \"href\")\n :sx-get (get prev \"href\") :sx-target \"#main-panel\"\n :sx-select \"#main-panel\" :sx-swap \"outerHTML\"\n :sx-push-url \"true\"\n :class \"text-stone-400 hover:text-violet-600\"\n :aria-label \"Previous\"\n \"<\")\n (a :href (get path \"href\")\n :sx-get (get path \"href\") :sx-target \"#main-panel\"\n :sx-select \"#main-panel\" :sx-swap \"outerHTML\"\n :sx-push-url \"true\"\n :class \"font-medium\"\n (get path \"label\"))\n (a :href (get next \"href\")\n :sx-get (get next \"href\") :sx-target \"#main-panel\"\n :sx-select \"#main-panel\" :sx-swap \"outerHTML\"\n :sx-push-url \"true\"\n :class \"text-stone-400 hover:text-violet-600\"\n :aria-label \"Next\"\n \">\"))))" "lisp")) (p "One row per selected level. Shows the current node with left/right arrows to siblings.")) (~docs/subsection :title "~plans/nav-redesign/nav-list" - (~docs/code :code (highlight "(defcomp ~plans/nav-redesign/nav-list (&key items level)\n ;; Renders a wrapped list of links — the children of the current level\n (div :class (str \"flex flex-wrap justify-center gap-x-4 gap-y-2 py-2\"\n (nav-level-classes level))\n (map (fn (item)\n (a :href (get item \"href\")\n :sx-get (get item \"href\") :sx-target \"#main-panel\"\n :sx-select \"#main-panel\" :sx-swap \"outerHTML\"\n :sx-push-url \"true\"\n :class \"hover:text-violet-700 transition-colors\"\n (get item \"label\")))\n items)))" "lisp")) + (~docs/code :src (highlight "(defcomp ~plans/nav-redesign/nav-list (&key items level)\n ;; Renders a wrapped list of links — the children of the current level\n (div :class (str \"flex flex-wrap justify-center gap-x-4 gap-y-2 py-2\"\n (nav-level-classes level))\n (map (fn (item)\n (a :href (get item \"href\")\n :sx-get (get item \"href\") :sx-target \"#main-panel\"\n :sx-select \"#main-panel\" :sx-swap \"outerHTML\"\n :sx-push-url \"true\"\n :class \"hover:text-violet-700 transition-colors\"\n (get item \"label\")))\n items)))" "lisp")) (p "The children of the current level, rendered as a centered wrapped list of plain links.")) (~docs/subsection :title "~plans/nav-redesign/nav — the composition" - (~docs/code :code (highlight "(defcomp ~plans/nav-redesign/nav (&key trail children-items level)\n ;; trail = list of {node, siblings} from root to current\n ;; children-items = children of the deepest selected node\n ;; level = depth of children\n (div :class \"max-w-3xl mx-auto px-4\"\n ;; Logo\n (~plans/nav-redesign/logo)\n ;; Breadcrumb trail (one row per selected ancestor)\n (map-indexed (fn (i crumb)\n (~nav-breadcrumb\n :path (get crumb \"node\")\n :siblings (get crumb \"siblings\")\n :level (+ i 1)))\n trail)\n ;; Children of the deepest selected node\n (when children-items\n (~plans/nav-redesign/nav-list :items children-items :level level))))" "lisp")) + (~docs/code :src (highlight "(defcomp ~plans/nav-redesign/nav (&key trail children-items level)\n ;; trail = list of {node, siblings} from root to current\n ;; children-items = children of the deepest selected node\n ;; level = depth of children\n (div :class \"max-w-3xl mx-auto px-4\"\n ;; Logo\n (~plans/nav-redesign/logo)\n ;; Breadcrumb trail (one row per selected ancestor)\n (map-indexed (fn (i crumb)\n (~nav-breadcrumb\n :path (get crumb \"node\")\n :siblings (get crumb \"siblings\")\n :level (+ i 1)))\n trail)\n ;; Children of the deepest selected node\n (when children-items\n (~plans/nav-redesign/nav-list :items children-items :level level))))" "lisp")) (p "That's the entire navigation. Three small components composed. No bars, no dropdowns, no mobile variants."))) ;; ----------------------------------------------------------------------- @@ -116,7 +116,7 @@ (~docs/section :title "Path Resolution" :id "resolution" (p "Given a URL path, compute the breadcrumb trail and children. This is a tree walk:") - (~docs/code :code (highlight "(define resolve-nav-path\n (fn (tree current-href)\n ;; Walk sx-nav-tree, find the node matching current-href,\n ;; return the trail of ancestors + current children.\n ;;\n ;; Returns: {:trail (list of {:node N :siblings S})\n ;; :children (list) or nil\n ;; :depth number}\n ;;\n ;; Example: current-href = \"/etc/plans/typed-sx\"\n ;; → trail: [{:node Plans :siblings [Docs, CSSX, ...]}\n ;; {:node Typed-SX :siblings [Status, Reader-Macros, ...]}]\n ;; → children: nil (leaf node)\n ;; → depth: 2\n (let ((result (walk-nav-tree tree current-href (list))))\n result)))" "lisp")) + (~docs/code :src (highlight "(define resolve-nav-path\n (fn (tree current-href)\n ;; Walk sx-nav-tree, find the node matching current-href,\n ;; return the trail of ancestors + current children.\n ;;\n ;; Returns: {:trail (list of {:node N :siblings S})\n ;; :children (list) or nil\n ;; :depth number}\n ;;\n ;; Example: current-href = \"/etc/plans/typed-sx\"\n ;; → trail: [{:node Plans :siblings [Docs, CSSX, ...]}\n ;; {:node Typed-SX :siblings [Status, Reader-Macros, ...]}]\n ;; → children: nil (leaf node)\n ;; → depth: 2\n (let ((result (walk-nav-tree tree current-href (list))))\n result)))" "lisp")) (p "This runs server-side (it's a pure function, no IO). The layout component calls it with the current URL and passes the result to " (code "~plans/nav-redesign/nav") ". Same pattern as the current " (code "find-current") " but produces a richer result.") @@ -181,11 +181,11 @@ (~docs/section :title "Layout Simplification" :id "layout" (p "The defpage layout declarations currently specify section, sub-label, sub-href, sub-nav, selected — five params to configure two menu bars. The new layout takes one param: the nav trail.") - (~docs/code :code (highlight ";; Current (verbose, configures two bars)\n(defpage plan-page\n :path \"/etc/plans/\"\n :layout (:sx-section\n :section \"Plans\"\n :sub-label \"Plans\"\n :sub-href \"/etc/plans/\"\n :sub-nav (~nav-data/section-nav :items plans-nav-items\n :current (find-current plans-nav-items slug))\n :selected (or (find-current plans-nav-items slug) \"\"))\n :content (...))\n\n;; New (one param, nav computed from URL)\n(defpage plan-page\n :path \"/etc/plans/\"\n :layout (:sx-docs :path (str \"/etc/plans/\" slug))\n :content (...))" "lisp")) + (~docs/code :src (highlight ";; Current (verbose, configures two bars)\n(defpage plan-page\n :path \"/etc/plans/\"\n :layout (:sx-section\n :section \"Plans\"\n :sub-label \"Plans\"\n :sub-href \"/etc/plans/\"\n :sub-nav (~nav-data/section-nav :items plans-nav-items\n :current (find-current plans-nav-items slug))\n :selected (or (find-current plans-nav-items slug) \"\"))\n :content (...))\n\n;; New (one param, nav computed from URL)\n(defpage plan-page\n :path \"/etc/plans/\"\n :layout (:sx-docs :path (str \"/etc/plans/\" slug))\n :content (...))" "lisp")) (p "The layout component computes the nav trail internally from the path and the nav tree. No more passing section names, sub-labels, or pre-built nav components through layout params.") - (~docs/code :code (highlight "(defcomp ~plans/nav-redesign/docs-layout-full (&key path)\n (let ((nav-state (resolve-nav-path sx-nav-tree path)))\n (<> (~root-header-auto)\n (~sx-nav\n :trail (get nav-state \"trail\")\n :children-items (get nav-state \"children\")\n :level (get nav-state \"depth\")))))\n\n(defcomp ~plans/nav-redesign/docs-layout-oob (&key path)\n (let ((nav-state (resolve-nav-path sx-nav-tree path)))\n (<> (~oob-nav\n :trail (get nav-state \"trail\")\n :children-items (get nav-state \"children\")\n :level (get nav-state \"depth\"))\n (~root-header-auto true))))" "lisp")) + (~docs/code :src (highlight "(defcomp ~plans/nav-redesign/docs-layout-full (&key path)\n (let ((nav-state (resolve-nav-path sx-nav-tree path)))\n (<> (~root-header-auto)\n (~sx-nav\n :trail (get nav-state \"trail\")\n :children-items (get nav-state \"children\")\n :level (get nav-state \"depth\")))))\n\n(defcomp ~plans/nav-redesign/docs-layout-oob (&key path)\n (let ((nav-state (resolve-nav-path sx-nav-tree path)))\n (<> (~oob-nav\n :trail (get nav-state \"trail\")\n :children-items (get nav-state \"children\")\n :level (get nav-state \"depth\"))\n (~root-header-auto true))))" "lisp")) (p "Two layout components instead of twelve. Every defpage in docs.sx simplifies from five layout params to one.")) diff --git a/sx/sx/plans/predictive-prefetch.sx b/sx/sx/plans/predictive-prefetch.sx index 36ba0b9e..66ca35fe 100644 --- a/sx/sx/plans/predictive-prefetch.sx +++ b/sx/sx/plans/predictive-prefetch.sx @@ -96,17 +96,17 @@ (~docs/subsection :title "Eager Bundle" (p "The server already computes per-page component bundles. For key navigation paths — the main nav bar, section nav — the server can include " (em "linked routes' components") " in the initial bundle, not just the current page's.") - (~docs/code :code (highlight ";; defpage metadata declares eager prefetch targets\n(defpage docs-page\n :path \"/language/docs/\"\n :auth :public\n :prefetch :eager ;; bundle deps for all linked pure routes\n :content (case slug ...))" "lisp")) + (~docs/code :src (highlight ";; defpage metadata declares eager prefetch targets\n(defpage docs-page\n :path \"/language/docs/\"\n :auth :public\n :prefetch :eager ;; bundle deps for all linked pure routes\n :content (case slug ...))" "lisp")) (p "Implementation: " (code "components_for_page()") " already scans the page SX for component refs. Extend it to also scan for " (code "href") " attributes, match them against the page registry, and include those pages' deps in the bundle. The cost is a larger initial payload; the benefit is zero-latency navigation within a section.")) (~docs/subsection :title "Idle Timer" (p "After page load and initial render, use " (code "requestIdleCallback") " (or a fallback " (code "setTimeout") ") to scan visible nav links and batch-prefetch their missing components in a single request.") - (~docs/code :code (highlight "(define prefetch-visible-links-on-idle\n (fn ()\n (request-idle-callback\n (fn ()\n (let ((links (dom-query-all \"a[href][sx-get]\"))\n (all-missing (list)))\n (for-each\n (fn (link)\n (let ((missing (compute-missing-deps\n (url-pathname (dom-get-attr link \"href\")))))\n (when missing\n (for-each (fn (d) (append! all-missing d))\n missing))))\n links)\n (when (not (empty? all-missing))\n (prefetch-components (dedupe all-missing))))))))" "lisp")) + (~docs/code :src (highlight "(define prefetch-visible-links-on-idle\n (fn ()\n (request-idle-callback\n (fn ()\n (let ((links (dom-query-all \"a[href][sx-get]\"))\n (all-missing (list)))\n (for-each\n (fn (link)\n (let ((missing (compute-missing-deps\n (url-pathname (dom-get-attr link \"href\")))))\n (when missing\n (for-each (fn (d) (append! all-missing d))\n missing))))\n links)\n (when (not (empty? all-missing))\n (prefetch-components (dedupe all-missing))))))))" "lisp")) (p "Called once from " (code "boot-init") " after initial processing. Batches all missing deps into one network request. Low priority — browser handles it when idle.")) (~docs/subsection :title "Mouse Approach (Trajectory Prediction)" (p "Don't wait for the cursor to reach the link — predict where it's heading. Track the last few " (code "mousemove") " events, extrapolate the trajectory, and if it points toward a link, start prefetching before the hover event fires.") - (~docs/code :code (highlight "(define bind-approach-prefetch\n (fn (container)\n ;; Track mouse trajectory within a nav container.\n ;; On each mousemove, extrapolate position ~200ms ahead.\n ;; If projected point intersects a link's bounding box,\n ;; prefetch that link's route deps.\n (let ((last-x 0) (last-y 0) (last-t 0)\n (prefetched (dict)))\n (dom-add-listener container \"mousemove\"\n (fn (e)\n (let ((now (timestamp))\n (dt (- now last-t)))\n (when (> dt 16) ;; ~60fps throttle\n (let ((vx (/ (- (event-x e) last-x) dt))\n (vy (/ (- (event-y e) last-y) dt))\n (px (+ (event-x e) (* vx 200)))\n (py (+ (event-y e) (* vy 200)))\n (target (dom-element-at-point px py)))\n (when (and target (dom-has-attr? target \"href\")\n (not (get prefetched\n (dom-get-attr target \"href\"))))\n (let ((href (dom-get-attr target \"href\")))\n (set! prefetched\n (merge prefetched {href true}))\n (prefetch-route-deps\n (url-pathname href)))))\n (set! last-x (event-x e))\n (set! last-y (event-y e))\n (set! last-t now))))))))" "lisp")) + (~docs/code :src (highlight "(define bind-approach-prefetch\n (fn (container)\n ;; Track mouse trajectory within a nav container.\n ;; On each mousemove, extrapolate position ~200ms ahead.\n ;; If projected point intersects a link's bounding box,\n ;; prefetch that link's route deps.\n (let ((last-x 0) (last-y 0) (last-t 0)\n (prefetched (dict)))\n (dom-add-listener container \"mousemove\"\n (fn (e)\n (let ((now (timestamp))\n (dt (- now last-t)))\n (when (> dt 16) ;; ~60fps throttle\n (let ((vx (/ (- (event-x e) last-x) dt))\n (vy (/ (- (event-y e) last-y) dt))\n (px (+ (event-x e) (* vx 200)))\n (py (+ (event-y e) (* vy 200)))\n (target (dom-element-at-point px py)))\n (when (and target (dom-has-attr? target \"href\")\n (not (get prefetched\n (dom-get-attr target \"href\"))))\n (let ((href (dom-get-attr target \"href\")))\n (set! prefetched\n (merge prefetched {href true}))\n (prefetch-route-deps\n (url-pathname href)))))\n (set! last-x (event-x e))\n (set! last-y (event-y e))\n (set! last-t now))))))))" "lisp")) (p "This is the most speculative strategy — best suited for dense navigation areas (section sidebars, nav bars) where the cursor trajectory is a strong predictor. The " (code "prefetched") " dict prevents duplicate fetches within the same container interaction.")) (~docs/subsection :title "Components + Data (Hybrid Prefetch)" @@ -118,12 +118,12 @@ (li "Server returns " (em "only data") " (JSON or SX bindings), not the full rendered page") (li "Client evaluates the content expression with prefetched components + fetched data") (li "Result: faster than full server render, no redundant component transfer")) - (~docs/code :code (highlight ";; Declarative: prefetch components, fetch data on click\n(defpage reference-page\n :path \"/reference/\"\n :auth :public\n :prefetch :components ;; prefetch components, data stays server-fetched\n :data (reference-data slug)\n :content (~reference/attrs-content :attrs attrs))\n\n;; On click, client-side flow:\n;; 1. Components already prefetched (from hover/idle)\n;; 2. GET /reference/attributes → server returns data bindings\n;; 3. Client evals (reference-data slug) result + content expr\n;; 4. Renders locally with cached components" "lisp")) + (~docs/code :src (highlight ";; Declarative: prefetch components, fetch data on click\n(defpage reference-page\n :path \"/reference/\"\n :auth :public\n :prefetch :components ;; prefetch components, data stays server-fetched\n :data (reference-data slug)\n :content (~reference/attrs-content :attrs attrs))\n\n;; On click, client-side flow:\n;; 1. Components already prefetched (from hover/idle)\n;; 2. GET /reference/attributes → server returns data bindings\n;; 3. Client evals (reference-data slug) result + content expr\n;; 4. Renders locally with cached components" "lisp")) (p "This is a stepping stone toward full Phase 4 (client IO bridge) of the isomorphic roadmap — it achieves partial client rendering for data pages without needing a general-purpose client async evaluator. The server is a data service, the client is the renderer.")) (~docs/subsection :title "Declarative Configuration" (p "All strategies configured via " (code "defpage") " metadata and " (code "sx-prefetch") " attributes on links/containers:") - (~docs/code :code (highlight ";; Page-level: what to prefetch for routes linking TO this page\n(defpage docs-page\n :path \"/language/docs/\"\n :prefetch :eager) ;; bundle with linking page\n\n(defpage reference-page\n :path \"/reference/\"\n :prefetch :components) ;; prefetch components, data on click\n\n;; Link-level: override per-link\n(a :href \"/language/docs/components\"\n :sx-prefetch \"idle\") ;; prefetch after page idle\n\n;; Container-level: approach prediction for nav areas\n(nav :sx-prefetch \"approach\"\n (a :href \"/language/docs/\") (a :href \"/reference/\") ...)" "lisp")) + (~docs/code :src (highlight ";; Page-level: what to prefetch for routes linking TO this page\n(defpage docs-page\n :path \"/language/docs/\"\n :prefetch :eager) ;; bundle with linking page\n\n(defpage reference-page\n :path \"/reference/\"\n :prefetch :components) ;; prefetch components, data on click\n\n;; Link-level: override per-link\n(a :href \"/language/docs/components\"\n :sx-prefetch \"idle\") ;; prefetch after page idle\n\n;; Container-level: approach prediction for nav areas\n(nav :sx-prefetch \"approach\"\n (a :href \"/language/docs/\") (a :href \"/reference/\") ...)" "lisp")) (p "Priority cascade: explicit " (code "sx-prefetch") " on link > " (code ":prefetch") " on target defpage > default (hover). The system never prefetches the same components twice — " (code "_prefetch-pending") " and " (code "loaded-component-names") " handle dedup."))) ;; ----------------------------------------------------------------------- @@ -136,7 +136,7 @@ (~docs/subsection :title "Phase 1: Component Fetch Endpoint (Python)" (p "A new " (strong "public") " endpoint (not " (code "/internal/") " — the client's browser calls it) that returns component definitions by name.") - (~docs/code :code (highlight "GET //sx/components?names=~plans/predictive-prefetch/card,~essay-foo\n\nResponse (text/sx):\n(defcomp ~plans/predictive-prefetch/card (&key title &rest children)\n (div :class \"border rounded p-4\" (h2 title) children))\n(defcomp ~plans/predictive-prefetch/essay-foo (&key id)\n (div (~plans/predictive-prefetch/card :title id)))" "http")) + (~docs/code :src (highlight "GET //sx/components?names=~plans/predictive-prefetch/card,~essay-foo\n\nResponse (text/sx):\n(defcomp ~plans/predictive-prefetch/card (&key title &rest children)\n (div :class \"border rounded p-4\" (h2 title) children))\n(defcomp ~plans/predictive-prefetch/essay-foo (&key id)\n (div (~plans/predictive-prefetch/card :title id)))" "http")) (p "The server resolves transitive deps via " (code "deps.py") ", subtracts anything listed in the " (code "SX-Components") " request header (already loaded), serializes and returns. This is essentially " (code "components_for_request()") " driven by an explicit " (code "?names=") " param.") (p "Cache-friendly: the response is a pure function of component hash + requested names. " (code "Cache-Control: public, max-age=3600") " with the component hash as ETag.")) @@ -147,41 +147,41 @@ (div (h4 :class "font-semibold text-stone-700" "1. compute-missing-deps") (p "Given a pathname, find the page, return dep names not in " (code "loaded-component-names") ". Returns nil if page not found or has data (can't client-route anyway).") - (~docs/code :code (highlight "(define compute-missing-deps\n (fn (pathname)\n (let ((match (find-matching-route pathname _page-routes)))\n (when (and match (not (get match \"has-data\")))\n (let ((deps (or (get match \"deps\") (list)))\n (loaded (loaded-component-names)))\n (filter (fn (d) (not (contains? loaded d))) deps))))))" "lisp"))) + (~docs/code :src (highlight "(define compute-missing-deps\n (fn (pathname)\n (let ((match (find-matching-route pathname _page-routes)))\n (when (and match (not (get match \"has-data\")))\n (let ((deps (or (get match \"deps\") (list)))\n (loaded (loaded-component-names)))\n (filter (fn (d) (not (contains? loaded d))) deps))))))" "lisp"))) (div (h4 :class "font-semibold text-stone-700" "2. prefetch-components") (p "Fetch component definitions from the server for a list of names. Deduplicates in-flight requests. On success, parses and registers the returned definitions into the component env.") - (~docs/code :code (highlight "(define _prefetch-pending (dict))\n\n(define prefetch-components\n (fn (names)\n (let ((key (join \",\" (sort names))))\n (when (not (get _prefetch-pending key))\n (set! _prefetch-pending\n (merge _prefetch-pending {key true}))\n (fetch-components-from-server names\n (fn (sx-text)\n (sx-process-component-text sx-text)\n (dict-remove! _prefetch-pending key)))))))" "lisp"))) + (~docs/code :src (highlight "(define _prefetch-pending (dict))\n\n(define prefetch-components\n (fn (names)\n (let ((key (join \",\" (sort names))))\n (when (not (get _prefetch-pending key))\n (set! _prefetch-pending\n (merge _prefetch-pending {key true}))\n (fetch-components-from-server names\n (fn (sx-text)\n (sx-process-component-text sx-text)\n (dict-remove! _prefetch-pending key)))))))" "lisp"))) (div (h4 :class "font-semibold text-stone-700" "3. prefetch-route-deps") (p "High-level composition: compute missing deps for a route, fetch if any.") - (~docs/code :code (highlight "(define prefetch-route-deps\n (fn (pathname)\n (let ((missing (compute-missing-deps pathname)))\n (when (and missing (not (empty? missing)))\n (log-info (str \"sx:prefetch \"\n (len missing) \" components for \" pathname))\n (prefetch-components missing)))))" "lisp"))) + (~docs/code :src (highlight "(define prefetch-route-deps\n (fn (pathname)\n (let ((missing (compute-missing-deps pathname)))\n (when (and missing (not (empty? missing)))\n (log-info (str \"sx:prefetch \"\n (len missing) \" components for \" pathname))\n (prefetch-components missing)))))" "lisp"))) (div (h4 :class "font-semibold text-stone-700" "4. Trigger: link hover") (p "On mouseover of a boosted link, prefetch its route's missing components. Debounced 150ms to avoid fetching on quick mouse-throughs.") - (~docs/code :code (highlight "(define bind-prefetch-on-hover\n (fn (link)\n (let ((timer nil))\n (dom-add-listener link \"mouseover\"\n (fn (e)\n (clear-timeout timer)\n (set! timer (set-timeout\n (fn () (prefetch-route-deps\n (url-pathname (dom-get-attr link \"href\"))))\n 150))))\n (dom-add-listener link \"mouseout\"\n (fn (e) (clear-timeout timer))))))" "lisp"))) + (~docs/code :src (highlight "(define bind-prefetch-on-hover\n (fn (link)\n (let ((timer nil))\n (dom-add-listener link \"mouseover\"\n (fn (e)\n (clear-timeout timer)\n (set! timer (set-timeout\n (fn () (prefetch-route-deps\n (url-pathname (dom-get-attr link \"href\"))))\n 150))))\n (dom-add-listener link \"mouseout\"\n (fn (e) (clear-timeout timer))))))" "lisp"))) (div (h4 :class "font-semibold text-stone-700" "5. Trigger: viewport intersection (opt-in)") (p "More aggressive strategy: when a link scrolls into view, prefetch its route's deps. Opt-in via " (code "sx-prefetch=\"visible\"") " attribute.") - (~docs/code :code (highlight "(define bind-prefetch-on-visible\n (fn (link)\n (observe-intersection link\n (fn () (prefetch-route-deps\n (url-pathname (dom-get-attr link \"href\"))))\n true 0)))" "lisp"))) + (~docs/code :src (highlight "(define bind-prefetch-on-visible\n (fn (link)\n (observe-intersection link\n (fn () (prefetch-route-deps\n (url-pathname (dom-get-attr link \"href\"))))\n true 0)))" "lisp"))) (div (h4 :class "font-semibold text-stone-700" "6. Integration into process-elements") (p "During the existing hydration pass, for each boosted link:") - (~docs/code :code (highlight ";; In process-elements, after binding boost behavior:\n(when (and (should-boost-link? link)\n (dom-get-attr link \"href\"))\n (bind-prefetch-on-hover link))\n\n;; Explicit viewport prefetch:\n(when (dom-has-attr? link \"sx-prefetch\")\n (bind-prefetch-on-visible link))" "lisp"))))) + (~docs/code :src (highlight ";; In process-elements, after binding boost behavior:\n(when (and (should-boost-link? link)\n (dom-get-attr link \"href\"))\n (bind-prefetch-on-hover link))\n\n;; Explicit viewport prefetch:\n(when (dom-has-attr? link \"sx-prefetch\")\n (bind-prefetch-on-visible link))" "lisp"))))) (~docs/subsection :title "Phase 3: Boundary Declaration" (p "Two new IO primitives in " (code "boundary.sx") " (browser-only):") - (~docs/code :code (highlight ";; IO primitives (browser-only)\n(io fetch-components-from-server (names callback) -> void)\n(io sx-process-component-text (sx-text) -> void)" "lisp")) + (~docs/code :src (highlight ";; IO primitives (browser-only)\n(io fetch-components-from-server (names callback) -> void)\n(io sx-process-component-text (sx-text) -> void)" "lisp")) (p "These are thin wrappers around " (code "fetch()") " + the existing component script processing logic already in the boundary adapter.")) (~docs/subsection :title "Phase 4: Bootstrap" (p (code "bootstrap_js.py") " picks up the new functions from the spec and emits them into " (code "sx-browser.js") ". The two new boundary IO functions get implemented in the JS boundary adapter — the hand-written glue code that the bootstrapper doesn't generate.") - (~docs/code :code (highlight "// fetch-components-from-server: calls the endpoint\nfunction fetchComponentsFromServer(names, callback) {\n const url = `${routePrefix}/sx/components?names=${names.join(\",\")}`;\n const headers = {\n \"SX-Components\": loadedComponentNames().join(\",\")\n };\n fetch(url, { headers })\n .then(r => r.ok ? r.text() : \"\")\n .then(text => callback(text))\n .catch(() => {}); // silent fail — prefetch is best-effort\n}\n\n// sx-process-component-text: parse defcomp/defmacro into env\nfunction sxProcessComponentText(sxText) {\n if (!sxText) return;\n const frag = document.createElement(\"div\");\n frag.innerHTML =\n `\n;;\n;; L2 page (reactive island):\n;; \n;; \n;;\n;; Client-side navigation from L0 → L2:\n;; 1. L0 runtime handles the swap\n;; 2. New page declares tier=L2 in response header\n;; 3. L0 runtime loads sx-L2-delta.js dynamically\n;; 4. Island hydration proceeds\n\n(define page-tier\n (fn (page)\n ;; Analyze the page's component tree\n ;; If any component is defisland → L2\n ;; If any component uses on-event/toggle! → L1\n ;; Otherwise → L0\n (cond\n ((page-has-islands? page) :L2)\n ((page-has-dom-ops? page) :L1)\n (true :L0))))" "lisp")) + (~docs/code :src (highlight ";; Server emits the appropriate script for the page's tier\n;;\n;; L0 page (blog post, product listing):\n;; \n;;\n;; L2 page (reactive island):\n;; \n;; \n;;\n;; Client-side navigation from L0 → L2:\n;; 1. L0 runtime handles the swap\n;; 2. New page declares tier=L2 in response header\n;; 3. L0 runtime loads sx-L2-delta.js dynamically\n;; 4. Island hydration proceeds\n\n(define page-tier\n (fn (page)\n ;; Analyze the page's component tree\n ;; If any component is defisland → L2\n ;; If any component uses on-event/toggle! → L1\n ;; Otherwise → L0\n (cond\n ((page-has-islands? page) :L2)\n ((page-has-dom-ops? page) :L1)\n (true :L0))))" "lisp")) (~docs/subsection :title "SX-Tier Response Header" (p "The server includes the page's tier in the response:") - (~docs/code :code (highlight "HTTP/1.1 200 OK\nSX-Tier: L0\nSX-Components: ~card:bafy...,~plans/environment-images/nav:bafy...\n\n;; or for an island page:\nSX-Tier: L2\nSX-Components: ~counter-island:bafy..." "http")) + (~docs/code :src (highlight "HTTP/1.1 200 OK\nSX-Tier: L0\nSX-Components: ~card:bafy...,~plans/environment-images/nav:bafy...\n\n;; or for an island page:\nSX-Tier: L2\nSX-Components: ~counter-island:bafy..." "http")) (p "On client-side navigation, the engine reads " (code "SX-Tier") " from the response. If the new page requires a higher tier than currently loaded, it fetches the delta script before processing the swap. The delta script registers its additional primitives and the swap proceeds.")) (~docs/subsection :title "Cache Behavior" @@ -170,7 +170,7 @@ (~docs/section :title "Automatic Tier Detection" :id "auto-detect" (p (code "deps.sx") " already classifies components as pure or IO-dependent. Extend it to classify pages by tier:") - (~docs/code :code (highlight ";; Extend deps.sx with tier analysis\n;;\n;; Walk the page's component tree:\n;; - Any defisland → L2 minimum\n;; - Any on-event, toggle!, set-attr! call → L1 minimum \n;; - Any client-eval'd component (SX wire + defcomp) → L3\n;; - Otherwise → L0\n;;\n;; The tier is the MAX of all components' requirements.\n\n(define component-tier\n (fn (comp)\n (cond\n ((island? comp) :L2)\n ((has-dom-ops? (component-body comp)) :L1)\n (true :L0))))\n\n(define page-tier\n (fn (page-def)\n (let ((comp-tiers (map component-tier\n (page-all-components page-def))))\n (max-tier comp-tiers))))" "lisp")) + (~docs/code :src (highlight ";; Extend deps.sx with tier analysis\n;;\n;; Walk the page's component tree:\n;; - Any defisland → L2 minimum\n;; - Any on-event, toggle!, set-attr! call → L1 minimum \n;; - Any client-eval'd component (SX wire + defcomp) → L3\n;; - Otherwise → L0\n;;\n;; The tier is the MAX of all components' requirements.\n\n(define component-tier\n (fn (comp)\n (cond\n ((island? comp) :L2)\n ((has-dom-ops? (component-body comp)) :L1)\n (true :L0))))\n\n(define page-tier\n (fn (page-def)\n (let ((comp-tiers (map component-tier\n (page-all-components page-def))))\n (max-tier comp-tiers))))" "lisp")) (p "This runs at registration time (same phase as " (code "compute_all_deps") "). Each " (code "PageDef") " gains a " (code "tier") " field. The server uses it to select the script tag. No manual annotation needed — the tier is derived from what the page actually uses.")) @@ -236,7 +236,7 @@ (~docs/section :title "Build Pipeline" :id "pipeline" (p "The pipeline uses the same tools that already exist — " (code "js.sx") " for translation, " (code "bootstrap_js.py") " for platform assembly — but feeds them filtered define lists.") - (~docs/code :code (highlight ";; Build all tiers\n;;\n;; 1. Load all spec .sx files\n;; 2. Extract all defines (same as current bootstrap)\n;; 3. Run slice.sx to partition defines by tier\n;; 4. For each tier:\n;; a. js.sx translates the tier's define list\n;; b. Platform assembler wraps with minimal platform JS\n;; c. Output: sx-L{n}.js\n;; 5. Compute deltas: L1-delta = L1 - L0, L2-delta = L2 - L1, etc.\n\n;; The bootstrapper script orchestrates this:\n;;\n;; python bootstrap_js.py --tier L0 -o sx-L0.js\n;; python bootstrap_js.py --tier L1 --delta -o sx-L1-delta.js\n;; python bootstrap_js.py --tier L2 --delta -o sx-L2-delta.js\n;; python bootstrap_js.py -o sx-browser.js # full (L3, backward compat)" "lisp")) + (~docs/code :src (highlight ";; Build all tiers\n;;\n;; 1. Load all spec .sx files\n;; 2. Extract all defines (same as current bootstrap)\n;; 3. Run slice.sx to partition defines by tier\n;; 4. For each tier:\n;; a. js.sx translates the tier's define list\n;; b. Platform assembler wraps with minimal platform JS\n;; c. Output: sx-L{n}.js\n;; 5. Compute deltas: L1-delta = L1 - L0, L2-delta = L2 - L1, etc.\n\n;; The bootstrapper script orchestrates this:\n;;\n;; python bootstrap_js.py --tier L0 -o sx-L0.js\n;; python bootstrap_js.py --tier L1 --delta -o sx-L1-delta.js\n;; python bootstrap_js.py --tier L2 --delta -o sx-L2-delta.js\n;; python bootstrap_js.py -o sx-browser.js # full (L3, backward compat)" "lisp")) (p "The " (code "--delta") " flag emits only the defines not present in the previous tier. The delta file calls " (code "Sx.extend()") " to register its additions into the already-loaded runtime.") diff --git a/sx/sx/plans/rust-wasm-host.sx b/sx/sx/plans/rust-wasm-host.sx index 9a527436..9614052d 100644 --- a/sx/sx/plans/rust-wasm-host.sx +++ b/sx/sx/plans/rust-wasm-host.sx @@ -49,13 +49,13 @@ (h4 :class "font-semibold mt-4 mb-2" "Shared platform layer") (p (code "platform_js.py") " already contains all DOM, browser, fetch, timer, and storage implementations — bootstrapped from " (code "boundary.sx") ". These are pure JavaScript functions that call browser APIs. They don't depend on the evaluator.") (p "Extract them into a standalone " (code "sx-platform.js") " module. Both " (code "sx-browser.js") " (the current JS evaluator) and the new " (code "sx-wasm-shim.js") " import from the same platform module:") - (~docs/code :code (highlight " ┌─────────────────┐\n │ sx-platform.js │ ← DOM, fetch, timers, storage\n └────────┬────────┘\n │\n ┌──────────────┼──────────────┐\n │ │\n ┌─────────┴─────────┐ ┌────────┴────────┐\n │ sx-browser.js │ │ sx-wasm-shim.js │\n │ (JS tree-walker) │ │ (WASM instance │\n │ │ │ + handle table) │\n └────────────────────┘ └─────────────────┘" "text")) + (~docs/code :src (highlight " ┌─────────────────┐\n │ sx-platform.js │ ← DOM, fetch, timers, storage\n └────────┬────────┘\n │\n ┌──────────────┼──────────────┐\n │ │\n ┌─────────┴─────────┐ ┌────────┴────────┐\n │ sx-browser.js │ │ sx-wasm-shim.js │\n │ (JS tree-walker) │ │ (WASM instance │\n │ │ │ + handle table) │\n └────────────────────┘ └─────────────────┘" "text")) (p "One codebase for all browser primitives. Bug fixes apply to both targets. The evaluator is the only thing that changes — JS tree-walker vs Rust/WASM tree-walker.") (h4 :class "font-semibold mt-4 mb-2" "Opaque handle table") (p "Rust/WASM can't hold DOM node references directly. Instead, Rust values use " (code "Value::Handle(u32)") " — an opaque integer that indexes into a JavaScript-side handle table:") - (~docs/code :code (highlight "// JS side (in sx-wasm-shim.js)\nconst handles = []; // handle_id → DOM node\n\nfunction allocHandle(node) {\n const id = handles.length;\n handles.push(node);\n return id;\n}\n\nfunction getHandle(id) { return handles[id]; }\nfunction freeHandle(id) { handles[id] = null; }" "javascript")) - (~docs/code :code (highlight "// Rust side\n#[derive(Clone, Debug)]\nenum Value {\n Nil,\n Bool(bool),\n Number(f64),\n Str(String),\n Symbol(String),\n Keyword(String),\n List(Vec),\n Dict(Vec<(Value, Value)>),\n Lambda(Rc),\n Handle(u32), // ← opaque DOM node reference\n}" "rust")) + (~docs/code :src (highlight "// JS side (in sx-wasm-shim.js)\nconst handles = []; // handle_id → DOM node\n\nfunction allocHandle(node) {\n const id = handles.length;\n handles.push(node);\n return id;\n}\n\nfunction getHandle(id) { return handles[id]; }\nfunction freeHandle(id) { handles[id] = null; }" "javascript")) + (~docs/code :src (highlight "// Rust side\n#[derive(Clone, Debug)]\nenum Value {\n Nil,\n Bool(bool),\n Number(f64),\n Str(String),\n Symbol(String),\n Keyword(String),\n List(Vec),\n Dict(Vec<(Value, Value)>),\n Lambda(Rc),\n Handle(u32), // ← opaque DOM node reference\n}" "rust")) (p "When Rust calls a DOM primitive (e.g. " (code "createElement") "), it gets back a " (code "Handle(id)") ". When it passes that handle to " (code "appendChild") ", the JS shim looks up the real node. Rust never sees a DOM node — only integer IDs.") (h4 :class "font-semibold mt-4 mb-2" "The JS shim is thin") @@ -223,7 +223,7 @@ (li (strong "Step 2") " — " (code "sx-browser.js") " imports from " (code "sx-platform.js") " instead of containing the implementations inline") (li (strong "Step 3") " — " (code "sx-wasm-shim.js") " imports from the same " (code "sx-platform.js") " and wires the functions as WASM imports")) (p "Result: one implementation of every browser primitive, shared by both evaluator targets. Fix a bug in " (code "sx-platform.js") " and both JS and WASM evaluators get the fix.") - (~docs/code :code (highlight "// sx-platform.js (extracted from platform_js.py output)\nexport function createElement(tag) {\n return document.createElement(tag);\n}\nexport function setAttribute(el, key, val) {\n el.setAttribute(key, val);\n}\nexport function appendChild(parent, child) {\n parent.appendChild(child);\n}\nexport function addEventListener(el, event, callback) {\n el.addEventListener(event, callback);\n}\n// ... ~150 more browser primitives\n\n// sx-browser.js\nimport * as platform from './sx-platform.js';\n// Uses platform functions directly — evaluator is JS\n\n// sx-wasm-shim.js\nimport * as platform from './sx-platform.js';\n// Wraps platform functions for WASM import — evaluator is Rust" "javascript"))) + (~docs/code :src (highlight "// sx-platform.js (extracted from platform_js.py output)\nexport function createElement(tag) {\n return document.createElement(tag);\n}\nexport function setAttribute(el, key, val) {\n el.setAttribute(key, val);\n}\nexport function appendChild(parent, child) {\n parent.appendChild(child);\n}\nexport function addEventListener(el, event, callback) {\n el.addEventListener(event, callback);\n}\n// ... ~150 more browser primitives\n\n// sx-browser.js\nimport * as platform from './sx-platform.js';\n// Uses platform functions directly — evaluator is JS\n\n// sx-wasm-shim.js\nimport * as platform from './sx-platform.js';\n// Wraps platform functions for WASM import — evaluator is Rust" "javascript"))) ;; ----------------------------------------------------------------------- ;; Interaction with other plans diff --git a/sx/sx/plans/scoped-effects.sx b/sx/sx/plans/scoped-effects.sx index d9706222..b4e9f108 100644 --- a/sx/sx/plans/scoped-effects.sx +++ b/sx/sx/plans/scoped-effects.sx @@ -127,7 +127,7 @@ (li (strong "Accumulator") " — data flowing " (em "upward") " from descendants (writable via " (code "emit!") ")") (li (strong "Propagation mode") " — " (em "when") " effects on this scope are realised")) - (~docs/code :code (highlight "(scope \"name\"\n :value expr ;; downward (readable by descendants)\n :propagation :render ;; :render | :reactive | :morph\n body...)" "lisp")) + (~docs/code :src (highlight "(scope \"name\"\n :value expr ;; downward (readable by descendants)\n :propagation :render ;; :render | :reactive | :morph\n body...)" "lisp")) (p "Every existing mechanism is a scope with a specific configuration:") @@ -164,7 +164,7 @@ (td :class "py-2" ":reactive (cross-scope)")))) (~docs/subsection :title "Scopes compose by nesting" - (~docs/code :code (highlight ";; An island with a theme context and a morphable lake\n(scope \"my-island\" :propagation :reactive\n (let ((colour (signal \"violet\")))\n (scope \"theme\" :value {:primary colour} :propagation :render\n (div\n (h1 :style (str \"color:\" (deref (get (context \"theme\") :primary)))\n \"Themed heading\")\n (scope \"product-details\" :propagation :morph\n ;; Server morphs this on navigation\n ;; Reactive attrs on the h1 are protected\n (~product-card :id 42))))))" "lisp")) + (~docs/code :src (highlight ";; An island with a theme context and a morphable lake\n(scope \"my-island\" :propagation :reactive\n (let ((colour (signal \"violet\")))\n (scope \"theme\" :value {:primary colour} :propagation :render\n (div\n (h1 :style (str \"color:\" (deref (get (context \"theme\") :primary)))\n \"Themed heading\")\n (scope \"product-details\" :propagation :morph\n ;; Server morphs this on navigation\n ;; Reactive attrs on the h1 are protected\n (~product-card :id 42))))))" "lisp")) (p "Three scopes, three propagation modes, nested naturally. " "The reactive scope manages signal lifecycle. " "The render scope provides downward context. " @@ -213,7 +213,7 @@ (p "The (Down ↓, Reactive) cell — " (strong "reactive context") " — " "is the most interesting gap. It's React's Context + signals, but without " "the re-render avalanche that makes React Context slow.") - (~docs/code :code (highlight ";; Reactive context: value is a signal, propagation is reactive\n(scope \"theme\" :value (signal {:primary \"violet\"}) :propagation :reactive\n ;; Any descendant reads with context + deref\n ;; Only the specific DOM node that uses the value updates\n (h1 :style (str \"color:\" (get (deref (context \"theme\")) :primary))\n \"This h1 updates when the theme signal changes\")\n ;; Deep nesting doesn't matter — it's O(1) per subscriber\n (~deeply-nested-component-tree))" "lisp")) + (~docs/code :src (highlight ";; Reactive context: value is a signal, propagation is reactive\n(scope \"theme\" :value (signal {:primary \"violet\"}) :propagation :reactive\n ;; Any descendant reads with context + deref\n ;; Only the specific DOM node that uses the value updates\n (h1 :style (str \"color:\" (get (deref (context \"theme\")) :primary))\n \"This h1 updates when the theme signal changes\")\n ;; Deep nesting doesn't matter — it's O(1) per subscriber\n (~deeply-nested-component-tree))" "lisp")) (p "React re-renders " (em "every") " component that reads a changed context. " "SX's reactive context updates " (em "only") " the DOM nodes that " (code "deref") " the signal. " "Same API, fundamentally different performance characteristics."))) @@ -280,7 +280,7 @@ (~docs/subsection :title "The effect hierarchy" (p "Effects form a hierarchy. Lower-level effects are handled by higher-level scopes:") - (~docs/code :code (highlight ";; Effect hierarchy (innermost handled first)\n;;\n;; spread → element scope handles (merge attrs)\n;; emit! → nearest provide handles (accumulate)\n;; signal write → reactive scope handles (notify subscribers)\n;; morph → morph scope handles (diff + patch)\n;;\n;; If no handler is found, the effect bubbles up.\n;; Unhandled effects at the root are errors (like uncaught exceptions).\n;;\n;; This is why (emit! \"cssx\" rule) without a provider\n;; should error — there's no handler for the effect." "lisp")) + (~docs/code :src (highlight ";; Effect hierarchy (innermost handled first)\n;;\n;; spread → element scope handles (merge attrs)\n;; emit! → nearest provide handles (accumulate)\n;; signal write → reactive scope handles (notify subscribers)\n;; morph → morph scope handles (diff + patch)\n;;\n;; If no handler is found, the effect bubbles up.\n;; Unhandled effects at the root are errors (like uncaught exceptions).\n;;\n;; This is why (emit! \"cssx\" rule) without a provider\n;; should error — there's no handler for the effect." "lisp")) (p "The current " (code "collect!") " is a global accumulator — effectively an " "implicit top-level handler. Under the scope model, it would be an explicit " (code "provide") " in the layout that handles " (code "\"cssx\"") " effects."))) @@ -307,7 +307,7 @@ (~docs/subsection :title "2. Effect composition" (p "Because scopes nest, effects compose naturally:") - (~docs/code :code (highlight ";; Animation scope inside a reactive scope\n(scope \"island\" :propagation :reactive\n (let ((items (signal large-list)))\n (scope \"smooth\" :propagation :animation\n ;; Updates to items are batched per frame\n ;; The reactive scope tracks deps\n ;; The animation scope throttles DOM writes\n (for-each (fn (item)\n (div (get item \"name\"))) (deref items)))))" "lisp")) + (~docs/code :src (highlight ";; Animation scope inside a reactive scope\n(scope \"island\" :propagation :reactive\n (let ((items (signal large-list)))\n (scope \"smooth\" :propagation :animation\n ;; Updates to items are batched per frame\n ;; The reactive scope tracks deps\n ;; The animation scope throttles DOM writes\n (for-each (fn (item)\n (div (get item \"name\"))) (deref items)))))" "lisp")) (p "The reactive scope notifies when " (code "items") " changes. " "The animation scope batches the resulting DOM writes to the next frame. " "Neither scope knows about the other. They compose by nesting.")) @@ -315,7 +315,7 @@ (~docs/subsection :title "3. Server/client as effect boundary" (p "The deepest consequence: the server/client boundary becomes " (em "just another " "scope boundary") ". What crosses it is determined by the propagation mode:") - (~docs/code :code (highlight ";; The server renders this:\n(scope \"page\" :propagation :render\n (scope \"header\" :propagation :reactive\n ;; Client takes over — reactive scope\n (island-body...))\n (scope \"content\" :propagation :morph\n ;; Server controls — morphed on navigation\n (page-content...))\n (scope \"footer\" :propagation :render\n ;; Static — rendered once, never changes\n (footer...)))" "lisp")) + (~docs/code :src (highlight ";; The server renders this:\n(scope \"page\" :propagation :render\n (scope \"header\" :propagation :reactive\n ;; Client takes over — reactive scope\n (island-body...))\n (scope \"content\" :propagation :morph\n ;; Server controls — morphed on navigation\n (page-content...))\n (scope \"footer\" :propagation :render\n ;; Static — rendered once, never changes\n (footer...)))" "lisp")) (p "The renderer walks the tree. When it hits a reactive scope, it serialises " "state and emits a hydration marker. When it hits a morph scope, it emits " "a lake marker. When it hits a render scope, it just renders. " @@ -326,7 +326,7 @@ (p "Named stores (" (code "def-store/use-store") ") are scopes that transcend " "the render tree. Two islands sharing a signal is two reactive scopes " "referencing the same named scope:") - (~docs/code :code (highlight ";; Two islands, one shared scope\n(scope \"cart\" :value (signal []) :propagation :reactive :global true\n ;; Any island can read/write the cart\n ;; The scope transcends the render tree\n ;; Signal propagation handles cross-island updates)" "lisp")) + (~docs/code :src (highlight ";; Two islands, one shared scope\n(scope \"cart\" :value (signal []) :propagation :reactive :global true\n ;; Any island can read/write the cart\n ;; The scope transcends the render tree\n ;; Signal propagation handles cross-island updates)" "lisp")) (p "The " (code ":global") " flag lifts the scope out of the tree hierarchy " "into a named registry. " (code "def-store") " is syntax sugar for this."))) @@ -357,7 +357,7 @@ (code "collect!") " creates a lazy root scope with deduplication. " "All adapters use " (code "scope-push!/scope-pop!") " directly.") (p "The unified platform structure:") - (~docs/code :code (highlight "_scope_stacks = {} ;; {name: [{value, emitted: [], dedup: bool}]}" "python")) + (~docs/code :src (highlight "_scope_stacks = {} ;; {name: [{value, emitted: [], dedup: bool}]}" "python")) (p "See " (a :href "/sx/(geography.(scopes))" :class "text-violet-600 hover:underline" "scopes article") ".")) @@ -365,7 +365,7 @@ (~docs/subsection :title "Phase 3: effect handlers (future)" (p "Make propagation modes extensible. A " (code ":propagation") " value is a " "handler function that determines when and how effects are realised:") - (~docs/code :code (highlight ";; Custom propagation mode\n(define :debounced\n (fn (emit-fn delay)\n ;; Returns a handler that debounces effect realisation\n (let ((timer nil))\n (fn (effect)\n (when timer (clear-timeout timer))\n (set! timer (set-timeout\n (fn () (emit-fn effect)) delay))))))\n\n;; Use it\n(scope \"search\" :propagation (:debounced 300)\n ;; Effects in this scope are debounced by 300ms\n (input :on-input (fn (e)\n (emit! \"search\" (get-value e)))))" "lisp")) + (~docs/code :src (highlight ";; Custom propagation mode\n(define :debounced\n (fn (emit-fn delay)\n ;; Returns a handler that debounces effect realisation\n (let ((timer nil))\n (fn (effect)\n (when timer (clear-timeout timer))\n (set! timer (set-timeout\n (fn () (emit-fn effect)) delay))))))\n\n;; Use it\n(scope \"search\" :propagation (:debounced 300)\n ;; Effects in this scope are debounced by 300ms\n (input :on-input (fn (e)\n (emit! \"search\" (get-value e)))))" "lisp")) (p (strong "Delivers: ") "user-defined propagation modes (animation, debounce, throttle, " "worker, stream), effect composition by nesting, " "the full algebraic effects model."))) diff --git a/sx/sx/plans/self-hosting-bootstrapper.sx b/sx/sx/plans/self-hosting-bootstrapper.sx index 55c09505..88d7d375 100644 --- a/sx/sx/plans/self-hosting-bootstrapper.sx +++ b/sx/sx/plans/self-hosting-bootstrapper.sx @@ -176,7 +176,7 @@ (p "Python closures can read but not rebind outer variables. " (code "py.sx") " detects " (code "set!") " targets that cross lambda boundaries " "and routes them through a " (code "_cells") " dict:") - (~docs/code :code (highlight ";; SX ;; Python + (~docs/code :src (highlight ";; SX ;; Python (define counter def counter(): (fn () _cells = {} (let ((n 0)) _cells['n'] = 0 diff --git a/sx/sx/plans/spec-explorer.sx b/sx/sx/plans/spec-explorer.sx index 6a97a363..352a0bae 100644 --- a/sx/sx/plans/spec-explorer.sx +++ b/sx/sx/plans/spec-explorer.sx @@ -97,7 +97,7 @@ (p "The explorer shows effect badges on each function card, and the stats bar aggregates them across the whole file. Pure functions (green) are the nucleus — no side effects, fully deterministic, safe to cache, reorder, or parallelise.") - (~docs/code :code (highlight "(define signal :effects []\n (fn ((initial-value :as any))\n (make-signal initial-value)))\n\n(define reset! :effects [mutation]\n (fn ((s :as signal) value)\n (when (signal? s)\n (let ((old (signal-value s)))\n (when (not (identical? old value))\n (signal-set-value! s value)\n (notify-subscribers s))))))" "sx"))) + (~docs/code :src (highlight "(define signal :effects []\n (fn ((initial-value :as any))\n (make-signal initial-value)))\n\n(define reset! :effects [mutation]\n (fn ((s :as signal) value)\n (when (signal? s)\n (let ((old (signal-value s)))\n (when (not (identical? old value))\n (signal-set-value! s value)\n (notify-subscribers s))))))" "sx"))) ;; ----------------------------------------------------------------------- ;; Bootstrapper translations @@ -107,15 +107,15 @@ (p "Each function is translated by the actual bootstrappers that build the production runtime. The same " (code "signal") " function shown in three target languages:") (~docs/subsection :title "Python (via bootstrap_py.py)" - (~docs/code :code (highlight "def signal(initial_value):\n return make_signal(initial_value)" "python")) + (~docs/code :src (highlight "def signal(initial_value):\n return make_signal(initial_value)" "python")) (p :class "text-sm text-stone-500" (code "PyEmitter._emit_define()") " — the exact same code path that generates " (code "sx_ref.py") ".")) (~docs/subsection :title "JavaScript (via js.sx)" - (~docs/code :code (highlight "var signal = function(initial_value) {\n return make_signal(initial_value);\n};" "javascript")) + (~docs/code :src (highlight "var signal = function(initial_value) {\n return make_signal(initial_value);\n};" "javascript")) (p :class "text-sm text-stone-500" (code "js-emit-define") " — the self-hosting JS bootstrapper, written in SX, evaluated by the Python evaluator.")) (~docs/subsection :title "Z3 / SMT-LIB (via z3.sx)" - (~docs/code :code (highlight "; signal — Create a reactive signal container with an initial value.\n(declare-fun signal (Value) Value)" "lisp")) + (~docs/code :src (highlight "; signal — Create a reactive signal container with an initial value.\n(declare-fun signal (Value) Value)" "lisp")) (p :class "text-sm text-stone-500" (code "z3-translate") " — the first self-hosted bootstrapper, translating spec declarations to verification conditions for theorem provers."))) ;; ----------------------------------------------------------------------- @@ -127,12 +127,12 @@ (~docs/subsection :title "Tests" (p "Test files (" (code "test-signals.sx") ", " (code "test-eval.sx") ", etc.) use the " (code "defsuite") "/" (code "deftest") " framework. The explorer matches tests to functions by suite name and shows them on the function card.") - (~docs/code :code (highlight "(defsuite \"signal basics\"\n (deftest \"creates signal with value\"\n (let ((s (signal 42)))\n (assert-equal (deref s) 42)))\n (deftest \"reset changes value\"\n (let ((s (signal 0)))\n (reset! s 99)\n (assert-equal (deref s) 99))))" "sx"))) + (~docs/code :src (highlight "(defsuite \"signal basics\"\n (deftest \"creates signal with value\"\n (let ((s (signal 42)))\n (assert-equal (deref s) 42)))\n (deftest \"reset changes value\"\n (let ((s (signal 0)))\n (reset! s 99)\n (assert-equal (deref s) 99))))" "sx"))) (~docs/subsection :title "Proofs" (p (code "prove.sx") " verifies algebraic properties of SX primitives by bounded model checking. For each " (code "define-primitive") " with a " (code ":body") ", " (code "prove-translate") " translates to SMT-LIB and verifies satisfiability by construction.") (p "Properties from the " (code "sx-properties") " library are matched to functions and shown on their cards:") - (~docs/code :code (highlight ";; prove.sx property: +-commutative\n{:name \"+-commutative\"\n :vars (list \"a\" \"b\")\n :test (fn (a b) (= (+ a b) (+ b a)))\n :holds '(= (+ a b) (+ b a))}\n\n;; Result: verified — 1,681 ground instances tested" "sx")))) + (~docs/code :src (highlight ";; prove.sx property: +-commutative\n{:name \"+-commutative\"\n :vars (list \"a\" \"b\")\n :test (fn (a b) (= (+ a b) (+ b a)))\n :holds '(= (+ a b) (+ b a))}\n\n;; Result: verified — 1,681 ground instances tested" "sx")))) ;; ----------------------------------------------------------------------- ;; Architecture diff --git a/sx/sx/plans/sx-activity.sx b/sx/sx/plans/sx-activity.sx index ae812a61..56601bc3 100644 --- a/sx/sx/plans/sx-activity.sx +++ b/sx/sx/plans/sx-activity.sx @@ -31,12 +31,12 @@ (~docs/subsection :title "The Problem" (p "JSON-LD activities are verbose and require context resolution:") - (~docs/code :code (highlight "{\"@context\": \"https://www.w3.org/ns/activitystreams\",\n \"type\": \"Create\",\n \"actor\": \"https://example.com/users/alice\",\n \"object\": {\n \"type\": \"Note\",\n \"content\": \"

Hello world

\",\n \"attributedTo\": \"https://example.com/users/alice\"\n }}" "json")) + (~docs/code :src (highlight "{\"@context\": \"https://www.w3.org/ns/activitystreams\",\n \"type\": \"Create\",\n \"actor\": \"https://example.com/users/alice\",\n \"object\": {\n \"type\": \"Note\",\n \"content\": \"

Hello world

\",\n \"attributedTo\": \"https://example.com/users/alice\"\n }}" "json")) (p "Every consumer parses JSON, resolves @context, extracts fields, then builds their own UI around the raw data. The content is HTML embedded in a JSON string — two formats nested, neither evaluable.")) (~docs/subsection :title "SX Activity Format" (p "The same activity as SX:") - (~docs/code :code (highlight "(Create\n :actor \"https://example.com/users/alice\"\n :published \"2026-03-06T12:00:00Z\"\n :object (Note\n :attributed-to \"https://example.com/users/alice\"\n :content (p \"Hello world\")\n :media-type \"text/sx\"))" "lisp")) + (~docs/code :src (highlight "(Create\n :actor \"https://example.com/users/alice\"\n :published \"2026-03-06T12:00:00Z\"\n :object (Note\n :attributed-to \"https://example.com/users/alice\"\n :content (p \"Hello world\")\n :media-type \"text/sx\"))" "lisp")) (p "The content isn't a string containing markup — it " (em "is") " markup. The receiving server can evaluate it directly. The Note's content is a renderable SX expression.")) (~docs/subsection :title "Approach" @@ -44,7 +44,7 @@ (div (h4 :class "font-semibold text-stone-700" "1. Activity vocabulary in SX") (p "Map ActivityStreams types to SX symbols. Activities are lists with a type head and keyword properties:") - (~docs/code :code (highlight ";; Core activity types\n(Create :actor ... :object ...)\n(Update :actor ... :object ...)\n(Delete :actor ... :object ...)\n(Follow :actor ... :object ...)\n(Like :actor ... :object ...)\n(Announce :actor ... :object ...)\n\n;; Object types\n(Note :content ... :attributed-to ...)\n(Article :name ... :content ... :summary ...)\n(Image :url ... :media-type ... :cid ...)\n(Collection :total-items ... :items ...)" "lisp"))) + (~docs/code :src (highlight ";; Core activity types\n(Create :actor ... :object ...)\n(Update :actor ... :object ...)\n(Delete :actor ... :object ...)\n(Follow :actor ... :object ...)\n(Like :actor ... :object ...)\n(Announce :actor ... :object ...)\n\n;; Object types\n(Note :content ... :attributed-to ...)\n(Article :name ... :content ... :summary ...)\n(Image :url ... :media-type ... :cid ...)\n(Collection :total-items ... :items ...)" "lisp"))) (div (h4 :class "font-semibold text-stone-700" "2. Content negotiation") @@ -84,13 +84,13 @@ (div (h4 :class "font-semibold text-stone-700" "1. Component CID computation") (p "Each " (code "defcomp") " definition gets a content address:") - (~docs/code :code (highlight ";; Component source\n(defcomp ~plans/sx-activity/card (&key title &rest children)\n (div :class \"border rounded p-4\"\n (h2 title) children))\n\n;; CID = SHA3-256 of canonical serialized form\n;; → bafy...abc123\n;; Stored: ipfs://bafy...abc123 → component source" "lisp")) + (~docs/code :src (highlight ";; Component source\n(defcomp ~plans/sx-activity/card (&key title &rest children)\n (div :class \"border rounded p-4\"\n (h2 title) children))\n\n;; CID = SHA3-256 of canonical serialized form\n;; → bafy...abc123\n;; Stored: ipfs://bafy...abc123 → component source" "lisp")) (p "Canonical form: normalize whitespace, sort keyword args alphabetically, strip comments. Same component always produces same CID regardless of formatting.")) (div (h4 :class "font-semibold text-stone-700" "2. Component references in activities") (p "Activities declare which components they need by CID:") - (~docs/code :code (highlight "(Create\n :actor \"https://rose-ash.com/users/alice\"\n :requires (list\n \"bafy...card\" ;; ~plans/sx-activity/card component\n \"bafy...avatar\") ;; ~shared:misc/avatar component\n :object (Note\n :content (~plans/sx-activity/card :title \"Hello\"\n (~shared:misc/avatar :src \"ipfs://bafy...photo\")\n (p \"This renders with the card component.\"))))" "lisp")) + (~docs/code :src (highlight "(Create\n :actor \"https://rose-ash.com/users/alice\"\n :requires (list\n \"bafy...card\" ;; ~plans/sx-activity/card component\n \"bafy...avatar\") ;; ~shared:misc/avatar component\n :object (Note\n :content (~plans/sx-activity/card :title \"Hello\"\n (~shared:misc/avatar :src \"ipfs://bafy...photo\")\n (p \"This renders with the card component.\"))))" "lisp")) (p "The receiving browser fetches missing components from IPFS, verifies CIDs, registers them, then renders the content.")) (div @@ -106,7 +106,7 @@ (div (h4 :class "font-semibold text-stone-700" "4. Component publication") (p "Server-side: on component registration, compute CID and pin to IPFS. Track in " (code "IPFSPin") " model (already exists). Publish component availability via AP outbox:") - (~docs/code :code (highlight "(Create\n :actor \"https://rose-ash.com/apps/market\"\n :object (SxComponent\n :name \"~product-card\"\n :cid \"bafy...productcard\"\n :version \"1.0.0\"\n :deps (list \"bafy...card\" \"bafy...price-tag\")))" "lisp"))))) + (~docs/code :src (highlight "(Create\n :actor \"https://rose-ash.com/apps/market\"\n :object (SxComponent\n :name \"~product-card\"\n :cid \"bafy...productcard\"\n :version \"1.0.0\"\n :deps (list \"bafy...card\" \"bafy...price-tag\")))" "lisp"))))) (~docs/subsection :title "Verification" (ul :class "list-disc pl-5 text-stone-700 space-y-1" @@ -133,18 +133,18 @@ (div (h4 :class "font-semibold text-stone-700" "1. Media CID pipeline") (p "On upload: hash content → pin to IPFS → store CID in database. Activities reference media by CID alongside URL fallback:") - (~docs/code :code (highlight "(Image\n :cid \"bafy...photo123\"\n :url \"https://rose-ash.com/media/photo.jpg\" ;; fallback\n :media-type \"image/jpeg\"\n :width 1200 :height 800)" "lisp"))) + (~docs/code :src (highlight "(Image\n :cid \"bafy...photo123\"\n :url \"https://rose-ash.com/media/photo.jpg\" ;; fallback\n :media-type \"image/jpeg\"\n :width 1200 :height 800)" "lisp"))) (div (h4 :class "font-semibold text-stone-700" "2. DAG output federation") (p "artdag processing results (rendered video, processed images) already have CIDs. Federate them as activities:") - (~docs/code :code (highlight "(Create\n :actor \"https://rose-ash.com/users/alice\"\n :object (Artwork\n :name \"Sunset Remix\"\n :cid \"bafy...artwork\"\n :dag-cid \"bafy...dag\" ;; full DAG for reproduction\n :media-type \"video/mp4\"\n :sources (list\n (Image :cid \"bafy...src1\" :attribution \"...\")\n (Image :cid \"bafy...src2\" :attribution \"...\"))))" "lisp")) + (~docs/code :src (highlight "(Create\n :actor \"https://rose-ash.com/users/alice\"\n :object (Artwork\n :name \"Sunset Remix\"\n :cid \"bafy...artwork\"\n :dag-cid \"bafy...dag\" ;; full DAG for reproduction\n :media-type \"video/mp4\"\n :sources (list\n (Image :cid \"bafy...src1\" :attribution \"...\")\n (Image :cid \"bafy...src2\" :attribution \"...\"))))" "lisp")) (p "The " (code ":dag-cid") " lets anyone re-execute the processing pipeline. The artwork is both a result and a reproducible recipe.")) (div (h4 :class "font-semibold text-stone-700" "3. Shared SX content store") (p "Not just components and media — full page content can be content-addressed. An Article's body is SX, pinned to IPFS:") - (~docs/code :code (highlight "(Article\n :name \"Why S-Expressions\"\n :content-cid \"bafy...article-body\" ;; SX source on IPFS\n :requires (list \"bafy...doc-page\" \"bafy...code-block\")\n :summary \"Why SX uses s-expressions instead of HTML.\")" "lisp")) + (~docs/code :src (highlight "(Article\n :name \"Why S-Expressions\"\n :content-cid \"bafy...article-body\" ;; SX source on IPFS\n :requires (list \"bafy...doc-page\" \"bafy...code-block\")\n :summary \"Why SX uses s-expressions instead of HTML.\")" "lisp")) (p "The content outlives the server. Anyone with the CID can fetch, parse, and render the article with its original components.")) (div @@ -177,12 +177,12 @@ (div (h4 :class "font-semibold text-stone-700" "1. Component collections as AP actors") (p "Each server exposes a component registry actor:") - (~docs/code :code (highlight "(Service\n :id \"https://rose-ash.com/sx-registry\"\n :type \"SxComponentRegistry\"\n :name \"Rose Ash Components\"\n :outbox \"https://rose-ash.com/sx-registry/outbox\"\n :followers \"https://rose-ash.com/sx-registry/followers\")" "lisp")) + (~docs/code :src (highlight "(Service\n :id \"https://rose-ash.com/sx-registry\"\n :type \"SxComponentRegistry\"\n :name \"Rose Ash Components\"\n :outbox \"https://rose-ash.com/sx-registry/outbox\"\n :followers \"https://rose-ash.com/sx-registry/followers\")" "lisp")) (p "Follow the registry to receive component updates. The outbox is a chronological feed of Create/Update/Delete activities for components.")) (div (h4 :class "font-semibold text-stone-700" "2. Component metadata") - (~docs/code :code (highlight "(SxComponent\n :name \"~data-table\"\n :cid \"bafy...datatable\"\n :version \"2.1.0\"\n :deps (list \"bafy...sortable\" \"bafy...paginator\")\n :params (list\n (dict :name \"rows\" :type \"list\" :required true)\n (dict :name \"columns\" :type \"list\" :required true)\n (dict :name \"sortable\" :type \"boolean\" :default false))\n :css-atoms (list :border :rounded :p-4 :text-sm)\n :preview-cid \"bafy...screenshot\"\n :license \"MIT\")" "lisp")) + (~docs/code :src (highlight "(SxComponent\n :name \"~data-table\"\n :cid \"bafy...datatable\"\n :version \"2.1.0\"\n :deps (list \"bafy...sortable\" \"bafy...paginator\")\n :params (list\n (dict :name \"rows\" :type \"list\" :required true)\n (dict :name \"columns\" :type \"list\" :required true)\n (dict :name \"sortable\" :type \"boolean\" :default false))\n :css-atoms (list :border :rounded :p-4 :text-sm)\n :preview-cid \"bafy...screenshot\"\n :license \"MIT\")" "lisp")) (p "Dependencies are transitive CID references. CSS atoms declare which CSSX rules the component needs. Preview CID is a screenshot for registry browsing.")) (div @@ -197,7 +197,7 @@ (div (h4 :class "font-semibold text-stone-700" "4. Version resolution") (p "Components are immutable (CID = identity). \"Updating\" a component publishes a new CID. Activities reference specific CIDs, so old content always renders correctly. The registry tracks version history:") - (~docs/code :code (highlight "(Update\n :actor \"https://rose-ash.com/sx-registry\"\n :object (SxComponent\n :name \"~card\"\n :cid \"bafy...card-v2\" ;; new version\n :replaces \"bafy...card-v1\" ;; previous version\n :changelog \"Added subtitle slot\"))" "lisp"))))) + (~docs/code :src (highlight "(Update\n :actor \"https://rose-ash.com/sx-registry\"\n :object (SxComponent\n :name \"~card\"\n :cid \"bafy...card-v2\" ;; new version\n :replaces \"bafy...card-v1\" ;; previous version\n :changelog \"Added subtitle slot\"))" "lisp"))))) (~docs/subsection :title "Verification" (ul :class "list-disc pl-5 text-stone-700 space-y-1" @@ -232,7 +232,7 @@ (div (h4 :class "font-semibold text-stone-700" "2. Provenance chain in activities") - (~docs/code :code (highlight "(Create\n :actor \"https://rose-ash.com/users/alice\"\n :object (Note :content (p \"Hello\") :cid \"bafy...note\")\n :provenance (Anchor\n :tree-cid \"bafy...merkle-tree\"\n :leaf-index 42\n :ots-cid \"bafy...ots-proof\"\n :btc-txid \"abc123...def\"\n :btc-block 890123\n :anchored-at \"2026-03-06T12:00:00Z\"))" "lisp")) + (~docs/code :src (highlight "(Create\n :actor \"https://rose-ash.com/users/alice\"\n :object (Note :content (p \"Hello\") :cid \"bafy...note\")\n :provenance (Anchor\n :tree-cid \"bafy...merkle-tree\"\n :leaf-index 42\n :ots-cid \"bafy...ots-proof\"\n :btc-txid \"abc123...def\"\n :btc-block 890123\n :anchored-at \"2026-03-06T12:00:00Z\"))" "lisp")) (p "Any party can verify: fetch the OTS proof from IPFS, check the Merkle path from the activity's CID to the tree root, confirm the tree root is committed in the Bitcoin block.")) (div @@ -246,7 +246,7 @@ (div (h4 :class "font-semibold text-stone-700" "4. Verification UI") (p "Client-side provenance badge on federated content:") - (~docs/code :code (highlight "(defcomp ~plans/sx-activity/provenance-badge (&key anchor)\n (when anchor\n (details :class \"inline text-xs text-stone-400\"\n (summary \"✓ Anchored\")\n (dl :class \"mt-1 space-y-1\"\n (dt \"Bitcoin block\") (dd (get anchor \"btc-block\"))\n (dt \"Timestamp\") (dd (get anchor \"anchored-at\"))\n (dt \"Proof\") (dd (a :href (str \"ipfs://\" (get anchor \"ots-cid\"))\n \"OTS proof\"))))))" "lisp"))))) + (~docs/code :src (highlight "(defcomp ~plans/sx-activity/provenance-badge (&key anchor)\n (when anchor\n (details :class \"inline text-xs text-stone-400\"\n (summary \"✓ Anchored\")\n (dl :class \"mt-1 space-y-1\"\n (dt \"Bitcoin block\") (dd (get anchor \"btc-block\"))\n (dt \"Timestamp\") (dd (get anchor \"anchored-at\"))\n (dt \"Proof\") (dd (a :href (str \"ipfs://\" (get anchor \"ots-cid\"))\n \"OTS proof\"))))))" "lisp"))))) (~docs/subsection :title "Verification" (ul :class "list-disc pl-5 text-stone-700 space-y-1" @@ -282,7 +282,7 @@ (div (h4 :class "font-semibold text-stone-700" "Parsers and transforms") (p "A markdown parser is just an SX function: takes a string, returns an SX tree. Publish it to IPFS. Now anyone can use your markdown dialect. Same for: syntax highlighters, BBCode parsers, wiki markup, LaTeX subsets, CSV-to-table converters, JSON-to-SX adapters. " (strong "The parser ecosystem becomes shared infrastructure.")) - (~docs/code :code (highlight ";; A markdown parser, published to IPFS\n;; CID: bafy...md-parser\n(define parse-markdown\n (fn (source)\n ;; tokenize → build AST → return SX tree\n ;; (parse-markdown \"# Hello\\n**bold**\")\n ;; → (h1 \"Hello\") (p (strong \"bold\"))\n ...))\n\n;; Anyone can use it in their components\n(defcomp ~plans/sx-activity/blog-post (&key markdown-source)\n (div :class \"prose\"\n (parse-markdown markdown-source)))" "lisp"))) + (~docs/code :src (highlight ";; A markdown parser, published to IPFS\n;; CID: bafy...md-parser\n(define parse-markdown\n (fn (source)\n ;; tokenize → build AST → return SX tree\n ;; (parse-markdown \"# Hello\\n**bold**\")\n ;; → (h1 \"Hello\") (p (strong \"bold\"))\n ...))\n\n;; Anyone can use it in their components\n(defcomp ~plans/sx-activity/blog-post (&key markdown-source)\n (div :class \"prose\"\n (parse-markdown markdown-source)))" "lisp"))) (div (h4 :class "font-semibold text-stone-700" "Server-side and client-side logic") @@ -350,7 +350,7 @@ (~docs/subsection :title "Serverless applications on IPFS" (p "The logical conclusion: " (strong "entire web applications hosted on IPFS with no server at all.")) (p "An SX application is a tree of content-addressed artifacts: a root page definition, component dependencies, media, stylesheets, parsers, transforms. Pin the root CID to IPFS and the application is live. No server, no DNS, no hosting provider, no deployment pipeline. Someone gives you a CID, you paste it into an SX-aware browser, and the application runs.") - (~docs/code :code (highlight ";; An entire blog — one CID\n;; ipfs://bafy...my-blog\n(defpage blog-home\n :path \"/\"\n :requires (list\n \"bafy...article-layout\" ;; layout component\n \"bafy...md-parser\" ;; markdown parser\n \"bafy...syntax-highlight\" ;; code highlighting\n \"bafy...nav-component\") ;; navigation\n :content\n (~article-layout\n :title \"My Blog\"\n :nav (~nav-component\n :items (list\n (dict :label \"Post 1\" :cid \"bafy...post-1\")\n (dict :label \"Post 2\" :cid \"bafy...post-2\")))\n :body (~markdown-page\n :source-cid \"bafy...homepage-md\")))" "lisp")) + (~docs/code :src (highlight ";; An entire blog — one CID\n;; ipfs://bafy...my-blog\n(defpage blog-home\n :path \"/\"\n :requires (list\n \"bafy...article-layout\" ;; layout component\n \"bafy...md-parser\" ;; markdown parser\n \"bafy...syntax-highlight\" ;; code highlighting\n \"bafy...nav-component\") ;; navigation\n :content\n (~article-layout\n :title \"My Blog\"\n :nav (~nav-component\n :items (list\n (dict :label \"Post 1\" :cid \"bafy...post-1\")\n (dict :label \"Post 2\" :cid \"bafy...post-2\")))\n :body (~markdown-page\n :source-cid \"bafy...homepage-md\")))" "lisp")) (p "What this looks like in practice:") (ul :class "list-disc pl-5 text-stone-700 space-y-1" (li (strong "Personal sites: ") "A portfolio or blog is a handful of SX files + media. Pin to IPFS. Share the CID. No hosting costs, no domain renewal, no SSL certificates. The site is permanent.") diff --git a/sx/sx/plans/sx-ci.sx b/sx/sx/plans/sx-ci.sx index b3c285c4..f7934bf8 100644 --- a/sx/sx/plans/sx-ci.sx +++ b/sx/sx/plans/sx-ci.sx @@ -17,7 +17,7 @@ (~docs/section :title "Design" :id "design" (p :class "text-stone-600" "Pipeline definitions are " (code ".sx") " files. A minimal Python CLI runner evaluates them using " (code "sx_ref.py") ". CI-specific IO primitives (shell execution, Docker, git) are boundary-declared and only available to the pipeline runner — never to web components.") - (~docs/code :code (highlight ";; pipeline/deploy.sx\n(let ((targets (if (= (length ARGS) 0)\n (~plans/sx-ci/detect-changed :base \"HEAD~1\")\n (filter (fn (svc) (some (fn (a) (= a (get svc \"name\"))) ARGS))\n services))))\n (when (= (length targets) 0)\n (log-step \"No changes detected\")\n (exit 0))\n\n (log-step (str \"Deploying: \" (join \" \" (map (fn (s) (get s \"name\")) targets))))\n\n ;; Tests first\n (~unit-tests)\n (~sx-spec-tests)\n\n ;; Build, push, restart\n (for-each (fn (svc) (~plans/sx-ci/build-service :service svc)) targets)\n (for-each (fn (svc) (~restart-service :service svc)) targets)\n\n (log-step \"Deploy complete\"))" "lisp")) + (~docs/code :src (highlight ";; pipeline/deploy.sx\n(let ((targets (if (= (length ARGS) 0)\n (~plans/sx-ci/detect-changed :base \"HEAD~1\")\n (filter (fn (svc) (some (fn (a) (= a (get svc \"name\"))) ARGS))\n services))))\n (when (= (length targets) 0)\n (log-step \"No changes detected\")\n (exit 0))\n\n (log-step (str \"Deploying: \" (join \" \" (map (fn (s) (get s \"name\")) targets))))\n\n ;; Tests first\n (~unit-tests)\n (~sx-spec-tests)\n\n ;; Build, push, restart\n (for-each (fn (svc) (~plans/sx-ci/build-service :service svc)) targets)\n (for-each (fn (svc) (~restart-service :service svc)) targets)\n\n (log-step \"Deploy complete\"))" "lisp")) (p :class "text-stone-600" "Pipeline steps are components. " (code "~unit-tests") ", " (code "~plans/sx-ci/build-service") ", " (code "~plans/sx-ci/detect-changed") " are " (code "defcomp") " definitions that compose by nesting — the same mechanism used for page layouts, navigation, and every other piece of the system.")) @@ -73,7 +73,7 @@ (~docs/section :title "Reusable Steps" :id "steps" (p :class "text-stone-600" "Pipeline steps are components — same " (code "defcomp") " as UI components, same " (code "&key") " params, same composition by nesting:") - (~docs/code :code (highlight "(defcomp ~plans/sx-ci/detect-changed (&key base)\n (let ((files (git-diff-files (or base \"HEAD~1\") \"HEAD\")))\n (if (some (fn (f) (starts-with? f \"shared/\")) files)\n services\n (filter (fn (svc)\n (some (fn (f) (starts-with? f (str (get svc \"dir\") \"/\"))) files))\n services))))\n\n(defcomp ~plans/sx-ci/build-service (&key service)\n (let ((name (get service \"name\"))\n (tag (str registry \"/\" name \":latest\")))\n (log-step (str \"Building \" name))\n (docker-build :file (str (get service \"dir\") \"/Dockerfile\") :tag tag :context \".\")\n (docker-push tag)))\n\n(defcomp ~plans/sx-ci/bootstrap-check ()\n (log-step \"Checking bootstrapped files are up to date\")\n (shell-run! \"python shared/sx/ref/bootstrap_js.py\")\n (shell-run! \"python shared/sx/ref/bootstrap_py.py\")\n (let ((diff (shell-run \"git diff --name-only shared/static/scripts/sx-ref.js shared/sx/ref/sx_ref.py\")))\n (when (not (= (get diff \"stdout\") \"\"))\n (fail! \"Bootstrapped files are stale — rebootstrap and commit\"))))" "lisp")) + (~docs/code :src (highlight "(defcomp ~plans/sx-ci/detect-changed (&key base)\n (let ((files (git-diff-files (or base \"HEAD~1\") \"HEAD\")))\n (if (some (fn (f) (starts-with? f \"shared/\")) files)\n services\n (filter (fn (svc)\n (some (fn (f) (starts-with? f (str (get svc \"dir\") \"/\"))) files))\n services))))\n\n(defcomp ~plans/sx-ci/build-service (&key service)\n (let ((name (get service \"name\"))\n (tag (str registry \"/\" name \":latest\")))\n (log-step (str \"Building \" name))\n (docker-build :file (str (get service \"dir\") \"/Dockerfile\") :tag tag :context \".\")\n (docker-push tag)))\n\n(defcomp ~plans/sx-ci/bootstrap-check ()\n (log-step \"Checking bootstrapped files are up to date\")\n (shell-run! \"python shared/sx/ref/bootstrap_js.py\")\n (shell-run! \"python shared/sx/ref/bootstrap_py.py\")\n (let ((diff (shell-run \"git diff --name-only shared/static/scripts/sx-ref.js shared/sx/ref/sx_ref.py\")))\n (when (not (= (get diff \"stdout\") \"\"))\n (fail! \"Bootstrapped files are stale — rebootstrap and commit\"))))" "lisp")) (p :class "text-stone-600" "Compare this to GitHub Actions YAML, where \"reuse\" means composite actions with " (code "uses:") " references, input/output mappings, shell script blocks inside YAML strings, and a " (a :href "https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions" :class "text-violet-600 hover:underline" "100-page syntax reference") ". SX pipeline reuse is function composition. That is all it has ever been.")) diff --git a/sx/sx/plans/sx-protocol.sx b/sx/sx/plans/sx-protocol.sx index 5fb52584..0ba09a28 100644 --- a/sx/sx/plans/sx-protocol.sx +++ b/sx/sx/plans/sx-protocol.sx @@ -51,9 +51,9 @@ (h3 :class "text-lg font-semibold mt-6 mb-2" "URLs as S-Expressions") (p "A conventional URL:") - (~docs/code :code (highlight "https://site.com/blog/my-post?filter=published&sort=date" "text")) + (~docs/code :src (highlight "https://site.com/blog/my-post?filter=published&sort=date" "text")) (p "As an SX expression:") - (~docs/code :code (highlight "(get.site.com.(blog.(my-post.(filter.published.sort.date))))" "lisp")) + (~docs/code :src (highlight "(get.site.com.(blog.(my-post.(filter.published.sort.date))))" "lisp")) (ul (li "The protocol/verb is the first atom: " (code "get")) (li "The domain follows: " (code "site.com")) @@ -64,11 +64,11 @@ (p "Lisp conventionally uses spaces as separators. In URLs, spaces become " (code "%20") ". SX uses dots instead, which are URL-safe and semantically meaningful — a dot between " "two atoms is a " (strong "cons pair") ", the fundamental unit of Lisp structure.") - (~docs/code :code (highlight ";; Clean, URL-safe, valid Lisp\n(blog.(filter.published).(sort.date.desc))" "lisp")) + (~docs/code :src (highlight ";; Clean, URL-safe, valid Lisp\n(blog.(filter.published).(sort.date.desc))" "lisp")) (h3 :class "text-lg font-semibold mt-6 mb-2" "Verbs Are Just Atoms") (p "HTTP methods are not special syntax — they are simply the first element of the expression:") - (~docs/code :code (highlight "(get.site.com.(post.my-first-post)) ; read\n(post.site.com.(submit-post.(title.hello))) ; write\n(ws.site.com.(live-feed)) ; websocket / subscribe" "lisp")) + (~docs/code :src (highlight "(get.site.com.(post.my-first-post)) ; read\n(post.site.com.(submit-post.(title.hello))) ; write\n(ws.site.com.(live-feed)) ; websocket / subscribe" "lisp")) (p "No special protocol prefixes. No " (code "https://") " vs " (code "wss://") ". The verb is data, like everything else.")) @@ -81,7 +81,7 @@ (h3 :class "text-lg font-semibold mt-6 mb-2" "Queries Are URLs") (p "Because SX expressions are URLs, every query is a GET request:") - (~docs/code :code (highlight ";; This is a URL and a query simultaneously\n(get.site.com.(blog.(filter.(tag.lisp)).(limit.10)))" "lisp")) + (~docs/code :src (highlight ";; This is a URL and a query simultaneously\n(get.site.com.(blog.(filter.(tag.lisp)).(limit.10)))" "lisp")) (ul (li "Fully cacheable by CDNs") (li "Bookmarkable and shareable") @@ -91,19 +91,19 @@ (h3 :class "text-lg font-semibold mt-6 mb-2" "Responses Include Rendering") (p "GraphQL returns data. Graph-SX returns " (strong "hypermedia") " — data and its presentation in the same expression:") - (~docs/code :code (highlight ";; GraphQL response (dead data)\n{\"title\": \"My Post\", \"body\": \"Hello world\"}\n\n;; Graph-SX response (live hypermedia)\n(article\n (h1 \"My Post\")\n (p \"Hello world\")\n (a (href (get.site.com.(post.next-post))) \"Next\"))" "lisp")) + (~docs/code :src (highlight ";; GraphQL response (dead data)\n{\"title\": \"My Post\", \"body\": \"Hello world\"}\n\n;; Graph-SX response (live hypermedia)\n(article\n (h1 \"My Post\")\n (p \"Hello world\")\n (a (href (get.site.com.(post.next-post))) \"Next\"))" "lisp")) (p "The server returns what the resource " (strong "is") " and how to " (strong "present") " it in one unified structure. There is no separate rendering layer.") (h3 :class "text-lg font-semibold mt-6 mb-2" "Queries Are Transformations") (p "Because SX is a full programming language, the query and the transformation " "are the same expression:") - (~docs/code :code (highlight ";; Fetch, filter, and transform in one expression\n(map (lambda (p) (title p))\n (filter published?\n (posts (after \"2025\"))))" "lisp")) + (~docs/code :src (highlight ";; Fetch, filter, and transform in one expression\n(map (lambda (p) (title p))\n (filter published?\n (posts (after \"2025\"))))" "lisp")) (p "No separate processing step. No client-side data manipulation layer.")) (~docs/section :title "Components" :id "components" (p "SX supports server-side composable components via the " (code "~") " prefix convention:") - (~docs/code :code (highlight "(~get.everything-under-the-sun)" "lisp")) + (~docs/code :src (highlight "(~get.everything-under-the-sun)" "lisp")) (p "A " (code "~component") " is a named server-side function that:") (ol (li "Receives the expression as arguments") @@ -111,19 +111,19 @@ (li "Processes and composes results") (li "Returns hypermedia")) (p "Components compose naturally:") - (~docs/code :code (highlight "(~page.home\n (~hero.banner)\n (~get.latest-posts.(limit.5))\n (~get.featured.(filter.pinned)))" "lisp")) + (~docs/code :src (highlight "(~page.home\n (~hero.banner)\n (~get.latest-posts.(limit.5))\n (~get.featured.(filter.pinned)))" "lisp")) (p "This is equivalent to React Server Components — but without a framework, " "without a build step, and without leaving Lisp.")) (~docs/section :title "Cross-Domain Composition" :id "cross-domain" (p "Because domain and verb are just atoms, cross-domain queries are structurally " "identical to local ones:") - (~docs/code :code (highlight ";; Local\n(post.my-first-post)\n\n;; Remote — identical structure, qualified\n(get.site.com.(post.my-first-post))\n\n;; Composed across domains\n(~render\n (get.site.com.(post.my-first-post))\n (get.cdn.com.(image.hero)))" "lisp")) + (~docs/code :src (highlight ";; Local\n(post.my-first-post)\n\n;; Remote — identical structure, qualified\n(get.site.com.(post.my-first-post))\n\n;; Composed across domains\n(~render\n (get.site.com.(post.my-first-post))\n (get.cdn.com.(image.hero)))" "lisp")) (p "Network calls are function calls. Remote resources are just namespaced expressions.")) (~docs/section :title "Self-Describing and Introspectable" :id "introspectable" (p "Because the site is implemented in SX and served as SX, every page is introspectable:") - (~docs/code :code (highlight "(get.sx.dev.(about)) ; the about page\n(get.sx.dev.(source.(about))) ; the SX source for the about page\n(get.sx.dev.(eval.(source.about))) ; re-evaluate it live" "lisp")) + (~docs/code :src (highlight "(get.sx.dev.(about)) ; the about page\n(get.sx.dev.(source.(about))) ; the SX source for the about page\n(get.sx.dev.(eval.(source.about))) ; re-evaluate it live" "lisp")) (p "The site is its own documentation. The source is always one expression away.")) (~docs/section :title "Comparison" :id "comparison" @@ -185,13 +185,13 @@ (p "The logical conclusion of SX is a " (strong "new internet protocol") " in which the URL, the HTTP verb, the query language, the response format, " "and the rendering layer are all unified under one evaluable expression format.") - (~docs/code :code (highlight ";; The entire network request — protocol, domain, verb, query, all one expression\n(get.sx.dev.(blog.(filter.(tag.lisp)).(limit.10)))" "lisp")) + (~docs/code :src (highlight ";; The entire network request — protocol, domain, verb, query, all one expression\n(get.sx.dev.(blog.(filter.(tag.lisp)).(limit.10)))" "lisp")) (p "HTTP becomes one possible implementation of a more general principle:") (blockquote :class "border-l-4 border-violet-300 pl-4 italic text-stone-600 my-4" (p (strong "Evaluate this expression. Return an expression.")))) (~docs/section :title "Reference Implementation" :id "reference" (p "SX is implemented in SX. The reference implementation is self-hosting and available at:") - (~docs/code :code (highlight "(get.sx.dev.(source.evaluator))" "lisp")) + (~docs/code :src (highlight "(get.sx.dev.(source.evaluator))" "lisp")) (p :class "text-sm text-stone-500 mt-4 italic" "This proposal was written in conversation with Claude (Anthropic). The ideas are the author's own.")))) diff --git a/sx/sx/plans/sx-pub.sx b/sx/sx/plans/sx-pub.sx index ffefae7c..b87d1ffd 100644 --- a/sx/sx/plans/sx-pub.sx +++ b/sx/sx/plans/sx-pub.sx @@ -21,10 +21,10 @@ (~docs/subsection :title "1. The Rendering Gap" (p "An ActivityPub Note looks like this:") - (~docs/code :code "{\"type\": \"Note\",\n \"content\": \"

Hello world

\",\n \"attributedTo\": \"https://example.com/users/alice\"}") + (~docs/code :src "{\"type\": \"Note\",\n \"content\": \"

Hello world

\",\n \"attributedTo\": \"https://example.com/users/alice\"}") (p "The content is an HTML string embedded in a JSON string. Two formats nested, neither evaluable. Every client — Mastodon, Pleroma, Misskey, Lemmy — parses this independently and wraps it in their own UI. A poll renders differently everywhere. An event looks different everywhere. There is no shared component model because the protocol has no concept of components.") (p "In sx-pub, the content " (em "is") " the component:") - (~docs/code :code "(Note\n :attributed-to \"https://pub.sx-web.org/pub/actor\"\n :content (p \"Hello \" (strong \"world\")))") + (~docs/code :src "(Note\n :attributed-to \"https://pub.sx-web.org/pub/actor\"\n :content (p \"Hello \" (strong \"world\")))") (p "No HTML-in-JSON. No parsing step. The receiver evaluates the s-expression and gets rendered output. The content carries its own rendering semantics.")) (~docs/subsection :title "2. Inert Data vs. Evaluable Programs" @@ -63,23 +63,23 @@ (p "Everything is s-expressions. No JSON-LD, no @context resolution, no nested HTML-in-JSON strings.") (~docs/subsection :title "Actor" - (~docs/code :code "(SxActor\n :id \"https://pub.sx-web.org/pub/actor\"\n :type \"SxPublisher\"\n :name \"sx\"\n :summary \"SX language — specs, platforms, components\"\n :public-key-pem \"-----BEGIN PUBLIC KEY-----\\n...\"\n :inbox \"/pub/inbox\"\n :outbox \"/pub/outbox\"\n :followers \"/pub/followers\"\n :following \"/pub/following\"\n :collections (list\n (SxCollection :name \"core-specs\" :href \"/pub/core-specs\")\n (SxCollection :name \"platforms\" :href \"/pub/platforms\")\n (SxCollection :name \"components\" :href \"/pub/components\")))")) + (~docs/code :src "(SxActor\n :id \"https://pub.sx-web.org/pub/actor\"\n :type \"SxPublisher\"\n :name \"sx\"\n :summary \"SX language — specs, platforms, components\"\n :public-key-pem \"-----BEGIN PUBLIC KEY-----\\n...\"\n :inbox \"/pub/inbox\"\n :outbox \"/pub/outbox\"\n :followers \"/pub/followers\"\n :following \"/pub/following\"\n :collections (list\n (SxCollection :name \"core-specs\" :href \"/pub/core-specs\")\n (SxCollection :name \"platforms\" :href \"/pub/platforms\")\n (SxCollection :name \"components\" :href \"/pub/components\")))")) (~docs/subsection :title "Publish Activity" (p "When content is published, it's pinned to IPFS and announced to followers:") - (~docs/code :code "(Publish\n :actor \"https://pub.sx-web.org/pub/actor\"\n :published \"2026-03-24T12:00:00Z\"\n :object (SxDocument\n :name \"evaluator\"\n :path \"/pub/core-specs/evaluator\"\n :cid \"bafy...eval123\"\n :content-type \"text/sx\"\n :size 48000\n :hash \"sha3-256:abc123...\"\n :requires (list \"bafy...parser\" \"bafy...primitives\")\n :summary \"CEK machine evaluator — frames, utils, step function\"))") + (~docs/code :src "(Publish\n :actor \"https://pub.sx-web.org/pub/actor\"\n :published \"2026-03-24T12:00:00Z\"\n :object (SxDocument\n :name \"evaluator\"\n :path \"/pub/core-specs/evaluator\"\n :cid \"bafy...eval123\"\n :content-type \"text/sx\"\n :size 48000\n :hash \"sha3-256:abc123...\"\n :requires (list \"bafy...parser\" \"bafy...primitives\")\n :summary \"CEK machine evaluator — frames, utils, step function\"))") (p "The " (code ":requires") " field declares CID dependencies — a component that imports the parser declares that dependency. Receivers can pin the full dependency graph.")) (~docs/subsection :title "Follow — Subscribe to a Remote Server" - (~docs/code :code "(Follow\n :actor \"https://pub.sx-web.org/pub/actor\"\n :object \"https://other.example.com/pub/actor\")") + (~docs/code :src "(Follow\n :actor \"https://pub.sx-web.org/pub/actor\"\n :object \"https://other.example.com/pub/actor\")") (p "When accepted, the remote server delivers " (code "(Publish ...)") " activities to our inbox. We pin their CIDs locally.")) (~docs/subsection :title "Announce — Mirror Remote Content" - (~docs/code :code "(Announce\n :actor \"https://pub.sx-web.org/pub/actor\"\n :object \"bafy...remote-cid\")") + (~docs/code :src "(Announce\n :actor \"https://pub.sx-web.org/pub/actor\"\n :object \"bafy...remote-cid\")") (p "Re-publish content from a followed server to our own followers. The CID is already pinned — we're just signalling that we endorse it.")) (~docs/subsection :title "Anchor — Blockchain Provenance Record" - (~docs/code :code "(Anchor\n :tree-cid \"bafy...merkle-tree\"\n :leaf-index 42\n :ots-cid \"bafy...ots-proof\"\n :btc-txid \"abc123...def\"\n :btc-block 890123\n :anchored-at \"2026-03-24T12:00:00Z\")") + (~docs/code :src "(Anchor\n :tree-cid \"bafy...merkle-tree\"\n :leaf-index 42\n :ots-cid \"bafy...ots-proof\"\n :btc-txid \"abc123...def\"\n :btc-block 890123\n :anchored-at \"2026-03-24T12:00:00Z\")") (p "Embedded in every " (code "(Publish ...)") " activity. Receivers can verify: the Merkle tree was anchored in that Bitcoin block, and the CID is a leaf in that tree. Inclusion proof is verifiable offline."))) ;; ----------------------------------------------------------------------- @@ -195,7 +195,7 @@ (li "Construct " (code "(Publish ...)") " with anchor proof embedded") (li "Deliver to follower inboxes — " (strong "only now") " is it public")) (p :class "text-sm text-stone-500 mt-2 italic" "This means publishing isn't instant — there's a delay while the anchor confirms. That's a feature: it forces a deliberate pace and makes provenance airtight.") - (~docs/code :code "(Publish\n :actor \"https://pub.sx-web.org/pub/actor\"\n :published \"2026-03-24T12:00:00Z\"\n :object (SxDocument\n :name \"evaluator\"\n :cid \"bafy...eval123\"\n :anchor (Anchor\n :tree-cid \"bafy...merkle\"\n :ots-cid \"bafy...proof\"\n :btc-txid \"abc123...\"\n :btc-block 890123)))") + (~docs/code :src "(Publish\n :actor \"https://pub.sx-web.org/pub/actor\"\n :published \"2026-03-24T12:00:00Z\"\n :object (SxDocument\n :name \"evaluator\"\n :cid \"bafy...eval123\"\n :anchor (Anchor\n :tree-cid \"bafy...merkle\"\n :ots-cid \"bafy...proof\"\n :btc-txid \"abc123...\"\n :btc-block 890123)))") (p :class "text-sm text-stone-600" "Receivers verify: the CID was anchored in that Bitcoin block " (em "before") " the Publish was sent. No way to backdate.")) (~docs/subsection :title "Follow" @@ -233,7 +233,7 @@ (~docs/section :title "CID ↔ Path Translation" :id "cid-path" (p "Published SX refers to dependencies by CID — the immutable, universal identifier. But humans browse by path.") - (~docs/code :code ";; Canonical SX (stored in IPFS) references by CID\n(defcomp ~my-app/dashboard (&key data)\n ;; This component depends on a chart library published at another CID\n (let ((chart-lib (require \"bafy...chart-lib-cid\")))\n (div :class \"dashboard\"\n (chart-lib/bar-chart :data data))))\n\n;; But on pub.sx-web.org you browse by path:\n;; /pub/components/dashboard\n;; /pub/core-specs/evaluator\n;; /pub/platforms/javascript\n;;\n;; The DB maps: path → CID → IPFS content\n;; Following a remote server maps: their-path → CID → our local pin") + (~docs/code :src ";; Canonical SX (stored in IPFS) references by CID\n(defcomp ~my-app/dashboard (&key data)\n ;; This component depends on a chart library published at another CID\n (let ((chart-lib (require \"bafy...chart-lib-cid\")))\n (div :class \"dashboard\"\n (chart-lib/bar-chart :data data))))\n\n;; But on pub.sx-web.org you browse by path:\n;; /pub/components/dashboard\n;; /pub/core-specs/evaluator\n;; /pub/platforms/javascript\n;;\n;; The DB maps: path → CID → IPFS content\n;; Following a remote server maps: their-path → CID → our local pin") (p "When you follow another sx-pub server and they publish a component, you get the " (code "(Publish ...)") " activity with the CID. Your server pins it. Now " (code "(require \"bafy...\")") " resolves locally — no network round-trip, no central registry, no package manager.")) diff --git a/sx/sx/plans/sx-urls.sx b/sx/sx/plans/sx-urls.sx index f7167071..71a8d2d8 100644 --- a/sx/sx/plans/sx-urls.sx +++ b/sx/sx/plans/sx-urls.sx @@ -13,14 +13,14 @@ (code "/sx/(language.(doc.introduction))") ". Dots replace spaces as the URL-friendly " "separator — they are unreserved in RFC 3986, never percent-encoded, and visually clean. " "The parser treats dot as whitespace: " (code "s/./ /") " before parsing as SX.") - (~docs/code :code (highlight + (~docs/code :src (highlight ";; Current → SX URLs (dots = spaces)\n/language/specs/signals → /(language.(spec.signals))\n/language/specs/explore/signals → /(language.(spec.(explore.signals)))\n/language/docs/introduction → /(language.(doc.introduction))\n/etc/plans/spec-explorer → /(etc.(plan.spec-explorer))\n\n;; Direct component access — any defcomp is addressable\n/(~essays/sx-sucks/essay-sx-sucks)\n/(~plans/sx-urls/plan-sx-urls-content)\n/(~analyzer/bundle-analyzer-content)" "lisp"))) (~docs/section :title "Scoping — The 30-Year Ambiguity, Fixed" :id "scoping" (p "REST URLs have an inherent ambiguity: does a filter/parameter apply to " "the last segment, or the whole path? Consider:") - (~docs/code :code (highlight + (~docs/code :src (highlight ";; REST — ambiguous:\n/users/123/posts?filter=published\n;; Is the filter scoped to posts? Or to the user? Or the whole query?\n;; Nobody knows. Conventions vary. Documentation required.\n\n;; SX URLs — explicit scoping via nesting:\n/(hello.(sailor.(filter.hhh))) ;; filter scoped to sailor\n/(hello.sailor.(filter.hhh)) ;; filter scoped to hello\n\n;; These mean different things, both expressible.\n;; Parens make scope visible. No ambiguity. No documentation needed." "lisp")) (p "This is not a minor syntactic preference. REST has never been able to express " @@ -37,7 +37,7 @@ "Dots are unreserved in RFC 3986, never encoded, and read naturally as \"drill down.\"") (p "The rule is simple: " (strong "dot = space, nothing more") ". " "Parens carry all the structural meaning. Dots are syntactic sugar for URLs only:") - (~docs/code :code (highlight + (~docs/code :src (highlight ";; These are identical after dot→space transform:\n/(language.(doc.introduction)) → (language (doc introduction))\n/(geography.(hypermedia.(reference.attributes)))\n → (geography (hypermedia (reference attributes)))\n\n;; Parens are still required for nesting:\n/(language.doc.introduction) → (language doc introduction)\n;; = language(\"doc\", \"introduction\") — WRONG\n\n;; Correct nesting:\n/(language.(doc.introduction)) → (language (doc introduction))\n;; = language(doc(\"introduction\")) — RIGHT" "lisp")) (p "The server's URL handler does one thing before parsing: " @@ -45,7 +45,7 @@ (~docs/section :title "The Lisp Tax" :id "parens" (p "People will hate the parentheses. But consider what developers already accept:") - (~docs/code :code (highlight + (~docs/code :src (highlight ";; Developers happily write this every day:\nhttps://api.site.com/v2/users/123/posts?filter=published&sort=date&order=desc&limit=10&offset=20\n\n;; And they would complain about this?\nhttps://site.com/(users.(posts.123.(filter.published.sort.date.limit.10)))\n\n;; The second is shorter, structured, unambiguous, and composable." "lisp")) (p "The real question: who is reading these URLs?") @@ -60,7 +60,7 @@ (~docs/section :title "The Site Is a REPL" :id "repl" (p "The address bar becomes the input line of a REPL. The page is the output.") - (~docs/code :code (highlight + (~docs/code :src (highlight "/sx/(about) ;; renders the about page\n/(source.(about)) ;; returns the SX source for the about page\n/(eval.(source.(about))) ;; re-evaluates it live\n\n;; The killer demo:\n/(eval.(map.double.(list.1.2.3))) ;; actually returns (2 4 6)\n\n;; The website IS a REPL. The address bar IS the input." "lisp")) (p "You do not need to explain what SX is. You show someone a URL and they " @@ -72,7 +72,7 @@ (p "The " (code "~") " sigil means \"find and execute this component.\" " "Components can make onward queries, process results, and return composed content — " "like server-side includes but Lispy and composable.") - (~docs/code :code (highlight + (~docs/code :src (highlight ";; ~get is a component that fetches, processes, and returns\n/(~get.everything-under-the-sun)\n\n;; The flow:\n;; 1. Server finds ~get component in env\n;; 2. ~get makes onward queries\n;; 3. Processes and transforms results\n;; 4. Returns composed hypermedia\n\n;; Because it's all SX, you nest and compose:\n/(~page.home\n .(~hero.banner)\n .(~get.latest-posts.(limit.5))\n .(~get.featured.(filter.pinned)))\n\n;; Each ~component is independently:\n;; - cacheable (by its expression)\n;; - reusable (same component, different args)\n;; - testable (evaluate in isolation)" "lisp")) (p "This is what React Server Components are trying to do — server-side data resolution " @@ -167,7 +167,7 @@ (p "Any " (code "defcomp") " is directly addressable via its " (code "~plans/content-addressed-components/name") ". " "The URL evaluator sees " (code "~essays/sx-sucks/essay-sx-sucks") ", looks it up in the component env, " "evaluates it, wraps in " (code "~layouts/doc") ", and returns.") - (~docs/code :code (highlight + (~docs/code :src (highlight ";; Page functions are convenience wrappers:\n/(etc.(essay.sx-sucks)) ;; dispatches via case statement\n\n;; But you can bypass them entirely:\n/(~essays/sx-sucks/essay-sx-sucks) ;; direct component — no routing needed\n\n;; Every defcomp is instantly URL-accessible:\n/(~plans/sx-urls/plan-sx-urls-content) ;; this very page\n/(~analyzer/bundle-analyzer-content) ;; tools\n/(~docs-content/docs-evaluator-content) ;; docs" "lisp")) (p "New components are instantly URL-accessible without routing wiring. " @@ -217,26 +217,26 @@ "is parsed as SX and evaluated with a " (strong "soft eval") ": " "known function names are called; unknown symbols self-evaluate to their name as a string; " "components (" (code "~plans/content-addressed-components/name") ") are looked up in the component env.") - (~docs/code :code (highlight + (~docs/code :src (highlight "/sx/(language.(doc.introduction))\n\n;; After dot→space: (language (doc introduction))\n;; 1. Eval `introduction` → not a known function → \"introduction\"\n;; 2. Eval (doc \"introduction\") → call doc(\"introduction\") → page content\n;; 3. Eval (language content) → call language(content) → passes through\n;; 4. Router wraps result in (~layouts/doc :path \"(language (doc introduction))\" ...)\n\n/(~essays/sx-sucks/essay-sx-sucks)\n;; 1. Eval ~essays/sx-sucks/essay-sx-sucks → component lookup → evaluate → content\n;; 2. Router wraps in ~layouts/doc" "lisp")) (~docs/subsection :title "Section Functions" (p "Structural functions that encode hierarchy and pass through content:") - (~docs/code :code (highlight + (~docs/code :src (highlight "(define language\n (fn (&rest args)\n (if (empty? args) (language-index-content) (first args))))\n\n(define geography\n (fn (&rest args)\n (if (empty? args) (geography-index-content) (first args))))\n\n;; Sub-sections also pass through\n(define hypermedia\n (fn (&rest args)\n (if (empty? args) (hypermedia-index-content) (first args))))" "lisp"))) (~docs/subsection :title "Page Functions" (p "Leaf functions that dispatch to content components. " "Data-dependent pages call helpers directly — the async evaluator handles IO:") - (~docs/code :code (highlight + (~docs/code :src (highlight "(define doc\n (fn (&rest args)\n (let ((slug (first-or-nil args)))\n (if (nil? slug)\n (~docs-content/docs-introduction-content)\n (case slug\n \"introduction\" (~docs-content/docs-introduction-content)\n \"getting-started\" (~docs-content/docs-getting-started-content)\n ...)))))\n\n(define bootstrapper\n (fn (&rest args)\n (let ((slug (first-or-nil args))\n (data (when slug (bootstrapper-data slug))))\n (if (nil? slug)\n (~specs/bootstrappers-index-content)\n (if (get data \"bootstrapper-not-found\")\n (~specs/not-found :slug slug)\n (case slug\n \"python\" (~specs/bootstrapper-py-content ...)\n ...))))))" "lisp")))) (~docs/section :title "The Catch-All Route" :id "route" (p "The entire routing layer becomes one handler:") - (~docs/code :code (highlight + (~docs/code :src (highlight "@app.get(\"/\")\nasync def sx_home():\n return await eval_sx_url(\"/\")\n\n@app.get(\"/\")\nasync def sx_eval_route(expr):\n return await eval_sx_url(f\"/{expr}\")" "python")) (p (code "eval_sx_url") " in seven steps:") @@ -252,7 +252,7 @@ "so they match first.")) (~docs/section :title "Composability" :id "composability" - (~docs/code :code (highlight + (~docs/code :src (highlight ";; Direct component access\n/(~essays/sx-sucks/essay-sx-sucks)\n/(~specs-explorer/spec-explorer-content)\n\n;; URL special forms\n/(source.(~essays/sx-sucks/essay-sx-sucks)) ;; view defcomp source\n/(inspect.(language.(doc.primitives))) ;; deps, render plan\n/(diff.(language.(spec.signals)).(language.(spec.eval))) ;; side by side\n/(eval.(map.double.(list.1.2.3))) ;; REPL in the URL bar\n\n;; Components as query resolvers\n/(~page.home\n .(~hero.banner)\n .(~get.latest-posts.(limit.5))\n .(~get.featured.(filter.pinned)))\n\n;; Scoping is explicit\n/(users.(posts.123.(filter.published))) ;; filter scoped to posts\n/(users.posts.123.(filter.published)) ;; filter scoped to users\n\n;; Cross-service (future)\n/(market.(product.42.:fields.(name.price)))\n/(subscribe.(etc.(plan.status)))" "lisp"))) diff --git a/sx/sx/plans/theorem-prover.sx b/sx/sx/plans/theorem-prover.sx index 8b9b021f..9d21e920 100644 --- a/sx/sx/plans/theorem-prover.sx +++ b/sx/sx/plans/theorem-prover.sx @@ -45,7 +45,7 @@ (~docs/section :title "Phase 1: Definitional satisfiability" :id "phase1" (p :class "text-stone-600" "Every " (code "define-primitive") " with a " (code ":body") " produces a " (code "forall") " assertion in SMT-LIB. For example, " (code "(define-primitive \"inc\" :params (n) :body (+ n 1))") " becomes:") - (~docs/code :code (highlight "; inc\n(declare-fun inc (Int) Int)\n(assert (forall (((n Int)))\n (= (inc n) (+ n 1))))\n(check-sat)" "lisp")) + (~docs/code :src (highlight "; inc\n(declare-fun inc (Int) Int)\n(assert (forall (((n Int)))\n (= (inc n) (+ n 1))))\n(check-sat)" "lisp")) (p :class "text-stone-600" "This is satisfiable by construction: define " (code "inc(n) = n + 1") " and the assertion holds. " (code "prove.sx") " verifies this mechanically for every primitive — it parses the SMT-LIB, extracts the definition, builds a model, and evaluates it with test values.") @@ -168,7 +168,7 @@ "Each property also generates SMT-LIB for unbounded verification by an external solver. The strategy: assert the " (em "negation") " of the universal property. If Z3 returns " (code "unsat") ", the property holds for " (em "all") " integers — not just the bounded domain.") (p :class "text-stone-600" (code "prove.sx") " reuses " (code "z3-expr") " from " (code "z3.sx") " to translate the property AST to SMT-LIB. Properties with preconditions use " (code "=>") " (implication). The same SX expression is both the bounded test and the formal verification condition.") - (~docs/code :code (highlight smtlib-sample "lisp"))) + (~docs/code :src (highlight smtlib-sample "lisp"))) ;; --- What it tells us --- (~docs/section :title "What this tells us about SX" :id "implications" @@ -245,9 +245,9 @@ (code "prove-search") " walks tuples looking for counterexamples. " (code "sx-properties") " declares 34 algebraic laws as test functions with quoted ASTs. " (code "prove-property-smtlib") " translates properties to SMT-LIB verification conditions via " (code "z3-expr") ".") - (~docs/code :code (highlight prove-source "lisp"))) + (~docs/code :src (highlight prove-source "lisp"))) (~docs/section :title "The translator: z3.sx" :id "z3-source" (p :class "text-stone-600" "The translator that " (code "prove.sx") " depends on. SX expressions that walk other SX expressions and emit SMT-LIB strings. Both files together: ~760 lines of s-expressions, no host language logic.") - (~docs/code :code (highlight z3-source "lisp"))))) + (~docs/code :src (highlight z3-source "lisp"))))) diff --git a/sx/sx/plans/typed-sx.sx b/sx/sx/plans/typed-sx.sx index ce6f48b6..631e00e9 100644 --- a/sx/sx/plans/typed-sx.sx +++ b/sx/sx/plans/typed-sx.sx @@ -61,20 +61,20 @@ (p "Small, practical, no type theory PhD required.") (~docs/subsection :title "Base types" - (~docs/code :code (highlight ";; Atomic types\nnumber string boolean nil symbol keyword element\n\n;; Nullable (already used in boundary.sx)\nstring? ;; (or string nil)\ndict? ;; (or dict nil)\nnumber? ;; (or number nil)\n\n;; The top type — anything goes\nany\n\n;; The bottom type — never returns (e.g. abort)\nnever" "lisp"))) + (~docs/code :src (highlight ";; Atomic types\nnumber string boolean nil symbol keyword element\n\n;; Nullable (already used in boundary.sx)\nstring? ;; (or string nil)\ndict? ;; (or dict nil)\nnumber? ;; (or number nil)\n\n;; The top type — anything goes\nany\n\n;; The bottom type — never returns (e.g. abort)\nnever" "lisp"))) (~docs/subsection :title "Compound types" - (~docs/code :code (highlight ";; Collections with element types\n(list-of number) ;; list where every element is a number\n(list-of string) ;; list of strings\n(list-of any) ;; list (same as untyped)\n(dict-of string number) ;; dict with string keys, number values\n(dict-of string any) ;; dict with string keys (typical kwargs)\n\n;; Union types\n(or string number) ;; either string or number\n(or string nil) ;; same as string?\n\n;; Function types\n(-> number number) ;; number → number\n(-> string string boolean) ;; (string, string) → boolean\n(-> (list-of any) number) ;; list → number\n(-> &rest any number) ;; variadic → number" "lisp"))) + (~docs/code :src (highlight ";; Collections with element types\n(list-of number) ;; list where every element is a number\n(list-of string) ;; list of strings\n(list-of any) ;; list (same as untyped)\n(dict-of string number) ;; dict with string keys, number values\n(dict-of string any) ;; dict with string keys (typical kwargs)\n\n;; Union types\n(or string number) ;; either string or number\n(or string nil) ;; same as string?\n\n;; Function types\n(-> number number) ;; number → number\n(-> string string boolean) ;; (string, string) → boolean\n(-> (list-of any) number) ;; list → number\n(-> &rest any number) ;; variadic → number" "lisp"))) (~docs/subsection :title "Component types" - (~docs/code :code (highlight ";; Component type is its keyword signature\n;; Derived automatically from defcomp — no annotation needed\n\n(comp :title string :price number &rest element)\n;; keyword args children type\n\n;; This is NOT a new syntax for defcomp.\n;; It's the TYPE that a defcomp declaration produces.\n;; The checker infers it from parse-comp-params + annotations." "lisp"))) + (~docs/code :src (highlight ";; Component type is its keyword signature\n;; Derived automatically from defcomp — no annotation needed\n\n(comp :title string :price number &rest element)\n;; keyword args children type\n\n;; This is NOT a new syntax for defcomp.\n;; It's the TYPE that a defcomp declaration produces.\n;; The checker infers it from parse-comp-params + annotations." "lisp"))) (p "That's the core. No higher-kinded types, no dependent types, no type classes. Just: what goes in, what comes out, can it be nil.") (~docs/subsection :title "User-defined types" - (~docs/code :code (highlight ";; Type alias — a name for an existing type\n(deftype price number)\n(deftype html-string string)\n\n;; Union — one of several types\n(deftype renderable (union string number nil))\n(deftype key-type (union string keyword))\n\n;; Record — typed dict shape\n(deftype card-props\n {:title string\n :subtitle string?\n :price number})\n\n;; Parameterized — generic over a type variable\n(deftype (maybe a) (union nil a))\n(deftype (list-of-pairs a b) (list-of (dict-of a b)))\n(deftype (result a e) (union (ok a) (err e)))" "lisp")) + (~docs/code :src (highlight ";; Type alias — a name for an existing type\n(deftype price number)\n(deftype html-string string)\n\n;; Union — one of several types\n(deftype renderable (union string number nil))\n(deftype key-type (union string keyword))\n\n;; Record — typed dict shape\n(deftype card-props\n {:title string\n :subtitle string?\n :price number})\n\n;; Parameterized — generic over a type variable\n(deftype (maybe a) (union nil a))\n(deftype (list-of-pairs a b) (list-of (dict-of a b)))\n(deftype (result a e) (union (ok a) (err e)))" "lisp")) (p (code "deftype") " is a declaration form — zero runtime cost, purely for the checker. The type registry resolves user-defined type names during " (code "subtype?") " and " (code "infer-type") ". Records enable typed keyword args for components:") - (~docs/code :code (highlight ";; Define a prop shape\n(deftype card-props\n {:title string\n :subtitle string?\n :price number\n :on-click (-> any nil)?})\n\n;; Use it in a component\n(defcomp ~plans/typed-sx/product-card (&key (props :as card-props) &rest children)\n (div :class \"card\"\n (h2 (get props :title))\n (span (format-decimal (get props :price) 2))\n children))\n\n;; Checker validates dict literals against record shape:\n(~plans/typed-sx/product-card :props {:title \"Widget\" :price \"oops\"})\n;; ^^^^^^\n;; ERROR: :price expects number, got string" "lisp")))) + (~docs/code :src (highlight ";; Define a prop shape\n(deftype card-props\n {:title string\n :subtitle string?\n :price number\n :on-click (-> any nil)?})\n\n;; Use it in a component\n(defcomp ~plans/typed-sx/product-card (&key (props :as card-props) &rest children)\n (div :class \"card\"\n (h2 (get props :title))\n (span (format-decimal (get props :price) 2))\n children))\n\n;; Checker validates dict literals against record shape:\n(~plans/typed-sx/product-card :props {:title \"Widget\" :price \"oops\"})\n;; ^^^^^^\n;; ERROR: :price expects number, got string" "lisp")))) ;; ----------------------------------------------------------------------- ;; Annotation syntax @@ -84,15 +84,15 @@ (p "Annotations are optional. Three places they can appear:") (~docs/subsection :title "1. Component params" - (~docs/code :code (highlight ";; Current (unchanged, still works)\n(defcomp ~plans/typed-sx/product-card (&key title price image-url &rest children)\n (div ...))\n\n;; Annotated — colon after param name\n(defcomp ~plans/typed-sx/product-card (&key (title : string)\n (price : number)\n (image-url : string?)\n &rest children)\n (div ...))\n\n;; Parenthesized pairs: (name : type)\n;; Unannotated params default to `any`\n;; &rest children is always (list-of element)" "lisp")) + (~docs/code :src (highlight ";; Current (unchanged, still works)\n(defcomp ~plans/typed-sx/product-card (&key title price image-url &rest children)\n (div ...))\n\n;; Annotated — colon after param name\n(defcomp ~plans/typed-sx/product-card (&key (title : string)\n (price : number)\n (image-url : string?)\n &rest children)\n (div ...))\n\n;; Parenthesized pairs: (name : type)\n;; Unannotated params default to `any`\n;; &rest children is always (list-of element)" "lisp")) (p "The " (code "(name : type)") " syntax is unambiguous — a 3-element list where the second element is the symbol " (code ":") ". The parser already handles lists inside parameter lists. " (code "parse-comp-params") " gains a branch: if a param is a list of length 3 with " (code ":") " in the middle, extract name and type.")) (~docs/subsection :title "2. Define/lambda return types" - (~docs/code :code (highlight ";; Current (unchanged)\n(define total-price\n (fn (items)\n (reduce + 0 (map (fn (i) (get i \"price\")) items))))\n\n;; Annotated — :returns after params\n(define total-price\n (fn ((items : (list-of dict)) :returns number)\n (reduce + 0 (map (fn (i) (get i \"price\")) items))))" "lisp")) + (~docs/code :src (highlight ";; Current (unchanged)\n(define total-price\n (fn (items)\n (reduce + 0 (map (fn (i) (get i \"price\")) items))))\n\n;; Annotated — :returns after params\n(define total-price\n (fn ((items : (list-of dict)) :returns number)\n (reduce + 0 (map (fn (i) (get i \"price\")) items))))" "lisp")) (p (code ":returns") " is already the convention in " (code "primitives.sx") " and " (code "boundary.sx") ". Same keyword, same position (after params), same meaning.")) (~docs/subsection :title "3. Let bindings" - (~docs/code :code (highlight ";; Current (unchanged)\n(let ((x (compute-value)))\n (+ x 1))\n\n;; Annotated\n(let (((x : number) (compute-value)))\n (+ x 1))\n\n;; Usually unnecessary — the checker infers let binding\n;; types from the right-hand side. Only annotate when\n;; the inference is ambiguous (e.g. the RHS returns `any`)." "lisp")) + (~docs/code :src (highlight ";; Current (unchanged)\n(let ((x (compute-value)))\n (+ x 1))\n\n;; Annotated\n(let (((x : number) (compute-value)))\n (+ x 1))\n\n;; Usually unnecessary — the checker infers let binding\n;; types from the right-hand side. Only annotate when\n;; the inference is ambiguous (e.g. the RHS returns `any`)." "lisp")) (p "All annotations are syntactically backward-compatible. Unannotated code parses and runs identically. The annotations are simply ignored by evaluators that don't have the type checker loaded.")) @@ -145,7 +145,7 @@ (p "The practical sweet spot: " (strong "annotate component params, nothing else.") " Components are the public API — the boundary between independent pieces of code. Their params are the contract. Internal lambdas and let bindings benefit less from annotations because the checker can infer their types from context.") - (~docs/code :code (highlight ";; Sweet spot: annotate the interface, infer the rest\n(defcomp ~plans/typed-sx/price-display (&key (price : number)\n (currency : string)\n (sale-price : number?))\n ;; Everything below is inferred:\n ;; formatted → string (str returns string)\n ;; discount → number (- returns number)\n ;; has-sale → boolean (and returns boolean)\n (let ((formatted (str currency (format-number price 2)))\n (has-sale (and sale-price (< sale-price price)))\n (discount (if has-sale\n (round (* 100 (/ (- price sale-price) price)))\n 0)))\n (div :class \"price\"\n (span :class (if has-sale \"line-through text-stone-400\" \"font-bold\")\n formatted)\n (when has-sale\n (span :class \"text-green-700 font-bold ml-2\"\n (str currency (format-number sale-price 2))\n (span :class \"text-xs ml-1\" (str \"(-\" discount \"%)\")))))))" "lisp"))) + (~docs/code :src (highlight ";; Sweet spot: annotate the interface, infer the rest\n(defcomp ~plans/typed-sx/price-display (&key (price : number)\n (currency : string)\n (sale-price : number?))\n ;; Everything below is inferred:\n ;; formatted → string (str returns string)\n ;; discount → number (- returns number)\n ;; has-sale → boolean (and returns boolean)\n (let ((formatted (str currency (format-number price 2)))\n (has-sale (and sale-price (< sale-price price)))\n (discount (if has-sale\n (round (* 100 (/ (- price sale-price) price)))\n 0)))\n (div :class \"price\"\n (span :class (if has-sale \"line-through text-stone-400\" \"font-bold\")\n formatted)\n (when has-sale\n (span :class \"text-green-700 font-bold ml-2\"\n (str currency (format-number sale-price 2))\n (span :class \"text-xs ml-1\" (str \"(-\" discount \"%)\")))))))" "lisp"))) ;; ----------------------------------------------------------------------- ;; Error reporting @@ -154,7 +154,7 @@ (~docs/section :title "Error Reporting" :id "errors" (p "Type errors are reported at registration time with source location, expected type, actual type, and the full call chain.") - (~docs/code :code (highlight ";; Example error output:\n;;\n;; TYPE ERROR in ~checkout-summary (checkout.sx:34)\n;;\n;; (str \"Total: \" (compute-total items))\n;; ^^^^^^^^^^^^^^^^^\n;; Argument 2 of `str` expects: string\n;; Got: number (from compute-total :returns number)\n;;\n;; Fix: (str \"Total: \" (str (compute-total items)))\n;;\n;;\n;; TYPE ERROR in ~reactive-islands/event-bridge/product-page (products.sx:12)\n;;\n;; (~plans/typed-sx/product-card :title product-name :price \"29.99\")\n;; ^^^^^^\n;; Keyword :price of ~plans/typed-sx/product-card expects: number\n;; Got: string (literal \"29.99\")\n;;\n;; Fix: (~plans/typed-sx/product-card :title product-name :price 29.99)" "lisp")) + (~docs/code :src (highlight ";; Example error output:\n;;\n;; TYPE ERROR in ~checkout-summary (checkout.sx:34)\n;;\n;; (str \"Total: \" (compute-total items))\n;; ^^^^^^^^^^^^^^^^^\n;; Argument 2 of `str` expects: string\n;; Got: number (from compute-total :returns number)\n;;\n;; Fix: (str \"Total: \" (str (compute-total items)))\n;;\n;;\n;; TYPE ERROR in ~reactive-islands/event-bridge/product-page (products.sx:12)\n;;\n;; (~plans/typed-sx/product-card :title product-name :price \"29.99\")\n;; ^^^^^^\n;; Keyword :price of ~plans/typed-sx/product-card expects: number\n;; Got: string (literal \"29.99\")\n;;\n;; Fix: (~plans/typed-sx/product-card :title product-name :price 29.99)" "lisp")) (p "Severity levels:") (div :class "overflow-x-auto rounded border border-stone-200 mb-4" @@ -186,7 +186,7 @@ (~docs/section :title "Nil Narrowing" :id "nil" (p "The most common real-world type error in SX: passing a possibly-nil value where a non-nil is required. " (code "get") " returns " (code "any") " (might be nil). " (code "current-user") " returns " (code "dict?") " (explicitly nullable). Piping these into " (code "str") " or arithmetic without checking is the #1 source of runtime errors.") - (~docs/code :code (highlight ";; Before: runtime error if user is nil\n(defcomp ~plans/typed-sx/greeting (&key (user : dict?))\n (h1 (str \"Hello, \" (get user \"name\"))))\n ;; ^^^ TYPE WARNING: user is dict?, get needs non-nil first arg\n\n;; After: checker enforces nil handling\n(defcomp ~plans/typed-sx/greeting (&key (user : dict?))\n (if user\n (h1 (str \"Hello, \" (get user \"name\")))\n ;; In this branch, checker narrows user to `dict` (not nil)\n (h1 \"Hello, guest\")))\n ;; No warning — nil case handled" "lisp")) + (~docs/code :src (highlight ";; Before: runtime error if user is nil\n(defcomp ~plans/typed-sx/greeting (&key (user : dict?))\n (h1 (str \"Hello, \" (get user \"name\"))))\n ;; ^^^ TYPE WARNING: user is dict?, get needs non-nil first arg\n\n;; After: checker enforces nil handling\n(defcomp ~plans/typed-sx/greeting (&key (user : dict?))\n (if user\n (h1 (str \"Hello, \" (get user \"name\")))\n ;; In this branch, checker narrows user to `dict` (not nil)\n (h1 \"Hello, guest\")))\n ;; No warning — nil case handled" "lisp")) (p "Narrowing rules:") (ul :class "list-disc pl-5 text-stone-700 space-y-1" @@ -203,7 +203,7 @@ (~docs/section :title "Component Signature Verification" :id "signatures" (p "The highest-value check: verifying that component call sites match declared signatures. This is where most bugs live.") - (~docs/code :code (highlight ";; Definition\n(defcomp ~plans/typed-sx/product-card (&key (title : string)\n (price : number)\n (image-url : string?)\n &rest children)\n ...)\n\n;; Call site checks:\n(~plans/typed-sx/product-card :title \"Widget\" :price 29.99) ;; OK\n(~plans/typed-sx/product-card :title \"Widget\") ;; ERROR: :price required\n(~plans/typed-sx/product-card :title 42 :price 29.99) ;; ERROR: :title expects string\n(~plans/typed-sx/product-card :title \"Widget\" :price 29.99\n (p \"Description\") (p \"Details\")) ;; OK: children\n(~plans/typed-sx/product-card :titel \"Widget\" :price 29.99) ;; WARNING: :titel unknown\n ;; (did you mean :title?)" "lisp")) + (~docs/code :src (highlight ";; Definition\n(defcomp ~plans/typed-sx/product-card (&key (title : string)\n (price : number)\n (image-url : string?)\n &rest children)\n ...)\n\n;; Call site checks:\n(~plans/typed-sx/product-card :title \"Widget\" :price 29.99) ;; OK\n(~plans/typed-sx/product-card :title \"Widget\") ;; ERROR: :price required\n(~plans/typed-sx/product-card :title 42 :price 29.99) ;; ERROR: :title expects string\n(~plans/typed-sx/product-card :title \"Widget\" :price 29.99\n (p \"Description\") (p \"Details\")) ;; OK: children\n(~plans/typed-sx/product-card :titel \"Widget\" :price 29.99) ;; WARNING: :titel unknown\n ;; (did you mean :title?)" "lisp")) (p "The checker walks every component call in every component body. For each call:") (ol :class "list-decimal pl-5 text-stone-700 space-y-1" @@ -222,7 +222,7 @@ (~docs/section :title "Thread-First Type Flow" :id "thread-first" (p "The " (code "->") " (thread-first) form is SX's primary composition operator. Type checking it means verifying each step's output matches the next step's input:") - (~docs/code :code (highlight ";; (-> items\n;; (filter active?) ;; (list-of dict) → (list-of dict)\n;; (map name) ;; (list-of dict) → (list-of string)\n;; (join \", \")) ;; (list-of string) → string\n;;\n;; Type flow: (list-of dict) → (list-of dict) → (list-of string) → string\n;; Each step's output is the next step's first argument.\n\n;; ERROR example:\n;; (-> items\n;; (filter active?)\n;; (join \", \") ;; join expects (list-of string),\n;; (map name)) ;; got string — wrong order!\n;;\n;; TYPE ERROR: step 3 (map) expects (list-of any) as first arg\n;; got: string (from join)" "lisp")) + (~docs/code :src (highlight ";; (-> items\n;; (filter active?) ;; (list-of dict) → (list-of dict)\n;; (map name) ;; (list-of dict) → (list-of string)\n;; (join \", \")) ;; (list-of string) → string\n;;\n;; Type flow: (list-of dict) → (list-of dict) → (list-of string) → string\n;; Each step's output is the next step's first argument.\n\n;; ERROR example:\n;; (-> items\n;; (filter active?)\n;; (join \", \") ;; join expects (list-of string),\n;; (map name)) ;; got string — wrong order!\n;;\n;; TYPE ERROR: step 3 (map) expects (list-of any) as first arg\n;; got: string (from join)" "lisp")) (p "The checker threads the inferred type through each step. If any step's input type doesn't match the previous step's output type, it reports the exact point where the pipeline breaks.")) @@ -271,19 +271,19 @@ (p (code "deftype") " introduces named types — aliases, unions, records, and parameterized types. All are declaration-only, zero runtime cost, resolved at check time.") (~docs/subsection :title "Type aliases" - (~docs/code :code (highlight ";; Simple name for an existing type\n(deftype price number)\n(deftype html-string string)\n(deftype user-id (or string number))\n\n;; Use anywhere a type is expected\n(defcomp ~plans/typed-sx/price-tag (&key (amount :as price) (label :as string))\n (span :class \"price\" (str label \": $\" (format-decimal amount 2))))" "lisp")) + (~docs/code :src (highlight ";; Simple name for an existing type\n(deftype price number)\n(deftype html-string string)\n(deftype user-id (or string number))\n\n;; Use anywhere a type is expected\n(defcomp ~plans/typed-sx/price-tag (&key (amount :as price) (label :as string))\n (span :class \"price\" (str label \": $\" (format-decimal amount 2))))" "lisp")) (p "Aliases are transparent — " (code "price") " IS " (code "number") " for all checking purposes. They exist for documentation and domain semantics.")) (~docs/subsection :title "Union types" - (~docs/code :code (highlight ";; Named unions\n(deftype renderable (union string number nil component))\n(deftype key-type (union string keyword))\n(deftype falsy (union nil false))\n\n;; The checker narrows unions in branches:\n(define handle-input\n (fn ((val :as (union string number)))\n (if (string? val)\n (upper val) ;; narrowed to string — upper is valid\n (+ val 1)))) ;; narrowed to number — + is valid" "lisp")) + (~docs/code :src (highlight ";; Named unions\n(deftype renderable (union string number nil component))\n(deftype key-type (union string keyword))\n(deftype falsy (union nil false))\n\n;; The checker narrows unions in branches:\n(define handle-input\n (fn ((val :as (union string number)))\n (if (string? val)\n (upper val) ;; narrowed to string — upper is valid\n (+ val 1)))) ;; narrowed to number — + is valid" "lisp")) (p "Union types compose with narrowing — " (code "if (string? x)") " in the then-branch narrows " (code "(union string number)") " to " (code "string") ". Same flow typing that already works for nullable.")) (~docs/subsection :title "Record types (typed dicts)" - (~docs/code :code (highlight ";; Typed dict shape\n(deftype card-props\n {:title string\n :subtitle string?\n :price number\n :tags (list-of string)})\n\n;; Checker validates dict literals against shape:\n{:title \"Widget\" :price \"oops\"}\n;; ERROR: :price expects number, got string\n\n{:title \"Widget\" :price 29.99}\n;; WARNING: missing :tags (required field)\n\n;; Record types enable typed component props:\n(defcomp ~plans/typed-sx/product-card (&key (props :as card-props) &rest children)\n (div :class \"card\"\n (h2 (get props :title))\n children))" "lisp")) + (~docs/code :src (highlight ";; Typed dict shape\n(deftype card-props\n {:title string\n :subtitle string?\n :price number\n :tags (list-of string)})\n\n;; Checker validates dict literals against shape:\n{:title \"Widget\" :price \"oops\"}\n;; ERROR: :price expects number, got string\n\n{:title \"Widget\" :price 29.99}\n;; WARNING: missing :tags (required field)\n\n;; Record types enable typed component props:\n(defcomp ~plans/typed-sx/product-card (&key (props :as card-props) &rest children)\n (div :class \"card\"\n (h2 (get props :title))\n children))" "lisp")) (p "Records are the big win. Components pass dicts everywhere — config, props, context. A record type makes " (code "get") " on a known-shape dict return the field's type instead of " (code "any") ". This is where " (code "deftype") " pays for itself.")) (~docs/subsection :title "Parameterized types" - (~docs/code :code (highlight ";; Generic over type variables\n(deftype (maybe a) (union nil a))\n(deftype (result a e) (union {:ok a} {:err e}))\n(deftype (pair a b) {:fst a :snd b})\n\n;; Used in signatures:\n(define find-user : (-> number (maybe user-record))\n (fn (id) ...))\n\n;; Checker instantiates: (maybe user-record) = (union nil user-record)\n;; So the caller must handle nil." "lisp")) + (~docs/code :src (highlight ";; Generic over type variables\n(deftype (maybe a) (union nil a))\n(deftype (result a e) (union {:ok a} {:err e}))\n(deftype (pair a b) {:fst a :snd b})\n\n;; Used in signatures:\n(define find-user : (-> number (maybe user-record))\n (fn (id) ...))\n\n;; Checker instantiates: (maybe user-record) = (union nil user-record)\n;; So the caller must handle nil." "lisp")) (p "Parameterized types are substitution-based — " (code "(maybe string)") " expands to " (code "(union nil string)") " at check time. No inference of type parameters (that would require Hindley-Milner). You write " (code "(maybe string)") " explicitly, the checker substitutes and verifies."))) ;; ----------------------------------------------------------------------- @@ -294,7 +294,7 @@ (p "The pragmatic middle: static effect " (em "checking") " without algebraic effect " (em "handlers") ". Functions declare what side effects they use. The checker enforces that effects don't leak across boundaries. No continuations, no runtime cost.") (~docs/subsection :title "Effect declarations" - (~docs/code :code (highlight ";; Declare named effects\n(defeffect io) ;; Database, HTTP, file system\n(defeffect dom) ;; Browser DOM manipulation\n(defeffect async) ;; Asynchronous operations\n(defeffect state) ;; Mutable state (set!, dict-set!, append!)\n\n;; Functions declare their effects in brackets\n(define fetch-user : (-> number user) [io async]\n (fn (id) (query \"SELECT * FROM users WHERE id = $1\" id)))\n\n(define toggle-class : (-> element string nil) [dom]\n (fn (el cls) (set-attr! el :class cls)))\n\n;; Pure by default — no annotation means no effects\n(define add-prices : (-> (list-of number) number)\n (fn (prices) (reduce + 0 prices)))" "lisp"))) + (~docs/code :src (highlight ";; Declare named effects\n(defeffect io) ;; Database, HTTP, file system\n(defeffect dom) ;; Browser DOM manipulation\n(defeffect async) ;; Asynchronous operations\n(defeffect state) ;; Mutable state (set!, dict-set!, append!)\n\n;; Functions declare their effects in brackets\n(define fetch-user : (-> number user) [io async]\n (fn (id) (query \"SELECT * FROM users WHERE id = $1\" id)))\n\n(define toggle-class : (-> element string nil) [dom]\n (fn (el cls) (set-attr! el :class cls)))\n\n;; Pure by default — no annotation means no effects\n(define add-prices : (-> (list-of number) number)\n (fn (prices) (reduce + 0 prices)))" "lisp"))) (~docs/subsection :title "What it checks" (ol :class "list-decimal pl-5 text-stone-700 space-y-2" @@ -327,14 +327,14 @@ (p "This is exactly the information " (code "deps.sx") " already computes — which components have IO refs. Effects promote it from a runtime classification to a static type-level property. Pure components get an ironclad guarantee: memoize, cache, SSR anywhere, serialize for client — provably safe.")) (~docs/subsection :title "Effect propagation" - (~docs/code :code (highlight ";; Effects propagate through calls:\n(define fetch-prices : (-> (list-of number)) [io async]\n (fn () (query \"SELECT price FROM products\")))\n\n(define render-total : (-> element) [io async] ;; must declare, calls fetch-prices\n (fn ()\n (let ((prices (fetch-prices)))\n (span (str \"$\" (reduce + 0 prices))))))\n\n;; ERROR if you forget:\n(define render-total : (-> element) ;; no effects declared\n (fn ()\n (let ((prices (fetch-prices))) ;; ERROR: calls [io async] from pure context\n (span (str \"$\" (reduce + 0 prices))))))" "lisp")) + (~docs/code :src (highlight ";; Effects propagate through calls:\n(define fetch-prices : (-> (list-of number)) [io async]\n (fn () (query \"SELECT price FROM products\")))\n\n(define render-total : (-> element) [io async] ;; must declare, calls fetch-prices\n (fn ()\n (let ((prices (fetch-prices)))\n (span (str \"$\" (reduce + 0 prices))))))\n\n;; ERROR if you forget:\n(define render-total : (-> element) ;; no effects declared\n (fn ()\n (let ((prices (fetch-prices))) ;; ERROR: calls [io async] from pure context\n (span (str \"$\" (reduce + 0 prices))))))" "lisp")) (p "The checker walks call graphs and verifies that every function's declared effects are a superset of its callees' effects. This is transitive — if A calls B calls C, and C has " (code "[io]") ", then A must also declare " (code "[io]") ".")) (~docs/subsection :title "Gradual effects" (p "Like gradual types, effects are opt-in. Unannotated functions are assumed to have " (em "all") " effects — they can call anything, and anything can call them. This is safe (no false positives) but provides no guarantees. As you annotate more functions, the checker catches more violations.") (p "The practical sweet spot: annotate " (code "defcomp") " declarations (they're the public API) and let the checker verify that pure components don't accidentally depend on IO. Internal helpers can stay unannotated until they matter.") - (~docs/code :code (highlight ";; Annotated component — checker enforces purity\n(defcomp ~plans/typed-sx/price-display [pure] (&key (price :as number))\n (span :class \"price\" (str \"$\" (format-decimal price 2))))\n\n;; ERROR: pure component calls IO\n(defcomp ~plans/typed-sx/price-display [pure] (&key (product-id :as number))\n (let ((product (fetch-product product-id))) ;; ERROR: [io] in [pure] context\n (span :class \"price\" (str \"$\" (get product \"price\")))))" "lisp"))) + (~docs/code :src (highlight ";; Annotated component — checker enforces purity\n(defcomp ~plans/typed-sx/price-display [pure] (&key (price :as number))\n (span :class \"price\" (str \"$\" (format-decimal price 2))))\n\n;; ERROR: pure component calls IO\n(defcomp ~plans/typed-sx/price-display [pure] (&key (product-id :as number))\n (let ((product (fetch-product product-id))) ;; ERROR: [io] in [pure] context\n (span :class \"price\" (str \"$\" (get product \"price\")))))" "lisp"))) (~docs/subsection :title "Relationship to deps.sx and boundary.sx" (p "Effects don't replace the existing systems — they formalize them:") @@ -397,7 +397,7 @@ (~docs/subsection :title "Phase 5: Typed Primitives (done)" (p "Add param types to " (code "primitives.sx") " declarations.") - (~docs/code :code (highlight ";; Current\n(define-primitive \"+\"\n :params (&rest args)\n :returns \"number\"\n :doc \"Sum all arguments.\")\n\n;; Extended\n(define-primitive \"+\"\n :params (&rest (args : number))\n :returns \"number\"\n :doc \"Sum all arguments.\")" "lisp")) + (~docs/code :src (highlight ";; Current\n(define-primitive \"+\"\n :params (&rest args)\n :returns \"number\"\n :doc \"Sum all arguments.\")\n\n;; Extended\n(define-primitive \"+\"\n :params (&rest (args : number))\n :returns \"number\"\n :doc \"Sum all arguments.\")" "lisp")) (p "This is the biggest payoff for effort: ~80 primitives gain param types, enabling the checker to catch every mistyped primitive call across the entire codebase.")) (~docs/subsection :title "Phase 6: User-Defined Types (deftype)" diff --git a/sx/sx/plans/wasm-bytecode-vm.sx b/sx/sx/plans/wasm-bytecode-vm.sx index 164d34cd..b0d3e600 100644 --- a/sx/sx/plans/wasm-bytecode-vm.sx +++ b/sx/sx/plans/wasm-bytecode-vm.sx @@ -31,7 +31,7 @@ (h4 :class "font-semibold mt-4 mb-2" "1. Bytecode format — bytecode.sx") (p "A spec for the bytecode instruction set. Stack-based VM (simpler than register-based, natural fit for s-expressions). Instructions:") - (~docs/code :code (highlight ";; Core instructions\nPUSH_CONST idx ;; push constant from pool\nPUSH_NIL ;; push nil\nPUSH_TRUE / PUSH_FALSE\nLOOKUP idx ;; look up symbol by index\nSET idx ;; define/set symbol\nCALL n ;; call top-of-stack with n args\nTAIL_CALL n ;; tail call (TCO)\nRETURN\nJUMP offset ;; unconditional jump\nJUMP_IF_FALSE offset ;; conditional jump\nMAKE_LAMBDA idx n_params ;; create closure\nMAKE_LIST n ;; collect n stack values into list\nMAKE_DICT n ;; collect 2n stack values into dict\nPOP ;; discard top\nDUP ;; duplicate top" "lisp")) + (~docs/code :src (highlight ";; Core instructions\nPUSH_CONST idx ;; push constant from pool\nPUSH_NIL ;; push nil\nPUSH_TRUE / PUSH_FALSE\nLOOKUP idx ;; look up symbol by index\nSET idx ;; define/set symbol\nCALL n ;; call top-of-stack with n args\nTAIL_CALL n ;; tail call (TCO)\nRETURN\nJUMP offset ;; unconditional jump\nJUMP_IF_FALSE offset ;; conditional jump\nMAKE_LAMBDA idx n_params ;; create closure\nMAKE_LIST n ;; collect n stack values into list\nMAKE_DICT n ;; collect 2n stack values into dict\nPOP ;; discard top\nDUP ;; duplicate top" "lisp")) (p "Bytecode modules contain: a " (strong "constant pool") " (strings, numbers, symbols), a " (strong "code section") " (instruction bytes), and a " (strong "metadata section") " (source maps, component/island declarations for the host to register).") (h4 :class "font-semibold mt-4 mb-2" "2. Compiler — compile.sx") @@ -59,11 +59,11 @@ (h4 :class "font-semibold mt-4 mb-2" "Strategy A: Direct calls") (p "Each DOM operation (" (code "createElement") ", " (code "setAttribute") ", " (code "appendChild") ") is a separate WASM→JS call. Simple, works, but ~50ns overhead per call. For a page with 1,000 DOM operations, that's ~50μs — negligible.") - (~docs/code :code (highlight "// JS side — imported by WASM\nfunction domCreateElement(tag_ptr, tag_len) {\n const tag = readString(tag_ptr, tag_len);\n return storeHandle(document.createElement(tag));\n}\n\n// Rust side\nextern \"C\" { fn dom_create_element(tag: *const u8, len: u32) -> u32; }" "javascript")) + (~docs/code :src (highlight "// JS side — imported by WASM\nfunction domCreateElement(tag_ptr, tag_len) {\n const tag = readString(tag_ptr, tag_len);\n return storeHandle(document.createElement(tag));\n}\n\n// Rust side\nextern \"C\" { fn dom_create_element(tag: *const u8, len: u32) -> u32; }" "javascript")) (h4 :class "font-semibold mt-4 mb-2" "Strategy B: Command buffer") (p "Batch DOM operations in WASM memory as a command buffer. Flush to JS in one call. JS walks the buffer and applies all operations. Fewer boundary crossings, but more complex.") - (~docs/code :code (highlight ";; Command buffer format (in shared WASM memory)\n;; [CREATE_ELEMENT, tag_idx, handle_out]\n;; [SET_ATTR, handle, key_idx, val_idx]\n;; [APPEND_CHILD, parent_handle, child_handle]\n;; [SET_TEXT, handle, text_idx]\n;; Then: (flush-dom-commands)" "lisp")) + (~docs/code :src (highlight ";; Command buffer format (in shared WASM memory)\n;; [CREATE_ELEMENT, tag_idx, handle_out]\n;; [SET_ATTR, handle, key_idx, val_idx]\n;; [APPEND_CHILD, parent_handle, child_handle]\n;; [SET_TEXT, handle, text_idx]\n;; Then: (flush-dom-commands)" "lisp")) (p "Strategy A is simpler and sufficient for SX workloads. Strategy B is an optimisation if profiling shows the boundary crossing matters. " (strong "Start with A, measure, switch to B only if needed."))) ;; ----------------------------------------------------------------------- @@ -170,7 +170,7 @@ (~docs/section :title "Dual Target: JS or WASM from the Same Spec" :id "dual-target" (p "The key insight: this is " (strong "not a replacement") " for the JS evaluator. It's " (strong "another compilation target from the same spec") ". The existing bootstrapper pipeline already proves this pattern:") - (~docs/code :code (highlight "eval.sx ──→ bootstrap_js.py ──→ sx-ref.js (browser, JS eval)\n ──→ bootstrap_py.py ──→ sx_ref.py (server, Python eval)\n ──→ bootstrap_rs.py ──→ sx-vm.wasm (browser, WASM eval) ← new" "text")) + (~docs/code :src (highlight "eval.sx ──→ bootstrap_js.py ──→ sx-ref.js (browser, JS eval)\n ──→ bootstrap_py.py ──→ sx_ref.py (server, Python eval)\n ──→ bootstrap_rs.py ──→ sx-vm.wasm (browser, WASM eval) ← new" "text")) (p "All three outputs have identical semantics because they're compiled from the same source. The choice of which to use is a " (strong "deployment decision") ", not an architectural one:") (ul :class "list-disc list-inside space-y-2 mt-2" (li (strong "JS-only") " — current default. Works everywhere. Zero WASM dependency. Ship sx-browser.js + SX source text.") diff --git a/sx/sx/protocols.sx b/sx/sx/protocols.sx index 995ab477..162460ad 100644 --- a/sx/sx/protocols.sx +++ b/sx/sx/protocols.sx @@ -5,7 +5,7 @@ (~docs/section :title "The text/sx content type" :id "content-type" (p :class "text-stone-600" "sx responses use content type text/sx. The body is s-expression source code. The client parses and evaluates it, then renders the result into the DOM.") - (~docs/code :code (highlight "HTTP/1.1 200 OK\nContent-Type: text/sx\nSX-Css-Hash: a1b2c3d4\n\n(div :class \"p-4\"\n (~card :title \"Hello\"))" "bash"))) + (~docs/code :src (highlight "HTTP/1.1 200 OK\nContent-Type: text/sx\nSX-Css-Hash: a1b2c3d4\n\n(div :class \"p-4\"\n (~card :title \"Hello\"))" "bash"))) (~docs/section :title "Request lifecycle" :id "lifecycle" (p :class "text-stone-600" "1. User interacts with an element that has sx-get/sx-post/etc.") @@ -30,7 +30,7 @@ "Rose Ash runs as independent microservices. Each service can expose HTML or sx fragments that other services compose into their pages. Fragment endpoints return text/sx or text/html.") (p :class "text-stone-600" "The frag resolver is an I/O primitive in the render tree:") - (~docs/code :code (highlight "(frag \"blog\" \"link-card\" :slug \"hello\")" "lisp"))) + (~docs/code :src (highlight "(frag \"blog\" \"link-card\" :slug \"hello\")" "lisp"))) (~docs/section :title "SxExpr wrapping" :id "wrapping" (p :class "text-stone-600" "When a fragment returns text/sx, the response is wrapped in an SxExpr and embedded directly in the render tree. When it returns text/html, it's wrapped in a ~rich-text component that inserts the HTML via raw!. This allows transparent composition across service boundaries.")) diff --git a/sx/sx/provide.sx b/sx/sx/provide.sx index 666c84b0..2441fa55 100644 --- a/sx/sx/provide.sx +++ b/sx/sx/provide.sx @@ -84,19 +84,19 @@ (p (code "provide") " creates a named scope with a value and an empty accumulator. " "The body expressions execute with the scope active. When the body completes, " "the scope is popped.") - (~docs/code :code (highlight "(provide name value\n body...)\n\n;; Example: theme context\n(provide \"theme\" {:primary \"violet\"}\n (h1 \"Title\") ;; can read (context \"theme\")\n (p \"Body\")) ;; scope active for all children" "lisp")) + (~docs/code :src (highlight "(provide name value\n body...)\n\n;; Example: theme context\n(provide \"theme\" {:primary \"violet\"}\n (h1 \"Title\") ;; can read (context \"theme\")\n (p \"Body\")) ;; scope active for all children" "lisp")) (p (code "provide") " is a special form, not a function — the body is evaluated " "inside the scope, not before it.")) (~docs/subsection :title "context" (p "Reads the value from the nearest enclosing " (code "provide") " with the given name. " "Errors if no provider and no default given.") - (~docs/code :code (highlight "(provide \"theme\" {:primary \"violet\" :font \"serif\"}\n (get (context \"theme\") :primary)) ;; → \"violet\"\n\n;; With default (no error when missing):\n(context \"theme\" {:primary \"stone\"}) ;; → {:primary \"stone\"}" "lisp"))) + (~docs/code :src (highlight "(provide \"theme\" {:primary \"violet\" :font \"serif\"}\n (get (context \"theme\") :primary)) ;; → \"violet\"\n\n;; With default (no error when missing):\n(context \"theme\" {:primary \"stone\"}) ;; → {:primary \"stone\"}" "lisp"))) (~docs/subsection :title "emit!" (p "Appends a value to the nearest enclosing provider's accumulator. " "Tolerant: returns nil silently when no provider exists.") - (~docs/code :code (highlight "(provide \"scripts\" nil\n (emit! \"scripts\" \"analytics.js\")\n (emit! \"scripts\" \"charts.js\")\n ;; accumulator now has both scripts\n )\n\n;; Outside any provider — silently does nothing:\n(emit! \"scripts\" \"orphan.js\") ;; → nil, no error" "lisp")) + (~docs/code :src (highlight "(provide \"scripts\" nil\n (emit! \"scripts\" \"analytics.js\")\n (emit! \"scripts\" \"charts.js\")\n ;; accumulator now has both scripts\n )\n\n;; Outside any provider — silently does nothing:\n(emit! \"scripts\" \"orphan.js\") ;; → nil, no error" "lisp")) (p "Tolerance is critical. Spreads emit into " (code "\"element-attrs\"") " — but a spread might be evaluated in a fragment, a " (code "begin") " block, or a " (code "map") " call where no element provider exists. " @@ -105,7 +105,7 @@ (~docs/subsection :title "emitted" (p "Returns the list of values emitted into the nearest provider with the given name. " "Empty list if no provider.") - (~docs/code :code (highlight "(provide \"scripts\" nil\n (emit! \"scripts\" \"a.js\")\n (emit! \"scripts\" \"b.js\")\n (emitted \"scripts\")) ;; → (\"a.js\" \"b.js\")" "lisp")))) + (~docs/code :src (highlight "(provide \"scripts\" nil\n (emit! \"scripts\" \"a.js\")\n (emit! \"scripts\" \"b.js\")\n (emitted \"scripts\")) ;; → (\"a.js\" \"b.js\")" "lisp")))) ;; ===================================================================== ;; II. Two directions, one mechanism @@ -166,7 +166,7 @@ (p "For " (code "emit!") ", this means emissions go to the " (em "nearest") " provider. " "A spread inside a nested element emits to that element, not an ancestor.") - (~docs/code :code (highlight ";; Nested elements = nested providers\n(div ;; provider A\n (span ;; provider B\n (make-spread {:class \"inner\"})) ;; emits to B\n (make-spread {:class \"outer\"})) ;; emits to A\n;; →
" "lisp"))) + (~docs/code :src (highlight ";; Nested elements = nested providers\n(div ;; provider A\n (span ;; provider B\n (make-spread {:class \"inner\"})) ;; emits to B\n (make-spread {:class \"outer\"})) ;; emits to A\n;; →
" "lisp"))) ;; ===================================================================== ;; V. Across all adapters diff --git a/sx/sx/reactive-islands/demo.sx b/sx/sx/reactive-islands/demo.sx index 9c872fe1..fbb1effb 100644 --- a/sx/sx/reactive-islands/demo.sx +++ b/sx/sx/reactive-islands/demo.sx @@ -27,84 +27,84 @@ (~docs/page :title "Signal + Computed + Effect" (p "A signal holds a value. A computed derives from it. Click the buttons — the counter and doubled value update instantly, no server round-trip.") (~reactive-islands/index/demo-counter :initial 0) - (~docs/code :code (highlight "(defisland ~reactive-islands/demo/counter (&key initial)\n (let ((count (signal (or initial 0)))\n (doubled (computed (fn () (* 2 (deref count))))))\n (div :class \"...\"\n (button :on-click (fn (e) (swap! count dec)) \"−\")\n (span (deref count))\n (button :on-click (fn (e) (swap! count inc)) \"+\")\n (p \"doubled: \" (deref doubled)))))" "lisp")) + (~docs/code :src (highlight "(defisland ~reactive-islands/demo/counter (&key initial)\n (let ((count (signal (or initial 0)))\n (doubled (computed (fn () (* 2 (deref count))))))\n (div :class \"...\"\n (button :on-click (fn (e) (swap! count dec)) \"−\")\n (span (deref count))\n (button :on-click (fn (e) (swap! count inc)) \"+\")\n (p \"doubled: \" (deref doubled)))))" "lisp")) (p (code "(deref count)") " in a text position creates a reactive text node. When " (code "count") " changes, " (em "only that text node") " updates. " (code "doubled") " recomputes automatically. No diffing."))) (defcomp ~reactive-islands/demo/example-temperature () (~docs/page :title "Temperature Converter" (p "Two derived values from one signal. Click to change Celsius — Fahrenheit updates reactively.") (~reactive-islands/index/demo-temperature) - (~docs/code :code (highlight "(defisland ~reactive-islands/demo/temperature ()\n (let ((celsius (signal 20)))\n (div :class \"...\"\n (button :on-click (fn (e) (swap! celsius (fn (c) (- c 5)))) \"−5\")\n (span (deref celsius))\n (button :on-click (fn (e) (swap! celsius (fn (c) (+ c 5)))) \"+5\")\n (span \"°C = \")\n (span (+ (* (deref celsius) 1.8) 32))\n (span \"°F\"))))" "lisp")) + (~docs/code :src (highlight "(defisland ~reactive-islands/demo/temperature ()\n (let ((celsius (signal 20)))\n (div :class \"...\"\n (button :on-click (fn (e) (swap! celsius (fn (c) (- c 5)))) \"−5\")\n (span (deref celsius))\n (button :on-click (fn (e) (swap! celsius (fn (c) (+ c 5)))) \"+5\")\n (span \"°C = \")\n (span (+ (* (deref celsius) 1.8) 32))\n (span \"°F\"))))" "lisp")) (p "The actual implementation uses " (code "computed") " for Fahrenheit: " (code "(computed (fn () (+ (* (deref celsius) 1.8) 32)))") ". The " (code "(deref fahrenheit)") " in the span creates a reactive text node that updates when celsius changes."))) (defcomp ~reactive-islands/demo/example-stopwatch () (~docs/page :title "Effect + Cleanup: Stopwatch" (p "Effects can return cleanup functions. This stopwatch starts a " (code "set-interval") " — the cleanup clears it when the running signal toggles off.") (~reactive-islands/index/demo-stopwatch) - (~docs/code :code (highlight "(defisland ~reactive-islands/demo/stopwatch ()\n (let ((running (signal false))\n (elapsed (signal 0))\n (time-text (create-text-node \"0.0s\"))\n (btn-text (create-text-node \"Start\")))\n ;; Timer: effect creates interval, cleanup clears it\n (effect (fn ()\n (when (deref running)\n (let ((id (set-interval (fn () (swap! elapsed inc)) 100)))\n (fn () (clear-interval id))))))\n ;; Display: updates text node when elapsed changes\n (effect (fn ()\n (let ((e (deref elapsed)))\n (dom-set-text-content time-text\n (str (floor (/ e 10)) \".\" (mod e 10) \"s\")))))\n ;; Button label\n (effect (fn ()\n (dom-set-text-content btn-text\n (if (deref running) \"Stop\" \"Start\"))))\n (div :class \"...\"\n (span time-text)\n (button :on-click (fn (e) (swap! running not)) btn-text)\n (button :on-click (fn (e)\n (reset! running false) (reset! elapsed 0)) \"Reset\"))))" "lisp")) + (~docs/code :src (highlight "(defisland ~reactive-islands/demo/stopwatch ()\n (let ((running (signal false))\n (elapsed (signal 0))\n (time-text (create-text-node \"0.0s\"))\n (btn-text (create-text-node \"Start\")))\n ;; Timer: effect creates interval, cleanup clears it\n (effect (fn ()\n (when (deref running)\n (let ((id (set-interval (fn () (swap! elapsed inc)) 100)))\n (fn () (clear-interval id))))))\n ;; Display: updates text node when elapsed changes\n (effect (fn ()\n (let ((e (deref elapsed)))\n (dom-set-text-content time-text\n (str (floor (/ e 10)) \".\" (mod e 10) \"s\")))))\n ;; Button label\n (effect (fn ()\n (dom-set-text-content btn-text\n (if (deref running) \"Stop\" \"Start\"))))\n (div :class \"...\"\n (span time-text)\n (button :on-click (fn (e) (swap! running not)) btn-text)\n (button :on-click (fn (e)\n (reset! running false) (reset! elapsed 0)) \"Reset\"))))" "lisp")) (p "Three effects, each tracking different signals. The timer effect's cleanup fires before each re-run — toggling " (code "running") " off clears the interval. No hook rules: effects can appear anywhere, in any order."))) (defcomp ~reactive-islands/demo/example-imperative () (~docs/page :title "Imperative Pattern" (p "For complex reactivity (dynamic classes, conditional text), use the imperative pattern: " (code "create-text-node") " + " (code "effect") " + " (code "dom-set-text-content") ".") (~reactive-islands/index/demo-imperative) - (~docs/code :code (highlight "(defisland ~reactive-islands/demo/imperative ()\n (let ((count (signal 0))\n (text-node (create-text-node \"0\")))\n ;; Explicit effect: re-runs when count changes\n (effect (fn ()\n (dom-set-text-content text-node (str (deref count)))))\n (div :class \"...\"\n (span text-node)\n (button :on-click (fn (e) (swap! count inc)) \"+\"))))" "lisp")) + (~docs/code :src (highlight "(defisland ~reactive-islands/demo/imperative ()\n (let ((count (signal 0))\n (text-node (create-text-node \"0\")))\n ;; Explicit effect: re-runs when count changes\n (effect (fn ()\n (dom-set-text-content text-node (str (deref count)))))\n (div :class \"...\"\n (span text-node)\n (button :on-click (fn (e) (swap! count inc)) \"+\"))))" "lisp")) (p "Two patterns exist: " (strong "declarative") " (" (code "(span (deref sig))") " — auto-reactive via " (code "reactive-text") ") and " (strong "imperative") " (" (code "create-text-node") " + " (code "effect") " — explicit, full control). Use declarative for simple text, imperative for dynamic classes, conditional DOM, or complex updates."))) (defcomp ~reactive-islands/demo/example-reactive-list () (~docs/page :title "Reactive List" (p "When " (code "map") " is used with " (code "(deref signal)") " inside an island, it auto-upgrades to a reactive list. With " (code ":key") " attributes, existing DOM nodes are reused across updates — only additions, removals, and reorderings touch the DOM.") (~reactive-islands/index/demo-reactive-list) - (~docs/code :code (highlight "(defisland ~reactive-islands/demo/reactive-list ()\n (let ((next-id (signal 1))\n (items (signal (list)))\n (add-item (fn (e)\n (batch (fn ()\n (swap! items (fn (old)\n (append old (dict \"id\" (deref next-id)\n \"text\" (str \"Item \" (deref next-id))))))\n (swap! next-id inc)))))\n (remove-item (fn (id)\n (swap! items (fn (old)\n (filter (fn (item) (not (= (get item \"id\") id))) old))))))\n (div\n (button :on-click add-item \"Add Item\")\n (span (deref (computed (fn () (len (deref items))))) \" items\")\n (ul\n (map (fn (item)\n (li :key (str (get item \"id\"))\n (span (get item \"text\"))\n (button :on-click (fn (e) (remove-item (get item \"id\"))) \"✕\")))\n (deref items))))))" "lisp")) + (~docs/code :src (highlight "(defisland ~reactive-islands/demo/reactive-list ()\n (let ((next-id (signal 1))\n (items (signal (list)))\n (add-item (fn (e)\n (batch (fn ()\n (swap! items (fn (old)\n (append old (dict \"id\" (deref next-id)\n \"text\" (str \"Item \" (deref next-id))))))\n (swap! next-id inc)))))\n (remove-item (fn (id)\n (swap! items (fn (old)\n (filter (fn (item) (not (= (get item \"id\") id))) old))))))\n (div\n (button :on-click add-item \"Add Item\")\n (span (deref (computed (fn () (len (deref items))))) \" items\")\n (ul\n (map (fn (item)\n (li :key (str (get item \"id\"))\n (span (get item \"text\"))\n (button :on-click (fn (e) (remove-item (get item \"id\"))) \"✕\")))\n (deref items))))))" "lisp")) (p (code ":key") " identifies each list item. When items change, the reconciler matches old and new keys — reusing existing DOM nodes, inserting new ones, and removing stale ones. Without keys, the list falls back to clear-and-rerender. " (code "batch") " groups the two signal writes into one update pass."))) (defcomp ~reactive-islands/demo/example-input-binding () (~docs/page :title "Input Binding" (p "The " (code ":bind") " attribute creates a two-way link between a signal and a form element. Type in the input — the signal updates. Change the signal — the input updates. Works with text inputs, checkboxes, radios, textareas, and selects.") (~reactive-islands/index/demo-input-binding) - (~docs/code :code (highlight "(defisland ~reactive-islands/demo/input-binding ()\n (let ((name (signal \"\"))\n (agreed (signal false)))\n (div\n (input :type \"text\" :bind name\n :placeholder \"Type your name...\")\n (span \"Hello, \" (strong (deref name)) \"!\")\n (input :type \"checkbox\" :bind agreed)\n (when (deref agreed)\n (p \"Thanks for agreeing!\")))))" "lisp")) + (~docs/code :src (highlight "(defisland ~reactive-islands/demo/input-binding ()\n (let ((name (signal \"\"))\n (agreed (signal false)))\n (div\n (input :type \"text\" :bind name\n :placeholder \"Type your name...\")\n (span \"Hello, \" (strong (deref name)) \"!\")\n (input :type \"checkbox\" :bind agreed)\n (when (deref agreed)\n (p \"Thanks for agreeing!\")))))" "lisp")) (p (code ":bind") " detects the element type automatically — text inputs use " (code "value") " + " (code "input") " event, checkboxes use " (code "checked") " + " (code "change") " event. The effect only updates the DOM when the value actually changed, preventing cursor jump."))) (defcomp ~reactive-islands/demo/example-portal () (~docs/page :title "Portals" (p "A " (code "portal") " renders children into a DOM node " (em "outside") " the island's subtree. Essential for modals, tooltips, and toasts — anything that must escape " (code "overflow:hidden") " or z-index stacking.") (~reactive-islands/index/demo-portal) - (~docs/code :code (highlight "(defisland ~reactive-islands/demo/portal ()\n (let ((open? (signal false)))\n (div\n (button :on-click (fn (e) (swap! open? not))\n (if (deref open?) \"Close Modal\" \"Open Modal\"))\n (portal \"#portal-root\"\n (when (deref open?)\n (div :class \"fixed inset-0 bg-black/50 ...\"\n :on-click (fn (e) (reset! open? false))\n (div :class \"bg-white rounded-lg p-6 ...\"\n :on-click (fn (e) (stop-propagation e))\n (h2 \"Portal Modal\")\n (p \"Rendered outside the island's DOM.\")\n (button :on-click (fn (e) (reset! open? false))\n \"Close\"))))))))" "lisp")) + (~docs/code :src (highlight "(defisland ~reactive-islands/demo/portal ()\n (let ((open? (signal false)))\n (div\n (button :on-click (fn (e) (swap! open? not))\n (if (deref open?) \"Close Modal\" \"Open Modal\"))\n (portal \"#portal-root\"\n (when (deref open?)\n (div :class \"fixed inset-0 bg-black/50 ...\"\n :on-click (fn (e) (reset! open? false))\n (div :class \"bg-white rounded-lg p-6 ...\"\n :on-click (fn (e) (stop-propagation e))\n (h2 \"Portal Modal\")\n (p \"Rendered outside the island's DOM.\")\n (button :on-click (fn (e) (reset! open? false))\n \"Close\"))))))))" "lisp")) (p "The portal content lives in " (code "#portal-root") " (typically at the page body level), not inside the island. On island disposal, portal content is automatically removed from its target — the " (code "register-in-scope") " mechanism handles cleanup."))) (defcomp ~reactive-islands/demo/example-error-boundary () (~docs/page :title "Error Boundaries" (p "When an island's rendering or effect throws, " (code "error-boundary") " catches the error and renders a fallback. The fallback receives the error and a retry function. Partial effects created before the error are disposed automatically.") (~reactive-islands/index/demo-error-boundary) - (~docs/code :code (highlight "(defisland ~reactive-islands/demo/error-boundary ()\n (let ((throw? (signal false)))\n (error-boundary\n ;; Fallback: receives (err retry-fn)\n (fn (err retry-fn)\n (div :class \"p-3 bg-red-50 border border-red-200 rounded\"\n (p :class \"text-red-700\" (error-message err))\n (button :on-click (fn (e)\n (reset! throw? false) (invoke retry-fn))\n \"Retry\")))\n ;; Children: the happy path\n (do\n (when (deref throw?) (error \"Intentional explosion!\"))\n (p \"Everything is fine.\")))))" "lisp")) + (~docs/code :src (highlight "(defisland ~reactive-islands/demo/error-boundary ()\n (let ((throw? (signal false)))\n (error-boundary\n ;; Fallback: receives (err retry-fn)\n (fn (err retry-fn)\n (div :class \"p-3 bg-red-50 border border-red-200 rounded\"\n (p :class \"text-red-700\" (error-message err))\n (button :on-click (fn (e)\n (reset! throw? false) (invoke retry-fn))\n \"Retry\")))\n ;; Children: the happy path\n (do\n (when (deref throw?) (error \"Intentional explosion!\"))\n (p \"Everything is fine.\")))))" "lisp")) (p "React equivalent: " (code "componentDidCatch") " / " (code "ErrorBoundary") ". SX's version is simpler — one form, not a class. The " (code "error-boundary") " form is a render-dom special form in " (code "adapter-dom.sx") "."))) (defcomp ~reactive-islands/demo/example-refs () (~docs/page :title "Refs — Imperative DOM Access" (p "The " (code ":ref") " attribute captures a DOM element handle into a dict. Use it for imperative operations: focusing, measuring, reading values.") (~reactive-islands/index/demo-refs) - (~docs/code :code (highlight "(defisland ~reactive-islands/demo/refs ()\n (let ((my-ref (dict \"current\" nil))\n (msg (signal \"\")))\n (input :ref my-ref :type \"text\"\n :placeholder \"I can be focused programmatically\")\n (button :on-click (fn (e)\n (dom-focus (get my-ref \"current\")))\n \"Focus Input\")\n (button :on-click (fn (e)\n (let ((el (get my-ref \"current\")))\n (reset! msg (str \"value: \" (dom-get-prop el \"value\")))))\n \"Read Input\")\n (when (not (= (deref msg) \"\"))\n (p (deref msg)))))" "lisp")) + (~docs/code :src (highlight "(defisland ~reactive-islands/demo/refs ()\n (let ((my-ref (dict \"current\" nil))\n (msg (signal \"\")))\n (input :ref my-ref :type \"text\"\n :placeholder \"I can be focused programmatically\")\n (button :on-click (fn (e)\n (dom-focus (get my-ref \"current\")))\n \"Focus Input\")\n (button :on-click (fn (e)\n (let ((el (get my-ref \"current\")))\n (reset! msg (str \"value: \" (dom-get-prop el \"value\")))))\n \"Read Input\")\n (when (not (= (deref msg) \"\"))\n (p (deref msg)))))" "lisp")) (p "React equivalent: " (code "useRef") ". In SX, a ref is just " (code "(dict \"current\" nil)") " — no special API. The " (code ":ref") " attribute sets " (code "(dict-set! ref \"current\" el)") " when the element is created. Read it with " (code "(get ref \"current\")") "."))) (defcomp ~reactive-islands/demo/example-dynamic-class () (~docs/page :title "Dynamic Class and Style" (p "React uses " (code "className") " and " (code "style") " props with state. SX does the same — " (code "(deref signal)") " inside a " (code ":class") " or " (code ":style") " attribute creates a reactive binding. The attribute updates when the signal changes.") (~reactive-islands/index/demo-dynamic-class) - (~docs/code :code (highlight "(defisland ~reactive-islands/demo/dynamic-class ()\n (let ((danger (signal false))\n (size (signal 16)))\n (div\n (button :on-click (fn (e) (swap! danger not))\n (if (deref danger) \"Safe mode\" \"Danger mode\"))\n (button :on-click (fn (e) (swap! size (fn (s) (+ s 2))))\n \"Bigger\")\n ;; Reactive class — recomputed when danger changes\n (div :class (str \"p-3 rounded font-medium \"\n (if (deref danger)\n \"bg-red-100 text-red-800\"\n \"bg-green-100 text-green-800\"))\n ;; Reactive style — recomputed when size changes\n :style (str \"font-size:\" (deref size) \"px\")\n \"This element's class and style are reactive.\"))))" "lisp")) + (~docs/code :src (highlight "(defisland ~reactive-islands/demo/dynamic-class ()\n (let ((danger (signal false))\n (size (signal 16)))\n (div\n (button :on-click (fn (e) (swap! danger not))\n (if (deref danger) \"Safe mode\" \"Danger mode\"))\n (button :on-click (fn (e) (swap! size (fn (s) (+ s 2))))\n \"Bigger\")\n ;; Reactive class — recomputed when danger changes\n (div :class (str \"p-3 rounded font-medium \"\n (if (deref danger)\n \"bg-red-100 text-red-800\"\n \"bg-green-100 text-green-800\"))\n ;; Reactive style — recomputed when size changes\n :style (str \"font-size:\" (deref size) \"px\")\n \"This element's class and style are reactive.\"))))" "lisp")) (p "React equivalent: " (code "className={danger ? 'red' : 'green'}") " and " (code "style={{fontSize: size}}") ". In SX the " (code "str") " + " (code "if") " + " (code "deref") " pattern handles it — no " (code "classnames") " library needed. For complex conditional classes, use a " (code "computed") " or a CSSX " (code "defcomp") " that returns a class string."))) (defcomp ~reactive-islands/demo/example-resource () (~docs/page :title "Resource + Suspense Pattern" (p (code "resource") " wraps an async operation into a signal with " (code "loading") "/" (code "data") "/" (code "error") " states. Combined with " (code "cond") " + " (code "deref") ", this is the suspense pattern — no special form needed.") (~reactive-islands/index/demo-resource) - (~docs/code :code (highlight "(defisland ~reactive-islands/demo/resource ()\n (let ((data (resource (fn ()\n ;; Any promise-returning function\n (promise-delayed 1500\n (dict \"name\" \"Ada Lovelace\"\n \"role\" \"First Programmer\"))))))\n ;; This IS the suspense pattern:\n (let ((state (deref data)))\n (cond\n (get state \"loading\")\n (div \"Loading...\")\n (get state \"error\")\n (div \"Error: \" (get state \"error\"))\n :else\n (div (get (get state \"data\") \"name\"))))))" "lisp")) + (~docs/code :src (highlight "(defisland ~reactive-islands/demo/resource ()\n (let ((data (resource (fn ()\n ;; Any promise-returning function\n (promise-delayed 1500\n (dict \"name\" \"Ada Lovelace\"\n \"role\" \"First Programmer\"))))))\n ;; This IS the suspense pattern:\n (let ((state (deref data)))\n (cond\n (get state \"loading\")\n (div \"Loading...\")\n (get state \"error\")\n (div \"Error: \" (get state \"error\"))\n :else\n (div (get (get state \"data\") \"name\"))))))" "lisp")) (p "React equivalent: " (code "Suspense") " + " (code "use()") " or " (code "useSWR") ". SX doesn't need a special " (code "suspense") " form because " (code "resource") " returns a signal and " (code "cond") " + " (code "deref") " creates reactive conditional rendering. When the promise resolves, the signal updates and the " (code "cond") " branch switches automatically."))) (defcomp ~reactive-islands/demo/example-transition () (~docs/page :title "Transition Pattern" (p "React's " (code "startTransition") " defers non-urgent updates so typing stays responsive. In SX: " (code "schedule-idle") " + " (code "batch") ". The filter runs during idle time, not blocking the input event.") (~reactive-islands/index/demo-transition) - (~docs/code :code (highlight "(defisland ~reactive-islands/demo/transition ()\n (let ((query (signal \"\"))\n (all-items (list \"Signals\" \"Effects\" ...))\n (filtered (signal (list)))\n (pending (signal false)))\n (reset! filtered all-items)\n ;; Filter effect — deferred via schedule-idle\n (effect (fn ()\n (let ((q (lower (deref query))))\n (if (= q \"\")\n (do (reset! pending false)\n (reset! filtered all-items))\n (do (reset! pending true)\n (schedule-idle (fn ()\n (batch (fn ()\n (reset! filtered\n (filter (fn (item)\n (contains? (lower item) q))\n all-items))\n (reset! pending false))))))))))\n (div\n (input :bind query :placeholder \"Filter...\")\n (when (deref pending) (span \"Filtering...\"))\n (ul (map (fn (item) (li :key item item))\n (deref filtered))))))" "lisp")) + (~docs/code :src (highlight "(defisland ~reactive-islands/demo/transition ()\n (let ((query (signal \"\"))\n (all-items (list \"Signals\" \"Effects\" ...))\n (filtered (signal (list)))\n (pending (signal false)))\n (reset! filtered all-items)\n ;; Filter effect — deferred via schedule-idle\n (effect (fn ()\n (let ((q (lower (deref query))))\n (if (= q \"\")\n (do (reset! pending false)\n (reset! filtered all-items))\n (do (reset! pending true)\n (schedule-idle (fn ()\n (batch (fn ()\n (reset! filtered\n (filter (fn (item)\n (contains? (lower item) q))\n all-items))\n (reset! pending false))))))))))\n (div\n (input :bind query :placeholder \"Filter...\")\n (when (deref pending) (span \"Filtering...\"))\n (ul (map (fn (item) (li :key item item))\n (deref filtered))))))" "lisp")) (p "React equivalent: " (code "startTransition(() => setFiltered(...))") ". SX uses " (code "schedule-idle") " (" (code "requestIdleCallback") " under the hood) to defer the expensive " (code "filter") " operation, and " (code "batch") " to group the result into one update. Fine-grained signals already avoid the jank that makes transitions critical in React — this pattern is for truly expensive computations."))) (defcomp ~reactive-islands/demo/example-stores () @@ -112,26 +112,26 @@ (p "React uses " (code "Context") " or state management libraries for cross-component state. SX uses " (code "def-store") " / " (code "use-store") " — named signal containers that persist across island creation/destruction.") (~reactive-islands/index/demo-store-writer) (~reactive-islands/index/demo-store-reader) - (~docs/code :code (highlight ";; Island A — creates/writes the store\n(defisland ~reactive-islands/demo/store-writer ()\n (let ((store (def-store \"theme\" (fn ()\n (dict \"color\" (signal \"violet\")\n \"dark\" (signal false))))))\n (select :bind (get store \"color\")\n (option :value \"violet\" \"Violet\")\n (option :value \"blue\" \"Blue\"))\n (input :type \"checkbox\" :bind (get store \"dark\"))))\n\n;; Island B — reads the same store, different island\n(defisland ~reactive-islands/demo/store-reader ()\n (let ((store (use-store \"theme\")))\n (div :class (str \"bg-\" (deref (get store \"color\")) \"-100\")\n \"Styled by signals from Island A\")))" "lisp")) + (~docs/code :src (highlight ";; Island A — creates/writes the store\n(defisland ~reactive-islands/demo/store-writer ()\n (let ((store (def-store \"theme\" (fn ()\n (dict \"color\" (signal \"violet\")\n \"dark\" (signal false))))))\n (select :bind (get store \"color\")\n (option :value \"violet\" \"Violet\")\n (option :value \"blue\" \"Blue\"))\n (input :type \"checkbox\" :bind (get store \"dark\"))))\n\n;; Island B — reads the same store, different island\n(defisland ~reactive-islands/demo/store-reader ()\n (let ((store (use-store \"theme\")))\n (div :class (str \"bg-\" (deref (get store \"color\")) \"-100\")\n \"Styled by signals from Island A\")))" "lisp")) (p "React equivalent: " (code "createContext") " + " (code "useContext") " or Redux/Zustand. Stores are simpler — just named dicts of signals at page scope. " (code "def-store") " creates once, " (code "use-store") " retrieves. Stores survive island disposal but clear on full page navigation."))) (defcomp ~reactive-islands/demo/example-event-bridge-demo () (~docs/page :title "Event Bridge" (p "Server-rendered content inside an island (an htmx \"lake\") can communicate with island signals via DOM custom events. Buttons with " (code "data-sx-emit") " dispatch events that island effects catch.") (~reactive-islands/index/demo-event-bridge) - (~docs/code :code (highlight ";; Island listens for custom events from server-rendered content\n(defisland ~reactive-islands/demo/event-bridge ()\n (let ((messages (signal (list))))\n ;; Bridge: auto-listen for \"inbox:message\" events\n (bridge-event container \"inbox:message\" messages\n (fn (detail) (append (deref messages) (get detail \"text\"))))\n (div\n ;; Lake content (server-rendered) has data-sx-emit buttons\n (div :id \"lake\"\n :sx-get \"/my-content\"\n :sx-swap \"innerHTML\"\n :sx-trigger \"load\")\n ;; Island reads the signal reactively\n (ul (map (fn (msg) (li msg)) (deref messages))))))" "lisp")) + (~docs/code :src (highlight ";; Island listens for custom events from server-rendered content\n(defisland ~reactive-islands/demo/event-bridge ()\n (let ((messages (signal (list))))\n ;; Bridge: auto-listen for \"inbox:message\" events\n (bridge-event container \"inbox:message\" messages\n (fn (detail) (append (deref messages) (get detail \"text\"))))\n (div\n ;; Lake content (server-rendered) has data-sx-emit buttons\n (div :id \"lake\"\n :sx-get \"/my-content\"\n :sx-swap \"innerHTML\"\n :sx-trigger \"load\")\n ;; Island reads the signal reactively\n (ul (map (fn (msg) (li msg)) (deref messages))))))" "lisp")) (p "The " (code "data-sx-emit") " attribute is processed by the client engine — it adds a click handler that dispatches a CustomEvent with the JSON from " (code "data-sx-emit-detail") ". The event bubbles up to the island container where " (code "bridge-event") " catches it."))) (defcomp ~reactive-islands/demo/example-defisland () (~docs/page :title "How defisland Works" (p (code "defisland") " creates a reactive component. Same calling convention as " (code "defcomp") " — keyword args, rest children — but with a reactive boundary. Inside an island, " (code "deref") " subscribes DOM nodes to signals.") - (~docs/code :code (highlight ";; Definition — same syntax as defcomp\n(defisland ~reactive-islands/demo/counter (&key initial)\n (let ((count (signal (or initial 0))))\n (div\n (span (deref count)) ;; reactive text node\n (button :on-click (fn (e) (swap! count inc)) ;; event handler\n \"+\"))))\n\n;; Usage — same as any component\n(~reactive-islands/demo/counter :initial 42)\n\n;; Server-side rendering:\n;;
\n;; 42\n;;
\n;;\n;; Client hydrates: signals + effects + event handlers attach" "lisp")) + (~docs/code :src (highlight ";; Definition — same syntax as defcomp\n(defisland ~reactive-islands/demo/counter (&key initial)\n (let ((count (signal (or initial 0))))\n (div\n (span (deref count)) ;; reactive text node\n (button :on-click (fn (e) (swap! count inc)) ;; event handler\n \"+\"))))\n\n;; Usage — same as any component\n(~reactive-islands/demo/counter :initial 42)\n\n;; Server-side rendering:\n;;
\n;; 42\n;;
\n;;\n;; Client hydrates: signals + effects + event handlers attach" "lisp")) (p "Each " (code "deref") " call registers the enclosing DOM node as a subscriber. Signal changes update " (em "only") " the subscribed nodes — no virtual DOM, no diffing, no component re-renders."))) (defcomp ~reactive-islands/demo/example-tests () (~docs/page :title "Test Suite" (p "17 tests verify the signal runtime against the spec. All pass in the Python test runner (which uses the hand-written evaluator with native platform primitives).") - (~docs/code :code (highlight ";; Signal basics (6 tests)\n(assert-true (signal? (signal 42)))\n(assert-equal 42 (deref (signal 42)))\n(assert-equal 5 (deref 5)) ;; non-signal passthrough\n\n;; reset! changes value\n(let ((s (signal 0)))\n (reset! s 10)\n (assert-equal 10 (deref s)))\n\n;; reset! does NOT notify when value unchanged (identical? check)\n\n;; Computed (3 tests)\n(let ((a (signal 3)) (b (signal 4))\n (sum (computed (fn () (+ (deref a) (deref b))))))\n (assert-equal 7 (deref sum))\n (reset! a 10)\n (assert-equal 14 (deref sum)))\n\n;; Effects (4 tests) — immediate run, re-run on change, dispose, cleanup\n;; Batch (1 test) — defers notifications, deduplicates subscribers\n;; defisland (3 tests) — creates island, callable, accepts children" "lisp")) + (~docs/code :src (highlight ";; Signal basics (6 tests)\n(assert-true (signal? (signal 42)))\n(assert-equal 42 (deref (signal 42)))\n(assert-equal 5 (deref 5)) ;; non-signal passthrough\n\n;; reset! changes value\n(let ((s (signal 0)))\n (reset! s 10)\n (assert-equal 10 (deref s)))\n\n;; reset! does NOT notify when value unchanged (identical? check)\n\n;; Computed (3 tests)\n(let ((a (signal 3)) (b (signal 4))\n (sum (computed (fn () (+ (deref a) (deref b))))))\n (assert-equal 7 (deref sum))\n (reset! a 10)\n (assert-equal 14 (deref sum)))\n\n;; Effects (4 tests) — immediate run, re-run on change, dispose, cleanup\n;; Batch (1 test) — defers notifications, deduplicates subscribers\n;; defisland (3 tests) — creates island, callable, accepts children" "lisp")) (p :class "mt-2 text-sm text-stone-500" "Run: " (code "python3 shared/sx/tests/run.py signals")))) (defcomp ~reactive-islands/demo/example-coverage () diff --git a/sx/sx/reactive-islands/event-bridge.sx b/sx/sx/reactive-islands/event-bridge.sx index c16b36ca..379cf162 100644 --- a/sx/sx/reactive-islands/event-bridge.sx +++ b/sx/sx/reactive-islands/event-bridge.sx @@ -17,10 +17,10 @@ (li (strong "Event bubbles: ") "The event bubbles up through the DOM tree until it reaches the island container.") (li (strong "Effect catches: ") "An effect inside the island listens for the event name and updates a signal.")) - (~docs/code :code (highlight ";; Island with an event bridge\n(defisland ~reactive-islands/event-bridge/product-page (&key product)\n (let ((cart-items (signal (list))))\n\n ;; Bridge: listen for \"cart:add\" events from server content\n (bridge-event container \"cart:add\" cart-items\n (fn (detail)\n (append (deref cart-items)\n (dict :id (get detail \"id\")\n :name (get detail \"name\")\n :price (get detail \"price\")))))\n\n (div\n ;; Island header with reactive cart count\n (div :class \"flex justify-between\"\n (h1 (get product \"name\"))\n (span :class \"badge\" (length (deref cart-items)) \" items\"))\n\n ;; htmx lake — server-rendered product details\n ;; This content is swapped by sx-get, not rendered by the island\n (div :id \"product-details\"\n :sx-get (str \"/products/\" (get product \"id\") \"/details\")\n :sx-swap \"innerHTML\"\n :sx-trigger \"load\"))))" "lisp")) + (~docs/code :src (highlight ";; Island with an event bridge\n(defisland ~reactive-islands/event-bridge/product-page (&key product)\n (let ((cart-items (signal (list))))\n\n ;; Bridge: listen for \"cart:add\" events from server content\n (bridge-event container \"cart:add\" cart-items\n (fn (detail)\n (append (deref cart-items)\n (dict :id (get detail \"id\")\n :name (get detail \"name\")\n :price (get detail \"price\")))))\n\n (div\n ;; Island header with reactive cart count\n (div :class \"flex justify-between\"\n (h1 (get product \"name\"))\n (span :class \"badge\" (length (deref cart-items)) \" items\"))\n\n ;; htmx lake — server-rendered product details\n ;; This content is swapped by sx-get, not rendered by the island\n (div :id \"product-details\"\n :sx-get (str \"/products/\" (get product \"id\") \"/details\")\n :sx-swap \"innerHTML\"\n :sx-trigger \"load\"))))" "lisp")) (p "The server handler for " (code "/products/:id/details") " returns HTML with emit attributes:") - (~docs/code :code (highlight ";; Server-rendered response (pure HTML, no signals)\n(div\n (p (get product \"description\"))\n (div :class \"flex gap-2 mt-4\"\n (button\n :data-sx-emit \"cart:add\"\n :data-sx-emit-detail (json-serialize\n (dict :id (get product \"id\")\n :name (get product \"name\")\n :price (get product \"price\")))\n :class \"bg-violet-600 text-white px-4 py-2 rounded\"\n \"Add to Cart\")))" "lisp")) + (~docs/code :src (highlight ";; Server-rendered response (pure HTML, no signals)\n(div\n (p (get product \"description\"))\n (div :class \"flex gap-2 mt-4\"\n (button\n :data-sx-emit \"cart:add\"\n :data-sx-emit-detail (json-serialize\n (dict :id (get product \"id\")\n :name (get product \"name\")\n :price (get product \"price\")))\n :class \"bg-violet-600 text-white px-4 py-2 rounded\"\n \"Add to Cart\")))" "lisp")) (p "The button is plain server HTML. When clicked, the client's event bridge dispatches " (code "cart:add") " with the JSON detail. The island effect catches it and appends to " (code "cart-items") ". The badge updates reactively.")) (~docs/section :title "Why signals survive swaps" :id "survival" @@ -33,7 +33,7 @@ (~docs/section :title "Spec" :id "spec" (p "The event bridge is spec'd in " (code "signals.sx") " (sections 12-13). Three functions:") - (~docs/code :code (highlight ";; Low-level: dispatch a custom event\n(emit-event el \"cart:add\" {:id 42 :name \"Widget\"})\n\n;; Low-level: listen for a custom event\n(on-event container \"cart:add\" (fn (e)\n (swap! items (fn (old) (append old (event-detail e))))))\n\n;; High-level: bridge an event directly to a signal\n;; Creates an effect with automatic cleanup on dispose\n(bridge-event container \"cart:add\" items\n (fn (detail) (append (deref items) detail)))" "lisp")) + (~docs/code :src (highlight ";; Low-level: dispatch a custom event\n(emit-event el \"cart:add\" {:id 42 :name \"Widget\"})\n\n;; Low-level: listen for a custom event\n(on-event container \"cart:add\" (fn (e)\n (swap! items (fn (old) (append old (event-detail e))))))\n\n;; High-level: bridge an event directly to a signal\n;; Creates an effect with automatic cleanup on dispose\n(bridge-event container \"cart:add\" items\n (fn (detail) (append (deref items) detail)))" "lisp")) (p "Platform interface required:") (div :class "overflow-x-auto rounded border border-stone-200 mt-2" diff --git a/sx/sx/reactive-islands/index.sx b/sx/sx/reactive-islands/index.sx index 9d62e8ea..7d95297f 100644 --- a/sx/sx/reactive-islands/index.sx +++ b/sx/sx/reactive-islands/index.sx @@ -52,7 +52,7 @@ "Islands that share state via signal props or named stores (" (code "def-store") " / " (code "use-store") ").")))) (~docs/section :title "Signal Primitives" :id "signals" - (~docs/code :code (highlight "(signal v) ;; create a reactive container\n(deref s) ;; read value — subscribes in reactive context\n(reset! s v) ;; write new value — notifies subscribers\n(swap! s f) ;; update via function: (f old-value)\n(computed fn) ;; derived signal — auto-tracks dependencies\n(effect fn) ;; side effect — re-runs when deps change\n(batch fn) ;; group writes — one notification pass" "lisp")) + (~docs/code :src (highlight "(signal v) ;; create a reactive container\n(deref s) ;; read value — subscribes in reactive context\n(reset! s v) ;; write new value — notifies subscribers\n(swap! s f) ;; update via function: (f old-value)\n(computed fn) ;; derived signal — auto-tracks dependencies\n(effect fn) ;; side effect — re-runs when deps change\n(batch fn) ;; group writes — one notification pass" "lisp")) (p "Signals are values, not hooks. Create them anywhere — conditionals, loops, closures. No rules of hooks. Pass them as arguments, store them in dicts, share between islands.")) (~docs/section :title "Island Lifecycle" :id "lifecycle" @@ -81,12 +81,12 @@ (~docs/section :title "Event Bridge" :id "event-bridge" (p "A lake has no access to island signals, but can communicate back via DOM custom events. Elements with " (code "data-sx-emit") " dispatch a " (code "CustomEvent") " on click; an island effect catches it and updates a signal.") - (~docs/code :code (highlight ";; Island listens for events from server-rendered lake content\n(bridge-event container \"cart:add\" items\n (fn (detail) (append (deref items) detail)))\n\n;; Server-rendered button dispatches CustomEvent on click\n(button :data-sx-emit \"cart:add\"\n :data-sx-emit-detail (json-serialize (dict :id 42))\n \"Add to Cart\")" "lisp")) + (~docs/code :src (highlight ";; Island listens for events from server-rendered lake content\n(bridge-event container \"cart:add\" items\n (fn (detail) (append (deref items) detail)))\n\n;; Server-rendered button dispatches CustomEvent on click\n(button :data-sx-emit \"cart:add\"\n :data-sx-emit-detail (json-serialize (dict :id 42))\n \"Add to Cart\")" "lisp")) (p "Three primitives: " (code "emit-event") " (dispatch), " (code "on-event") " (listen), " (code "bridge-event") " (listen + update signal with automatic cleanup).")) (~docs/section :title "Named Stores" :id "stores" (p "A named store is a dict of signals at " (em "page") " scope — not island scope. Multiple islands share the same signals. Stores survive island destruction and recreation.") - (~docs/code :code (highlight ";; Create once — idempotent, returns existing on second call\n(def-store \"cart\" (fn ()\n (dict :items (signal (list))\n :count (computed (fn () (length (deref items)))))))\n\n;; Use from any island, anywhere in the DOM\n(let ((store (use-store \"cart\")))\n (span (deref (get store \"count\"))))" "lisp")) + (~docs/code :src (highlight ";; Create once — idempotent, returns existing on second call\n(def-store \"cart\" (fn ()\n (dict :items (signal (list))\n :count (computed (fn () (length (deref items)))))))\n\n;; Use from any island, anywhere in the DOM\n(let ((store (use-store \"cart\")))\n (span (deref (get store \"count\"))))" "lisp")) (p (code "def-store") " creates, " (code "use-store") " retrieves, " (code "clear-stores") " wipes all on full page navigation.")) (~docs/section :title "Design Principles" :id "principles" diff --git a/sx/sx/reactive-islands/marshes.sx b/sx/sx/reactive-islands/marshes.sx index b0b71eda..039af4eb 100644 --- a/sx/sx/reactive-islands/marshes.sx +++ b/sx/sx/reactive-islands/marshes.sx @@ -37,11 +37,11 @@ (h4 :class "font-semibold mt-4 mb-2" "Mechanism: data-sx-signal") (p "A server-rendered element carries a " (code "data-sx-signal") " attribute naming a store signal and its new value. When the morph processes this element, it writes to the signal instead of (or in addition to) updating the DOM.") - (~docs/code :code (highlight ";; Server response includes:\n(div :data-sx-signal \"cart-count:7\"\n (span \"7 items\"))\n\n;; The morph sees data-sx-signal, parses it:\n;; store name = \"cart-count\"\n;; value = 7\n;; Then: (reset! (use-store \"cart-count\") 7)\n;;\n;; Any island anywhere on the page that reads cart-count\n;; updates immediately — fine-grained, no re-render." "lisp")) + (~docs/code :src (highlight ";; Server response includes:\n(div :data-sx-signal \"cart-count:7\"\n (span \"7 items\"))\n\n;; The morph sees data-sx-signal, parses it:\n;; store name = \"cart-count\"\n;; value = 7\n;; Then: (reset! (use-store \"cart-count\") 7)\n;;\n;; Any island anywhere on the page that reads cart-count\n;; updates immediately — fine-grained, no re-render." "lisp")) (h4 :class "font-semibold mt-4 mb-2" "Mechanism: sx-on-settle") (p "An " (code "sx-on-settle") " attribute on a hypermedia trigger element. After the swap completes and the DOM settles, the SX expression is evaluated. This gives the response a chance to run arbitrary reactive logic.") - (~docs/code :code (highlight ";; A search form that updates a signal after results arrive:\n(form :sx-post \"/search\" :sx-target \"#results\"\n :sx-on-settle (reset! (use-store \"result-count\") result-count)\n (input :name \"q\" :placeholder \"Search...\"))" "lisp")) + (~docs/code :src (highlight ";; A search form that updates a signal after results arrive:\n(form :sx-post \"/search\" :sx-target \"#results\"\n :sx-on-settle (reset! (use-store \"result-count\") result-count)\n (input :name \"q\" :placeholder \"Search...\"))" "lisp")) (h4 :class "font-semibold mt-4 mb-2" "Mechanism: event bridge (already exists)") (p "The event bridge (" (code "data-sx-emit") ") already provides server → island communication via custom DOM events. Marshes generalise this: " (code "data-sx-signal") " is a declarative shorthand for the common case of \"server says update this value.\"") @@ -59,7 +59,7 @@ (h4 :class "font-semibold mt-4 mb-2" "Mechanism: marsh tag") (p "A " (code "marsh") " is a zone inside an island where server content is " (em "re-evaluated") " by the island's reactive evaluator, not just inserted as static DOM. When the morph updates a marsh, the new content is parsed as SX and rendered in the island's signal context.") - (~docs/code :code (highlight ";; Inside an island — a marsh re-evaluates on morph:\n(defisland ~reactive-islands/marshes/product-card (&key product-id)\n (let ((quantity (signal 1))\n (variant (signal nil)))\n (div :class \"card\"\n ;; Lake: server content, inserted as static HTML\n (lake :id \"description\"\n (p \"Loading...\"))\n ;; Marsh: server content, evaluated with access to island signals\n (marsh :id \"controls\"\n ;; Initial content from server — has signal references:\n (div\n (select :bind variant\n (option :value \"red\" \"Red\")\n (option :value \"blue\" \"Blue\"))\n (input :type \"number\" :bind quantity))))))" "lisp")) + (~docs/code :src (highlight ";; Inside an island — a marsh re-evaluates on morph:\n(defisland ~reactive-islands/marshes/product-card (&key product-id)\n (let ((quantity (signal 1))\n (variant (signal nil)))\n (div :class \"card\"\n ;; Lake: server content, inserted as static HTML\n (lake :id \"description\"\n (p \"Loading...\"))\n ;; Marsh: server content, evaluated with access to island signals\n (marsh :id \"controls\"\n ;; Initial content from server — has signal references:\n (div\n (select :bind variant\n (option :value \"red\" \"Red\")\n (option :value \"blue\" \"Blue\"))\n (input :type \"number\" :bind quantity))))))" "lisp")) (p "When the server sends updated marsh content (e.g., new variant options fetched from a database), the island re-evaluates it in its signal scope. The new " (code "select") " options bind to the existing " (code "variant") " signal. The reactive graph reconnects seamlessly.") (h4 :class "font-semibold mt-4 mb-2" "Lake vs. Marsh") @@ -100,12 +100,12 @@ (h4 :class "font-semibold mt-4 mb-2" "3a: Signal-bound hypermedia attributes") (p "Hypermedia trigger attributes (" (code "sx-get") ", " (code "sx-post") ", " (code "sx-target") ", " (code "sx-swap") ") can reference signals. The URL, target, and swap strategy become reactive.") - (~docs/code :code (highlight ";; A search input whose endpoint depends on reactive state:\n(defisland ~reactive-islands/marshes/smart-search ()\n (let ((mode (signal \"products\"))\n (query (signal \"\")))\n (div\n ;; Mode selector — changes what we're searching\n (select :bind mode\n (option :value \"products\" \"Products\")\n (option :value \"events\" \"Events\")\n (option :value \"posts\" \"Posts\"))\n ;; Search input — endpoint changes reactively\n (input :type \"text\" :bind query\n :sx-get (computed (fn () (str \"/search/\" (deref mode) \"?q=\" (deref query))))\n :sx-trigger \"input changed delay:300ms\"\n :sx-target \"#results\")\n (div :id \"results\"))))" "lisp")) + (~docs/code :src (highlight ";; A search input whose endpoint depends on reactive state:\n(defisland ~reactive-islands/marshes/smart-search ()\n (let ((mode (signal \"products\"))\n (query (signal \"\")))\n (div\n ;; Mode selector — changes what we're searching\n (select :bind mode\n (option :value \"products\" \"Products\")\n (option :value \"events\" \"Events\")\n (option :value \"posts\" \"Posts\"))\n ;; Search input — endpoint changes reactively\n (input :type \"text\" :bind query\n :sx-get (computed (fn () (str \"/search/\" (deref mode) \"?q=\" (deref query))))\n :sx-trigger \"input changed delay:300ms\"\n :sx-target \"#results\")\n (div :id \"results\"))))" "lisp")) (p "The " (code "sx-get") " URL isn't a static string — it's a computed signal. When the mode changes, the next search hits a different endpoint. The hypermedia trigger system reads the signal's current value at trigger time.") (h4 :class "font-semibold mt-4 mb-2" "3b: Reactive swap transforms") (p "A " (code "marsh-transform") " function that processes server content " (em "before") " it enters the DOM. The transform has access to island signals, so it can reshape the same server response differently based on client state.") - (~docs/code :code (highlight ";; View mode transforms how server results are displayed:\n(defisland ~reactive-islands/marshes/result-list ()\n (let ((view (signal \"list\"))\n (sort-key (signal \"date\")))\n (div\n (div :class \"flex gap-2\"\n (button :on-click (fn (e) (reset! view \"list\")) \"List\")\n (button :on-click (fn (e) (reset! view \"grid\")) \"Grid\")\n (button :on-click (fn (e) (reset! view \"compact\")) \"Compact\"))\n ;; Marsh: server sends a result list; client transforms its rendering\n (marsh :id \"results\"\n :transform (fn (sx-content)\n (case (deref view)\n \"grid\" (wrap-grid sx-content)\n \"compact\" (compact-view sx-content)\n :else sx-content))\n ;; Initial server content\n (div :class \"space-y-2\" \"Loading...\")))))" "lisp")) + (~docs/code :src (highlight ";; View mode transforms how server results are displayed:\n(defisland ~reactive-islands/marshes/result-list ()\n (let ((view (signal \"list\"))\n (sort-key (signal \"date\")))\n (div\n (div :class \"flex gap-2\"\n (button :on-click (fn (e) (reset! view \"list\")) \"List\")\n (button :on-click (fn (e) (reset! view \"grid\")) \"Grid\")\n (button :on-click (fn (e) (reset! view \"compact\")) \"Compact\"))\n ;; Marsh: server sends a result list; client transforms its rendering\n (marsh :id \"results\"\n :transform (fn (sx-content)\n (case (deref view)\n \"grid\" (wrap-grid sx-content)\n \"compact\" (compact-view sx-content)\n :else sx-content))\n ;; Initial server content\n (div :class \"space-y-2\" \"Loading...\")))))" "lisp")) (p "The server sends the same canonical result list every time. The " (code ":transform") " function — a reactive closure over the " (code "view") " signal — reshapes it into a grid, compact list, or default list. Change the view signal → existing content is re-transformed without a server round-trip. Fetch new results → they arrive pre-sorted, then the transform applies the current view.") (h4 :class "font-semibold mt-4 mb-2" "3c: Reactive interpretation") @@ -124,19 +124,19 @@ (p "Five new constructs, all specced in " (code ".sx") " files, bootstrapped to every host.") (h4 :class "font-semibold mt-4 mb-2" "1. marsh tag") - (~docs/code :code (highlight ";; In adapter-dom.sx / adapter-html.sx / adapter-sx.sx:\n;;\n;; (marsh :id \"controls\" :transform transform-fn children...)\n;;\n;; Server: renders as
children HTML
\n;; Client: wraps children in reactive evaluation scope\n;; Morph: re-parses incoming SX, evaluates in island scope, replaces DOM\n;;\n;; The :transform is optional. If present, it's called on the parsed SX\n;; before evaluation. The transform has full signal access." "lisp")) + (~docs/code :src (highlight ";; In adapter-dom.sx / adapter-html.sx / adapter-sx.sx:\n;;\n;; (marsh :id \"controls\" :transform transform-fn children...)\n;;\n;; Server: renders as
children HTML
\n;; Client: wraps children in reactive evaluation scope\n;; Morph: re-parses incoming SX, evaluates in island scope, replaces DOM\n;;\n;; The :transform is optional. If present, it's called on the parsed SX\n;; before evaluation. The transform has full signal access." "lisp")) (h4 :class "font-semibold mt-4 mb-2" "2. data-sx-signal (morph integration)") - (~docs/code :code (highlight ";; In engine.sx, morph-children:\n;;\n;; When processing a new element with data-sx-signal=\"name:value\":\n;; 1. Parse the attribute: store-name, signal-value\n;; 2. Look up (use-store store-name) — finds or creates the signal\n;; 3. (reset! signal parsed-value)\n;; 4. Remove the data-sx-signal attribute from DOM (consumed)\n;;\n;; Values are JSON-parsed: \"7\" → 7, '\"hello\"' → \"hello\",\n;; 'true' → true, '{...}' → dict" "lisp")) + (~docs/code :src (highlight ";; In engine.sx, morph-children:\n;;\n;; When processing a new element with data-sx-signal=\"name:value\":\n;; 1. Parse the attribute: store-name, signal-value\n;; 2. Look up (use-store store-name) — finds or creates the signal\n;; 3. (reset! signal parsed-value)\n;; 4. Remove the data-sx-signal attribute from DOM (consumed)\n;;\n;; Values are JSON-parsed: \"7\" → 7, '\"hello\"' → \"hello\",\n;; 'true' → true, '{...}' → dict" "lisp")) (h4 :class "font-semibold mt-4 mb-2" "3. Signal-bound hypermedia attributes") - (~docs/code :code (highlight ";; In orchestration.sx, resolve-trigger-attrs:\n;;\n;; Before issuing a fetch, read sx-get/sx-post/sx-target/sx-swap.\n;; If the value is a signal or computed, deref it at trigger time.\n;;\n;; (define resolve-trigger-url\n;; (fn (el attr)\n;; (let ((val (dom-get-attr el attr)))\n;; (if (signal? val) (deref val) val))))\n;;\n;; This means the URL is evaluated lazily — it reflects the current\n;; signal state at the moment the user acts, not when the DOM was built." "lisp")) + (~docs/code :src (highlight ";; In orchestration.sx, resolve-trigger-attrs:\n;;\n;; Before issuing a fetch, read sx-get/sx-post/sx-target/sx-swap.\n;; If the value is a signal or computed, deref it at trigger time.\n;;\n;; (define resolve-trigger-url\n;; (fn (el attr)\n;; (let ((val (dom-get-attr el attr)))\n;; (if (signal? val) (deref val) val))))\n;;\n;; This means the URL is evaluated lazily — it reflects the current\n;; signal state at the moment the user acts, not when the DOM was built." "lisp")) (h4 :class "font-semibold mt-4 mb-2" "4. marsh-transform (swap pipeline)") - (~docs/code :code (highlight ";; In orchestration.sx, process-swap:\n;;\n;; After receiving server HTML and before inserting into target:\n;; 1. Find the target element\n;; 2. If target has data-sx-marsh, find its transform function\n;; 3. Parse server content as SX\n;; 4. Call transform(sx-content) — transform is a reactive closure\n;; 5. Evaluate the transformed SX in the island's signal scope\n;; 6. Replace the marsh's DOM children\n;;\n;; The transform runs inside the island's tracking context,\n;; so computed/effect dependencies are captured automatically.\n;; When a signal the transform reads changes, the marsh re-transforms." "lisp")) + (~docs/code :src (highlight ";; In orchestration.sx, process-swap:\n;;\n;; After receiving server HTML and before inserting into target:\n;; 1. Find the target element\n;; 2. If target has data-sx-marsh, find its transform function\n;; 3. Parse server content as SX\n;; 4. Call transform(sx-content) — transform is a reactive closure\n;; 5. Evaluate the transformed SX in the island's signal scope\n;; 6. Replace the marsh's DOM children\n;;\n;; The transform runs inside the island's tracking context,\n;; so computed/effect dependencies are captured automatically.\n;; When a signal the transform reads changes, the marsh re-transforms." "lisp")) (h4 :class "font-semibold mt-4 mb-2" "5. sx-on-settle (post-swap hook)") - (~docs/code :code (highlight ";; In orchestration.sx, after swap completes:\n;;\n;; (define process-settle-hooks\n;; (fn (trigger-el)\n;; (let ((hook (dom-get-attr trigger-el \"sx-on-settle\")))\n;; (when hook\n;; (eval-expr (parse hook) (island-env trigger-el))))))\n;;\n;; The expression is evaluated in the nearest island's environment,\n;; giving it access to signals, stores, and island-local functions." "lisp"))) + (~docs/code :src (highlight ";; In orchestration.sx, after swap completes:\n;;\n;; (define process-settle-hooks\n;; (fn (trigger-el)\n;; (let ((hook (dom-get-attr trigger-el \"sx-on-settle\")))\n;; (when hook\n;; (eval-expr (parse hook) (island-env trigger-el))))))\n;;\n;; The expression is evaluated in the nearest island's environment,\n;; giving it access to signals, stores, and island-local functions." "lisp"))) ;; ===================================================================== ;; IV. The morph enters the marsh @@ -169,9 +169,9 @@ (td :class "px-3 py-2 font-mono text-sm text-stone-600" "data-sx-marsh") (td :class "px-3 py-2 text-stone-600" "Parse new content as SX, apply transform, evaluate in island scope, replace DOM"))))) - (~docs/code :code (highlight ";; Updated morph-island-children in engine.sx:\n\n(define morph-island-children\n (fn (old-island new-island)\n (let ((old-lakes (dom-query-all old-island \"[data-sx-lake]\"))\n (new-lakes (dom-query-all new-island \"[data-sx-lake]\"))\n (old-marshes (dom-query-all old-island \"[data-sx-marsh]\"))\n (new-marshes (dom-query-all new-island \"[data-sx-marsh]\")))\n ;; Build lookup maps\n (let ((new-lake-map (index-by-attr new-lakes \"data-sx-lake\"))\n (new-marsh-map (index-by-attr new-marshes \"data-sx-marsh\")))\n ;; Lakes: static DOM swap\n (for-each\n (fn (old-lake)\n (let ((id (dom-get-attr old-lake \"data-sx-lake\"))\n (new-lake (dict-get new-lake-map id)))\n (when new-lake\n (sync-attrs old-lake new-lake)\n (morph-children old-lake new-lake))))\n old-lakes)\n ;; Marshes: parse + evaluate + replace\n (for-each\n (fn (old-marsh)\n (let ((id (dom-get-attr old-marsh \"data-sx-marsh\"))\n (new-marsh (dict-get new-marsh-map id)))\n (when new-marsh\n (morph-marsh old-marsh new-marsh old-island))))\n old-marshes)\n ;; Signal updates from data-sx-signal\n (process-signal-updates new-island)))))" "lisp")) + (~docs/code :src (highlight ";; Updated morph-island-children in engine.sx:\n\n(define morph-island-children\n (fn (old-island new-island)\n (let ((old-lakes (dom-query-all old-island \"[data-sx-lake]\"))\n (new-lakes (dom-query-all new-island \"[data-sx-lake]\"))\n (old-marshes (dom-query-all old-island \"[data-sx-marsh]\"))\n (new-marshes (dom-query-all new-island \"[data-sx-marsh]\")))\n ;; Build lookup maps\n (let ((new-lake-map (index-by-attr new-lakes \"data-sx-lake\"))\n (new-marsh-map (index-by-attr new-marshes \"data-sx-marsh\")))\n ;; Lakes: static DOM swap\n (for-each\n (fn (old-lake)\n (let ((id (dom-get-attr old-lake \"data-sx-lake\"))\n (new-lake (dict-get new-lake-map id)))\n (when new-lake\n (sync-attrs old-lake new-lake)\n (morph-children old-lake new-lake))))\n old-lakes)\n ;; Marshes: parse + evaluate + replace\n (for-each\n (fn (old-marsh)\n (let ((id (dom-get-attr old-marsh \"data-sx-marsh\"))\n (new-marsh (dict-get new-marsh-map id)))\n (when new-marsh\n (morph-marsh old-marsh new-marsh old-island))))\n old-marshes)\n ;; Signal updates from data-sx-signal\n (process-signal-updates new-island)))))" "lisp")) - (~docs/code :code (highlight ";; morph-marsh: re-evaluate server content in island scope\n\n(define morph-marsh\n (fn (old-marsh new-marsh island-el)\n (let ((transform (get-marsh-transform old-marsh))\n (new-sx (dom-inner-sx new-marsh))\n (island-env (get-island-env island-el)))\n ;; Apply transform if present\n (let ((transformed (if transform (transform new-sx) new-sx)))\n ;; Dispose old reactive bindings in this marsh\n (dispose-marsh-scope old-marsh)\n ;; Evaluate the SX in island scope — creates new reactive bindings\n (with-marsh-scope old-marsh\n (let ((new-dom (render-to-dom transformed island-env)))\n (dom-replace-children old-marsh new-dom)))))))" "lisp"))) + (~docs/code :src (highlight ";; morph-marsh: re-evaluate server content in island scope\n\n(define morph-marsh\n (fn (old-marsh new-marsh island-el)\n (let ((transform (get-marsh-transform old-marsh))\n (new-sx (dom-inner-sx new-marsh))\n (island-env (get-island-env island-el)))\n ;; Apply transform if present\n (let ((transformed (if transform (transform new-sx) new-sx)))\n ;; Dispose old reactive bindings in this marsh\n (dispose-marsh-scope old-marsh)\n ;; Evaluate the SX in island scope — creates new reactive bindings\n (with-marsh-scope old-marsh\n (let ((new-dom (render-to-dom transformed island-env)))\n (dom-replace-children old-marsh new-dom)))))))" "lisp"))) ;; ===================================================================== ;; V. Signal lifecycle in marshes @@ -181,7 +181,7 @@ (p "Marshes introduce a sub-scope within the island's reactive context. When a marsh is re-evaluated (morph or transform change), its old effects and computeds must be disposed without disturbing the island's own reactive graph.") (~docs/subsection :title "Scoping" - (~docs/code :code (highlight ";; In signals.sx:\n\n(define with-marsh-scope\n (fn (marsh-el body-fn)\n ;; Create a child scope under the current island scope\n ;; All effects/computeds created during body-fn register here\n (let ((parent-scope (current-island-scope))\n (marsh-scope (create-child-scope parent-scope (dom-get-attr marsh-el \"data-sx-marsh\"))))\n (with-scope marsh-scope\n (body-fn)))))\n\n(define dispose-marsh-scope\n (fn (marsh-el)\n ;; Dispose all effects/computeds registered in this marsh's scope\n ;; Parent island scope and sibling marshes are unaffected\n (let ((scope (get-marsh-scope marsh-el)))\n (when scope (dispose-scope scope)))))" "lisp")) + (~docs/code :src (highlight ";; In signals.sx:\n\n(define with-marsh-scope\n (fn (marsh-el body-fn)\n ;; Create a child scope under the current island scope\n ;; All effects/computeds created during body-fn register here\n (let ((parent-scope (current-island-scope))\n (marsh-scope (create-child-scope parent-scope (dom-get-attr marsh-el \"data-sx-marsh\"))))\n (with-scope marsh-scope\n (body-fn)))))\n\n(define dispose-marsh-scope\n (fn (marsh-el)\n ;; Dispose all effects/computeds registered in this marsh's scope\n ;; Parent island scope and sibling marshes are unaffected\n (let ((scope (get-marsh-scope marsh-el)))\n (when scope (dispose-scope scope)))))" "lisp")) (p "The scoping hierarchy: " (strong "island") " → " (strong "marsh") " → " (strong "effects/computeds") ". Disposing a marsh disposes its subscope. Disposing an island disposes all its marshes. The signal graph is a tree, not a flat list.")) (~docs/subsection :title "Reactive transforms" @@ -197,17 +197,17 @@ (~docs/subsection :title "Swap strategy as signal" (p "The same server response inserted differently based on client state:") - (~docs/code :code (highlight ";; Chat app: append messages normally, morph when switching threads\n(defisland ~reactive-islands/marshes/chat ()\n (let ((mode (signal \"live\")))\n (div\n (div :sx-get \"/messages/latest\"\n :sx-trigger \"every 2s\"\n :sx-target \"#messages\"\n :sx-swap (computed (fn ()\n (if (= (deref mode) \"live\") \"beforeend\" \"innerHTML\")))\n (div :id \"messages\")))))" "lisp")) + (~docs/code :src (highlight ";; Chat app: append messages normally, morph when switching threads\n(defisland ~reactive-islands/marshes/chat ()\n (let ((mode (signal \"live\")))\n (div\n (div :sx-get \"/messages/latest\"\n :sx-trigger \"every 2s\"\n :sx-target \"#messages\"\n :sx-swap (computed (fn ()\n (if (= (deref mode) \"live\") \"beforeend\" \"innerHTML\")))\n (div :id \"messages\")))))" "lisp")) (p "In " (code "\"live\"") " mode, new messages append. Switch to thread view — the same polling endpoint now replaces the whole list. The server doesn't change. The client's reactive state changes the " (em "semantics") " of the swap.")) (~docs/subsection :title "URL rewriting as signal" (p "Reactive state transparently modifies request URLs:") - (~docs/code :code (highlight ";; Locale prefix — the server sees /fr/products, /en/products, etc.\n;; The author writes /products — the marsh layer prepends the locale.\n(def-store \"locale\" \"en\")\n\n;; In orchestration.sx, resolve-trigger-url:\n(define resolve-trigger-url\n (fn (el attr)\n (let ((raw (dom-get-attr el attr))\n (locale (deref (use-store \"locale\"))))\n (if (and locale (not (starts-with? raw (str \"/\" locale))))\n (str \"/\" locale raw)\n raw))))" "lisp")) + (~docs/code :src (highlight ";; Locale prefix — the server sees /fr/products, /en/products, etc.\n;; The author writes /products — the marsh layer prepends the locale.\n(def-store \"locale\" \"en\")\n\n;; In orchestration.sx, resolve-trigger-url:\n(define resolve-trigger-url\n (fn (el attr)\n (let ((raw (dom-get-attr el attr))\n (locale (deref (use-store \"locale\"))))\n (if (and locale (not (starts-with? raw (str \"/\" locale))))\n (str \"/\" locale raw)\n raw))))" "lisp")) (p "Every " (code "sx-get") " and " (code "sx-post") " URL passes through the resolver. A locale signal, a preview-mode signal, an A/B-test signal — any reactive state can transparently rewrite the request the server sees.")) (~docs/subsection :title "Content rewriting as signal" (p "Incoming server HTML passes through a reactive filter before insertion:") - (~docs/code :code (highlight ";; Dark mode — rewrites server classes before insertion\n(def-store \"theme\" \"light\")\n\n;; In orchestration.sx, after receiving server HTML:\n(define apply-theme-transform\n (fn (html-str)\n (if (= (deref (use-store \"theme\")) \"dark\")\n (-> html-str\n (replace-all \"bg-white\" \"bg-stone-900\")\n (replace-all \"text-stone-800\" \"text-stone-100\")\n (replace-all \"border-stone-200\" \"border-stone-700\"))\n html-str)))" "lisp")) + (~docs/code :src (highlight ";; Dark mode — rewrites server classes before insertion\n(def-store \"theme\" \"light\")\n\n;; In orchestration.sx, after receiving server HTML:\n(define apply-theme-transform\n (fn (html-str)\n (if (= (deref (use-store \"theme\")) \"dark\")\n (-> html-str\n (replace-all \"bg-white\" \"bg-stone-900\")\n (replace-all \"text-stone-800\" \"text-stone-100\")\n (replace-all \"border-stone-200\" \"border-stone-700\"))\n html-str)))" "lisp")) (p "The server renders canonical light-mode HTML. The client's theme signal rewrites it at the edge. No server-side theme support needed. No separate dark-mode templates. The same document, different interpretation.") (p "This is the Hegelian deepening: the reactive state isn't just " (em "alongside") " the hypermedia content. It " (em "constitutes the lens through which the content is perceived") ". The marsh isn't a zone in the DOM — it's a layer in the interpretation pipeline."))) @@ -297,9 +297,9 @@ (~reactive-islands/marshes/demo-marsh-product) - (~docs/code :code (highlight ";; Island with a store-backed price signal\n(defisland ~reactive-islands/marshes/demo-marsh-product ()\n (let ((price (def-store \"demo-price\" (fn () (signal 19.99))))\n (qty (signal 1))\n (total (computed (fn () (* (deref price) (deref qty))))))\n (div\n ;; Reactive price display — updates when store changes\n (span \"$\" (deref price))\n (span \"Qty:\") (button \"-\") (span (deref qty)) (button \"+\")\n (span \"Total: $\" (deref total))\n\n ;; Fetch from server — response arrives as hypermedia\n (button :sx-get \"/sx/(geography.(reactive.(api.flash-sale)))\"\n :sx-target \"#marsh-server-msg\"\n :sx-swap \"innerHTML\"\n \"Fetch Price\")\n ;; Server response lands here:\n (div :id \"marsh-server-msg\"))))" "lisp")) + (~docs/code :src (highlight ";; Island with a store-backed price signal\n(defisland ~reactive-islands/marshes/demo-marsh-product ()\n (let ((price (def-store \"demo-price\" (fn () (signal 19.99))))\n (qty (signal 1))\n (total (computed (fn () (* (deref price) (deref qty))))))\n (div\n ;; Reactive price display — updates when store changes\n (span \"$\" (deref price))\n (span \"Qty:\") (button \"-\") (span (deref qty)) (button \"+\")\n (span \"Total: $\" (deref total))\n\n ;; Fetch from server — response arrives as hypermedia\n (button :sx-get \"/sx/(geography.(reactive.(api.flash-sale)))\"\n :sx-target \"#marsh-server-msg\"\n :sx-swap \"innerHTML\"\n \"Fetch Price\")\n ;; Server response lands here:\n (div :id \"marsh-server-msg\"))))" "lisp")) - (~docs/code :code (highlight ";; Server returns SX content + a data-init script:\n;;\n;; (<>\n;; (p \"Flash sale! Price: $14.99\")\n;; (script :type \"text/sx\" :data-init\n;; \"(reset! (use-store \\\"demo-price\\\") 14.99)\"))\n;;\n;; The

is swapped in as normal hypermedia content.\n;; The script writes to the store signal.\n;; The island's (deref price), total, and savings\n;; all update reactively — no re-render, no diffing." "lisp")) + (~docs/code :src (highlight ";; Server returns SX content + a data-init script:\n;;\n;; (<>\n;; (p \"Flash sale! Price: $14.99\")\n;; (script :type \"text/sx\" :data-init\n;; \"(reset! (use-store \\\"demo-price\\\") 14.99)\"))\n;;\n;; The

is swapped in as normal hypermedia content.\n;; The script writes to the store signal.\n;; The island's (deref price), total, and savings\n;; all update reactively — no re-render, no diffing." "lisp")) (p "Two things happen from one server response: content appears in the swap target (hypermedia) and the price signal updates (reactivity). The island didn't fetch the price. The server didn't call a signal API. The response " (em "is") " both."))) @@ -323,7 +323,7 @@ :sx-swap "innerHTML" "Fetch from server")) - (~docs/code :code (highlight ";; Island A — creates the store, has control buttons\n(defisland ~reactive-islands/marshes/demo-marsh-store-writer ()\n (let ((price (def-store \"demo-price\" (fn () (signal 19.99)))))\n ;; (reset! price 14.99) is what data-sx-signal does during morph\n (button :on-click (fn (e) (reset! price 14.99))\n \"Flash Sale $14.99\")))\n\n;; Island B — reads the same store, different island\n(defisland ~reactive-islands/marshes/demo-marsh-store-reader ()\n (let ((price (def-store \"demo-price\" (fn () (signal 19.99)))))\n (span \"$\" (deref price))))\n\n;; Server returns: data-sx-signal writes to the store during morph\n;; (div :data-sx-signal \"demo-price:14.99\"\n;; (p \"Flash sale! Price updated.\"))" "lisp")) + (~docs/code :src (highlight ";; Island A — creates the store, has control buttons\n(defisland ~reactive-islands/marshes/demo-marsh-store-writer ()\n (let ((price (def-store \"demo-price\" (fn () (signal 19.99)))))\n ;; (reset! price 14.99) is what data-sx-signal does during morph\n (button :on-click (fn (e) (reset! price 14.99))\n \"Flash Sale $14.99\")))\n\n;; Island B — reads the same store, different island\n(defisland ~reactive-islands/marshes/demo-marsh-store-reader ()\n (let ((price (def-store \"demo-price\" (fn () (signal 19.99)))))\n (span \"$\" (deref price))))\n\n;; Server returns: data-sx-signal writes to the store during morph\n;; (div :data-sx-signal \"demo-price:14.99\"\n;; (p \"Flash sale! Price updated.\"))" "lisp")) (p "In production, the server response includes " (code "data-sx-signal=\"demo-price:14.99\"") ". The morph algorithm processes this attribute, calls " (code "(reset! (use-store \"demo-price\") 14.99)") ", and removes the attribute from the DOM. Every island reading that store updates instantly — fine-grained, no re-render."))) @@ -334,7 +334,7 @@ (~reactive-islands/marshes/demo-marsh-settle) - (~docs/code :code (highlight ";; sx-on-settle runs SX after the swap settles\n(defisland ~reactive-islands/marshes/demo-marsh-settle ()\n (let ((count (def-store \"settle-count\" (fn () (signal 0)))))\n (div\n ;; Reactive counter — updates from sx-on-settle\n (span \"Fetched: \" (deref count) \" times\")\n\n ;; Button with sx-on-settle hook\n (button :sx-get \"/sx/(geography.(reactive.(api.settle-data)))\"\n :sx-target \"#settle-result\"\n :sx-swap \"innerHTML\"\n :sx-on-settle \"(swap! (use-store \\\"settle-count\\\") inc)\"\n \"Fetch Item\")\n\n ;; Server content lands here (pure hypermedia)\n (div :id \"settle-result\"))))" "lisp")) + (~docs/code :src (highlight ";; sx-on-settle runs SX after the swap settles\n(defisland ~reactive-islands/marshes/demo-marsh-settle ()\n (let ((count (def-store \"settle-count\" (fn () (signal 0)))))\n (div\n ;; Reactive counter — updates from sx-on-settle\n (span \"Fetched: \" (deref count) \" times\")\n\n ;; Button with sx-on-settle hook\n (button :sx-get \"/sx/(geography.(reactive.(api.settle-data)))\"\n :sx-target \"#settle-result\"\n :sx-swap \"innerHTML\"\n :sx-on-settle \"(swap! (use-store \\\"settle-count\\\") inc)\"\n \"Fetch Item\")\n\n ;; Server content lands here (pure hypermedia)\n (div :id \"settle-result\"))))" "lisp")) (p "The server knows nothing about signals or counters. It returns plain content. The " (code "sx-on-settle") " hook is a client-side concern — it runs in the global SX environment with access to all primitives."))) @@ -345,7 +345,7 @@ (~reactive-islands/marshes/demo-marsh-signal-url) - (~docs/code :code (highlight ";; sx-get URL computed from a signal\n(defisland ~reactive-islands/marshes/demo-marsh-signal-url ()\n (let ((mode (signal \"products\"))\n (query (signal \"\")))\n (div\n ;; Mode selector — changes what we're searching\n (div :class \"flex gap-2\"\n (button :on-click (fn (e) (reset! mode \"products\"))\n :class (computed (fn () ...active-class...))\n \"Products\")\n (button :on-click (fn (e) (reset! mode \"events\")) \"Events\")\n (button :on-click (fn (e) (reset! mode \"posts\")) \"Posts\"))\n\n ;; Search button — URL is a computed expression\n (button :sx-get (computed (fn ()\n (str \"/sx/(geography.(reactive.(api.search-\"\n (deref mode) \")))\" \"?q=\" (deref query))))\n :sx-target \"#signal-results\"\n :sx-swap \"innerHTML\"\n \"Search\")\n\n (div :id \"signal-results\"))))" "lisp")) + (~docs/code :src (highlight ";; sx-get URL computed from a signal\n(defisland ~reactive-islands/marshes/demo-marsh-signal-url ()\n (let ((mode (signal \"products\"))\n (query (signal \"\")))\n (div\n ;; Mode selector — changes what we're searching\n (div :class \"flex gap-2\"\n (button :on-click (fn (e) (reset! mode \"products\"))\n :class (computed (fn () ...active-class...))\n \"Products\")\n (button :on-click (fn (e) (reset! mode \"events\")) \"Events\")\n (button :on-click (fn (e) (reset! mode \"posts\")) \"Posts\"))\n\n ;; Search button — URL is a computed expression\n (button :sx-get (computed (fn ()\n (str \"/sx/(geography.(reactive.(api.search-\"\n (deref mode) \")))\" \"?q=\" (deref query))))\n :sx-target \"#signal-results\"\n :sx-swap \"innerHTML\"\n \"Search\")\n\n (div :id \"signal-results\"))))" "lisp")) (p "No custom plumbing. The same " (code "reactive-attr") " mechanism that makes " (code ":class") " reactive also makes " (code ":sx-get") " reactive. " (code "get-verb-info") " reads " (code "dom-get-attr") " at trigger time — it sees the current URL because the effect already updated the DOM attribute."))) @@ -355,7 +355,7 @@ (~reactive-islands/marshes/demo-marsh-view-transform) - (~docs/code :code (highlight ";; View mode transforms display without refetch\n(defisland ~reactive-islands/marshes/demo-marsh-view-transform ()\n (let ((view (signal \"list\"))\n (items (signal nil)))\n (div\n ;; View toggle\n (div :class \"flex gap-2\"\n (button :on-click (fn (e) (reset! view \"list\")) \"List\")\n (button :on-click (fn (e) (reset! view \"grid\")) \"Grid\")\n (button :on-click (fn (e) (reset! view \"compact\")) \"Compact\"))\n\n ;; Fetch from server — stores raw data in signal\n (button :sx-get \"/sx/(geography.(reactive.(api.catalog)))\"\n :sx-target \"#catalog-raw\"\n :sx-swap \"innerHTML\"\n \"Fetch Catalog\")\n\n ;; Raw server content (hidden, used as data source)\n (div :id \"catalog-raw\" :class \"hidden\")\n\n ;; Reactive display — re-renders when view changes\n (div (computed (fn () (render-view (deref view) (deref items))))))))" "lisp")) + (~docs/code :src (highlight ";; View mode transforms display without refetch\n(defisland ~reactive-islands/marshes/demo-marsh-view-transform ()\n (let ((view (signal \"list\"))\n (items (signal nil)))\n (div\n ;; View toggle\n (div :class \"flex gap-2\"\n (button :on-click (fn (e) (reset! view \"list\")) \"List\")\n (button :on-click (fn (e) (reset! view \"grid\")) \"Grid\")\n (button :on-click (fn (e) (reset! view \"compact\")) \"Compact\"))\n\n ;; Fetch from server — stores raw data in signal\n (button :sx-get \"/sx/(geography.(reactive.(api.catalog)))\"\n :sx-target \"#catalog-raw\"\n :sx-swap \"innerHTML\"\n \"Fetch Catalog\")\n\n ;; Raw server content (hidden, used as data source)\n (div :id \"catalog-raw\" :class \"hidden\")\n\n ;; Reactive display — re-renders when view changes\n (div (computed (fn () (render-view (deref view) (deref items))))))))" "lisp")) (p "The view signal doesn't just toggle CSS classes — it fundamentally reshapes the DOM. List view shows description. Grid view arranges in columns. Compact view shows names only. All from the same server data, transformed by client state."))) diff --git a/sx/sx/reactive-islands/named-stores.sx b/sx/sx/reactive-islands/named-stores.sx index ec527244..7e4cc14c 100644 --- a/sx/sx/reactive-islands/named-stores.sx +++ b/sx/sx/reactive-islands/named-stores.sx @@ -14,7 +14,7 @@ (p "Named stores solve all three. A store is a named collection of signals that lives at " (em "page") " scope, not island scope.")) (~docs/section :title "def-store / use-store" :id "api" - (~docs/code :code (highlight ";; Create a named store — called once at page level\n;; The init function creates signals and computeds\n(def-store \"cart\" (fn ()\n (let ((items (signal (list))))\n (dict\n :items items\n :count (computed (fn () (length (deref items))))\n :total (computed (fn () (reduce + 0\n (map (fn (i) (get i \"price\")) (deref items)))))))))\n\n;; Use the store from any island — returns the signal dict\n(defisland ~reactive-islands/named-stores/cart-badge ()\n (let ((store (use-store \"cart\")))\n (span :class \"badge bg-violet-100 text-violet-800 px-2 py-1 rounded-full\"\n (deref (get store \"count\")))))\n\n(defisland ~reactive-islands/named-stores/cart-drawer ()\n (let ((store (use-store \"cart\")))\n (div :class \"p-4\"\n (h2 \"Cart\")\n (ul (map (fn (item)\n (li :class \"flex justify-between py-1\"\n (span (get item \"name\"))\n (span :class \"text-stone-500\" \"\£\" (get item \"price\"))))\n (deref (get store \"items\"))))\n (div :class \"border-t pt-2 font-semibold\"\n \"Total: \£\" (deref (get store \"total\"))))))" "lisp")) + (~docs/code :src (highlight ";; Create a named store — called once at page level\n;; The init function creates signals and computeds\n(def-store \"cart\" (fn ()\n (let ((items (signal (list))))\n (dict\n :items items\n :count (computed (fn () (length (deref items))))\n :total (computed (fn () (reduce + 0\n (map (fn (i) (get i \"price\")) (deref items)))))))))\n\n;; Use the store from any island — returns the signal dict\n(defisland ~reactive-islands/named-stores/cart-badge ()\n (let ((store (use-store \"cart\")))\n (span :class \"badge bg-violet-100 text-violet-800 px-2 py-1 rounded-full\"\n (deref (get store \"count\")))))\n\n(defisland ~reactive-islands/named-stores/cart-drawer ()\n (let ((store (use-store \"cart\")))\n (div :class \"p-4\"\n (h2 \"Cart\")\n (ul (map (fn (item)\n (li :class \"flex justify-between py-1\"\n (span (get item \"name\"))\n (span :class \"text-stone-500\" \"\£\" (get item \"price\"))))\n (deref (get store \"items\"))))\n (div :class \"border-t pt-2 font-semibold\"\n \"Total: \£\" (deref (get store \"total\"))))))" "lisp")) (p (code "def-store") " is " (strong "idempotent") " — calling it again with the same name returns the existing store. This means multiple components can call " (code "def-store") " defensively without double-creating.")) @@ -29,7 +29,7 @@ (~docs/section :title "Combining with event bridge" :id "combined" (p "Named stores + event bridge = full lake→island→island communication:") - (~docs/code :code (highlight ";; Store persists across island lifecycle\n(def-store \"cart\" (fn () ...))\n\n;; Island 1: product page with htmx lake\n(defisland ~reactive-islands/named-stores/product-island ()\n (let ((store (use-store \"cart\")))\n ;; Bridge server-rendered \"Add\" buttons to store\n (bridge-event container \"cart:add\" (get store \"items\")\n (fn (detail) (append (deref (get store \"items\")) detail)))\n ;; Lake content swapped via sx-get\n (div :id \"product-content\" :sx-get \"/products/featured\")))\n\n;; Island 2: cart badge in header (distant in DOM)\n(defisland ~reactive-islands/named-stores/cart-badge ()\n (let ((store (use-store \"cart\")))\n (span (deref (get store \"count\")))))" "lisp")) + (~docs/code :src (highlight ";; Store persists across island lifecycle\n(def-store \"cart\" (fn () ...))\n\n;; Island 1: product page with htmx lake\n(defisland ~reactive-islands/named-stores/product-island ()\n (let ((store (use-store \"cart\")))\n ;; Bridge server-rendered \"Add\" buttons to store\n (bridge-event container \"cart:add\" (get store \"items\")\n (fn (detail) (append (deref (get store \"items\")) detail)))\n ;; Lake content swapped via sx-get\n (div :id \"product-content\" :sx-get \"/products/featured\")))\n\n;; Island 2: cart badge in header (distant in DOM)\n(defisland ~reactive-islands/named-stores/cart-badge ()\n (let ((store (use-store \"cart\")))\n (span (deref (get store \"count\")))))" "lisp")) (p "User clicks \"Add to Cart\" in server-rendered product content. " (code "cart:add") " event fires. Product island catches it via bridge. Store's " (code "items") " signal updates. Cart badge — in a completely different island — updates reactively because it reads the same signal.")) diff --git a/sx/sx/reactive-islands/phase2.sx b/sx/sx/reactive-islands/phase2.sx index a63234b8..367ef0d2 100644 --- a/sx/sx/reactive-islands/phase2.sx +++ b/sx/sx/reactive-islands/phase2.sx @@ -58,7 +58,7 @@ (~docs/subsection :title "Design" (p "A new " (code ":bind") " attribute on " (code "input") ", " (code "textarea") ", and " (code "select") " elements. It takes a signal and creates a bidirectional link: signal value flows into the element, user input flows back into the signal.") - (~docs/code :code (highlight ";; Bind a signal to an input\n(defisland ~reactive-islands/phase2/login-form ()\n (let ((email (signal \"\"))\n (password (signal \"\")))\n (form :on-submit (fn (e)\n (dom-prevent-default e)\n (fetch-json \"POST\" \"/api/login\"\n (dict \"email\" (deref email)\n \"password\" (deref password))))\n (input :type \"email\" :bind email\n :placeholder \"Email\")\n (input :type \"password\" :bind password\n :placeholder \"Password\")\n (button :type \"submit\" \"Log in\"))))" "lisp")) + (~docs/code :src (highlight ";; Bind a signal to an input\n(defisland ~reactive-islands/phase2/login-form ()\n (let ((email (signal \"\"))\n (password (signal \"\")))\n (form :on-submit (fn (e)\n (dom-prevent-default e)\n (fetch-json \"POST\" \"/api/login\"\n (dict \"email\" (deref email)\n \"password\" (deref password))))\n (input :type \"email\" :bind email\n :placeholder \"Email\")\n (input :type \"password\" :bind password\n :placeholder \"Password\")\n (button :type \"submit\" \"Log in\"))))" "lisp")) (p "The " (code ":bind") " attribute is handled in " (code "adapter-dom.sx") "'s element rendering. For a signal " (code "s") ":") (ol :class "space-y-1 text-stone-600 list-decimal list-inside text-sm" @@ -69,7 +69,7 @@ (li "For select: bind to " (code "value") ", handle " (code "change") " event"))) (~docs/subsection :title "Spec changes" - (~docs/code :code (highlight ";; In adapter-dom.sx, inside render-dom-element:\n;; After processing :on-* event attrs, check for :bind\n(when (dict-has? kwargs \"bind\")\n (let ((sig (dict-get kwargs \"bind\")))\n (when (signal? sig)\n (bind-input el sig))))\n\n;; New function in adapter-dom.sx:\n(define bind-input\n (fn (el sig)\n (let ((tag (lower (dom-tag-name el)))\n (is-checkbox (or (= (dom-get-attr el \"type\") \"checkbox\")\n (= (dom-get-attr el \"type\") \"radio\"))))\n ;; Set initial value\n (if is-checkbox\n (dom-set-prop el \"checked\" (deref sig))\n (dom-set-prop el \"value\" (str (deref sig))))\n ;; Signal → element (effect, auto-tracked)\n (effect (fn ()\n (if is-checkbox\n (dom-set-prop el \"checked\" (deref sig))\n (let ((v (str (deref sig))))\n (when (!= (dom-get-prop el \"value\") v)\n (dom-set-prop el \"value\" v))))))\n ;; Element → signal (event listener)\n (dom-listen el (if is-checkbox \"change\" \"input\")\n (fn (e)\n (if is-checkbox\n (reset! sig (dom-get-prop el \"checked\"))\n (reset! sig (dom-get-prop el \"value\"))))))))" "lisp")) + (~docs/code :src (highlight ";; In adapter-dom.sx, inside render-dom-element:\n;; After processing :on-* event attrs, check for :bind\n(when (dict-has? kwargs \"bind\")\n (let ((sig (dict-get kwargs \"bind\")))\n (when (signal? sig)\n (bind-input el sig))))\n\n;; New function in adapter-dom.sx:\n(define bind-input\n (fn (el sig)\n (let ((tag (lower (dom-tag-name el)))\n (is-checkbox (or (= (dom-get-attr el \"type\") \"checkbox\")\n (= (dom-get-attr el \"type\") \"radio\"))))\n ;; Set initial value\n (if is-checkbox\n (dom-set-prop el \"checked\" (deref sig))\n (dom-set-prop el \"value\" (str (deref sig))))\n ;; Signal → element (effect, auto-tracked)\n (effect (fn ()\n (if is-checkbox\n (dom-set-prop el \"checked\" (deref sig))\n (let ((v (str (deref sig))))\n (when (!= (dom-get-prop el \"value\") v)\n (dom-set-prop el \"value\" v))))))\n ;; Element → signal (event listener)\n (dom-listen el (if is-checkbox \"change\" \"input\")\n (fn (e)\n (if is-checkbox\n (reset! sig (dom-get-prop el \"checked\"))\n (reset! sig (dom-get-prop el \"value\"))))))))" "lisp")) (p "Platform additions: " (code "dom-set-prop") " and " (code "dom-get-prop") " (property access, not attribute — " (code ".value") " not " (code "getAttribute") "). These go in the boundary as IO primitives.")) @@ -87,7 +87,7 @@ (~docs/subsection :title "Design" (p "When items have a " (code ":key") " attribute (or a key function), " (code "reactive-list") " should reconcile by key instead of clearing.") - (~docs/code :code (highlight ";; Keyed list — items matched by :key, reused across updates\n(defisland ~reactive-islands/phase2/todo-list ()\n (let ((items (signal (list\n (dict \"id\" 1 \"text\" \"Buy milk\")\n (dict \"id\" 2 \"text\" \"Write spec\")\n (dict \"id\" 3 \"text\" \"Ship it\")))))\n (ul\n (map (fn (item)\n (li :key (get item \"id\")\n (span (get item \"text\"))\n (button :on-click (fn (e) ...)\n \"Remove\")))\n (deref items)))))" "lisp")) + (~docs/code :src (highlight ";; Keyed list — items matched by :key, reused across updates\n(defisland ~reactive-islands/phase2/todo-list ()\n (let ((items (signal (list\n (dict \"id\" 1 \"text\" \"Buy milk\")\n (dict \"id\" 2 \"text\" \"Write spec\")\n (dict \"id\" 3 \"text\" \"Ship it\")))))\n (ul\n (map (fn (item)\n (li :key (get item \"id\")\n (span (get item \"text\"))\n (button :on-click (fn (e) ...)\n \"Remove\")))\n (deref items)))))" "lisp")) (p "The reconciliation algorithm:") (ol :class "space-y-1 text-stone-600 list-decimal list-inside text-sm" @@ -98,7 +98,7 @@ (li "Result: minimum DOM mutations. Focus, scroll, animations preserved."))) (~docs/subsection :title "Spec changes" - (~docs/code :code (highlight ";; In adapter-dom.sx, replace reactive-list's effect body:\n(define reactive-list\n (fn (map-fn items-sig env ns)\n (let ((marker (create-comment \"island-list\"))\n (key-map (dict)) ;; key → DOM node\n (key-order (list))) ;; current key order\n (effect (fn ()\n (let ((parent (dom-parent marker))\n (items (deref items-sig)))\n (when parent\n (let ((new-map (dict))\n (new-keys (list)))\n ;; Render or reuse each item\n (for-each (fn (item)\n (let ((rendered (render-item map-fn item env ns))\n (key (or (dom-get-attr rendered \"key\")\n (dom-get-data rendered \"key\")\n (identity-key item))))\n (dom-remove-attr rendered \"key\")\n (if (dict-has? key-map key)\n ;; Reuse existing\n (dict-set! new-map key (dict-get key-map key))\n ;; New node\n (dict-set! new-map key rendered))\n (append! new-keys key)))\n items)\n ;; Remove stale nodes\n (for-each (fn (k)\n (when (not (dict-has? new-map k))\n (dom-remove (dict-get key-map k))))\n key-order)\n ;; Reorder to match new-keys\n (let ((cursor marker))\n (for-each (fn (k)\n (let ((node (dict-get new-map k)))\n (when (not (= node (dom-next-sibling cursor)))\n (dom-insert-after cursor node))\n (set! cursor node)))\n new-keys))\n ;; Update state\n (set! key-map new-map)\n (set! key-order new-keys))))))\n marker)))" "lisp")) + (~docs/code :src (highlight ";; In adapter-dom.sx, replace reactive-list's effect body:\n(define reactive-list\n (fn (map-fn items-sig env ns)\n (let ((marker (create-comment \"island-list\"))\n (key-map (dict)) ;; key → DOM node\n (key-order (list))) ;; current key order\n (effect (fn ()\n (let ((parent (dom-parent marker))\n (items (deref items-sig)))\n (when parent\n (let ((new-map (dict))\n (new-keys (list)))\n ;; Render or reuse each item\n (for-each (fn (item)\n (let ((rendered (render-item map-fn item env ns))\n (key (or (dom-get-attr rendered \"key\")\n (dom-get-data rendered \"key\")\n (identity-key item))))\n (dom-remove-attr rendered \"key\")\n (if (dict-has? key-map key)\n ;; Reuse existing\n (dict-set! new-map key (dict-get key-map key))\n ;; New node\n (dict-set! new-map key rendered))\n (append! new-keys key)))\n items)\n ;; Remove stale nodes\n (for-each (fn (k)\n (when (not (dict-has? new-map k))\n (dom-remove (dict-get key-map k))))\n key-order)\n ;; Reorder to match new-keys\n (let ((cursor marker))\n (for-each (fn (k)\n (let ((node (dict-get new-map k)))\n (when (not (= node (dom-next-sibling cursor)))\n (dom-insert-after cursor node))\n (set! cursor node)))\n new-keys))\n ;; Update state\n (set! key-map new-map)\n (set! key-order new-keys))))))\n marker)))" "lisp")) (p "Falls back to current clear-and-rerender when no keys are present."))) @@ -110,7 +110,7 @@ (p "A portal renders children into a DOM node " (em "outside") " the island's subtree. Essential for modals, tooltips, dropdown menus, and toast notifications — anything that must escape overflow:hidden, z-index stacking, or layout constraints.") (~docs/subsection :title "Design" - (~docs/code :code (highlight ";; portal — render children into a target element\n(defisland ~reactive-islands/phase2/modal-trigger ()\n (let ((open? (signal false)))\n (div\n (button :on-click (fn (e) (swap! open? not))\n \"Open Modal\")\n\n ;; Portal: children rendered into #modal-root,\n ;; not into this island's DOM\n (portal \"#modal-root\"\n (when (deref open?)\n (div :class \"fixed inset-0 bg-black/50 flex items-center justify-center\"\n (div :class \"bg-white rounded-lg p-6 max-w-md\"\n (h2 \"Modal Title\")\n (p \"This is rendered outside the island's DOM subtree.\")\n (button :on-click (fn (e) (reset! open? false))\n \"Close\"))))))))" "lisp")) + (~docs/code :src (highlight ";; portal — render children into a target element\n(defisland ~reactive-islands/phase2/modal-trigger ()\n (let ((open? (signal false)))\n (div\n (button :on-click (fn (e) (swap! open? not))\n \"Open Modal\")\n\n ;; Portal: children rendered into #modal-root,\n ;; not into this island's DOM\n (portal \"#modal-root\"\n (when (deref open?)\n (div :class \"fixed inset-0 bg-black/50 flex items-center justify-center\"\n (div :class \"bg-white rounded-lg p-6 max-w-md\"\n (h2 \"Modal Title\")\n (p \"This is rendered outside the island's DOM subtree.\")\n (button :on-click (fn (e) (reset! open? false))\n \"Close\"))))))))" "lisp")) (p "Implementation in " (code "adapter-dom.sx") ":") (ol :class "space-y-1 text-stone-600 list-decimal list-inside text-sm" @@ -132,7 +132,7 @@ (p "When an island's rendering or effect throws, the error currently propagates to the top level and may crash other islands. An error boundary catches the error and renders a fallback UI.") (~docs/subsection :title "Design" - (~docs/code :code (highlight ";; error-boundary — catch errors in island subtrees\n(defisland ~reactive-islands/phase2/resilient-widget ()\n (error-boundary\n ;; Fallback: shown when children throw\n (fn (err)\n (div :class \"p-4 bg-red-50 border border-red-200 rounded\"\n (p :class \"text-red-700 font-medium\" \"Something went wrong\")\n (p :class \"text-red-500 text-sm\" (error-message err))))\n ;; Children: the happy path\n (do\n (~risky-component)\n (~another-component))))" "lisp")) + (~docs/code :src (highlight ";; error-boundary — catch errors in island subtrees\n(defisland ~reactive-islands/phase2/resilient-widget ()\n (error-boundary\n ;; Fallback: shown when children throw\n (fn (err)\n (div :class \"p-4 bg-red-50 border border-red-200 rounded\"\n (p :class \"text-red-700 font-medium\" \"Something went wrong\")\n (p :class \"text-red-500 text-sm\" (error-message err))))\n ;; Children: the happy path\n (do\n (~risky-component)\n (~another-component))))" "lisp")) (p "Implementation:") (ol :class "space-y-1 text-stone-600 list-decimal list-inside text-sm" @@ -147,11 +147,11 @@ (p "Suspense handles async operations in the render path — data fetching, lazy-loaded components, code splitting. Show a loading placeholder until the async work completes, then swap in the result.") (~docs/subsection :title "Design" - (~docs/code :code (highlight ";; suspense — async-aware rendering boundary\n(defisland ~reactive-islands/phase2/user-profile (&key user-id)\n (suspense\n ;; Fallback: shown during loading\n (div :class \"animate-pulse\"\n (div :class \"h-4 bg-stone-200 rounded w-3/4\")\n (div :class \"h-4 bg-stone-200 rounded w-1/2 mt-2\"))\n ;; Children: may contain async operations\n (let ((user (await (fetch-json (str \"/api/users/\" user-id)))))\n (div\n (h2 (get user \"name\"))\n (p (get user \"email\"))))))" "lisp")) + (~docs/code :src (highlight ";; suspense — async-aware rendering boundary\n(defisland ~reactive-islands/phase2/user-profile (&key user-id)\n (suspense\n ;; Fallback: shown during loading\n (div :class \"animate-pulse\"\n (div :class \"h-4 bg-stone-200 rounded w-3/4\")\n (div :class \"h-4 bg-stone-200 rounded w-1/2 mt-2\"))\n ;; Children: may contain async operations\n (let ((user (await (fetch-json (str \"/api/users/\" user-id)))))\n (div\n (h2 (get user \"name\"))\n (p (get user \"email\"))))))" "lisp")) (p "This requires a new primitive concept: a " (strong "resource") " — an async signal that transitions through loading → resolved → error states.") - (~docs/code :code (highlight ";; resource — async signal\n(define resource\n (fn (fetch-fn)\n ;; Returns a signal-like value:\n ;; {:loading true :data nil :error nil} initially\n ;; {:loading false :data result :error nil} on success\n ;; {:loading false :data nil :error err} on failure\n (let ((state (signal (dict \"loading\" true\n \"data\" nil\n \"error\" nil))))\n ;; Kick off the async operation\n (promise-then (fetch-fn)\n (fn (data) (reset! state (dict \"loading\" false\n \"data\" data\n \"error\" nil)))\n (fn (err) (reset! state (dict \"loading\" false\n \"data\" nil\n \"error\" err))))\n state)))" "lisp")) + (~docs/code :src (highlight ";; resource — async signal\n(define resource\n (fn (fetch-fn)\n ;; Returns a signal-like value:\n ;; {:loading true :data nil :error nil} initially\n ;; {:loading false :data result :error nil} on success\n ;; {:loading false :data nil :error err} on failure\n (let ((state (signal (dict \"loading\" true\n \"data\" nil\n \"error\" nil))))\n ;; Kick off the async operation\n (promise-then (fetch-fn)\n (fn (data) (reset! state (dict \"loading\" false\n \"data\" data\n \"error\" nil)))\n (fn (err) (reset! state (dict \"loading\" false\n \"data\" nil\n \"error\" err))))\n state)))" "lisp")) (p "Suspense is the rendering boundary; resource is the data primitive. Together they give a clean async data story without effects-that-fetch (React's " (code "useEffect") " + " (code "useState") " anti-pattern)."))) @@ -159,7 +159,7 @@ (p "Transitions mark updates as non-urgent. The UI stays interactive during expensive re-renders. React's " (code "startTransition") " defers state updates so that urgent updates (typing, clicking) aren't blocked by slow ones (filtering a large list, rendering a complex subtree).") (~docs/subsection :title "Design" - (~docs/code :code (highlight ";; transition — non-urgent signal update\n(defisland ~reactive-islands/phase2/search-results (&key items)\n (let ((query (signal \"\"))\n (filtered (signal items))\n (is-pending (signal false)))\n ;; Typing is urgent — updates immediately\n ;; Filtering is deferred — doesn't block input\n (effect (fn ()\n (let ((q (deref query)))\n (transition is-pending\n (fn ()\n (reset! filtered\n (filter (fn (item)\n (contains? (lower (get item \"name\")) (lower q)))\n items)))))))\n (div\n (input :bind query :placeholder \"Search...\")\n (when (deref is-pending)\n (span :class \"text-stone-400\" \"Filtering...\"))\n (ul (map (fn (item) (li (get item \"name\")))\n (deref filtered))))))" "lisp")) + (~docs/code :src (highlight ";; transition — non-urgent signal update\n(defisland ~reactive-islands/phase2/search-results (&key items)\n (let ((query (signal \"\"))\n (filtered (signal items))\n (is-pending (signal false)))\n ;; Typing is urgent — updates immediately\n ;; Filtering is deferred — doesn't block input\n (effect (fn ()\n (let ((q (deref query)))\n (transition is-pending\n (fn ()\n (reset! filtered\n (filter (fn (item)\n (contains? (lower (get item \"name\")) (lower q)))\n items)))))))\n (div\n (input :bind query :placeholder \"Search...\")\n (when (deref is-pending)\n (span :class \"text-stone-400\" \"Filtering...\"))\n (ul (map (fn (item) (li (get item \"name\")))\n (deref filtered))))))" "lisp")) (p (code "transition") " takes a pending-signal and a thunk. It sets pending to true, schedules the thunk via " (code "requestIdleCallback") " (or " (code "setTimeout 0") " as fallback), then sets pending to false when complete. Signal writes inside the thunk are batched and applied asynchronously.") (p "This is lower priority because SX's fine-grained updates already avoid the re-render-everything problem that makes transitions critical in React. But for truly large lists or expensive computations, deferral is still valuable."))) diff --git a/sx/sx/reactive-islands/plan.sx b/sx/sx/reactive-islands/plan.sx index b493f4eb..2364d25b 100644 --- a/sx/sx/reactive-islands/plan.sx +++ b/sx/sx/reactive-islands/plan.sx @@ -67,18 +67,18 @@ (p "The existing " (code "renderDOM") " function walks the AST and creates DOM nodes. Inside an island, it becomes signal-aware:") (~docs/subsection :title "Text bindings" - (~docs/code :code (highlight ";; (span (deref count)) creates:\n;; const text = document.createTextNode(sig.value)\n;; effect(() => text.nodeValue = sig.value)" "lisp")) + (~docs/code :src (highlight ";; (span (deref count)) creates:\n;; const text = document.createTextNode(sig.value)\n;; effect(() => text.nodeValue = sig.value)" "lisp")) (p "Only the text node updates. The span is untouched.")) (~docs/subsection :title "Attribute bindings" - (~docs/code :code (highlight ";; (div :class (str \"panel \" (if (deref open?) \"visible\" \"hidden\")))\n;; effect(() => div.className = ...)" "lisp"))) + (~docs/code :src (highlight ";; (div :class (str \"panel \" (if (deref open?) \"visible\" \"hidden\")))\n;; effect(() => div.className = ...)" "lisp"))) (~docs/subsection :title "Conditional fragments" - (~docs/code :code (highlight ";; (when (deref show?) (~details)) creates:\n;; A marker comment node, then:\n;; effect(() => show ? insert-after(marker, render(~details)) : remove)" "lisp")) + (~docs/code :src (highlight ";; (when (deref show?) (~details)) creates:\n;; A marker comment node, then:\n;; effect(() => show ? insert-after(marker, render(~details)) : remove)" "lisp")) (p "Equivalent to SolidJS's " (code "Show") " — but falls out naturally from the evaluator.")) (~docs/subsection :title "List rendering" - (~docs/code :code (highlight "(map (fn (item) (li :key (get item \"id\") (get item \"name\")))\n (deref items))" "lisp")) + (~docs/code :src (highlight "(map (fn (item) (li :key (get item \"id\") (get item \"name\")))\n (deref items))" "lisp")) (p "Keyed elements are reused and reordered. Unkeyed elements are morphed."))) (~docs/section :title "Status" :id "status" diff --git a/sx/sx/reactive-runtime.sx b/sx/sx/reactive-runtime.sx index 41303a9c..01d377ca 100644 --- a/sx/sx/reactive-runtime.sx +++ b/sx/sx/reactive-runtime.sx @@ -65,7 +65,7 @@ "The OCaml evaluator bootstraps them to JavaScript via the standard transpiler pipeline.") (~docs/subsection :title "Implementation Order" - (~docs/code :code (highlight + (~docs/code :src (highlight "L0 Ref → standalone, trivial (~35 LOC)\nL1 Foreign FFI → standalone, function factories (~100 LOC)\nL5 Keyed Lists → enhances existing reactive-list (~155 LOC)\nL2 State Machine → uses signals + dicts (~200 LOC)\nL4 Render Loop → uses L0 refs + existing rAF (~140 LOC)\nL3 Commands → extends stores, uses signals (~320 LOC)\nL6 App Shell → orchestrates all above (~330 LOC)\n Total: ~1280 LOC" "text")) @@ -74,7 +74,7 @@ (~docs/subsection :title "Build Integration" (p "One new entry in " (code "SPEC_MODULES") " (hosts/javascript/platform.py):") - (~docs/code :code (highlight + (~docs/code :src (highlight "SPEC_MODULES = {\n ...\n \"reactive-runtime\": (\"reactive-runtime.sx\", \"reactive-runtime (application patterns)\"),\n}\nSPEC_MODULE_ORDER = [..., \"reactive-runtime\"]" "python")) (p "Auto-included when the " (code "dom") " adapter is present. " @@ -110,7 +110,7 @@ (~docs/section :title "API" :id "ref-api" - (~docs/code :code (highlight + (~docs/code :src (highlight ";; Create a ref — auto-registers disposal in island scope\n(define my-ref (ref initial-value))\n\n;; Read (no tracking, no subscriptions)\n(ref-deref my-ref)\n\n;; Write (no notifications, no re-renders)\n(ref-set! my-ref new-value)\n\n;; Predicate\n(ref? my-ref) ;; => true" "lisp")) @@ -130,7 +130,7 @@ (~docs/section :title "Implementation" :id "ref-impl" - (~docs/code :code (highlight + (~docs/code :src (highlight "(define make-ref (fn (value)\n (dict \"__ref\" true \"value\" value)))\n\n(define ref? (fn (x)\n (and (dict? x) (has-key? x \"__ref\"))))\n\n(define ref (fn (initial-value)\n (let ((r (make-ref initial-value)))\n (register-in-scope (fn () (dict-set! r \"value\" nil)))\n r)))\n\n(define ref-deref (fn (r) (get r \"value\")))\n(define ref-set! (fn (r v) (dict-set! r \"value\" v)))" "lisp")) @@ -150,7 +150,7 @@ (~docs/section :title "API" :id "foreign-api" - (~docs/code :code (highlight + (~docs/code :src (highlight ";; Function factories — each returns a reusable function\n(define fill-rect (foreign-method \"fill-rect\"))\n(define set-fill-style! (foreign-prop-setter \"fill-style\"))\n(define get-width (foreign-prop-getter \"width\"))\n\n;; Usage — clean SX calls, no string method names\n(let ((ctx (host-call canvas \"getContext\" \"2d\")))\n (set-fill-style! ctx \"red\")\n (fill-rect ctx 0 0 (get-width canvas) 100))" "lisp")) @@ -163,7 +163,7 @@ (~docs/section :title "Implementation" :id "foreign-impl" - (~docs/code :code (highlight + (~docs/code :src (highlight "(define kebab->camel (fn (s)\n ;; \"fill-rect\" → \"fillRect\", \"font-size\" → \"fontSize\"\n (let ((parts (split s \"-\"))\n (first-part (first parts))\n (rest-parts (rest parts)))\n (str first-part\n (join \"\" (map (fn (p)\n (str (upper (slice p 0 1)) (slice p 1))) rest-parts))))))\n\n(define foreign-method (fn (method-name)\n (let ((camel (kebab->camel method-name)))\n (fn (obj &rest args)\n (apply host-call (concat (list obj camel) args))))))\n\n(define foreign-prop-getter (fn (prop-name)\n (let ((camel (kebab->camel prop-name)))\n (fn (obj) (host-get obj camel)))))\n\n(define foreign-prop-setter (fn (prop-name)\n (let ((camel (kebab->camel prop-name)))\n (fn (obj val) (host-set! obj camel val)))))" "lisp")) @@ -183,7 +183,7 @@ (~docs/section :title "API" :id "machine-api" - (~docs/code :code (highlight + (~docs/code :src (highlight "(define drawing-tool (make-machine\n {:initial :idle\n :states {:idle {:on {:pointer-down (fn (ev)\n {:state :drawing\n :actions (list (fn () (start-shape! ev)))})}}\n :drawing {:on {:pointer-move (fn (ev)\n {:state :drawing\n :actions (list (fn () (update-shape! ev)))})\n :pointer-up (fn (ev)\n {:state :idle\n :actions (list (fn () (finish-shape! ev)))})}}}}))\n\n;; Send events\n(machine-send! drawing-tool :pointer-down event)\n\n;; Read state (reactive — triggers re-render)\n(deref (machine-state drawing-tool)) ;; => :drawing\n(machine-matches? drawing-tool :idle) ;; => false" "lisp")) @@ -193,7 +193,7 @@ (~docs/section :title "Implementation Sketch" :id "machine-impl" - (~docs/code :code (highlight + (~docs/code :src (highlight "(define make-machine (fn (config)\n (let ((current (signal (get config :initial)))\n (states (get config :states)))\n (dict\n \"__machine\" true\n \"state\" current\n \"states\" states))))\n\n(define machine-state (fn (m) (get m \"state\")))\n\n(define machine-matches? (fn (m s)\n (= (deref (get m \"state\")) s)))\n\n(define machine-send! (fn (m event &rest data)\n (let ((current-state (deref (get m \"state\")))\n (state-config (get (get m \"states\") current-state))\n (handlers (get state-config :on))\n (handler (get handlers event)))\n (when handler\n (let ((result (apply handler data)))\n (when (get result :state)\n (reset! (get m \"state\") (get result :state)))\n (when (get result :actions)\n (for-each (fn (action) (action))\n (get result :actions))))))))" "lisp")) @@ -213,7 +213,7 @@ (~docs/section :title "API" :id "commands-api" - (~docs/code :code (highlight + (~docs/code :src (highlight "(define canvas-state (make-command-store\n {:initial {:elements (list) :selection nil}\n :commands {:add-element (fn (state el)\n (assoc state :elements\n (append (get state :elements) (list el))))\n :move-element (fn (state id dx dy) ...)}\n :max-history 100}))\n\n;; Dispatch commands\n(cmd-dispatch! canvas-state :add-element rect-1)\n\n;; Undo / redo\n(cmd-undo! canvas-state)\n(cmd-redo! canvas-state)\n\n;; Reactive predicates\n(deref (cmd-can-undo? canvas-state)) ;; => true\n(deref (cmd-can-redo? canvas-state)) ;; => false\n\n;; Transaction grouping — collapses into single undo entry\n(cmd-group-start! canvas-state \"drag\")\n(cmd-dispatch! canvas-state :move-element id 1 0)\n(cmd-dispatch! canvas-state :move-element id 1 0)\n(cmd-dispatch! canvas-state :move-element id 1 0)\n(cmd-group-end! canvas-state)\n;; One undo reverses all three moves" "lisp"))) @@ -247,13 +247,13 @@ (~docs/section :title "API" :id "loop-api" - (~docs/code :code (highlight + (~docs/code :src (highlight "(define render-loop (make-loop (fn (timestamp dt)\n (let ((ctx (ref-deref ctx-ref)))\n (clear-canvas! ctx)\n (draw-scene! ctx (deref elements))))))\n\n(loop-start! render-loop)\n(loop-stop! render-loop)\n(deref (loop-running? render-loop)) ;; => true (reactive)" "lisp"))) (~docs/section :title "Running-Ref Pattern" :id "loop-pattern" - (~docs/code :code (highlight + (~docs/code :src (highlight ";; Internal: no cancelAnimationFrame needed\n(let ((running (ref true))\n (last-ts (ref 0)))\n (define tick (fn (ts)\n (when (ref-deref running)\n (let ((dt (- ts (ref-deref last-ts))))\n (ref-set! last-ts ts)\n (user-fn ts dt))\n (request-animation-frame tick))))\n (request-animation-frame tick)\n ;; To stop: (ref-set! running false)\n ;; Loop dies naturally on next frame check" "lisp")) @@ -278,7 +278,7 @@ (~docs/section :title "API" :id "keyed-api" - (~docs/code :code (highlight + (~docs/code :src (highlight ";; Current: keys extracted from rendered DOM key attribute\n(map (fn (el) (~shape-handle :key (get el :id) el)) (deref items))\n\n;; Enhanced: explicit key function\n(map (fn (el) (~shape-handle el)) (deref items)\n :key (fn (el) (get el :id)))" "lisp")) @@ -311,13 +311,13 @@ (~docs/section :title "API" :id "app-api" - (~docs/code :code (highlight + (~docs/code :src (highlight "(define my-app (make-app\n {:entry ~drawing-app\n :stores (list canvas-state tool-state)\n :routes {\"/\" ~drawing-app\n \"/gallery\" ~gallery-view}\n :head (list (link :rel \"stylesheet\" :href \"/static/app.css\"))}))\n\n;; Server: generate minimal HTML shell\n(app-shell-html my-app)\n;; => ...

...
...\n\n;; Client: boot the app into the root element\n(app-boot my-app (dom-query \"#sx-app-root\"))\n\n;; Client: navigate between routes\n(app-navigate! my-app \"/gallery\")" "lisp"))) (~docs/section :title "What the Server Returns" :id "app-shell" - (~docs/code :code (highlight + (~docs/code :src (highlight "\n\n\n \n \n\n\n
\n \n\n" "html")) diff --git a/sx/sx/scopes.sx b/sx/sx/scopes.sx index 616276b7..e10728b0 100644 --- a/sx/sx/scopes.sx +++ b/sx/sx/scopes.sx @@ -73,7 +73,7 @@ "then pops it. The scope has three properties: a name, a downward value, and an " "upward accumulator.") - (~docs/code :code (highlight "(scope name body...) ;; scope with no value\n(scope name :value v body...) ;; scope with downward value" "lisp")) + (~docs/code :src (highlight "(scope name body...) ;; scope with no value\n(scope name :value v body...) ;; scope with downward value" "lisp")) (p "Within the body, " (code "context") " reads the value, " (code "emit!") " appends " "to the accumulator, and " (code "emitted") " reads what was accumulated.") @@ -101,7 +101,7 @@ (p (code "(provide name value body...)") " is exactly " (code "(scope name :value value body...)") ". It exists because " "the two-arg form is the common case.") - (~docs/code :code (highlight ";; These are equivalent:\n(provide \"theme\" {:primary \"violet\"}\n (h1 \"hello\"))\n\n(scope \"theme\" :value {:primary \"violet\"}\n (h1 \"hello\"))" "lisp"))) + (~docs/code :src (highlight ";; These are equivalent:\n(provide \"theme\" {:primary \"violet\"}\n (h1 \"hello\"))\n\n(scope \"theme\" :value {:primary \"violet\"}\n (h1 \"hello\"))" "lisp"))) (~docs/subsection :title "collect! — lazy root scope with dedup" (p (code "collect!") " is the most interesting sugar. When called, if no scope exists " @@ -145,7 +145,7 @@ (p "Each platform (Python, JavaScript) maintains a single data structure:") - (~docs/code :code (highlight "_scope_stacks = {} ;; {name: [{value, emitted: [], dedup: bool}]}" "python")) + (~docs/code :src (highlight "_scope_stacks = {} ;; {name: [{value, emitted: [], dedup: bool}]}" "python")) (p "Six operations on this structure:") @@ -171,7 +171,7 @@ (p "Before scopes, the platform had two separate mechanisms:") - (~docs/code :code (highlight ";; Before: two mechanisms\n_provide_stacks = {} ;; {name: [{value, emitted: []}]}\n_collect_buckets = {} ;; {name: [values...]}\n\n;; After: one mechanism\n_scope_stacks = {} ;; {name: [{value, emitted: [], dedup: bool}]}" "python")) + (~docs/code :src (highlight ";; Before: two mechanisms\n_provide_stacks = {} ;; {name: [{value, emitted: []}]}\n_collect_buckets = {} ;; {name: [values...]}\n\n;; After: one mechanism\n_scope_stacks = {} ;; {name: [{value, emitted: [], dedup: bool}]}" "python")) (p "The unification is not just code cleanup. It means:") (ul :class "space-y-1" diff --git a/sx/sx/spreads.sx b/sx/sx/spreads.sx index da83cb25..dca1f99d 100644 --- a/sx/sx/spreads.sx +++ b/sx/sx/spreads.sx @@ -129,7 +129,7 @@ (p "Spreads use " (code "scope") "/" (code "emit!") " (scoped, no dedup). " (code "collect!") "/" (code "collected") " is also backed by scopes — " "a lazy root scope with automatic deduplication. Used for CSS rule accumulation.") - (~docs/code :code (highlight ";; Deep inside a component tree:\n(collect! \"cssx\" \".sx-bg-red-500{background:red}\")\n\n;; At the flush point (once, in the layout):\n(let ((rules (collected \"cssx\")))\n (clear-collected! \"cssx\")\n (raw! (str \"\")))" "lisp")) + (~docs/code :src (highlight ";; Deep inside a component tree:\n(collect! \"cssx\" \".sx-bg-red-500{background:red}\")\n\n;; At the flush point (once, in the layout):\n(let ((rules (collected \"cssx\")))\n (clear-collected! \"cssx\")\n (raw! (str \"\")))" "lisp")) (p "Both are upward communication through the render tree, but with different " "semantics — " (code "emit!") " is scoped to the nearest provider, " (code "collect!") " is global and deduplicates.")) diff --git a/sx/sx/sx-tools.sx b/sx/sx/sx-tools.sx index ffb030ea..46af23b7 100644 --- a/sx/sx/sx-tools.sx +++ b/sx/sx/sx-tools.sx @@ -18,14 +18,14 @@ ;; ----------------------------------------------------------------- (~docs/section :title "Why raw text fails" :id "why-text-fails" (p "Claude Code reads " (code ".sx") " files as raw text and mentally reconstructs the tree structure by tracking bracket nesting. It does this imperfectly — especially in deep or wide trees where closing parentheses pile up and their correspondence to openers is lost. Consider the end of a complex island:") - (~docs/code :code "(set-cookie \"sx-home-stepper\" (freeze-to-sx \"home-stepper\"))))))))") + (~docs/code :src "(set-cookie \"sx-home-stepper\" (freeze-to-sx \"home-stepper\"))))))))") (p "Eight closing parentheses. Which one closes " (code "set-cookie") "? Which closes the " (code "fn") "? Which closes the binding pair? Which closes the letrec bindings list? Answering this requires counting backward through hundreds of lines. Counting is not what language models do well.") (p "The same problem compounds when writing. Claude generates plausible-looking s-expression fragments that are structurally wrong — a paren added, a paren dropped, a level of nesting off. The " (code "str_replace") " tool makes this worse: replacing a string inside a deeply nested form can silently unbalance the surrounding structure in ways that are not visible until the file fails to parse — or worse, parses into a different tree.")) ;; ----------------------------------------------------------------- (~docs/section :title "The fix: structural tools" :id "structural-tools" (p "If Claude sees trees when the underlying thing is a tree, both the reading and writing problems disappear. Instead of raw text, Claude gets an annotated tree view with explicit paths:") - (~docs/code :code + (~docs/code :src (str "[0] (defisland ~home/stepper\n" " [0,0] ~home/stepper\n" " [0,1] ()\n" @@ -41,7 +41,7 @@ " [0,2,2,5] (let ((_eff ...)) (div ...)))))")) (p "The structural correspondence that is invisible in raw text is explicit here. Every node has a path. If " (code "rebuild-preview") " appears at " (code "[0,2,2,2]") " instead of " (code "[0,2,2,1,12]") ", it is immediately obvious that it is a body expression, not a letrec binding. The bug that took an hour to find would be visible on inspection.") (p "For editing, Claude specifies tree operations rather than text replacements:") - (~docs/code :code + (~docs/code :src (str ";; Replace a node by path — the fragment is parsed before\n" ";; the file is touched. Bracket errors are impossible.\n" "(replace-node \"home-stepper.sx\" [0,2,2,1,12]\n" @@ -58,7 +58,7 @@ ;; ----------------------------------------------------------------- (~docs/section :title "Architecture" :id "architecture" (p "SX Tools is an SX application. The comprehension and editing logic is written in SX, runs on the OCaml evaluator, and is exposed through two interfaces: an MCP server for Claude Code, and an interactive web application for the developer.") - (~docs/code :code + (~docs/code :src (str " ┌─────────────────┐\n" " Claude Code ──▶ │ MCP Server │\n" " │ (OCaml stdio) │\n" @@ -85,7 +85,7 @@ (h4 :class "font-semibold text-stone-700 mt-6 mb-2" "Annotated tree view") (p "The primary comprehension tool. Every node gets its path label inline, making tree structure explicit. The structural correspondence that is invisible in raw text is readable by inspection, with no need to count brackets:") - (~docs/code :code + (~docs/code :src (str "[0] (defcomp ~card\n" " [0,1] (&key title subtitle &rest children)\n" " [0,2] (div :class \"card\"\n" @@ -97,7 +97,7 @@ (h4 :class "font-semibold text-stone-700 mt-6 mb-2" "Structural summary") (p "A folded view for large files, showing shape without detail. Claude orients itself in a 300-line island, identifies the region it needs, then calls " (code "read-subtree") " on just that part:") - (~docs/code :code + (~docs/code :src (str "(defisland ~home/stepper [0]\n" " (let [0,2]\n" " ((source ...) (code-tokens ...)) [0,2,1]\n" @@ -110,7 +110,7 @@ (h4 :class "font-semibold text-stone-700 mt-6 mb-2" "Context view") (p "Given a deep path, shows the enclosing chain back to the root. Essential for working deep in a tree without loading the entire file:") - (~docs/code :code + (~docs/code :src (str "Context for [0,2,2,1,12]:\n" " [0] defisland ~home/stepper\n" " [0,2] let ((source ...) ... (code-tokens ...))\n" @@ -120,7 +120,7 @@ (h4 :class "font-semibold text-stone-700 mt-6 mb-2" "Bracket-paired view") (p "The raw source annotated with matched pair labels, for cases where Claude needs to see the actual syntax and verify bracket correspondence:") - (~docs/code :code + (~docs/code :src (str "(₁defcomp ~card (₂&key title subtitle &rest children)₂\n" " (₃div :class \"card\"\n" " (₄h2 title)₄\n" @@ -149,7 +149,7 @@ ;; ----------------------------------------------------------------- (~docs/section :title "The protocol" :id "protocol" (p "The tools only work if they are actually used. The MCP server must be accompanied by a protocol — enforced via " (code "CLAUDE.md") " — that prevents fallback to raw text editing.") - (~docs/code :code + (~docs/code :src (str ";; Before doing anything in an .sx file:\n" ";; 1. summarise → structural overview of the whole file\n" ";; 2. read-subtree → expand the region you intend to work in\n" diff --git a/sx/sx/sx-urls.sx b/sx/sx/sx-urls.sx index 15df561c..4cc49856 100644 --- a/sx/sx/sx-urls.sx +++ b/sx/sx/sx-urls.sx @@ -16,7 +16,7 @@ "a flat sequence of slash-separated path segments, the URL encodes " "a nested function call that the server evaluates to produce a page.") (p "Every page on this site is addressed by an SX URL. You are currently reading:") - (~docs/code :code (highlight + (~docs/code :src (highlight "/sx/(applications.(sx-urls))" "lisp")) (p "This is a function call: " (code "applications") " is called with the result of " @@ -34,7 +34,7 @@ (p "The rule: " (strong "dot = space, nothing more") ". " "Before parsing, the server replaces every dot with a space. " "Parens carry all structural meaning.") - (~docs/code :code (highlight + (~docs/code :src (highlight ";; What you type in the browser:\n/(language.(doc.introduction))\n\n;; After dot→space transform:\n(language (doc introduction))\n\n;; This is standard SX. Parens are nesting. Atoms are arguments.\n;; 'introduction' is a string slug, 'doc' is a function, 'language' is a function." "lisp")) @@ -58,12 +58,12 @@ (p "REST URLs have an inherent ambiguity: " "does a filter apply to the last segment, or the whole path? " "S-expression nesting makes scope explicit.") - (~docs/code :code (highlight + (~docs/code :src (highlight ";; REST — ambiguous:\n/users/123/posts?filter=published\n;; Does 'filter' apply to posts? To the user? To the whole query?\n;; The answer depends on API documentation.\n\n;; SX URLs — explicit scoping:\n/(users.(posts.123.(filter.published))) ;; filter scoped to posts\n/(users.posts.123.(filter.published)) ;; filter scoped to the whole expression\n\n;; These are structurally different. The paren boundaries ARE scope boundaries.\n;; No documentation needed — the syntax tells you." "lisp")) (p "This extends to every level of nesting on this site:") - (~docs/code :code (highlight + (~docs/code :src (highlight ";; These all have different scoping:\n/(language.(spec.signals)) ;; 'signals' scoped to spec\n/(language.(spec.(explore.signals))) ;; 'signals' scoped to explore\n/(language.(spec.(explore.signals.:section.\"batch\"))) ;; keyword scoped to explore call" "lisp")) (p "What took REST thirty years of convention documents to approximate, " @@ -103,7 +103,7 @@ (~docs/subsection :title "Leaf pages" (p "Leaf pages are the innermost function calls. " "The slug becomes a string argument to the page function:") - (~docs/code :code (highlight + (~docs/code :src (highlight ";; doc(\"introduction\") — the slug auto-quotes to a string\n/(language.(doc.introduction))\n\n;; spec(\"core\") — same pattern, different function\n/(language.(spec.core))\n\n;; explore(\"signals\") — nested deeper\n/(language.(spec.(explore.signals)))\n\n;; example(\"progress-bar\") — three levels of nesting\n/(geography.(hypermedia.(example.progress-bar)))" "lisp")) @@ -153,7 +153,7 @@ (~docs/section :title "Direct Component URLs" :id "direct" (p "Every " (code "defcomp") " in the component environment is directly " "addressable by its " (code "~name") " — no page function, no routing wiring, no case statement.") - (~docs/code :code (highlight + (~docs/code :src (highlight ";; Any component is instantly a URL:\n/(~essays/sx-sucks/essay-sx-sucks) ;; the essay\n/(~plans/sx-urls/plan-sx-urls-content) ;; the SX URLs plan\n/(~docs-content/docs-evaluator-content) ;; evaluator docs\n/(~analyzer/bundle-analyzer-content) ;; bundle analyzer tool" "lisp")) (p "Try it:") @@ -176,17 +176,17 @@ (~docs/subsection :title "Section functions pass through" (p (code "language") ", " (code "geography") ", " (code "applications") ", " (code "etc") " are identity on their argument. They exist to provide structure:") - (~docs/code :code (highlight + (~docs/code :src (highlight ";; Section function definition:\n(define language\n (fn (content)\n (if (nil? content) nil content)))\n\n;; /(language.(doc.introduction))\n;; Eval steps:\n;; 1. (doc \"introduction\") → page content AST\n;; 2. (language ) → passes content through\n;; 3. Wrap in (~layouts/doc :path \"...\" )" "lisp")) (p "Section functions with no argument return their index page:") - (~docs/code :code (highlight + (~docs/code :src (highlight "(define home\n (fn (content)\n (if (nil? content) '(~docs-content/home-content) content)))\n\n;; /(home) → (~docs-content/home-content)\n;; / → same thing" "lisp"))) (~docs/subsection :title "Page functions dispatch on slug" (p "Page functions take a slug string and return a quoted component expression:") - (~docs/code :code (highlight + (~docs/code :src (highlight "(define doc\n (fn (slug)\n (if (nil? slug)\n '(~docs-content/docs-introduction-content)\n (case slug\n \"introduction\" '(~docs-content/docs-introduction-content)\n \"getting-started\" '(~docs-content/docs-getting-started-content)\n \"components\" '(~docs-content/docs-components-content)\n \"evaluator\" '(~docs-content/docs-evaluator-content)\n \"primitives\"\n (let ((data (primitives-data)))\n `(~docs-content/docs-primitives-content\n :prims (~docs/primitives-tables :primitives ,data)))\n :else '(~docs-content/docs-introduction-content)))))" "lisp")) (p "The " (code "'") " (quote) is critical: page functions return " @@ -197,7 +197,7 @@ (~docs/subsection :title "Data-dependent pages" (p "Some pages need server data. The page function calls an IO helper, " "then splices data into the component call with quasiquote:") - (~docs/code :code (highlight + (~docs/code :src (highlight "(define isomorphism\n (fn (slug)\n (case slug\n \"bundle-analyzer\"\n (let ((data (bundle-analyzer-data))) ;; IO: reads component env\n `(~analyzer/bundle-analyzer-content\n :pages ,(get data \"pages\")\n :total-components ,(get data \"total-components\")\n :pure-count ,(get data \"pure-count\")\n :io-count ,(get data \"io-count\")))\n :else '(~plans/isomorphic/plan-isomorphic-content))))" "lisp")) (p "Visit " @@ -213,7 +213,7 @@ "The handler does four things:") (~docs/subsection :title "The handler" - (~docs/code :code (highlight + (~docs/code :src (highlight ";; Python handler (sx_router.py) — simplified:\n\nasync def eval_sx_url(raw_path):\n # 1. Build env: all components + all page functions\n env = get_component_env() | get_page_helpers(\"sx\")\n\n # 2. Spec function: parse URL, auto-quote unknowns\n # This is bootstrapped from router.sx → sx_ref.py\n expr = prepare_url_expr(path, env)\n\n # 3. Evaluate: page functions resolve, data fetched\n page_ast = await async_eval(expr, env, ctx)\n\n # 4. Wrap in layout, render to HTML\n wrapped = [Symbol(\"~layouts/doc\"), Keyword(\"path\"), path, page_ast]\n content_sx = await _eval_slot(wrapped, env, ctx)\n return full_page_sx(content_sx)" "python")) (p "The handler imports " (code "prepare_url_expr") " from the bootstrapped " @@ -225,7 +225,7 @@ ", all three atoms are symbols. But " (code "introduction") " isn't a function — it's a slug. The " (code "auto-quote-unknowns") " function " "walks the AST and replaces unknown symbols with their string name:") - (~docs/code :code (highlight + (~docs/code :src (highlight ";; From router.sx — the auto-quoting spec:\n(define auto-quote-unknowns :effects []\n (fn (expr env)\n (if (not (list? expr)) expr\n (if (empty? expr) expr\n ;; Head stays as symbol (function position)\n (cons (first expr)\n (map (fn (child)\n (cond\n (list? child)\n (auto-quote-unknowns child env)\n (= (type-of child) \"symbol\")\n (let ((name (symbol-name child)))\n (if (or (env-has? env name)\n (starts-with? name \":\")\n (starts-with? name \"~\")\n (starts-with? name \"!\"))\n child ;; known → keep as symbol\n name)) ;; unknown → string\n :else child))\n (rest expr)))))))" "lisp")) (p "This is checked against the " (em "actual environment") " at request time — " @@ -236,7 +236,7 @@ (p "API endpoints are defined with " (code "defhandler") " and use the same " "nested SX URL structure. These are live endpoints on this site — " "they return SX wire format that the client renders:") - (~docs/code :code (highlight + (~docs/code :src (highlight ";; From sx/sx/handlers/ref-api.sx — live API endpoint:\n(defhandler ref-time :method \"GET\"\n :path \"/sx/(geography.(hypermedia.(reference.(api.time))))\"\n (span :id \"time\" (str \"Server time: \" (format-time (now)))))\n\n;; The endpoint is an SX expression that returns SX.\n;; The URL IS the address. The handler IS the content." "lisp")) (p "Try these live endpoints — each returns SX wire format:") @@ -259,7 +259,7 @@ (p "More handler examples from the " (a :href "/sx/(geography.(hypermedia.(reference.attributes)))" :class "text-violet-600 hover:underline" "attributes reference") ":") - (~docs/code :code (highlight + (~docs/code :src (highlight ";; POST handler — receives form data, returns SX:\n(defhandler ref-greet :method \"POST\"\n :path \"/sx/(geography.(hypermedia.(reference.(api.greet))))\"\n (let ((name (request-form \"name\")))\n (div :id \"result\" :class \"p-4 border rounded\"\n (p (str \"Hello, \" (if (empty? name) \"world\" name) \"!\")))))\n\n;; DELETE handler — receives path parameter:\n(defhandler ref-delete-item :method \"DELETE\"\n :path \"/sx/(geography.(hypermedia.(reference.(api.(item.)))))\"\n \"\")\n\n;; GET handler with query params:\n(defhandler ref-trigger-search :method \"GET\"\n :path \"/sx/(geography.(hypermedia.(reference.(api.trigger-search))))\"\n (let ((q (request-arg \"q\")))\n (div :id \"search-results\"\n (p (str \"Results for: \" q)))))" "lisp")))) @@ -270,7 +270,7 @@ "without a server round-trip.") (~docs/subsection :title "The client route check" - (~docs/code :code (highlight + (~docs/code :src (highlight ";; From orchestration.sx — client-side route decision:\n;;\n;; 1. Match the URL against the page registry\n;; 2. Check if target layout matches current layout\n;; 3. Check if all component dependencies are loaded\n;; 4. If pure (no IO): render client-side, no server request\n;; 5. If data-dependent: fetch data, then render\n;; 6. If layout changes: fall through to server (needs OOB header)\n\n(define try-client-route :effects [mutation io]\n (fn (pathname target-sel)\n (let ((match (find-matching-route pathname _page-routes)))\n (if (nil? match)\n false ;; no match → server handles it\n (if (not (= (get match \"layout\") (current-page-layout)))\n false ;; layout change → server (needs OOB update)\n ;; ... render client-side\n )))))" "lisp")) (p "Pure pages (no " (code "has-data") " flag) render instantly in the browser. " @@ -281,7 +281,7 @@ (p "The " (code "prepare-url-expr") " function from " (code "router.sx") " is bootstrapped " "to JavaScript as " (code "Sx.prepareUrlExpr") ". The browser uses it for " "client-side navigation, relative URL resolution, and the address bar REPL:") - (~docs/code :code (highlight + (~docs/code :src (highlight ";; From router.sx — bootstrapped to both Python and JavaScript:\n(define prepare-url-expr :effects []\n (fn (url-path env)\n (let ((expr (url-to-expr url-path)))\n (if (empty? expr)\n expr\n (auto-quote-unknowns expr env)))))\n\n;; url-to-expr: strip /, dots→spaces, parse\n;; auto-quote-unknowns: unknown symbols → strings\n;; Result: ready for standard eval" "lisp")) (p "One spec, two hosts. The Python server and JavaScript client share " @@ -298,7 +298,7 @@ (~docs/subsection :title "One dot: apply at current level" (p "A single dot appends at the current nesting depth. " "It's like calling a function with an extra argument:") - (~docs/code :code (highlight + (~docs/code :src (highlight ";; Current page: /(geography.(hypermedia.(example.progress-bar)))\n;;\n;; .click-to-load\n;; = \"at the current level, apply click-to-load\"\n;; → /(geography.(hypermedia.(example.click-to-load)))\n;;\n;; The inner expression (example.progress-bar) becomes (example.click-to-load).\n;; The outer nesting (geography.(hypermedia.(...))) is preserved." "lisp")) (p "This is a sibling navigation — same parent, different leaf.")) @@ -306,21 +306,21 @@ (~docs/subsection :title "Two dots: pop one level, apply" (p "Two dots remove the innermost nesting level, then optionally apply a new one. " "Like " (code "cd ..") " in a filesystem — but structural, not string-based:") - (~docs/code :code (highlight + (~docs/code :src (highlight ";; Current: /(geography.(hypermedia.(example.progress-bar)))\n;;\n;; .. → /(geography.(hypermedia.(example)))\n;; ..inline-edit → /(geography.(hypermedia.(example.inline-edit)))\n;; ..reference → /(geography.(hypermedia.(reference)))\n;;\n;; Pop the innermost expression, replace with the new slug.\n;; The outer nesting is preserved." "lisp"))) (~docs/subsection :title "Three+ dots: pop multiple levels" (p "Each additional dot pops one more level of nesting. " "N dots = pop N-1 levels:") - (~docs/code :code (highlight + (~docs/code :src (highlight ";; Current: /(geography.(hypermedia.(example.progress-bar)))\n;;\n;; ... → /(geography.(hypermedia)) ;; pop 2 levels\n;; .... → /(geography) ;; pop 3 levels\n;; ..... → / ;; pop 4 levels (root)\n;;\n;; Combine with a slug to navigate across sections:\n;; ...reactive.(examples) → /(geography.(reactive.(examples))) ;; pop 2, into reactive\n;; ....language.(doc.intro)\n;; → /(language.(doc.intro)) ;; pop 3, into language" "lisp"))) (~docs/subsection :title "Why this is functional, not textual" (p "REST relative URLs are string operations — remove path segments, append new ones. " "This breaks when the hierarchy is structural rather than linear.") - (~docs/code :code (highlight + (~docs/code :src (highlight ";; REST relative: ../inline-edit on /geography/hypermedia/examples/progress-bar\n;; → /geography/hypermedia/examples/inline-edit\n;; Works! But only because the hierarchy happens to be flat.\n\n;; What about navigating to a sibling section?\n;; REST: ../../reference/attributes\n;; How many ../ do you need? You have to count path segments.\n;; The answer changes if the base path changes.\n\n;; SX relative: ..reference.attributes\n;; from /(geography.(hypermedia.(example.progress-bar)))\n;; → /(geography.(hypermedia.(reference.attributes)))\n;;\n;; The dots operate on nesting depth, not string segments.\n;; Add a level of nesting? The relative URL still works.\n;; Restructure the tree? Relative links within a subtree are unaffected." "lisp")) (p "Relative SX URLs are " (em "structurally stable") " — " @@ -329,7 +329,7 @@ (~docs/subsection :title "Keyword arguments: functional parameters" (p "URLs can carry keyword arguments that parameterize the innermost expression. " "Keywords use the same " (code ":name") " syntax as SX function calls:") - (~docs/code :code (highlight + (~docs/code :src (highlight ";; Set a keyword on a page:\n/(language.(spec.(explore.signals.:page.3)))\n→ (language (spec (explore signals :page 3)))\n\n;; :page is a keyword argument to the explore function.\n;; It's the same syntax you'd use when calling a component:\n;; (~paginator :current-page 3 :total-pages 10)\n\n;; Delta values — relative keyword modification:\n;; .:page.+1 → increment current :page by 1\n;; .:page.-1 → decrement current :page by 1\n;;\n;; This is pagination as URL algebra:\n;; Current: /(language.(spec.(explore.signals.:page.3)))\n;; .:page.+1 → /(language.(spec.(explore.signals.:page.4)))\n;; No JavaScript, no state management. A URL transform." "lisp"))) @@ -337,7 +337,7 @@ (p "Relative resolution is defined in " (code "router.sx") " as a pure function. " "It parses the current URL's expression structure, pops levels, " "splices the new content, and serializes back:") - (~docs/code :code (highlight + (~docs/code :src (highlight ";; From router.sx — pure function, no IO:\n(define resolve-relative-url :effects []\n (fn (current-url relative-url)\n ;; Parse current URL's expression\n ;; Count dots to determine pop level\n ;; Pop that many nesting levels\n ;; Append new content\n ;; Serialize back to URL format\n ...))\n\n;; Tested with 50+ cases in test-router.sx:\n(resolve-relative-url\n \"/sx/(geography.(hypermedia.(example.progress-bar)))\"\n \"..inline-edit\")\n→ \"/sx/(geography.(hypermedia.(example.inline-edit)))\"\n\n(resolve-relative-url\n \"/sx/(language.(spec.(explore.signals.:page.3)))\"\n \".:page.+1\")\n→ \"/sx/(language.(spec.(explore.signals.:page.4)))\"" "lisp")) (p "This function is bootstrapped to both Python and JavaScript. " @@ -386,7 +386,7 @@ (~docs/subsection :title "How special forms are parsed" (p "The " (code "parse-sx-url") " function in " (code "router.sx") " detects and " "decomposes special form URLs:") - (~docs/code :code (highlight + (~docs/code :src (highlight ";; From router.sx — special form detection:\n(define parse-sx-url :effects []\n (fn (url)\n ;; Returns a typed descriptor:\n ;; {:type \"special-form\" :form \"!source\" :inner \"(~essay)\"}\n ;; {:type \"absolute\" :raw \"/(language.(doc.intro))\"}\n ;; {:type \"relative\" :raw \"..eval\"}\n ;; {:type \"direct-component\" :name \"~essay-sx-sucks\"}\n ...))\n\n;; The ! prefix is detected in the expression head:\n;; /(!source.(~essay)) → head is !source → special form\n;; /(language.(doc.intro)) → head is language → normal\n\n;; Each special form takes the inner expression as its argument:\n;; !source wraps whatever follows in a source viewer\n;; !inspect wraps whatever follows in an analysis view\n;; !diff takes two inner expressions" "lisp")) (p "Special forms compose with the rest of the URL algebra. " @@ -427,26 +427,26 @@ (~docs/subsection :title "Step 1: Parse" (p "Strip the leading " (code "/") ", replace dots with spaces, parse as SX. " "This is the " (code "url-to-expr") " function from " (code "router.sx") ":") - (~docs/code :code (highlight + (~docs/code :src (highlight ";; Input: /sx/(language.(doc.introduction))\n;; Strip prefix: (language.(doc.introduction))\n;; Dots→spaces: (language (doc introduction))\n;; Parse: [Symbol(\"language\"), [Symbol(\"doc\"), Symbol(\"introduction\")]]" "lisp"))) (~docs/subsection :title "Step 2: Auto-quote" (p "Unknown symbols become strings. Known functions stay as symbols. " "This is " (code "auto-quote-unknowns") " — checked against the live env:") - (~docs/code :code (highlight + (~docs/code :src (highlight ";; 'language' is in env (section function) → stays Symbol\n;; 'doc' is in env (page function) → stays Symbol\n;; 'introduction' is NOT in env → becomes \"introduction\"\n;;\n;; Result: [Symbol(\"language\"), [Symbol(\"doc\"), \"introduction\"]]" "lisp"))) (~docs/subsection :title "Step 3: Evaluate" (p "Standard inside-out evaluation — same as any SX expression:") - (~docs/code :code (highlight + (~docs/code :src (highlight ";; 1. Eval \"introduction\" → \"introduction\" (string, self-evaluating)\n;; 2. Eval (doc \"introduction\") → calls doc function\n;; → returns '(~docs-content/docs-introduction-content)\n;; 3. Eval (language ) → calls language function\n;; → passes content through (identity)\n;; 4. Result: [Symbol(\"~docs-content/docs-introduction-content\")]" "lisp"))) (~docs/subsection :title "Step 4: Wrap and render" (p "The router wraps the result in " (code "~layouts/doc") " with the URL as " (code ":path") ":") - (~docs/code :code (highlight + (~docs/code :src (highlight ";; Wrap: (~layouts/doc :path \"/sx/(language.(doc.introduction))\" )\n;; Render: aser expands ~layouts/doc → nav, breadcrumbs, layout shell\n;; aser expands ~docs-content/docs-introduction-content → page HTML\n;; Return: full HTML page (or OOB wire format for HTMX requests)" "lisp"))) @@ -473,14 +473,14 @@ (~docs/subsection :title "sx-get — HTMX-style fetching" (p (code "sx-get") " fetches content from an SX URL and swaps it into the DOM:") - (~docs/code :code (highlight + (~docs/code :src (highlight ";; Fetch and swap a section:\n(div :sx-get \"/sx/(geography.(hypermedia.(example.progress-bar)))\"\n :sx-trigger \"click\"\n :sx-target \"#content\"\n :sx-swap \"innerHTML\"\n \"Load Progress Bar Example\")\n\n;; Paginated content with keyword deltas:\n(button :sx-get \".:page.+1\"\n :sx-trigger \"click\"\n :sx-target \"#results\"\n :sx-swap \"innerHTML\"\n \"Next Page\")" "lisp"))) (~docs/subsection :title "Pagination — URLs as algebra" (p "The relative URL algebra makes pagination trivial. " "No state, no event handlers — just URLs:") - (~docs/code :code (highlight + (~docs/code :src (highlight ";; Paginator component — pure URL navigation:\n(defcomp ~paginator (&key current-page total-pages)\n (nav :class \"flex gap-2\"\n (when (> current-page 1)\n (a :href \".:page.-1\" \"Previous\")) ;; delta: decrement :page\n (span (str \"Page \" current-page \" of \" total-pages))\n (when (< current-page total-pages)\n (a :href \".:page.+1\" \"Next\")))) ;; delta: increment :page\n\n;; On /(language.(spec.(explore.signals.:page.3))):\n;; \"Previous\" → .:page.-1 → :page becomes 2\n;; \"Next\" → .:page.+1 → :page becomes 4\n;;\n;; Each link is a static href. Server renders the right page.\n;; Back button works. Bookmarkable. Shareable. Cacheable." "lisp")))) @@ -548,7 +548,7 @@ (li (strong "No client library") " — " (code "curl") " returns content")) (p "HTTP verbs align naturally with SX URL semantics:") - (~docs/code :code (highlight + (~docs/code :src (highlight ";; GET — pure evaluation, cacheable:\nGET /sx/(language.(doc.introduction))\n\n;; POST — side effects via defhandler:\nPOST /sx/(geography.(hypermedia.(reference.(api.greet))))\n\n;; DELETE — with path parameters:\nDELETE /sx/(geography.(hypermedia.(reference.(api.(item.42)))))\n\n;; PUT — full replacement:\nPUT /sx/(geography.(hypermedia.(example.(api.putpatch))))" "lisp"))) @@ -560,7 +560,7 @@ "special form detection, and auto-quoting as pure functions.") (p "Key functions:") - (~docs/code :code (highlight + (~docs/code :src (highlight ";; URL → expression (dots→spaces, parse):\n(url-to-expr \"(language.(doc.intro))\")\n→ (language (doc intro))\n\n;; Auto-quote unknowns against env:\n(auto-quote-unknowns '(language (doc intro)) env)\n→ (language (doc \"intro\")) ;; intro not in env → string\n\n;; Full pipeline:\n(prepare-url-expr \"(language.(doc.intro))\" env)\n→ (language (doc \"intro\")) ;; ready for eval\n\n;; Classify URL type:\n(parse-sx-url \"/sx/(language.(doc.intro))\")\n→ {:type \"absolute\" :raw \"/sx/(language.(doc.intro))\"}\n\n(parse-sx-url \"/sx/(!source.(~essay))\")\n→ {:type \"special-form\" :form \"!source\" :inner \"(~essay)\"}\n\n(parse-sx-url \"..eval\")\n→ {:type \"relative\" :raw \"..eval\"}\n\n;; Resolve relative URLs:\n(resolve-relative-url\n \"/sx/(geography.(hypermedia.(example.progress-bar)))\"\n \"..inline-edit\")\n→ \"/sx/(geography.(hypermedia.(example.inline-edit)))\"" "lisp")) @@ -574,7 +574,7 @@ ;; ----------------------------------------------------------------- (~docs/section :title "The Lisp Tax" :id "parens" (p "People will object to the parentheses. Consider what they already accept:") - (~docs/code :code (highlight + (~docs/code :src (highlight ";; Developers write this every day:\nhttps://api.site.com/v2/users/123/posts?filter=published&sort=date&order=desc&limit=10\n\n;; And would complain about this?\nhttps://site.com/(users.(posts.123.(filter.published.sort.date.limit.10)))\n\n;; The second is shorter, structured, unambiguous, and composable." "lisp")) diff --git a/sx/sx/testing.sx b/sx/sx/testing.sx index affd008e..05bb688e 100644 --- a/sx/sx/testing.sx +++ b/sx/sx/testing.sx @@ -211,7 +211,7 @@ Per-spec platform functions: (h2 :class "text-2xl font-semibold text-stone-800" "Node.js: run.js") (div :class "rounded-lg border border-stone-200 bg-white p-5 space-y-3" (h3 :class "text-sm font-medium text-stone-500 uppercase tracking-wide" "Usage") - (~docs/code :code + (~docs/code :src (highlight "# Run all specs\nnode shared/sx/tests/run.js\n\n# Run specific specs\nnode shared/sx/tests/run.js eval parser\n\n# Legacy mode (monolithic test.sx)\nnode shared/sx/tests/run.js --legacy" "bash"))) (p :class "text-stone-600 text-sm" "Uses " @@ -227,7 +227,7 @@ Per-spec platform functions: (h2 :class "text-2xl font-semibold text-stone-800" "Python: run.py") (div :class "rounded-lg border border-stone-200 bg-white p-5 space-y-3" (h3 :class "text-sm font-medium text-stone-500 uppercase tracking-wide" "Usage") - (~docs/code :code + (~docs/code :src (highlight "# Run all specs\npython shared/sx/tests/run.py\n\n# Run specific specs\npython shared/sx/tests/run.py eval parser\n\n# Legacy mode (monolithic test.sx)\npython shared/sx/tests/run.py --legacy" "bash"))) (p :class "text-stone-600 text-sm" "Uses the hand-written Python evaluator (" diff --git a/sx/sxc/docs.sx b/sx/sxc/docs.sx index 13bf4920..a9e10057 100644 --- a/sx/sxc/docs.sx +++ b/sx/sxc/docs.sx @@ -14,9 +14,10 @@ (h3 :class "text-xl font-semibold text-stone-700" title) children)) -(defcomp ~docs/code (&key code) +(defcomp ~docs/code (&key src) (div :class "not-prose bg-stone-100 rounded-lg p-5 mx-auto max-w-3xl" - (pre :class "text-sm leading-relaxed whitespace-pre-wrap break-words" (code code)))) + (pre :class "text-sm leading-relaxed whitespace-pre-wrap break-words font-mono" + (code src)))) (defcomp ~docs/note (&key &rest children) (div :class "border-l-4 border-violet-400 bg-violet-50 p-4 text-stone-700 text-sm"