Fix empty code blocks: rename ~docs/code param, fix batched IO dispatch

Two bugs caused code blocks to render empty across the site:

1. ~docs/code component had parameter named `code` which collided with
   the HTML <code> tag name. Renamed to `src` and updated all 57
   callers. Added font-mono class for explicit monospace.

2. Batched IO dispatch in ocaml_bridge.py only skipped one leading
   number (batch ID) but the format has two (epoch + ID):
   (io-request EPOCH ID "name" args...). Changed to skip all leading
   numbers so the string name is correctly found. This fixes highlight
   and other batchable helpers returning empty results.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-25 18:08:40 +00:00
parent 739d04672b
commit 6f96452f70
58 changed files with 440 additions and 437 deletions

View File

@@ -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:]

View File

@@ -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")

View File

@@ -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/<name>\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

View File

@@ -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 <script type=\"text/sx\" data-components> block. The client caches them in localStorage keyed by a content hash. On subsequent page loads, the client sends an SX-Components header listing what it has. The server only sends definitions the client is missing.")
@@ -60,11 +60,11 @@
(~docs/section :title "Special forms" :id "special"
(p :class "text-stone-600"
"Special forms have lazy evaluation — arguments are not evaluated before the form runs:")
(~docs/code :code (highlight ";; Conditionals\n(if condition then-expr else-expr)\n(when condition body...)\n(cond (test1 body1) (test2 body2) (else default))\n\n;; Bindings\n(let ((name value) (name2 value2)) body...)\n(define name value)\n\n;; Functions\n(lambda (x y) (+ x y))\n(fn (x) (* x x))\n\n;; Sequencing\n(do expr1 expr2 expr3)\n(begin expr1 expr2)\n\n;; Threading\n(-> value (fn1 arg) (fn2 arg))" "lisp")))
(~docs/code :src (highlight ";; Conditionals\n(if condition then-expr else-expr)\n(when condition body...)\n(cond (test1 body1) (test2 body2) (else default))\n\n;; Bindings\n(let ((name value) (name2 value2)) body...)\n(define name value)\n\n;; Functions\n(lambda (x y) (+ x y))\n(fn (x) (* x x))\n\n;; Sequencing\n(do expr1 expr2 expr3)\n(begin expr1 expr2)\n\n;; Threading\n(-> value (fn1 arg) (fn2 arg))" "lisp")))
(~docs/section :title "Higher-order forms" :id "higher"
(p :class "text-stone-600"
"These operate on collections with function arguments:")
(~docs/code :code (highlight "(map (fn (x) (* x 2)) (list 1 2 3)) ;; => (2 4 6)\n(filter (fn (x) (> x 2)) (list 1 2 3 4 5)) ;; => (3 4 5)\n(reduce (fn (acc x) (+ acc x)) 0 (list 1 2 3)) ;; => 6\n(some (fn (x) (> x 3)) (list 1 2 3 4)) ;; => true\n(every? (fn (x) (> x 0)) (list 1 2 3)) ;; => true" "lisp")))))
(~docs/code :src (highlight "(map (fn (x) (* x 2)) (list 1 2 3)) ;; => (2 4 6)\n(filter (fn (x) (> x 2)) (list 1 2 3 4 5)) ;; => (3 4 5)\n(reduce (fn (acc x) (+ acc x)) 0 (list 1 2 3)) ;; => 6\n(some (fn (x) (> x 3)) (list 1 2 3 4)) ;; => true\n(every? (fn (x) (> x 0)) (list 1 2 3)) ;; => true" "lisp")))))
(defcomp ~docs-content/docs-primitives-content (&key prims)
(~docs/page :title "Primitives"
@@ -87,7 +87,7 @@
(~docs/section :title "Python API" :id "python"
(p :class "text-stone-600"
"The server-side sx library provides several entry points for rendering:")
(~docs/code :code (highlight "from shared.sx.helpers import sx_page, sx_response, sx_call\nfrom shared.sx.parser import SxExpr\n\n# Build a component call from Python kwargs\nsx_call(\"card\", title=\"Hello\", subtitle=\"World\")\n\n# Return an sx wire-format response\nreturn sx_response(sx_call(\"card\", title=\"Hello\"))\n\n# Return a full HTML page shell with sx boot\nreturn sx_page(ctx, page_sx)" "python")))
(~docs/code :src (highlight "from shared.sx.helpers import sx_page, sx_response, sx_call\nfrom shared.sx.parser import SxExpr\n\n# Build a component call from Python kwargs\nsx_call(\"card\", title=\"Hello\", subtitle=\"World\")\n\n# Return an sx wire-format response\nreturn sx_response(sx_call(\"card\", title=\"Hello\"))\n\n# Return a full HTML page shell with sx boot\nreturn sx_page(ctx, page_sx)" "python")))
(~docs/section :title "sx_call" :id "sx-call"
(p :class "text-stone-600"
"sx_call converts Python kwargs to an s-expression component call. Snake_case becomes kebab-case. SxExpr values are inlined without quoting. None becomes nil. Bools become true/false."))

View File

@@ -159,4 +159,4 @@
(p :class "text-xs text-stone-500"
(span :class "font-semibold" "Tail position: ") tail-position))
(when (not (= example ""))
(~docs/code :code (highlight example "lisp")))))
(~docs/code :src (highlight example "lisp")))))

View File

@@ -50,7 +50,7 @@
"But the crucial move — the one that makes this a genuine Hegelian synthesis rather than a mere juxtaposition — is " (strong "the morph") ". When the server sends new content and the client merges it into the existing DOM, hydrated islands are " (em "preserved") ". The server updates the lake. The islands keep their state. The server's new representation flows around the islands like water around rocks. The client's interiority survives the server's authority.")
(p :class "text-stone-600"
"This is " (em "Aufhebung") " in its precise meaning: cancellation, preservation, and elevation. The thesis (server authority) is " (em "cancelled") " — the server no longer has total control over the page. It is " (em "preserved") " — the server still renders the document, still determines structure, still delivers representations. It is " (em "elevated") " — the server now renders " (em "around") " reactive islands, acknowledging their autonomy. Simultaneously, the antithesis (client autonomy) is cancelled (the client no longer controls the whole page), preserved (islands keep their state), and elevated (client state now coexists with server-driven updates).")
(~docs/code :code (highlight ";; The island: reactive state coexists with server lakes.\n;; Lakes are server-morphable slots — the water within the island.\n\n(defisland ~essays/hegelian-synthesis/header ()\n (let ((families (list \"violet\" \"rose\" \"blue\" \"emerald\"))\n (idx (signal 0))\n (current (computed (fn ()\n (nth families (mod (deref idx) (len families)))))))\n (a :href \"/\" :sx-get \"/\" :sx-target \"#main-panel\"\n ;; Lake: server can update the logo\n (lake :id \"logo\"\n (span :style (cssx ...) \"(<sx>)\"))\n ;; Reactive: signal-bound, NOT in a lake\n (span :style (cssx (:text (colour (deref current) 500)))\n :on-click (fn (e) (swap! idx inc))\n \"reactive\")\n ;; Lake: server can update the copyright\n (lake :id \"copyright\"\n (p \"© 2026\")))))\n\n;; Click: colour changes (client state)\n;; Server sends new page — morph enters the island\n;; Lakes update from server content\n;; Reactive span keeps its colour — state survives" "lisp"))
(~docs/code :src (highlight ";; The island: reactive state coexists with server lakes.\n;; Lakes are server-morphable slots — the water within the island.\n\n(defisland ~essays/hegelian-synthesis/header ()\n (let ((families (list \"violet\" \"rose\" \"blue\" \"emerald\"))\n (idx (signal 0))\n (current (computed (fn ()\n (nth families (mod (deref idx) (len families)))))))\n (a :href \"/\" :sx-get \"/\" :sx-target \"#main-panel\"\n ;; Lake: server can update the logo\n (lake :id \"logo\"\n (span :style (cssx ...) \"(<sx>)\"))\n ;; Reactive: signal-bound, NOT in a lake\n (span :style (cssx (:text (colour (deref current) 500)))\n :on-click (fn (e) (swap! idx inc))\n \"reactive\")\n ;; Lake: server can update the copyright\n (lake :id \"copyright\"\n (p \"© 2026\")))))\n\n;; Click: colour changes (client state)\n;; Server sends new page — morph enters the island\n;; Lakes update from server content\n;; Reactive span keeps its colour — state survives" "lisp"))
(p :class "text-stone-600"
"The " (code "lake") " tag is the key. Inside the island, " (code "(lake :id \"logo\" ...)") " marks a region as server territory — the server can update its content during a morph. The reactive " (code "span") " with its signal-bound style is " (em "not") " in a lake — it is island territory, untouchable by the morph. The morph enters the island, finds the lakes, updates them from the server's new representation, and " (em "flows around") " the reactive nodes like water around rocks.")
(p :class "text-stone-600"

View File

@@ -54,7 +54,7 @@
"The SX specification is written in SX. This sounds like the same trick as HTML — the spec in its own format — but it is categorically different.")
(p :class "text-stone-600"
(code "eval.sx") " defines the SX evaluator as s-expressions. Not as documentation " (em "about") " the evaluator. As the evaluator " (em "itself") ". A bootstrapper reads " (code "eval.sx") " and produces a working evaluator — in JavaScript, in Python, in any target language. " (code "parser.sx") " defines the SX parser as s-expressions. A bootstrapper reads it and produces a working parser. " (code "render.sx") " defines the renderer. " (code "primitives.sx") " defines the primitive operations.")
(~docs/code :code (highlight ";; From eval.sx — the evaluator defining itself:\n\n(define eval-expr\n (fn (expr env mode)\n (cond\n ((number? expr) expr)\n ((string? expr) expr)\n ((boolean? expr) expr)\n ((nil? expr) expr)\n ((symbol? expr) (resolve-symbol expr env))\n ((keyword? expr) (keyword-name expr))\n ((dict? expr) (eval-dict expr env mode))\n ((list? expr)\n (let ((head (first expr)))\n (if (symbol? head)\n (let ((name (symbol-name head)))\n (if (special-form? name)\n (eval-special-form name (rest expr) env mode)\n (eval-call expr env mode)))\n (eval-call expr env mode)))))))" "lisp"))
(~docs/code :src (highlight ";; From eval.sx — the evaluator defining itself:\n\n(define eval-expr\n (fn (expr env mode)\n (cond\n ((number? expr) expr)\n ((string? expr) expr)\n ((boolean? expr) expr)\n ((nil? expr) expr)\n ((symbol? expr) (resolve-symbol expr env))\n ((keyword? expr) (keyword-name expr))\n ((dict? expr) (eval-dict expr env mode))\n ((list? expr)\n (let ((head (first expr)))\n (if (symbol? head)\n (let ((name (symbol-name head)))\n (if (special-form? name)\n (eval-special-form name (rest expr) env mode)\n (eval-call expr env mode)))\n (eval-call expr env mode)))))))" "lisp"))
(p :class "text-stone-600"
"This is not documentation. It is a " (em "program that defines the rules of its own interpretation") ". The evaluator that processes SX expressions is itself an SX expression. The spec does not merely " (em "describe") " how SX works — it " (em "is") " how SX works. Give this file to a bootstrapper, and out comes a functioning evaluator. The specification is executable. The definition " (em "is") " the implementation.")
(p :class "text-stone-600"
@@ -67,7 +67,7 @@
"Every practical advantage of SX over JSON and HTML for hypermedia flows from this single property.")
(p :class "text-stone-600"
(strong "Progressive discovery") " works because controls are not metadata interpreted by convention — they are expressions evaluated by the same evaluator that processes content. The server renders conditional controls using the language itself:")
(~docs/code :code (highlight "(when can-cancel\n (button :sx-post \"/orders/4281/cancel\"\n :sx-confirm \"Cancel this order?\"\n \"Cancel order\"))" "lisp"))
(~docs/code :src (highlight "(when can-cancel\n (button :sx-post \"/orders/4281/cancel\"\n :sx-confirm \"Cancel this order?\"\n \"Cancel order\"))" "lisp"))
(p :class "text-stone-600"
"The " (code "when") " is not a convention layered on data. It is a special form defined in " (code "eval.sx") ". The server evaluates it. The client sees only the controls that survive evaluation. The state machine is authored in the language, not in metadata " (em "about") " the language.")
(p :class "text-stone-600"

View File

@@ -47,7 +47,7 @@
"JSON is data notation. It has objects, arrays, strings, numbers, booleans, and null. It parses in one pass. It validates structurally. It is ubiquitous.")
(p :class "text-stone-600"
"JSON is not homoiconic because it has no concept of evaluation. It is " (em "inert") " data. To make JSON a programming language, you must invent a convention for representing code — and every such convention reinvents s-expressions with worse ergonomics:")
(~docs/code :code (highlight ";; JSON \"code\" (actual example from various JSON-based DSLs)\n{\"if\": [{\">\": [\"$.count\", 0]},\n {\"map\": [\"$.items\", {\"fn\": [\"item\", {\"get\": [\"item\", \"name\"]}]}]},\n {\"literal\": \"No items\"}]}\n\n;; The same thing in s-expressions\n(if (> count 0)\n (map (fn (item) (get item \"name\")) items)\n \"No items\")" "lisp"))
(~docs/code :src (highlight ";; JSON \"code\" (actual example from various JSON-based DSLs)\n{\"if\": [{\">\": [\"$.count\", 0]},\n {\"map\": [\"$.items\", {\"fn\": [\"item\", {\"get\": [\"item\", \"name\"]}]}]},\n {\"literal\": \"No items\"}]}\n\n;; The same thing in s-expressions\n(if (> count 0)\n (map (fn (item) (get item \"name\")) items)\n \"No items\")" "lisp"))
(p :class "text-stone-600"
"The JSON version is an s-expression encoded in JSON's syntax — lists-of-lists with a head element that determines semantics. It has strictly more punctuation (colons, commas, braces, brackets, quotes around keys) and strictly less readability. Every JSON-based DSL that reaches sufficient complexity converges on this pattern and then wishes it had just used s-expressions."))

View File

@@ -60,7 +60,7 @@
(p :class "text-stone-600"
"A " (code "defcomp") " definition is a Form:")
(~docs/code :code
(~docs/code :src
(str "(defcomp ~card (&key title subtitle &rest children)\n"
" (div :class \"rounded-lg shadow-sm p-4\"\n"
" (h2 :class \"font-bold\" title)\n"
@@ -134,7 +134,7 @@
"The SX evaluator does something structurally similar. "
"When the browser receives SX wire format \u2014 ")
(~docs/code :code
(~docs/code :src
"(~card :title \"Plato\" :subtitle \"428 BC\")")
(p :class "text-stone-600"

View File

@@ -44,7 +44,7 @@
(~docs/section :title "What SX does differently" :id "sx-approach"
(p :class "text-stone-600"
"An SX component is a single expression that contains its structure, its style (as keyword-resolved CSS classes), and its behavior (event bindings, conditionals, data flow). Nothing is in a separate file unless it genuinely represents a separate concern.")
(~docs/code :code "(defcomp ~essays/separation-of-concerns/product-card (&key product on-add)
(~docs/code :src "(defcomp ~essays/separation-of-concerns/product-card (&key product on-add)
(div :class \"rounded-lg border border-stone-200 p-4 hover:shadow-md transition-shadow\"
(img :src (get product \"image\") :alt (get product \"name\")
:class \"w-full h-48 object-cover rounded\")

View File

@@ -31,7 +31,7 @@
(~docs/section :title "Boundary types" :id "types"
(p :class "text-stone-600"
"Only these types may cross the host-SX boundary:")
(~docs/code :code (highlight "(define-boundary-types\n (list \"number\" \"string\" \"boolean\" \"nil\" \"keyword\"\n \"list\" \"dict\" \"sx-source\" \"style-value\"))" "lisp"))
(~docs/code :src (highlight "(define-boundary-types\n (list \"number\" \"string\" \"boolean\" \"nil\" \"keyword\"\n \"list\" \"dict\" \"sx-source\" \"style-value\"))" "lisp"))
(p :class "text-stone-600"
"No Python " (code :class "text-violet-700 text-sm" "datetime") " objects. No ORM models. No Quart request objects. If a host function returns a " (code :class "text-violet-700 text-sm" "datetime") ", it must convert to an ISO string before crossing. If it returns a database row, it must convert to a plain dict. The boundary validation checks this recursively — lists and dicts have their elements checked too.")
(p :class "text-stone-600"
@@ -58,8 +58,8 @@
"One enforcement that is not automated but equally important: " (strong "SX source code must not be constructed as Python strings") ". S-expressions belong in " (code :class "text-violet-700 text-sm" ".sx") " files. Python belongs in " (code :class "text-violet-700 text-sm" ".py") " files. If you see a Python f-string that builds " (code :class "text-violet-700 text-sm" "(div :class ...)") ", that is a boundary violation.")
(p :class "text-stone-600"
"The correct pattern: Python returns " (strong "data") " (dicts, lists, strings). " (code :class "text-violet-700 text-sm" ".sx") " files receive data via keyword args and compose the markup. The only exception is " (code :class "text-violet-700 text-sm" "SxExpr") " wrappers for pre-rendered fragments — and those should be built with " (code :class "text-violet-700 text-sm" "sx_call()") " or " (code :class "text-violet-700 text-sm" "_sx_fragment()") ", never with f-strings.")
(~docs/code :code (highlight ";; CORRECT: .sx file composes markup from data\n(defcomp ~essays/server-architecture/my-page (&key items)\n (div :class \"space-y-4\"\n (map (fn (item)\n (div :class \"border rounded p-3\"\n (h3 (get item \"title\"))\n (p (get item \"desc\"))))\n items)))" "lisp"))
(~docs/code :code (highlight "# CORRECT: Python returns data\ndef _my_page_data():\n return {\"items\": [{\"title\": \"A\", \"desc\": \"B\"}]}\n\n# WRONG: Python builds SX source\ndef _my_page_data():\n return SxExpr(f'(div (h3 \"{title}\"))') # NO" "python")))
(~docs/code :src (highlight ";; CORRECT: .sx file composes markup from data\n(defcomp ~essays/server-architecture/my-page (&key items)\n (div :class \"space-y-4\"\n (map (fn (item)\n (div :class \"border rounded p-3\"\n (h3 (get item \"title\"))\n (p (get item \"desc\"))))\n items)))" "lisp"))
(~docs/code :src (highlight "# CORRECT: Python returns data\ndef _my_page_data():\n return {\"items\": [{\"title\": \"A\", \"desc\": \"B\"}]}\n\n# WRONG: Python builds SX source\ndef _my_page_data():\n return SxExpr(f'(div (h3 \"{title}\"))') # NO" "python")))
(~docs/section :title "Why this matters for multiple languages" :id "languages"
(p :class "text-stone-600"

View File

@@ -18,7 +18,7 @@
"A typical web project requires the AI to context-switch between HTML (angle brackets, void elements, boolean attributes), CSS (selectors, properties, at-rules, a completely different syntax from HTML), JavaScript (statements, expressions, classes, closures, async/await), and whatever templating language glues them together (Jinja delimiters, ERB tags, JSX interpolation). Each is a separate grammar. Each has edge cases. Each interacts with the others in ways that are hard to predict.")
(p :class "text-stone-600"
"In SX, structure, style, logic, and data are all s-expressions:")
(~docs/code :code (highlight ";; Structure\n(div :class \"card\" (h2 title) (p body))\n\n;; Style\n(cssx card-style\n :bg white :rounded-lg :shadow-md :p 6)\n\n;; Logic\n(if (> (length items) 0)\n (map render-item items)\n (p \"No items found.\"))\n\n;; Data\n{:name \"Alice\" :role \"admin\" :active true}\n\n;; Component definition\n(defcomp ~essays/sx-and-ai/user-card (&key user)\n (div :class \"card\"\n (h2 (get user \"name\"))\n (span :class \"badge\" (get user \"role\"))))" "lisp"))
(~docs/code :src (highlight ";; Structure\n(div :class \"card\" (h2 title) (p body))\n\n;; Style\n(cssx card-style\n :bg white :rounded-lg :shadow-md :p 6)\n\n;; Logic\n(if (> (length items) 0)\n (map render-item items)\n (p \"No items found.\"))\n\n;; Data\n{:name \"Alice\" :role \"admin\" :active true}\n\n;; Component definition\n(defcomp ~essays/sx-and-ai/user-card (&key user)\n (div :class \"card\"\n (h2 (get user \"name\"))\n (span :class \"badge\" (get user \"role\"))))" "lisp"))
(p :class "text-stone-600"
"The AI learns one syntax and applies it everywhere. The mental model does not fragment across subsystems. A " (code "div") " and an " (code "if") " and a " (code "defcomp") " are all lists. The model that generates one can generate all three, because they are the same thing."))
@@ -43,7 +43,7 @@
"A React component's interface is spread across prop types (or TypeScript interfaces), JSDoc comments, Storybook stories, and whatever documentation someone wrote. An AI reading a component must synthesize information from multiple sources to understand what it accepts and what it produces.")
(p :class "text-stone-600"
"An SX component declares everything in one expression:")
(~docs/code :code (highlight "(defcomp ~essays/sx-and-ai/product-card (&key title price image &rest children)\n (div :class \"rounded border p-4\"\n (img :src image :alt title)\n (h3 :class \"font-bold\" title)\n (span :class \"text-lg\" (format-price price))\n children))" "lisp"))
(~docs/code :src (highlight "(defcomp ~essays/sx-and-ai/product-card (&key title price image &rest children)\n (div :class \"rounded border p-4\"\n (img :src image :alt title)\n (h3 :class \"font-bold\" title)\n (span :class \"text-lg\" (format-price price))\n children))" "lisp"))
(p :class "text-stone-600"
"The AI reads this and knows: it takes " (code "title") ", " (code "price") ", and " (code "image") " as keyword arguments, and " (code "children") " as rest arguments. It knows the output structure — a " (code "div") " with an image, heading, price, and slot for children. It knows this because the definition " (em "is") " the documentation. There is no separate spec to consult, no type file to find, no ambiguity about which props are required.")
(p :class "text-stone-600"
@@ -54,8 +54,8 @@
"LLMs operate on tokens. Every token costs compute, latency, and money. The information density of a representation — how much semantics per token — directly affects how much an AI can see, generate, and reason about within its context window and output budget.")
(p :class "text-stone-600"
"Compare equivalent UI definitions:")
(~docs/code :code (highlight ";; SX: 42 tokens\n(div :class \"card p-4\"\n (h2 :class \"font-bold\" title)\n (p body)\n (when footer\n (div :class \"mt-4 border-t pt-2\" footer)))" "lisp"))
(~docs/code :code (highlight "// React/JSX: ~75 tokens\n<div className=\"card p-4\">\n <h2 className=\"font-bold\">{title}</h2>\n <p>{body}</p>\n {footer && (\n <div className=\"mt-4 border-t pt-2\">{footer}</div>\n )}\n</div>" "python"))
(~docs/code :src (highlight ";; SX: 42 tokens\n(div :class \"card p-4\"\n (h2 :class \"font-bold\" title)\n (p body)\n (when footer\n (div :class \"mt-4 border-t pt-2\" footer)))" "lisp"))
(~docs/code :src (highlight "// React/JSX: ~75 tokens\n<div className=\"card p-4\">\n <h2 className=\"font-bold\">{title}</h2>\n <p>{body}</p>\n {footer && (\n <div className=\"mt-4 border-t pt-2\">{footer}</div>\n )}\n</div>" "python"))
(p :class "text-stone-600"
"The SX version is roughly 40% fewer tokens for equivalent semantics. No closing tags. No curly-brace interpolation. No " (code "className") " vs " (code "class") " distinction. Every token carries meaning. Over an entire application — dozens of components, hundreds of expressions — this compounds into significantly more code visible per context window and significantly less output the model must generate."))
@@ -64,7 +64,7 @@
"The hardest thing for AI to get right in conventional frameworks is composition — how pieces fit together. React has rules about hooks. Vue has template vs script vs style sections. Angular has modules, declarations, and dependency injection. Each framework's composition model is a set of conventions the AI must learn and apply correctly.")
(p :class "text-stone-600"
"S-expressions compose by nesting. A list inside a list is a composition. There are no rules beyond this:")
(~docs/code :code (highlight ";; Compose components by nesting — that's it\n(~page-layout :title \"Dashboard\"\n (~sidebar\n (~nav-menu :items menu-items))\n (~main-content\n (map ~essays/sx-and-ai/user-card users)\n (~pagination :page current-page :total total-pages)))" "lisp"))
(~docs/code :src (highlight ";; Compose components by nesting — that's it\n(~page-layout :title \"Dashboard\"\n (~sidebar\n (~nav-menu :items menu-items))\n (~main-content\n (map ~essays/sx-and-ai/user-card users)\n (~pagination :page current-page :total total-pages)))" "lisp"))
(p :class "text-stone-600"
"No imports to manage. No registration steps. No render props, higher-order components, or composition APIs. The AI generates a nested structure and it works, because nesting is the only composition mechanism. This eliminates an entire class of errors that plague AI-generated code in conventional frameworks — the kind where each piece works in isolation but the assembly is wrong."))

View File

@@ -20,7 +20,7 @@
"SX has a peculiar architecture. At its centre sits a specification — a set of s-expression files that define the language. Not a description of the language. Not documentation " (em "about") " the language. The specification " (em "is") " the language. It is simultaneously a formal definition and executable code. You can read it as a document or run it as a program. It does not describe how to build an SX evaluator; it " (em "is") " an SX evaluator, expressed in the language it defines.")
(p :class "text-stone-600"
"This is the nucleus. Everything else radiates outward from it.")
(~docs/code :code (highlight ";; The spec defines eval-expr\n;; eval-expr evaluates the spec\n;; The spec is an artifact that makes itself\n\n(define eval-expr\n (fn (expr env)\n (cond\n (number? expr) expr\n (string? expr) expr\n (symbol? expr) (env-get env (symbol-name expr))\n (list? expr) (eval-list expr env)\n :else expr)))" "lisp"))
(~docs/code :src (highlight ";; The spec defines eval-expr\n;; eval-expr evaluates the spec\n;; The spec is an artifact that makes itself\n\n(define eval-expr\n (fn (expr env)\n (cond\n (number? expr) expr\n (string? expr) expr\n (symbol? expr) (env-get env (symbol-name expr))\n (list? expr) (eval-list expr env)\n :else expr)))" "lisp"))
(p :class "text-stone-600"
"From this nucleus, concentric rings unfurl:"))

View File

@@ -143,7 +143,7 @@
(li (strong "Subscribe") " — register the captured continuation as a signal subscriber")
(li (strong "Return") " — flow the current signal value through the rest of the expression"))
(p "When the signal changes, the captured continuation is re-invoked with the new value. The " (code "update-fn") " on the ReactiveResetFrame mutates the DOM. No explicit " (code "effect()") " wrapping needed.")
(~docs/code :code (highlight
(~docs/code :src (highlight
";; User writes:\n(div :class (str \"count-\" (deref counter))\n (str \"Value: \" (deref counter)))\n\n;; CEK sees (deref counter) → signal? → reactive-reset on stack?\n;; Yes: capture (str \"count-\" [HOLE]) as continuation\n;; Register as subscriber. Return current value.\n;; When counter changes: re-invoke continuation → update DOM."
"lisp")))))
@@ -567,11 +567,11 @@
(~docs/section :title "Freeze" :id "freeze"
(p "Take a computation mid-flight and freeze it:")
(~docs/code :code (highlight
(~docs/code :src (highlight
"(let ((expr (sx-parse \"(+ 1 (* 2 3))\"))\n (state (make-cek-state (first expr) (make-env) (list))))\n ;; Step 4 times\n (set! state (cek-step (cek-step (cek-step (cek-step state)))))\n ;; Freeze to SX\n (cek-freeze state))"
"lisp"))
(p "The frozen state is pure SX:")
(~docs/code :code (highlight
(~docs/code :src (highlight
"{:phase \"continue\"\n :control nil\n :value 1\n :env {}\n :kont ({:type \"arg\"\n :f (primitive \"+\")\n :evaled ()\n :remaining ((* 2 3))\n :env {}})}"
"lisp"))
(p "Everything is data. The continuation frame says: \u201cI was adding 1 to something, "
@@ -579,7 +579,7 @@
(~docs/section :title "Thaw and resume" :id "thaw"
(p "Parse the frozen SX back. Thaw it. Resume:")
(~docs/code :code (highlight
(~docs/code :src (highlight
"(let ((frozen (sx-parse frozen-text))\n (state (cek-thaw (first frozen))))\n (cek-run state))\n;; => 7"
"lisp"))
(p "Native functions like " (code "+") " serialize as " (code "(primitive \"+\")")
@@ -595,7 +595,7 @@
(~docs/section :title "Content addressing" :id "content-addressing"
(p "Hash the frozen SX " (code "\u2192") " content identifier. "
"Same state always produces the same CID. Store by CID, retrieve by CID, verify by CID.")
(~docs/code :code (highlight
(~docs/code :src (highlight
"(freeze-to-cid \"widget\")\n;; => \"d9eea67b\"\n\n(thaw-from-cid \"d9eea67b\")\n;; Signals restored. Same CID = same state."
"lisp"))
(p "Try it: change the values, click Content-address. Copy the CID. "
@@ -729,7 +729,7 @@
"The address IS the content."))
(~docs/section :title "How it works" :id "how"
(~docs/code :code (highlight
(~docs/code :src (highlight
";; Freeze a scope \u2192 hash \u2192 CID\n(freeze-to-cid \"widget\")\n;; => \"d9eea67b\"\n\n;; The frozen SX is stored by CID\n(content-get \"d9eea67b\")\n;; => {:name \"widget\" :signals {:count 42 :name \"hello\"}}\n\n;; Thaw from CID \u2192 signals restored\n(thaw-from-cid \"d9eea67b\")\n;; Signals reset to frozen values"
"lisp"))
(p "The hash is djb2 for now \u2014 deterministic and fast. "
@@ -783,36 +783,36 @@
(p "The CEK machine is pure data\u2192data. Each step takes a state dict and returns a new one. "
"Type an expression, click Step to advance one CEK transition.")
(~geography/cek/demo-stepper :initial-expr "(let ((x 10)) (+ x (* 2 3)))")
(~docs/code :code (highlight (component-source "~geography/cek/demo-stepper") "lisp")))
(~docs/code :src (highlight (component-source "~geography/cek/demo-stepper") "lisp")))
(~docs/section :title "Render stepper" :id "render-stepper"
(p "Watch a component render itself. The CEK evaluates the expression — "
"when it encounters " (code "(div ...)") ", the render adapter produces HTML in one step. "
"Click Run to see the rendered output appear in the preview.")
(~geography/cek/demo-render-stepper)
(~docs/code :code (highlight (component-source "~geography/cek/demo-render-stepper") "lisp")))
(~docs/code :src (highlight (component-source "~geography/cek/demo-render-stepper") "lisp")))
(~docs/section :title "1. Counter" :id "demo-counter"
(p (code "(deref count)") " in text position creates a reactive text node. " (code "(deref doubled)") " is a computed that updates when count changes.")
(~geography/cek/demo-counter :initial 0)
(~docs/code :code (highlight (component-source "~geography/cek/demo-counter") "lisp")))
(~docs/code :src (highlight (component-source "~geography/cek/demo-counter") "lisp")))
(~docs/section :title "2. Computed chain" :id "demo-chain"
(p "Three levels of computed: base -> doubled -> quadrupled. Change base, all propagate.")
(~geography/cek/demo-chain)
(~docs/code :code (highlight (component-source "~geography/cek/demo-chain") "lisp")))
(~docs/code :src (highlight (component-source "~geography/cek/demo-chain") "lisp")))
(~docs/section :title "3. Reactive attributes" :id "demo-attr"
(p (code "(deref sig)") " in " (code ":class") " position. The CEK evaluates the " (code "str") " expression, and when the signal changes, the continuation re-evaluates and updates the attribute.")
(~geography/cek/demo-reactive-attr)
(~docs/code :code (highlight (component-source "~geography/cek/demo-reactive-attr") "lisp")))
(~docs/code :src (highlight (component-source "~geography/cek/demo-reactive-attr") "lisp")))
(~docs/section :title "4. Effect + cleanup" :id "demo-stopwatch"
(p "Effects still work through CEK. This stopwatch uses " (code "effect") " with cleanup — toggling the signal clears the interval.")
(~geography/cek/demo-stopwatch)
(~docs/code :code (highlight (component-source "~geography/cek/demo-stopwatch") "lisp")))
(~docs/code :src (highlight (component-source "~geography/cek/demo-stopwatch") "lisp")))
(~docs/section :title "5. Batch coalescing" :id "demo-batch"
(p "Two signals updated in " (code "batch") " — one notification cycle. Compare render counts between batch and no-batch.")
(~geography/cek/demo-batch)
(~docs/code :code (highlight (component-source "~geography/cek/demo-batch") "lisp")))))
(~docs/code :src (highlight (component-source "~geography/cek/demo-batch") "lisp")))))

View File

@@ -23,7 +23,7 @@
(~docs/section :title "Recipes as SX" :id "recipes-as-sx"
(p "Art DAG recipes are already s-expression effect chains. Currently executed by L1 Celery workers via Python. The SX version: recipes " (em "are") " SX programs. They evaluate in an environment that has media processing primitives. A recipe doesn't \"call\" a GPU function " (em "- ") "it evaluates in an environment where " (code "gpu-exec") " is a primitive.")
(~docs/code :code (highlight ";; A recipe is an SX program in a media-processing environment\n(define composite-layers\n (fn (base-cid overlay-cid blend)\n (let ((base (resolve-cid base-cid))\n (overlay (resolve-cid overlay-cid)))\n (gpu-exec :op \"composite\"\n :layers (list base overlay)\n :blend blend\n :output :stream))))" "lisp"))
(~docs/code :src (highlight ";; A recipe is an SX program in a media-processing environment\n(define composite-layers\n (fn (base-cid overlay-cid blend)\n (let ((base (resolve-cid base-cid))\n (overlay (resolve-cid overlay-cid)))\n (gpu-exec :op \"composite\"\n :layers (list base overlay)\n :blend blend\n :output :stream))))" "lisp"))
(p "This isn't a DSL embedded in Python. It's SX, the same language that renders pages and defines components. The recipe author uses " (code "let") ", " (code "fn") ", " (code "map") ", " (code "if") " " (em "- ") "the full language. The only difference is what primitives are available. " (code "resolve-cid") " fetches content-addressed data. " (code "gpu-exec") " dispatches GPU operations. These are primitives, not library calls. They exist in the environment the same way " (code "+") " and " (code "str") " exist.")
(p "The recipe is data. It's an s-expression. You can parse it, analyze it, transform it, serialize it, hash it, store it, transmit it. You can inspect a recipe's dependency graph the same way " (code "deps.sx") " inspects component dependencies. You can type-check a recipe the same way " (code "typed-sx") " type-checks components. The tools already exist. They just need a new set of primitives to reason about."))
@@ -33,7 +33,7 @@
(~docs/section :title "Split execution" :id "split-execution"
(p "Some recipe steps run against cached data (fast, local). Others need GPU. Others need live input. The evaluator doesn't dispatch " (em "- ") "the boundary declarations do. " (code "(with-boundary (gpu-compute) ...)") " migrates to a GPU-capable host. " (code "(with-boundary (live-ingest) ...)") " opens WebRTC feeds. The recipe author doesn't manage infrastructure " (em "- ") "they declare capabilities, and execution flows to where they exist.")
(~docs/code :code (highlight "(define live-composite\n (fn (recipe-cid camera-count)\n (let ((recipe (resolve-cid recipe-cid)))\n ;; Phase 1: cached data (local, fast)\n (let ((base-layers (map resolve-cid (get recipe \"layers\"))))\n ;; Phase 2: GPU processing\n (with-boundary (gpu-compute)\n (let ((composed (gpu-exec :op \"composite\"\n :layers base-layers\n :blend (get recipe \"blend\"))))\n ;; Phase 3: live feeds\n (with-boundary (live-ingest)\n (let ((feeds (map (fn (i)\n (open-feed :protocol \"webrtc\"\n :label (str \"camera-\" i)))\n (range 0 camera-count))))\n ;; Phase 4: encode and stream\n (with-boundary (encoding)\n (encode-stream\n :sources (concat (list composed) feeds)\n :codec \"h264\"\n :output :stream))))))))))" "lisp"))
(~docs/code :src (highlight "(define live-composite\n (fn (recipe-cid camera-count)\n (let ((recipe (resolve-cid recipe-cid)))\n ;; Phase 1: cached data (local, fast)\n (let ((base-layers (map resolve-cid (get recipe \"layers\"))))\n ;; Phase 2: GPU processing\n (with-boundary (gpu-compute)\n (let ((composed (gpu-exec :op \"composite\"\n :layers base-layers\n :blend (get recipe \"blend\"))))\n ;; Phase 3: live feeds\n (with-boundary (live-ingest)\n (let ((feeds (map (fn (i)\n (open-feed :protocol \"webrtc\"\n :label (str \"camera-\" i)))\n (range 0 camera-count))))\n ;; Phase 4: encode and stream\n (with-boundary (encoding)\n (encode-stream\n :sources (concat (list composed) feeds)\n :codec \"h264\"\n :output :stream))))))))))" "lisp"))
(p "Four phases. Four capability requirements. The program reads linearly " (em "- ") "resolve cached layers, composite on GPU, open live feeds, encode output. But execution migrates across hosts as needed. The " (code "with-boundary") " blocks are the seams. Everything inside a boundary block runs on a host that provides those capabilities. Everything outside runs wherever the program started.")
(p "This is the same mechanism described in the generative SX plan's environment migration section. " (code "with-boundary") " serializes the environment (" (code "env-snapshot") "), ships the pending expression to a capable host, and execution continues there. The recipe author writes a linear program. The runtime makes it distributed."))
@@ -56,7 +56,7 @@
(~docs/section :title "Feed generation" :id "feed-generation"
(p "The executing program creates new endpoints as a side effect. " (code "(open-feed ...)") " doesn't return data " (em "- ") "it returns a connectable endpoint. WebRTC peers, SSE streams, WebSocket channels. These are generative acts: the endpoint didn't exist before the recipe ran. The program grew its own input surface.")
(p "When the recipe completes or the island disposes, feeds are cleaned up via the disposal mechanism. An " (code "effect") " in island scope that opens a feed returns a cleanup function. When the island unmounts, the cleanup runs, the feed closes, the endpoint disappears. The lifecycle is automatic.")
(~docs/code :code (highlight ";; A feed is a connectable endpoint, not raw data\n(define create-camera-mosaic\n (fn (camera-ids)\n ;; Each open-feed returns a connectable URL, not bytes\n (let ((feeds (map (fn (id)\n (open-feed :protocol \"webrtc\"\n :label (str \"cam-\" id)\n :quality \"720p\"))\n camera-ids)))\n ;; The mosaic recipe composes feeds as inputs\n (gpu-exec :op \"mosaic\"\n :inputs feeds\n :layout \"grid\"\n :output (open-feed :protocol \"sse\"\n :label \"mosaic-output\"\n :format \"mjpeg\")))))" "lisp"))
(~docs/code :src (highlight ";; A feed is a connectable endpoint, not raw data\n(define create-camera-mosaic\n (fn (camera-ids)\n ;; Each open-feed returns a connectable URL, not bytes\n (let ((feeds (map (fn (id)\n (open-feed :protocol \"webrtc\"\n :label (str \"cam-\" id)\n :quality \"720p\"))\n camera-ids)))\n ;; The mosaic recipe composes feeds as inputs\n (gpu-exec :op \"mosaic\"\n :inputs feeds\n :layout \"grid\"\n :output (open-feed :protocol \"sse\"\n :label \"mosaic-output\"\n :format \"mjpeg\")))))" "lisp"))
(p "The output is itself a feed. A client connects to the mosaic output URL and receives composed frames. The feeds are the program's I/O surface " (em "- ") "they exist because the program created them, and they die when the program stops. No static route configuration. No service mesh. The program declares what it needs and creates what it produces."))
;; =====================================================================
@@ -65,7 +65,7 @@
(~docs/section :title "The client boundary" :id "client-boundary"
(p "The browser is just another execution environment with its own primitive set: " (code "render-to-dom") ", " (code "signal") ", " (code "deref") ", " (code "open-feed") " (as WebRTC consumer). A streaming art-dag response arrives as SX wire format. The client evaluates it in island scope " (em "- ") "signals bind to stream frames, " (code "reactive-list") " renders feed thumbnails, " (code "computed") " derives overlay parameters. The server pushes frames; the client renders them reactively. No special video player " (em "- ") "just signals and DOM.")
(~docs/code :code (highlight "(defisland ~plans/art-dag-sx/live-canvas ()\n (let ((frames (signal nil))\n (feed-url (signal nil)))\n ;; Connect to stream when URL arrives\n (effect (fn ()\n (when (deref feed-url)\n (connect-stream (deref feed-url)\n :on-frame (fn (f) (reset! frames f))))))\n (div :class \"relative aspect-video bg-black rounded\"\n (when (deref frames)\n (canvas :width 1920 :height 1080\n :draw (fn (ctx)\n (draw-frame ctx (deref frames)))))\n (when (not (deref frames))\n (p :class \"absolute inset-0 flex items-center justify-center text-white/50\"\n \"Waiting for stream...\")))))" "lisp"))
(~docs/code :src (highlight "(defisland ~plans/art-dag-sx/live-canvas ()\n (let ((frames (signal nil))\n (feed-url (signal nil)))\n ;; Connect to stream when URL arrives\n (effect (fn ()\n (when (deref feed-url)\n (connect-stream (deref feed-url)\n :on-frame (fn (f) (reset! frames f))))))\n (div :class \"relative aspect-video bg-black rounded\"\n (when (deref frames)\n (canvas :width 1920 :height 1080\n :draw (fn (ctx)\n (draw-frame ctx (deref frames)))))\n (when (not (deref frames))\n (p :class \"absolute inset-0 flex items-center justify-center text-white/50\"\n \"Waiting for stream...\")))))" "lisp"))
(p "The island is reactive. When " (code "frames") " updates, only the canvas redraws. When " (code "feed-url") " updates, the effect reconnects. No polling loop. No WebSocket message handler parsing JSON. The stream is a signal source. The DOM is a signal consumer. The reactive graph connects them.")
(p "This is the same island architecture from the reactive islands plan " (em "- ") "signals, effects, computed, disposal. The only difference is the data source. Instead of an HTMX response mutating a signal, a WebRTC stream mutates a signal. The rendering pipeline doesn't know or care where the data comes from. It reacts."))
@@ -76,7 +76,7 @@
(~docs/section :title "L1/L2 integration" :id "l1-l2"
(p "L1 is the compute layer (Celery workers, GPU nodes). L2 is the registry (ActivityPub, recipe discovery). In SX terms: L1 hosts provide " (code "gpu-exec") ", " (code "encode-stream") ", " (code "resolve-cid-local") ". L2 hosts provide " (code "discover-recipe") ", " (code "publish-recipe") ", " (code "federate-activity") ".")
(p "An SX program that needs both crosses boundaries as needed " (em "- ") "fetch recipe metadata from L2, execute it on L1, publish results back to L2.")
(~docs/code :code (highlight ";; A full pipeline crossing L1 and L2 boundaries\n(define render-and-publish\n (fn (recipe-name output-label)\n ;; L2: discover the recipe\n (with-boundary (registry)\n (let ((recipe-cid (discover-recipe :name recipe-name\n :version \"latest\")))\n ;; L1: execute the recipe\n (with-boundary (gpu-compute)\n (let ((result-cid (gpu-exec :recipe (resolve-cid recipe-cid)\n :output :cid)))\n ;; L2: publish the result\n (with-boundary (registry)\n (publish-recipe\n :name output-label\n :input-cid recipe-cid\n :output-cid result-cid\n :activity \"Create\"))))))))" "lisp"))
(~docs/code :src (highlight ";; A full pipeline crossing L1 and L2 boundaries\n(define render-and-publish\n (fn (recipe-name output-label)\n ;; L2: discover the recipe\n (with-boundary (registry)\n (let ((recipe-cid (discover-recipe :name recipe-name\n :version \"latest\")))\n ;; L1: execute the recipe\n (with-boundary (gpu-compute)\n (let ((result-cid (gpu-exec :recipe (resolve-cid recipe-cid)\n :output :cid)))\n ;; L2: publish the result\n (with-boundary (registry)\n (publish-recipe\n :name output-label\n :input-cid recipe-cid\n :output-cid result-cid\n :activity \"Create\"))))))))" "lisp"))
(p "Three boundary crossings. L2 to find the recipe. L1 to execute it. L2 to publish the result. The program reads as a linear sequence of operations. The runtime handles the dispatch " (em "- ") "which host provides " (code "discover-recipe") ", which host provides " (code "gpu-exec") ", which host provides " (code "publish-recipe") ". The program author doesn't configure endpoints or manage connections. They declare capabilities."))
;; =====================================================================

View File

@@ -43,12 +43,12 @@
(h4 :class "font-semibold mt-4 mb-2" "Phase 1 — Async call hook")
(p "The bootstrapped evaluator calls primitives via " (code "apply(fn, args)") ". In the Python host, " (code "apply") " is a platform primitive. Replace it with an async-aware version:")
(~docs/code :code (highlight "(define apply-fn\n (fn (f args)\n ;; Platform provides: if f returns a coroutine, await it\n (apply-maybe-async f args)))" "lisp"))
(~docs/code :src (highlight "(define apply-fn\n (fn (f args)\n ;; Platform provides: if f returns a coroutine, await it\n (apply-maybe-async f args)))" "lisp"))
(p "The bootstrapper emits " (code "apply_maybe_async") " as a Python " (code "async def") " that checks if the result is a coroutine and awaits it if so. Pure functions return immediately. IO primitives return coroutines that get awaited. " (strong "Zero overhead for pure calls") " — just an " (code "isinstance") " check.")
(h4 :class "font-semibold mt-4 mb-2" "Phase 2 — Async trampoline")
(p "The spec's trampoline loop resolves thunks synchronously. The Python bootstrapper emits an " (code "async def trampoline") " variant that can await thunks whose bodies contain IO calls. The trampoline structure is identical — only the " (code "await") " keyword is added.")
(~docs/code :code (highlight "# Bootstrapper emits this for Python async target\nasync def trampoline(val):\n while isinstance(val, Thunk):\n val = await eval_expr(val.expr, val.env)\n return val" "python"))
(~docs/code :src (highlight "# Bootstrapper emits this for Python async target\nasync def trampoline(val):\n while isinstance(val, Thunk):\n val = await eval_expr(val.expr, val.env)\n return val" "python"))
(h4 :class "font-semibold mt-4 mb-2" "Phase 3 — Aser as spec module")
(p "The " (code "_aser") " rendering mode (evaluate control flow, serialize HTML/components as SX source) should be specced as a module in " (code "render.sx") " alongside " (code "render-to-html") " and " (code "render-to-dom") ". It's currently hand-written Python because it predates the spec, but its logic is pure SX: walk the AST, eval certain forms, serialize others.")

View File

@@ -26,7 +26,7 @@
(p "Currently wrapped in explicit " (code "effect") " calls. With deref-as-shift, "
"the continuation captures this automatically:")
(~docs/code :code (highlight
(~docs/code :src (highlight
";; User writes:\n(div :class (str \"count-\" (deref counter))\n (str \"Value: \" (deref counter)))\n\n;; Renderer internally wraps each expression:\n(div :class (reactive-reset update-attr-fn (str \"count-\" (deref counter)))\n (reactive-reset update-text-fn (str \"Value: \" (deref counter))))\n\n;; When (deref counter) hits a signal inside reactive-reset:\n;; 1. Shift: capture continuation (str \"count-\" [HOLE])\n;; 2. Register continuation as signal subscriber\n;; 3. Return current value for initial render\n;; When counter changes:\n;; Re-invoke continuation with new value → update-fn updates DOM"
"lisp")))
@@ -42,17 +42,17 @@
(~docs/subsection :title "1a. platform_js.py — SPEC_MODULES + platform code"
(p "Add to " (code "SPEC_MODULES") " dict:")
(~docs/code :code (highlight
(~docs/code :src (highlight
"\"frames\": (\"frames.sx\", \"frames (CEK continuation frames)\"),\n\"cek\": (\"cek.sx\", \"cek (explicit CEK machine evaluator)\"),\n\n# Add ordering (new constant):\nSPEC_MODULE_ORDER = [\"deps\", \"frames\", \"page-helpers\", \"router\", \"signals\", \"cek\"]"
"python"))
(p "Add " (code "PLATFORM_CEK_JS") " constant (mirrors " (code "PLATFORM_CEK_PY") "):")
(~docs/code :code (highlight
(~docs/code :src (highlight
"// Primitive aliases used by cek.sx\nvar inc = PRIMITIVES[\"inc\"];\nvar dec = PRIMITIVES[\"dec\"];\nvar zip_pairs = PRIMITIVES[\"zip-pairs\"];\n\nfunction makeCekContinuation(captured, restKont) {\n var c = new Continuation(function(v) { return v !== undefined ? v : NIL; });\n c._cek_data = {\"captured\": captured, \"rest-kont\": restKont};\n return c;\n}\nfunction continuationData(c) {\n return (c && c._cek_data) ? c._cek_data : {};\n}"
"javascript"))
(p "Add " (code "CEK_FIXUPS_JS") " — iterative " (code "cek-run") " override:")
(~docs/code :code (highlight
(~docs/code :src (highlight
"cekRun = function(state) {\n while (!cekTerminal_p(state)) { state = cekStep(state); }\n return cekValue(state);\n};"
"javascript")))
@@ -68,12 +68,12 @@
(~docs/subsection :title "1c. js.sx — RENAMES for predicate functions"
(p "Default mangling handles most names. Only add RENAMES where " (code "?")
" suffix needs clean JS names:")
(~docs/code :code (highlight
(~docs/code :src (highlight
"\"cek-terminal?\" \"cekTerminalP\"\n\"kont-empty?\" \"kontEmptyP\"\n\"make-cek-continuation\" \"makeCekContinuation\"\n\"continuation-data\" \"continuationData\""
"lisp")))
(~docs/subsection :title "1d. bootstrap_py.py — RENAMES for CEK predicates"
(~docs/code :code (highlight
(~docs/code :src (highlight
"\"cek-terminal?\": \"cek_terminal_p\",\n\"kont-empty?\": \"kont_empty_p\",\n\"make-cek-continuation\": \"make_cek_continuation\",\n\"continuation-data\": \"continuation_data\","
"python")))
@@ -92,18 +92,18 @@
(p "New frame types in " (code "frames.sx") " that enable deref-as-shift.")
(~docs/subsection :title "2a. New frame constructors"
(~docs/code :code (highlight
(~docs/code :src (highlight
";; ReactiveResetFrame: delimiter for reactive deref-as-shift\n;; Carries an update-fn that gets called with new values on re-render.\n(define make-reactive-reset-frame\n (fn (env update-fn first-render?)\n {:type \"reactive-reset\" :env env :update-fn update-fn\n :first-render first-render?}))\n\n;; DerefFrame: awaiting evaluation of deref's argument\n(define make-deref-frame\n (fn (env)\n {:type \"deref\" :env env}))"
"lisp")))
(~docs/subsection :title "2b. Update kont-capture-to-reset"
(p "Must stop at EITHER " (code "\"reset\"") " OR " (code "\"reactive-reset\"") ":")
(~docs/code :code (highlight
(~docs/code :src (highlight
"(define kont-capture-to-reset\n (fn (kont)\n (define scan\n (fn (k captured)\n (if (empty? k)\n (error \"shift without enclosing reset\")\n (let ((frame (first k)))\n (if (or (= (frame-type frame) \"reset\")\n (= (frame-type frame) \"reactive-reset\"))\n (list captured (rest k))\n (scan (rest k) (append captured (list frame))))))))\n (scan kont (list))))"
"lisp")))
(~docs/subsection :title "2c. Helpers to scan for ReactiveResetFrame"
(~docs/code :code (highlight
(~docs/code :src (highlight
"(define has-reactive-reset-frame?\n (fn (kont)\n (if (empty? kont) false\n (if (= (frame-type (first kont)) \"reactive-reset\") true\n (has-reactive-reset-frame? (rest kont))))))\n\n;; Returns 3 values: (captured, frame, rest)\n(define kont-capture-to-reactive-reset\n (fn (kont)\n (define scan\n (fn (k captured)\n (if (empty? k)\n (error \"reactive deref without enclosing reactive-reset\")\n (let ((frame (first k)))\n (if (= (frame-type frame) \"reactive-reset\")\n (list captured frame (rest k))\n (scan (rest k) (append captured (list frame))))))))\n (scan kont (list))))"
"lisp"))))
@@ -118,30 +118,30 @@
(~docs/subsection :title "3a. Add to special form dispatch in cek.sx"
(p "In the dispatch table (around where " (code "reset") " and " (code "shift") " are):")
(~docs/code :code (highlight
(~docs/code :src (highlight
"(= name \"deref\") (step-sf-deref args env kont)"
"lisp")))
(~docs/subsection :title "3b. step-sf-deref"
(p "Evaluates the argument first (push DerefFrame), then decides whether to shift:")
(~docs/code :code (highlight
(~docs/code :src (highlight
"(define step-sf-deref\n (fn (args env kont)\n (make-cek-state\n (first args) env\n (kont-push (make-deref-frame env) kont))))"
"lisp")))
(~docs/subsection :title "3c. Handle DerefFrame in step-continue"
(p "When the deref argument is evaluated, decide: shift or return.")
(~docs/code :code (highlight
(~docs/code :src (highlight
"(= ft \"deref\")\n (let ((val value)\n (fenv (get frame \"env\")))\n (if (not (signal? val))\n ;; Not a signal: pass through\n (make-cek-value val fenv rest-k)\n ;; Signal: check for ReactiveResetFrame\n (if (has-reactive-reset-frame? rest-k)\n ;; Perform reactive shift\n (reactive-shift-deref val fenv rest-k)\n ;; No reactive-reset: normal deref (scope-based tracking)\n (do\n (let ((ctx (context \"sx-reactive\" nil)))\n (when ctx\n (let ((dep-list (get ctx \"deps\"))\n (notify-fn (get ctx \"notify\")))\n (when (not (contains? dep-list val))\n (append! dep-list val)\n (signal-add-sub! val notify-fn)))))\n (make-cek-value (signal-value val) fenv rest-k)))))"
"lisp")))
(~docs/subsection :title "3d. reactive-shift-deref — the heart"
(~docs/code :code (highlight
(~docs/code :src (highlight
"(define reactive-shift-deref\n (fn (sig env kont)\n (let ((scan-result (kont-capture-to-reactive-reset kont))\n (captured-frames (first scan-result))\n (reset-frame (nth scan-result 1))\n (remaining-kont (nth scan-result 2))\n (update-fn (get reset-frame \"update-fn\")))\n ;; Sub-scope for nested subscriber cleanup on re-invocation\n (let ((sub-disposers (list)))\n (let ((subscriber\n (fn ()\n ;; Dispose previous nested subscribers\n (for-each (fn (d) (invoke d)) sub-disposers)\n (set! sub-disposers (list))\n ;; Re-invoke: push fresh ReactiveResetFrame (first-render=false)\n (let ((new-reset (make-reactive-reset-frame env update-fn false))\n (new-kont (concat captured-frames\n (list new-reset)\n remaining-kont)))\n (with-island-scope\n (fn (d) (append! sub-disposers d))\n (fn ()\n (cek-run\n (make-cek-value (signal-value sig) env new-kont))))))))\n ;; Register subscriber\n (signal-add-sub! sig subscriber)\n ;; Register cleanup with island scope\n (register-in-scope\n (fn ()\n (signal-remove-sub! sig subscriber)\n (for-each (fn (d) (invoke d)) sub-disposers)))\n ;; Return current value for initial render\n (make-cek-value (signal-value sig) env remaining-kont))))))"
"lisp")))
(~docs/subsection :title "3e. Handle ReactiveResetFrame in step-continue"
(p "When expression completes normally (or after re-invocation):")
(~docs/code :code (highlight
(~docs/code :src (highlight
"(= ft \"reactive-reset\")\n (let ((update-fn (get frame \"update-fn\"))\n (first? (get frame \"first-render\")))\n ;; On re-render (not first), call update-fn with new value\n (when (and update-fn (not first?))\n (invoke update-fn value))\n (make-cek-value value env rest-k))"
"lisp"))
(p (strong "Key:") " On first render, update-fn is NOT called — the value flows back to the caller "
@@ -157,24 +157,24 @@
(p "Add CEK reactive path alongside existing effect-based path, controlled by opt-in flag.")
(~docs/subsection :title "4a. Opt-in flag"
(~docs/code :code (highlight
(~docs/code :src (highlight
"(define *use-cek-reactive* false)\n(define enable-cek-reactive! (fn () (set! *use-cek-reactive* true)))"
"lisp")))
(~docs/subsection :title "4b. CEK reactive attribute binding"
(~docs/code :code (highlight
(~docs/code :src (highlight
"(define cek-reactive-attr\n (fn (el attr-name expr env)\n (let ((update-fn (fn (val)\n (cond\n (or (nil? val) (= val false)) (dom-remove-attr el attr-name)\n (= val true) (dom-set-attr el attr-name \"\")\n :else (dom-set-attr el attr-name (str val))))))\n ;; Mark for morph protection\n (let ((existing (or (dom-get-attr el \"data-sx-reactive-attrs\") \"\"))\n (updated (if (empty? existing) attr-name (str existing \",\" attr-name))))\n (dom-set-attr el \"data-sx-reactive-attrs\" updated))\n ;; Initial render via CEK with ReactiveResetFrame\n (let ((initial (cek-run\n (make-cek-state expr env\n (list (make-reactive-reset-frame env update-fn true))))))\n (invoke update-fn initial)))))"
"lisp")))
(~docs/subsection :title "4c. Modify render-dom-element dispatch"
(p "In attribute processing, add conditional:")
(~docs/code :code (highlight
(~docs/code :src (highlight
"(context \"sx-island-scope\" nil)\n (if *use-cek-reactive*\n (cek-reactive-attr el attr-name attr-expr env)\n (reactive-attr el attr-name\n (fn () (trampoline (eval-expr attr-expr env)))))"
"lisp"))
(p "Similarly for text positions and conditional rendering."))
(~docs/subsection :title "4d. CEK reactive text"
(~docs/code :code (highlight
(~docs/code :src (highlight
"(define cek-reactive-text\n (fn (expr env)\n (let ((node (create-text-node \"\"))\n (update-fn (fn (val)\n (dom-set-text-content node (str val)))))\n (let ((initial (cek-run\n (make-cek-state expr env\n (list (make-reactive-reset-frame env update-fn true))))))\n (dom-set-text-content node (str initial))\n node))))"
"lisp")))
@@ -224,7 +224,7 @@
(~docs/section :title "Multi-Deref Handling" :id "multi-deref"
(~docs/code :code (highlight "(str (deref first-name) \" \" (deref last-name))" "lisp"))
(~docs/code :src (highlight "(str (deref first-name) \" \" (deref last-name))" "lisp"))
(ol :class "list-decimal pl-6 mb-6 space-y-3"
(li (strong "Initial render:") " First " (code "deref") " hits signal → shifts, captures "

View File

@@ -83,7 +83,7 @@
(~docs/subsection :title "Implementation"
(p "New spec function in a " (code "canonical.sx") " module:")
(~docs/code :code (highlight "(define canonical-serialize\n (fn (node)\n ;; Produce a canonical s-expression string from an AST node.\n ;; Deterministic: same AST always produces same output.\n ;; Used for CID computation — NOT for human-readable output.\n (case (type-of node)\n \"list\"\n (str \"(\" (join \" \" (map canonical-serialize node)) \")\")\n \"dict\"\n (let ((sorted-keys (sort (keys node))))\n (str \"{\" (join \" \"\n (map (fn (k)\n (str \":\" k \" \" (canonical-serialize (get node k))))\n sorted-keys)) \"}\"))\n \"string\"\n (str '\"' (escape-canonical node) '\"')\n \"number\"\n (canonical-number node)\n \"symbol\"\n (symbol-name node)\n \"keyword\"\n (str \":\" (keyword-name node))\n \"boolean\"\n (if node \"true\" \"false\")\n \"nil\"\n \"nil\")))" "lisp"))
(~docs/code :src (highlight "(define canonical-serialize\n (fn (node)\n ;; Produce a canonical s-expression string from an AST node.\n ;; Deterministic: same AST always produces same output.\n ;; Used for CID computation — NOT for human-readable output.\n (case (type-of node)\n \"list\"\n (str \"(\" (join \" \" (map canonical-serialize node)) \")\")\n \"dict\"\n (let ((sorted-keys (sort (keys node))))\n (str \"{\" (join \" \"\n (map (fn (k)\n (str \":\" k \" \" (canonical-serialize (get node k))))\n sorted-keys)) \"}\"))\n \"string\"\n (str '\"' (escape-canonical node) '\"')\n \"number\"\n (canonical-number node)\n \"symbol\"\n (symbol-name node)\n \"keyword\"\n (str \":\" (keyword-name node))\n \"boolean\"\n (if node \"true\" \"false\")\n \"nil\"\n \"nil\")))" "lisp"))
(p "This function must be bootstrapped to both Python and JS — the server computes CIDs at registration time, the client verifies them on fetch.")
(p "The canonical serializer is distinct from " (code "serialize()") " for display. " (code "serialize(pretty=True)") " remains for human-readable output. " (code "canonical-serialize") " is for hashing only.")))
@@ -103,11 +103,11 @@
(li (strong "Hash function:") " SHA3-256 (already used by artdag for content addressing)")
(li (strong "Codec:") " raw (the content is the canonical SX source bytes, not a DAG-PB wrapper)")
(li (strong "Base encoding:") " base32lower for URL-safe representation (" (code "bafy...") " prefix)"))
(~docs/code :code (highlight ";; CID computation pipeline\n(define component-cid\n (fn (component)\n ;; 1. Reconstruct full defcomp form\n ;; 2. Canonical serialize\n ;; 3. SHA3-256 hash\n ;; 4. Wrap as CIDv1\n (let ((source (canonical-serialize\n (list 'defcomp\n (symbol (str \"~\" (component-name component)))\n (component-params-list component)\n (component-body component)))))\n (cid-v1 :sha3-256 :raw (encode-utf8 source)))))" "lisp")))
(~docs/code :src (highlight ";; CID computation pipeline\n(define component-cid\n (fn (component)\n ;; 1. Reconstruct full defcomp form\n ;; 2. Canonical serialize\n ;; 3. SHA3-256 hash\n ;; 4. Wrap as CIDv1\n (let ((source (canonical-serialize\n (list 'defcomp\n (symbol (str \"~\" (component-name component)))\n (component-params-list component)\n (component-body component)))))\n (cid-v1 :sha3-256 :raw (encode-utf8 source)))))" "lisp")))
(~docs/subsection :title "Where CIDs Live"
(p "Each " (code "Component") " object gains a " (code "cid") " field, computed at registration time:")
(~docs/code :code (highlight ";; types.py extension\n@dataclass\nclass Component:\n name: str\n params: list[str]\n has_children: bool\n body: Any\n closure: dict[str, Any]\n css_classes: set[str]\n deps: set[str] # by name\n io_refs: set[str]\n cid: str | None = None # computed after registration\n dep_cids: dict[str, str] | None = None # name → CID" "python"))
(~docs/code :src (highlight ";; types.py extension\n@dataclass\nclass Component:\n name: str\n params: list[str]\n has_children: bool\n body: Any\n closure: dict[str, Any]\n css_classes: set[str]\n deps: set[str] # by name\n io_refs: set[str]\n cid: str | None = None # computed after registration\n dep_cids: dict[str, str] | None = None # name → CID" "python"))
(p "After " (code "compute_all_deps()") " runs, a new " (code "compute_all_cids()") " pass fills in CIDs for every component. Dependency CIDs are also recorded — when a component references " (code "~card") ", we store both the name and card's CID."))
(~docs/subsection :title "CID Stability"
@@ -130,7 +130,7 @@
(p :class "text-violet-800" "Metadata that travels with a CID — what a component needs, what it provides, whether it's safe to run. Enough information to resolve, validate, and render without fetching the source first."))
(~docs/subsection :title "Manifest Structure"
(~docs/code :code (highlight ";; Component manifest — published alongside the source\n(SxComponent\n :name \"~product-card\"\n :cid \"bafy...productcard\"\n :source-bytes 847\n :params (:title :price :image-url)\n :has-children true\n :pure true\n :deps (\n {:name \"~card\" :cid \"bafy...card\"}\n {:name \"~price-tag\" :cid \"bafy...pricetag\"}\n {:name \"~lazy-image\" :cid \"bafy...lazyimg\"})\n :css-atoms (:border :rounded :p-4 :text-sm :font-bold\n :text-green-700 :line-through :text-stone-400)\n :author \"https://rose-ash.com/apps/market\"\n :published \"2026-03-06T14:30:00Z\")" "lisp"))
(~docs/code :src (highlight ";; Component manifest — published alongside the source\n(SxComponent\n :name \"~product-card\"\n :cid \"bafy...productcard\"\n :source-bytes 847\n :params (:title :price :image-url)\n :has-children true\n :pure true\n :deps (\n {:name \"~card\" :cid \"bafy...card\"}\n {:name \"~price-tag\" :cid \"bafy...pricetag\"}\n {:name \"~lazy-image\" :cid \"bafy...lazyimg\"})\n :css-atoms (:border :rounded :p-4 :text-sm :font-bold\n :text-green-700 :line-through :text-stone-400)\n :author \"https://rose-ash.com/apps/market\"\n :published \"2026-03-06T14:30:00Z\")" "lisp"))
(p "Key fields:")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li (code ":cid") " — content address of the canonical serialized source")
@@ -163,11 +163,11 @@
(li "Creates/updates " (code "IPFSPin") " record with " (code "pin_type=\"component\""))
(li "Publishes manifest to IPFS (separate CID)")
(li "Optionally announces via AP outbox for federated discovery"))
(~docs/code :code (highlight ";; IPFSPin usage for components\nIPFSPin(\n content_hash=\"sha3-256:abcdef...\",\n ipfs_cid=\"bafy...productcard\",\n pin_type=\"component\",\n source_type=\"market\", # which service defined it\n metadata={\n \"name\": \"~product-card\",\n \"manifest_cid\": \"bafy...manifest\",\n \"deps\": [\"bafy...card\", \"bafy...pricetag\"],\n \"pure\": True\n }\n)" "python")))
(~docs/code :src (highlight ";; IPFSPin usage for components\nIPFSPin(\n content_hash=\"sha3-256:abcdef...\",\n ipfs_cid=\"bafy...productcard\",\n pin_type=\"component\",\n source_type=\"market\", # which service defined it\n metadata={\n \"name\": \"~product-card\",\n \"manifest_cid\": \"bafy...manifest\",\n \"deps\": [\"bafy...card\", \"bafy...pricetag\"],\n \"pure\": True\n }\n)" "python")))
(~docs/subsection :title "Client-Side: Resolution"
(p "New spec module " (code "resolve.sx") " — the client-side component resolution pipeline:")
(~docs/code :code (highlight "(define resolve-component-by-cid\n (fn (cid callback)\n ;; Resolution cascade:\n ;; 1. Check component env (already loaded?)\n ;; 2. Check localStorage (keyed by CID = cache-forever)\n ;; 3. Check origin server (/sx/components?cid=bafy...)\n ;; 4. Fetch from IPFS gateway\n ;; 5. Verify hash matches CID\n ;; 6. Parse, validate purity, register, callback\n (let ((cached (local-storage-get (str \"sx-cid:\" cid))))\n (if cached\n (do\n (register-component-source cached)\n (callback true))\n (fetch-component-by-cid cid\n (fn (source)\n (if (verify-cid cid source)\n (do\n (local-storage-set (str \"sx-cid:\" cid) source)\n (register-component-source source)\n (callback true))\n (do\n (log-warn (str \"sx:cid verification failed \" cid))\n (callback false)))))))))" "lisp"))
(~docs/code :src (highlight "(define resolve-component-by-cid\n (fn (cid callback)\n ;; Resolution cascade:\n ;; 1. Check component env (already loaded?)\n ;; 2. Check localStorage (keyed by CID = cache-forever)\n ;; 3. Check origin server (/sx/components?cid=bafy...)\n ;; 4. Fetch from IPFS gateway\n ;; 5. Verify hash matches CID\n ;; 6. Parse, validate purity, register, callback\n (let ((cached (local-storage-get (str \"sx-cid:\" cid))))\n (if cached\n (do\n (register-component-source cached)\n (callback true))\n (fetch-component-by-cid cid\n (fn (source)\n (if (verify-cid cid source)\n (do\n (local-storage-set (str \"sx-cid:\" cid) source)\n (register-component-source source)\n (callback true))\n (do\n (log-warn (str \"sx:cid verification failed \" cid))\n (callback false)))))))))" "lisp"))
(p "The cache-forever semantics are the key insight: because CIDs are content-addressed, a cached component " (strong "can never be stale") ". If the source changes, it gets a new CID. Old CIDs remain valid forever. There is no cache invalidation problem."))
(~docs/subsection :title "Resolution Cascade"
@@ -219,7 +219,7 @@
(~docs/subsection :title "Guarantee 1: Purity is Structural"
(p "SX boundary enforcement isn't a runtime sandbox — it's a registration-time structural check. When a component is loaded from IPFS and parsed, " (code "compute_all_io_refs()") " walks its entire AST and transitive dependencies. If " (em "any") " node references an IO primitive, the component is classified as IO-dependent and " (strong "rejected for untrusted registration."))
(p "This means the evaluator literally doesn't have IO primitives in scope when running an IPFS-loaded component. It's not that we catch IO calls — the names don't resolve. There's nothing to catch.")
(~docs/code :code (highlight "(define register-untrusted-component\n (fn (source origin)\n ;; Parse the defcomp from source\n ;; Run compute-all-io-refs on the parsed component\n ;; If io_refs is non-empty → REJECT\n ;; If pure → register in env with :origin metadata\n (let ((comp (parse-component source)))\n (if (not (component-pure? comp))\n (do\n (log-warn (str \"sx:reject IO component from \" origin))\n nil)\n (do\n (register-component comp)\n (log-info (str \"sx:registered \" (component-name comp)\n \" from \" origin))\n comp)))))" "lisp")))
(~docs/code :src (highlight "(define register-untrusted-component\n (fn (source origin)\n ;; Parse the defcomp from source\n ;; Run compute-all-io-refs on the parsed component\n ;; If io_refs is non-empty → REJECT\n ;; If pure → register in env with :origin metadata\n (let ((comp (parse-component source)))\n (if (not (component-pure? comp))\n (do\n (log-warn (str \"sx:reject IO component from \" origin))\n nil)\n (do\n (register-component comp)\n (log-info (str \"sx:registered \" (component-name comp)\n \" from \" origin))\n comp)))))" "lisp")))
(~docs/subsection :title "Guarantee 2: Content Verification"
(p "The CID IS the hash. When you fetch " (code "bafy...abc") " from any source — IPFS gateway, origin server, peer — you hash the response and compare. If it doesn't match, you reject it. No MITM attack can alter the content without changing the CID.")
@@ -279,17 +279,17 @@
(~docs/subsection :title "CID References in Page Registry"
(p "The page registry (shipped to the client as " (code "<script type=\"text/sx-pages\">") ") currently lists deps by name. Extend to include CIDs:")
(~docs/code :code (highlight "{:name \"docs-page\" :path \"/language/docs/<slug>\"\n :auth \"public\" :has-data false\n :deps ({:name \"~essay-foo\" :cid \"bafy...essay\"}\n {:name \"~doc-code\" :cid \"bafy...doccode\"})\n :content \"(case slug ...)\" :closure {}}" "lisp"))
(~docs/code :src (highlight "{:name \"docs-page\" :path \"/language/docs/<slug>\"\n :auth \"public\" :has-data false\n :deps ({:name \"~essay-foo\" :cid \"bafy...essay\"}\n {:name \"~doc-code\" :cid \"bafy...doccode\"})\n :content \"(case slug ...)\" :closure {}}" "lisp"))
(p "The " (a :href "/sx/(etc.(plan.predictive-prefetch))" :class "text-violet-700 underline" "predictive prefetch system") " uses these CIDs to fetch components from the resolution cascade rather than only from the origin server's " (code "/sx/components") " endpoint."))
(~docs/subsection :title "SX Response Component Headers"
(p "Currently, " (code "SX-Components") " header lists loaded component names. Extend to support CIDs:")
(~docs/code :code (highlight "Request:\nSX-Components: ~card:bafy...card,~plans/environment-images/nav:bafy...nav\n\nResponse:\nSX-Component-CIDs: ~plans/content-addressed-components/essay-foo:bafy...essay,~docs/code:bafy...doccode\n\n;; Response body only includes defs the client doesn't have\n(defcomp ~plans/content-addressed-components/essay-foo ...)" "http"))
(~docs/code :src (highlight "Request:\nSX-Components: ~card:bafy...card,~plans/environment-images/nav:bafy...nav\n\nResponse:\nSX-Component-CIDs: ~plans/content-addressed-components/essay-foo:bafy...essay,~docs/code:bafy...doccode\n\n;; Response body only includes defs the client doesn't have\n(defcomp ~plans/content-addressed-components/essay-foo ...)" "http"))
(p "The client can then verify received components match their declared CIDs. If the origin server is compromised, CID verification catches the tampered response."))
(~docs/subsection :title "Federated Content"
(p "When an ActivityPub activity arrives with SX content, it declares component requirements by CID:")
(~docs/code :code (highlight "(Create\n :actor \"https://other-instance.com/users/bob\"\n :object (Note\n :content (~product-card :title \"Bob's Widget\" :price 29.99)\n :requires (list\n {:name \"~product-card\" :cid \"bafy...prodcard\"}\n {:name \"~price-tag\" :cid \"bafy...pricetag\"})))" "lisp"))
(~docs/code :src (highlight "(Create\n :actor \"https://other-instance.com/users/bob\"\n :object (Note\n :content (~product-card :title \"Bob's Widget\" :price 29.99)\n :requires (list\n {:name \"~product-card\" :cid \"bafy...prodcard\"}\n {:name \"~price-tag\" :cid \"bafy...pricetag\"})))" "lisp"))
(p "The receiving browser resolves required components through the cascade. If Bob's instance is down, the components are still fetchable from IPFS. The content is self-describing and self-resolving.")))
;; -----------------------------------------------------------------------
@@ -304,12 +304,12 @@
(~docs/subsection :title "Component Registry as AP Actor"
(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. 'Update' means a new CID for the same name — consumers decide whether to adopt it."))
(~docs/subsection :title "Discovery Protocol"
(p "Webfinger-style lookup for components by name:")
(~docs/code :code (highlight "GET /.well-known/sx-component?name=~product-card\n\n{\n \"name\": \"~product-card\",\n \"cid\": \"bafy...prodcard\",\n \"manifest_cid\": \"bafy...manifest\",\n \"gateway\": \"https://rose-ash.com/ipfs/\",\n \"author\": \"https://rose-ash.com/apps/market\"\n}" "http"))
(~docs/code :src (highlight "GET /.well-known/sx-component?name=~product-card\n\n{\n \"name\": \"~product-card\",\n \"cid\": \"bafy...prodcard\",\n \"manifest_cid\": \"bafy...manifest\",\n \"gateway\": \"https://rose-ash.com/ipfs/\",\n \"author\": \"https://rose-ash.com/apps/market\"\n}" "http"))
(p "This is an optional convenience — any consumer that knows the CID can skip discovery and fetch directly from IPFS. Discovery answers the question: " (em "\"what's the current version of ~product-card on rose-ash.com?\""))
)

View File

@@ -66,7 +66,7 @@
(~docs/section :title "Image Format" :id "format"
(p "The image is itself an s-expression — the same format the spec is written in. This means the image can be parsed by the same parser, inspected by the same tools, and content-addressed by the same canonical serializer.")
(~docs/code :code (highlight "(sx-image\n :version 1\n :spec-cids {:eval \"bafy...eval\"\n :render \"bafy...render\"\n :parser \"bafy...parser\"\n :primitives \"bafy...prims\"\n :boundary \"bafy...boundary\"\n :signals \"bafy...signals\"}\n\n :components (\n (defcomp ~plans/environment-images/card (&key title subtitle &rest children)\n (div :class \"card\" (h2 title) (when subtitle (p subtitle)) children))\n (defcomp ~plans/environment-images/nav (&key items current)\n (nav :class \"nav\" (map (fn (item) ...) items)))\n ;; ... all registered components\n )\n\n :macros (\n (defmacro when (test &rest body)\n (list 'if test (cons 'begin body) nil))\n ;; ... all macros\n )\n\n :bindings (\n (define void-elements (list \"area\" \"base\" \"br\" \"col\" ...))\n (define boolean-attrs (list \"checked\" \"disabled\" ...))\n ;; ... all top-level defines\n )\n\n :primitive-names (\"str\" \"+\" \"-\" \"*\" \"/\" \"=\" \"<\" \">\" ...)\n :io-names (\"fetch-data\" \"call-action\" \"app-url\" ...))" "lisp"))
(~docs/code :src (highlight "(sx-image\n :version 1\n :spec-cids {:eval \"bafy...eval\"\n :render \"bafy...render\"\n :parser \"bafy...parser\"\n :primitives \"bafy...prims\"\n :boundary \"bafy...boundary\"\n :signals \"bafy...signals\"}\n\n :components (\n (defcomp ~plans/environment-images/card (&key title subtitle &rest children)\n (div :class \"card\" (h2 title) (when subtitle (p subtitle)) children))\n (defcomp ~plans/environment-images/nav (&key items current)\n (nav :class \"nav\" (map (fn (item) ...) items)))\n ;; ... all registered components\n )\n\n :macros (\n (defmacro when (test &rest body)\n (list 'if test (cons 'begin body) nil))\n ;; ... all macros\n )\n\n :bindings (\n (define void-elements (list \"area\" \"base\" \"br\" \"col\" ...))\n (define boolean-attrs (list \"checked\" \"disabled\" ...))\n ;; ... all top-level defines\n )\n\n :primitive-names (\"str\" \"+\" \"-\" \"*\" \"/\" \"=\" \"<\" \">\" ...)\n :io-names (\"fetch-data\" \"call-action\" \"app-url\" ...))" "lisp"))
(p "The " (code ":spec-cids") " field is the key. It links this image back to the exact spec that produced it. Anyone can verify the image by:")
(ol :class "list-decimal pl-5 text-stone-700 space-y-1"
@@ -83,7 +83,7 @@
(p "The image CID is computed by canonical-serializing the entire " (code "(sx-image ...)") " form and hashing it. Same process as component CIDs, just applied to the whole environment.")
(p "The relationship between spec CIDs and image CID is deterministic:")
(~docs/code :code (highlight ";; The image CID is a pure function of the spec CIDs\n;; (assuming a deterministic evaluator, which SX guarantees)\n(define image-cid-from-specs\n (fn (spec-cids)\n ;; 1. Fetch each spec file by CID\n ;; 2. Evaluate all specs in a fresh environment\n ;; 3. Extract components, macros, bindings\n ;; 4. Build (sx-image ...) form\n ;; 5. Canonical serialize\n ;; 6. Hash → CID\n ))" "lisp"))
(~docs/code :src (highlight ";; The image CID is a pure function of the spec CIDs\n;; (assuming a deterministic evaluator, which SX guarantees)\n(define image-cid-from-specs\n (fn (spec-cids)\n ;; 1. Fetch each spec file by CID\n ;; 2. Evaluate all specs in a fresh environment\n ;; 3. Extract components, macros, bindings\n ;; 4. Build (sx-image ...) form\n ;; 5. Canonical serialize\n ;; 6. Hash → CID\n ))" "lisp"))
(p "This means you can compute the expected image CID from the spec CIDs " (em "without") " having the image. If someone hands you an image claiming to be from spec " (code "bafy...eval") ", you can verify it by re-evaluating the spec and comparing CIDs. The image is a verifiable cache.")
(p "In practice, you'd only do this verification once per spec version. After that, the image CID is trusted by content-addressing — same bytes, same hash, forever."))
@@ -95,7 +95,7 @@
(~docs/section :title "Endpoint Provenance" :id "provenance"
(p "Every served page gains a provenance header linking it to the spec that rendered it:")
(~docs/code :code (highlight "HTTP/1.1 200 OK\nContent-Type: text/html\nSX-Spec: bafy...eval,bafy...render,bafy...parser,bafy...prims\nSX-Image: bafy...image\nSX-Page-Components: ~plans/environment-images/card:bafy...card,~plans/environment-images/nav:bafy...nav" "http"))
(~docs/code :src (highlight "HTTP/1.1 200 OK\nContent-Type: text/html\nSX-Spec: bafy...eval,bafy...render,bafy...parser,bafy...prims\nSX-Image: bafy...image\nSX-Page-Components: ~plans/environment-images/card:bafy...card,~plans/environment-images/nav:bafy...nav" "http"))
(p "Three levels of verification:")
(div :class "overflow-x-auto rounded border border-stone-200 mb-4"
@@ -134,7 +134,7 @@
(li "If no: evaluate spec files from source, build image, cache it")
(li "Register IO primitives and page helpers (host-specific, not in image)")
(li "Ready to serve"))
(~docs/code :code (highlight "(define load-environment\n (fn (spec-cids image-cache-dir)\n (let ((expected-image-cid (image-cid-for-specs spec-cids))\n (cached-path (str image-cache-dir \"/\" expected-image-cid \".sx\")))\n (if (file-exists? cached-path)\n ;; Fast path: deserialize\n (let ((image (parse (read-file cached-path))))\n (if (= (verify-image-cid image) expected-image-cid)\n (deserialize-image image)\n ;; Cache corrupted — rebuild\n (build-and-cache-image spec-cids image-cache-dir)))\n ;; Cold path: evaluate from source\n (build-and-cache-image spec-cids image-cache-dir)))))" "lisp")))
(~docs/code :src (highlight "(define load-environment\n (fn (spec-cids image-cache-dir)\n (let ((expected-image-cid (image-cid-for-specs spec-cids))\n (cached-path (str image-cache-dir \"/\" expected-image-cid \".sx\")))\n (if (file-exists? cached-path)\n ;; Fast path: deserialize\n (let ((image (parse (read-file cached-path))))\n (if (= (verify-image-cid image) expected-image-cid)\n (deserialize-image image)\n ;; Cache corrupted — rebuild\n (build-and-cache-image spec-cids image-cache-dir)))\n ;; Cold path: evaluate from source\n (build-and-cache-image spec-cids image-cache-dir)))))" "lisp")))
(~docs/subsection :title "Client Boot"
(p "The client already caches component definitions in localStorage keyed by bundle hash. Images extend this: cache the entire evaluated environment, not just individual components.")
@@ -153,7 +153,7 @@
(~docs/section :title "Standalone HTML Bundles" :id "standalone"
(p "An image can be inlined into a single HTML document, producing a fully self-contained application with no server dependency:")
(~docs/code :code (highlight "<!doctype html>\n<html>\n<head>\n <script type=\"text/sx-image\">\n (sx-image\n :version 1\n :spec-cids {...}\n :components (...)\n :macros (...)\n :bindings (...))\n </script>\n <script type=\"text/sx-pages\">\n (defpage home :path \"/\" :content (~home-page))\n </script>\n <script src=\"sx-ref.js\"></script>\n</head>\n<body>\n <div id=\"app\"></div>\n <script>\n // Deserialize image, register components, render\n Sx.bootFromImage(document.querySelector('[type=\"text/sx-image\"]'))\n </script>\n</body>\n</html>" "html"))
(~docs/code :src (highlight "<!doctype html>\n<html>\n<head>\n <script type=\"text/sx-image\">\n (sx-image\n :version 1\n :spec-cids {...}\n :components (...)\n :macros (...)\n :bindings (...))\n </script>\n <script type=\"text/sx-pages\">\n (defpage home :path \"/\" :content (~home-page))\n </script>\n <script src=\"sx-ref.js\"></script>\n</head>\n<body>\n <div id=\"app\"></div>\n <script>\n // Deserialize image, register components, render\n Sx.bootFromImage(document.querySelector('[type=\"text/sx-image\"]'))\n </script>\n</body>\n</html>" "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/<slug>\" ...)\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/<slug>\" ...)\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."))

View File

@@ -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\")))

View File

@@ -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") "."))

View File

@@ -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 "<script type=\"text/sx-lib\">") " blocks. The platform boot shim evaluates them before component scripts:")
(~docs/code :code (highlight "<script src=\"/static/scripts/sx-platform.js\"></script>\n<script src=\"/static/scripts/sx-evaluator.js\"></script>\n<script type=\"text/sx-lib\">\n ;; concatenated web/ framework: signals, deps, router,\n ;; engine, orchestration, boot\n</script>\n<script type=\"text/sx\" data-components>\n ;; page component definitions\n</script>" "html"))
(~docs/code :src (highlight "<script src=\"/static/scripts/sx-platform.js\"></script>\n<script src=\"/static/scripts/sx-evaluator.js\"></script>\n<script type=\"text/sx-lib\">\n ;; concatenated web/ framework: signals, deps, router,\n ;; engine, orchestration, boot\n</script>\n<script type=\"text/sx\" data-components>\n ;; page component definitions\n</script>" "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")))
;; -----------------------------------------------------------------------

View File

@@ -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/<slug>\" → 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/<slug>\" → 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 "<script type=\"text/sx-pages\">") ". Each entry carries name, path pattern, auth level, has-data flag, serialized content expression, and closure values.")
(~docs/code :code (highlight "{:name \"docs-page\" :path \"/language/docs/<slug>\"\n :auth \"public\" :has-data false\n :content \"(case slug ...)\" :closure {}}" "lisp"))
(~docs/code :src (highlight "{:name \"docs-page\" :path \"/language/docs/<slug>\"\n :auth \"public\" :has-data false\n :content \"(case slug ...)\" :closure {}}" "lisp"))
(p "boot.sx processes these at startup using the SX parser — the same " (code "parse") " function from parser.sx — building route entries with parsed patterns into the " (code "_page-routes") " table. No JSON dependency."))
(div
@@ -243,7 +243,7 @@
(div
(h4 :class "font-semibold text-stone-700" "1. Abstract resolve-page-data")
(p "Spec-level primitive in orchestration.sx. The spec says \"I need data for this page\" — platform provides transport:")
(~docs/code :code (highlight "(resolve-page-data page-name params\n (fn (data)\n ;; data is a dict — merge into env and render\n (let ((env (merge closure params data))\n (rendered (try-eval-content content-src env)))\n (swap-rendered-content target rendered pathname))))" "lisp"))
(~docs/code :src (highlight "(resolve-page-data page-name params\n (fn (data)\n ;; data is a dict — merge into env and render\n (let ((env (merge closure params data))\n (rendered (try-eval-content content-src env)))\n (swap-rendered-content target rendered pathname))))" "lisp"))
(p "Browser platform: HTTP fetch to " (code "/sx/data/<page-name>") ". Future platforms could use IPC, cache, WebSocket, etc."))
(div
@@ -353,7 +353,7 @@
(div
(h4 :class "font-semibold text-stone-700" "1. Suspense component")
(p "When streaming, the server renders the page with " (code "~shared:pages/suspense") " placeholders instead of awaiting IO:")
(~docs/code :code (highlight "(~app-body\n :header-rows (~shared:pages/suspense :id \"stream-headers\"\n :fallback (div :class \"h-12 bg-stone-200 animate-pulse\"))\n :content (~shared:pages/suspense :id \"stream-content\"\n :fallback (div :class \"p-8 animate-pulse\" ...)))" "lisp")))
(~docs/code :src (highlight "(~app-body\n :header-rows (~shared:pages/suspense :id \"stream-headers\"\n :fallback (div :class \"h-12 bg-stone-200 animate-pulse\"))\n :content (~shared:pages/suspense :id \"stream-content\"\n :fallback (div :class \"p-8 animate-pulse\" ...)))" "lisp")))
(div
(h4 :class "font-semibold text-stone-700" "2. Chunked transfer")
@@ -366,7 +366,7 @@
(div
(h4 :class "font-semibold text-stone-700" "3. Client resolution")
(p "Each resolution chunk is an inline script:")
(~docs/code :code (highlight "<script>\n window.__sxResolve(\"stream-content\",\n \"(~article :title \\\"Hello\\\")\")\n</script>" "html"))
(~docs/code :src (highlight "<script>\n window.__sxResolve(\"stream-content\",\n \"(~article :title \\\"Hello\\\")\")\n</script>" "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")

View File

@@ -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

View File

@@ -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:")

View File

@@ -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<binary WASM module>" "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<binary WASM module>" "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<div class=\"card\"><h2>Hello</h2><p class=\"text-stone-600\">World</p></div>\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<div class=\"card\"><h2>Hello</h2><p class=\"text-stone-600\">World</p></div>\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 <div class=\"card\"><h2>...</h2>...</div> ~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 <div class=\"card\"><h2>...</h2>...</div> ~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). "

View File

@@ -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/<slug>\"\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/<slug>\"\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/<slug>\"\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/<slug>\"\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."))

View File

@@ -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/<slug>\"\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/<slug>\"\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/<slug>\"\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/<slug>\"\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/<slug>\"\n :prefetch :eager) ;; bundle with linking page\n\n(defpage reference-page\n :path \"/reference/<slug>\"\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/<slug>\"\n :prefetch :eager) ;; bundle with linking page\n\n(defpage reference-page\n :path \"/reference/<slug>\"\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 /<service-prefix>/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 /<service-prefix>/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 `<script type=\"text/sx\" data-components>${sxText}<\\/script>`;\n Sx.processScripts(frag);\n}" "javascript"))))
(~docs/code :src (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 `<script type=\"text/sx\" data-components>${sxText}<\\/script>`;\n Sx.processScripts(frag);\n}" "javascript"))))
;; -----------------------------------------------------------------------
;; Request flow
@@ -189,7 +189,7 @@
(~docs/section :title "Request Flow" :id "request-flow"
(p "End-to-end example: user hovers a link, components prefetch, click goes client-side.")
(~docs/code :code (highlight "User hovers link \"/language/docs/sx-manifesto\"\n |\n +-- bind-prefetch-on-hover fires (150ms debounce)\n |\n +-- compute-missing-deps(\"/language/docs/sx-manifesto\")\n | +-- find-matching-route -> page with deps:\n | | [\"~essay-sx-manifesto\", \"~doc-code\"]\n | +-- loaded-component-names -> [\"~nav\", \"~footer\", \"~doc-code\"]\n | +-- missing: [\"~essay-sx-manifesto\"]\n |\n +-- prefetch-components([\"~essay-sx-manifesto\"])\n | +-- GET /sx/components?names=~essay-sx-manifesto\n | | Headers: SX-Components: ~plans/environment-images/nav,~footer,~doc-code\n | +-- Server resolves transitive deps\n | | (also needs ~plans/predictive-prefetch/rich-text, subtracts already-loaded)\n | +-- Response:\n | (defcomp ~plans/predictive-prefetch/essay-sx-manifesto ...) \n | (defcomp ~plans/predictive-prefetch/rich-text ...)\n |\n +-- sx-process-component-text registers defcomps in env\n |\n +-- User clicks link\n +-- try-client-route(\"/language/docs/sx-manifesto\")\n +-- has-all-deps? -> true (prefetched!)\n +-- eval content -> DOM\n +-- Client-side render, no server roundtrip" "text")))
(~docs/code :src (highlight "User hovers link \"/language/docs/sx-manifesto\"\n |\n +-- bind-prefetch-on-hover fires (150ms debounce)\n |\n +-- compute-missing-deps(\"/language/docs/sx-manifesto\")\n | +-- find-matching-route -> page with deps:\n | | [\"~essay-sx-manifesto\", \"~doc-code\"]\n | +-- loaded-component-names -> [\"~nav\", \"~footer\", \"~doc-code\"]\n | +-- missing: [\"~essay-sx-manifesto\"]\n |\n +-- prefetch-components([\"~essay-sx-manifesto\"])\n | +-- GET /sx/components?names=~essay-sx-manifesto\n | | Headers: SX-Components: ~plans/environment-images/nav,~footer,~doc-code\n | +-- Server resolves transitive deps\n | | (also needs ~plans/predictive-prefetch/rich-text, subtracts already-loaded)\n | +-- Response:\n | (defcomp ~plans/predictive-prefetch/essay-sx-manifesto ...) \n | (defcomp ~plans/predictive-prefetch/rich-text ...)\n |\n +-- sx-process-component-text registers defcomps in env\n |\n +-- User clicks link\n +-- try-client-route(\"/language/docs/sx-manifesto\")\n +-- has-all-deps? -> true (prefetched!)\n +-- eval content -> DOM\n +-- Client-side render, no server roundtrip" "text")))
;; -----------------------------------------------------------------------
;; File changes

View File

@@ -13,19 +13,19 @@
(~docs/subsection :title "Dispatch Character: #"
(p "Lisp tradition. " (code "#") " is NOT in " (code "ident-start") " or " (code "ident-char") " — completely free. Pattern:")
(~docs/code :code (highlight "#;expr → (read and discard expr, return next)\n#|...| → raw string literal\n#'expr → (quote expr)" "lisp")))
(~docs/code :src (highlight "#;expr → (read and discard expr, return next)\n#|...| → raw string literal\n#'expr → (quote expr)" "lisp")))
(~docs/subsection :title "#; — Datum comment"
(p "Scheme/Racket standard. Reads and discards the next expression. Preserves balanced parens.")
(~docs/code :code (highlight "(list 1 #;(this is commented out) 2 3) → (list 1 2 3)" "lisp")))
(~docs/code :src (highlight "(list 1 #;(this is commented out) 2 3) → (list 1 2 3)" "lisp")))
(~docs/subsection :title "#|...| — Raw string"
(p "No escape processing. Everything between " (code "#|") " and " (code "|") " is literal. Enables inline markdown, regex patterns, code examples.")
(~docs/code :code (highlight "(~md #|## Title\n\nSome **bold** text with \"quotes\" and \\backslashes.|)" "lisp")))
(~docs/code :src (highlight "(~md #|## Title\n\nSome **bold** text with \"quotes\" and \\backslashes.|)" "lisp")))
(~docs/subsection :title "#' — Quote shorthand"
(p "Currently no single-char quote (" (code "`") " is quasiquote).")
(~docs/code :code (highlight "#'my-function → (quote my-function)" "lisp")))
(~docs/code :src (highlight "#'my-function → (quote my-function)" "lisp")))
(~docs/subsection :title "Extensible dispatch: #name"
(p "User-defined reader macros via " (code "#name expr") ". The parser reads an identifier after " (code "#") ", looks up a handler in the reader macro registry, and calls it with the next parsed expression. See the " (a :href "/sx/(etc.(plan.reader-macro-demo))" :class "text-violet-600 hover:underline" "#z3 demo") " for a working example that translates SX spec declarations to SMT-LIB.")))
@@ -39,15 +39,15 @@
(~docs/subsection :title "1. Spec: parser.sx"
(p "Add " (code "#") " dispatch to " (code "read-expr") " (after the " (code ",") "/" (code ",@") " case, before number). Add " (code "read-raw-string") " helper function.")
(~docs/code :code (highlight ";; Reader macro dispatch\n(= ch \"#\")\n (do (set! pos (inc pos))\n (if (>= pos len-src)\n (error \"Unexpected end of input after #\")\n (let ((dispatch-ch (nth source pos)))\n (cond\n ;; #; — datum comment: read and discard next expr\n (= dispatch-ch \";\")\n (do (set! pos (inc pos))\n (read-expr) ;; read and discard\n (read-expr)) ;; return the NEXT expr\n\n ;; #| — raw string\n (= dispatch-ch \"|\")\n (do (set! pos (inc pos))\n (read-raw-string))\n\n ;; #' — quote shorthand\n (= dispatch-ch \"'\")\n (do (set! pos (inc pos))\n (list (make-symbol \"quote\") (read-expr)))\n\n :else\n (error (str \"Unknown reader macro: #\" dispatch-ch))))))" "lisp"))
(~docs/code :src (highlight ";; Reader macro dispatch\n(= ch \"#\")\n (do (set! pos (inc pos))\n (if (>= pos len-src)\n (error \"Unexpected end of input after #\")\n (let ((dispatch-ch (nth source pos)))\n (cond\n ;; #; — datum comment: read and discard next expr\n (= dispatch-ch \";\")\n (do (set! pos (inc pos))\n (read-expr) ;; read and discard\n (read-expr)) ;; return the NEXT expr\n\n ;; #| — raw string\n (= dispatch-ch \"|\")\n (do (set! pos (inc pos))\n (read-raw-string))\n\n ;; #' — quote shorthand\n (= dispatch-ch \"'\")\n (do (set! pos (inc pos))\n (list (make-symbol \"quote\") (read-expr)))\n\n :else\n (error (str \"Unknown reader macro: #\" dispatch-ch))))))" "lisp"))
(p "The " (code "read-raw-string") " helper:")
(~docs/code :code (highlight "(define read-raw-string\n (fn ()\n (let ((buf \"\"))\n (define raw-loop\n (fn ()\n (if (>= pos len-src)\n (error \"Unterminated raw string\")\n (let ((ch (nth source pos)))\n (if (= ch \"|\")\n (do (set! pos (inc pos)) nil) ;; done\n (do (set! buf (str buf ch))\n (set! pos (inc pos))\n (raw-loop)))))))\n (raw-loop)\n buf)))" "lisp")))
(~docs/code :src (highlight "(define read-raw-string\n (fn ()\n (let ((buf \"\"))\n (define raw-loop\n (fn ()\n (if (>= pos len-src)\n (error \"Unterminated raw string\")\n (let ((ch (nth source pos)))\n (if (= ch \"|\")\n (do (set! pos (inc pos)) nil) ;; done\n (do (set! buf (str buf ch))\n (set! pos (inc pos))\n (raw-loop)))))))\n (raw-loop)\n buf)))" "lisp")))
(~docs/subsection :title "2. Python: parser.py"
(p "Add " (code "#") " dispatch to " (code "_parse_expr()") " (after " (code ",") "/" (code ",@") " handling ~line 252). Add " (code "_read_raw_string()") " method to Tokenizer.")
(~docs/code :code (highlight "# In _parse_expr(), after the comma/splice-unquote block:\nif raw == \"#\":\n tok._advance(1) # consume the #\n if tok.pos >= len(tok.text):\n raise ParseError(\"Unexpected end of input after #\",\n tok.pos, tok.line, tok.col)\n dispatch = tok.text[tok.pos]\n if dispatch == \";\":\n tok._advance(1)\n _parse_expr(tok) # read and discard\n return _parse_expr(tok) # return next\n if dispatch == \"|\":\n tok._advance(1)\n return tok._read_raw_string()\n if dispatch == \"'\":\n tok._advance(1)\n return [Symbol(\"quote\"), _parse_expr(tok)]\n raise ParseError(f\"Unknown reader macro: #{dispatch}\",\n tok.pos, tok.line, tok.col)" "python"))
(~docs/code :src (highlight "# In _parse_expr(), after the comma/splice-unquote block:\nif raw == \"#\":\n tok._advance(1) # consume the #\n if tok.pos >= len(tok.text):\n raise ParseError(\"Unexpected end of input after #\",\n tok.pos, tok.line, tok.col)\n dispatch = tok.text[tok.pos]\n if dispatch == \";\":\n tok._advance(1)\n _parse_expr(tok) # read and discard\n return _parse_expr(tok) # return next\n if dispatch == \"|\":\n tok._advance(1)\n return tok._read_raw_string()\n if dispatch == \"'\":\n tok._advance(1)\n return [Symbol(\"quote\"), _parse_expr(tok)]\n raise ParseError(f\"Unknown reader macro: #{dispatch}\",\n tok.pos, tok.line, tok.col)" "python"))
(p "The " (code "_read_raw_string()") " method on Tokenizer:")
(~docs/code :code (highlight "def _read_raw_string(self) -> str:\n buf = []\n while self.pos < len(self.text):\n ch = self.text[self.pos]\n if ch == \"|\":\n self._advance(1)\n return \"\".join(buf)\n buf.append(ch)\n self._advance(1)\n raise ParseError(\"Unterminated raw string\",\n self.pos, self.line, self.col)" "python")))
(~docs/code :src (highlight "def _read_raw_string(self) -> str:\n buf = []\n while self.pos < len(self.text):\n ch = self.text[self.pos]\n if ch == \"|\":\n self._advance(1)\n return \"\".join(buf)\n buf.append(ch)\n self._advance(1)\n raise ParseError(\"Unterminated raw string\",\n self.pos, self.line, self.col)" "python")))
(~docs/subsection :title "3. JS: auto-transpiled"
(p "JS parser comes from bootstrap of parser.sx — spec change handles it automatically."))
@@ -57,7 +57,7 @@
(~docs/subsection :title "5. Grammar update"
(p "Add reader macro syntax to the grammar comment at the top of parser.sx:")
(~docs/code :code (highlight ";; reader → '#;' expr (datum comment)\n;; | '#|' raw-chars '|' (raw string)\n;; | \"#'\" expr (quote shorthand)" "lisp"))))
(~docs/code :src (highlight ";; reader → '#;' expr (datum comment)\n;; | '#|' raw-chars '|' (raw string)\n;; | \"#'\" expr (quote shorthand)" "lisp"))))
;; -----------------------------------------------------------------------
;; Files

View File

@@ -85,7 +85,7 @@
(p "Per the " (a :href "/sx/(etc.(plan.self-hosting-bootstrapper))" :class "text-violet-700 underline" "self-hosting principle") ", the slicer is not a build tool — it's a spec module. " (code "slice.sx") " analyzes the spec's own dependency graph and determines which defines belong to which tier.")
(p (code "js.sx") " (the self-hosting SX-to-JavaScript translator) already compiles the full spec. Slicing is a filter: " (code "js.sx") " translates only the defines that " (code "slice.sx") " selects for a given tier.")
(~docs/code :code (highlight ";; slice.sx — determine which defines each tier needs\n;;\n;; Input: the full list of defines from all spec files\n;; Output: a filtered list for the requested tier\n\n(define tier-deps\n ;; Which spec modules each tier requires\n {:L0 (list \"engine\" \"boot-partial\")\n :L1 (list \"engine\" \"boot-partial\" \"dom-partial\")\n :L2 (list \"engine\" \"boot-partial\" \"dom-partial\"\n \"signals\" \"dom-island\")\n :L3 (list \"eval\" \"render\" \"parser\"\n \"engine\" \"orchestration\" \"boot\"\n \"dom\" \"signals\" \"router\")})\n\n(define slice-defines\n (fn (tier all-defines)\n ;; 1. Get the module list for this tier\n ;; 2. Walk each define's dependency references\n ;; 3. Include a define only if ALL its deps are\n ;; satisfiable within the tier's module set\n ;; 4. Return the filtered define list\n (let ((modules (get tier-deps tier)))\n (filter\n (fn (d) (tier-satisfies? modules (define-deps d)))\n all-defines))))" "lisp"))
(~docs/code :src (highlight ";; slice.sx — determine which defines each tier needs\n;;\n;; Input: the full list of defines from all spec files\n;; Output: a filtered list for the requested tier\n\n(define tier-deps\n ;; Which spec modules each tier requires\n {:L0 (list \"engine\" \"boot-partial\")\n :L1 (list \"engine\" \"boot-partial\" \"dom-partial\")\n :L2 (list \"engine\" \"boot-partial\" \"dom-partial\"\n \"signals\" \"dom-island\")\n :L3 (list \"eval\" \"render\" \"parser\"\n \"engine\" \"orchestration\" \"boot\"\n \"dom\" \"signals\" \"router\")})\n\n(define slice-defines\n (fn (tier all-defines)\n ;; 1. Get the module list for this tier\n ;; 2. Walk each define's dependency references\n ;; 3. Include a define only if ALL its deps are\n ;; satisfiable within the tier's module set\n ;; 4. Return the filtered define list\n (let ((modules (get tier-deps tier)))\n (filter\n (fn (d) (tier-satisfies? modules (define-deps d)))\n all-defines))))" "lisp"))
(p "The pipeline becomes:")
(ol :class "list-decimal pl-5 text-stone-700 space-y-2"
@@ -103,7 +103,7 @@
(~docs/section :title "Define-Level Dependency Analysis" :id "deps"
(p "The slicer needs to know which defines reference which other defines. This is a simpler version of what " (code "deps.sx") " does for components — but at the define level, not the component level.")
(~docs/code :code (highlight ";; Walk a define's body AST, collect all symbol references\n;; that resolve to other top-level defines.\n;;\n;; (define morph-children\n;; (fn (old-node new-node)\n;; ...uses morph-node, morph-attrs, create-element...))\n;;\n;; → deps: #{morph-node morph-attrs create-element}\n\n(define define-refs\n (fn (body all-define-names)\n ;; Collect all symbols in body that appear in all-define-names\n (let ((refs (make-set)))\n (walk-ast body\n (fn (node)\n (when (and (symbol? node)\n (set-has? all-define-names (symbol-name node)))\n (set-add! refs (symbol-name node)))))\n refs)))" "lisp"))
(~docs/code :src (highlight ";; Walk a define's body AST, collect all symbol references\n;; that resolve to other top-level defines.\n;;\n;; (define morph-children\n;; (fn (old-node new-node)\n;; ...uses morph-node, morph-attrs, create-element...))\n;;\n;; → deps: #{morph-node morph-attrs create-element}\n\n(define define-refs\n (fn (body all-define-names)\n ;; Collect all symbols in body that appear in all-define-names\n (let ((refs (make-set)))\n (walk-ast body\n (fn (node)\n (when (and (symbol? node)\n (set-has? all-define-names (symbol-name node)))\n (set-add! refs (symbol-name node)))))\n refs)))" "lisp"))
(p "From these per-define deps, we build the full dependency graph and compute transitive closures. A tier's define set is the transitive closure of its entry points:")
@@ -139,7 +139,7 @@
(p "The bootstrapped code (from js.sx) is only half the story. Each define also depends on platform primitives — the hand-written JS glue that js.sx doesn't produce. These live in " (code "bootstrap_js.py") "'s PLATFORM_* blocks.")
(p "The slicer must track platform deps too:")
(~docs/code :code (highlight ";; Platform primitives referenced by tier\n;;\n;; L0 needs: createElement, setAttribute, morphAttrs,\n;; fetch (for sx-get/post), pushState, replaceState\n;;\n;; L1 adds: classList.toggle, addEventListener\n;;\n;; L2 adds: createComment, createTextNode, domRemove,\n;; domChildNodes — the reactive DOM primitives\n;;\n;; L3 adds: everything in PLATFORM_PARSER_JS,\n;; full DOM adapter, async IO bridge\n\n(define platform-deps\n {:L0 (list :dom-morph :fetch :history)\n :L1 (list :dom-morph :fetch :history :dom-events)\n :L2 (list :dom-morph :fetch :history :dom-events\n :dom-reactive :signal-constructors)\n :L3 (list :dom-morph :fetch :history :dom-events\n :dom-reactive :signal-constructors\n :parser :evaluator :async-io)})" "lisp"))
(~docs/code :src (highlight ";; Platform primitives referenced by tier\n;;\n;; L0 needs: createElement, setAttribute, morphAttrs,\n;; fetch (for sx-get/post), pushState, replaceState\n;;\n;; L1 adds: classList.toggle, addEventListener\n;;\n;; L2 adds: createComment, createTextNode, domRemove,\n;; domChildNodes — the reactive DOM primitives\n;;\n;; L3 adds: everything in PLATFORM_PARSER_JS,\n;; full DOM adapter, async IO bridge\n\n(define platform-deps\n {:L0 (list :dom-morph :fetch :history)\n :L1 (list :dom-morph :fetch :history :dom-events)\n :L2 (list :dom-morph :fetch :history :dom-events\n :dom-reactive :signal-constructors)\n :L3 (list :dom-morph :fetch :history :dom-events\n :dom-reactive :signal-constructors\n :parser :evaluator :async-io)})" "lisp"))
(p "The platform JS blocks in " (code "bootstrap_js.py") " are already organized by adapter (" (code "PLATFORM_DOM_JS") ", " (code "PLATFORM_ENGINE_PURE_JS") ", etc). Slicing further subdivides these into the minimal set each tier needs.")
(p "This subdivision also happens in SX: " (code "slice.sx") " declares which platform blocks each tier requires. " (code "js.sx") " doesn't need to change — it translates defines. The bootstrapper script reads the slice spec and assembles the platform preamble accordingly."))
@@ -152,11 +152,11 @@
(p "The simplest approach: one file per tier. The server knows each page's tier (from " (code "defpage") " metadata or component analysis) and serves the right script tag.")
(p "Better: a base file (L0) that all pages load, plus tier deltas loaded on demand.")
(~docs/code :code (highlight ";; Server emits the appropriate script for the page's tier\n;;\n;; L0 page (blog post, product listing):\n;; <script src=\"/static/scripts/sx-L0.js\"></script>\n;;\n;; L2 page (reactive island):\n;; <script src=\"/static/scripts/sx-L0.js\"></script>\n;; <script src=\"/static/scripts/sx-L2-delta.js\"></script>\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;; <script src=\"/static/scripts/sx-L0.js\"></script>\n;;\n;; L2 page (reactive island):\n;; <script src=\"/static/scripts/sx-L0.js\"></script>\n;; <script src=\"/static/scripts/sx-L2-delta.js\"></script>\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.")

View File

@@ -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<Value>),\n Dict(Vec<(Value, Value)>),\n Lambda(Rc<Closure>),\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<Value>),\n Dict(Vec<(Value, Value)>),\n Lambda(Rc<Closure>),\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

View File

@@ -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.")))

View File

@@ -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

View File

@@ -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

View File

@@ -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\": \"<p>Hello world</p>\",\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\": \"<p>Hello world</p>\",\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.")

View File

@@ -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."))

View File

@@ -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."))))

View File

@@ -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\": \"<p>Hello <strong>world</strong></p>\",\n \"attributedTo\": \"https://example.com/users/alice\"}")
(~docs/code :src "{\"type\": \"Note\",\n \"content\": \"<p>Hello <strong>world</strong></p>\",\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."))

View File

@@ -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(\"/<path:expr>\")\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")))

View File

@@ -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")))))

View File

@@ -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)"

View File

@@ -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.")

View File

@@ -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."))

View File

@@ -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;; → <div class=\"outer\"><span class=\"inner\"></span></div>" "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;; → <div class=\"outer\"><span class=\"inner\"></span></div>" "lisp")))
;; =====================================================================
;; V. Across all adapters

View File

@@ -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;; <div data-sx-island=\"counter\" data-sx-state='{\"initial\":42}'>\n;; <span>42</span><button>+</button>\n;; </div>\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;; <div data-sx-island=\"counter\" data-sx-state='{\"initial\":42}'>\n;; <span>42</span><button>+</button>\n;; </div>\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 ()

View File

@@ -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"

View File

@@ -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"

View File

@@ -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 <div data-sx-marsh=\"controls\">children HTML</div>\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 <div data-sx-marsh=\"controls\">children HTML</div>\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 <p> 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 <p> 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.")))

View File

@@ -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."))

View File

@@ -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.")))

View File

@@ -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"

View File

@@ -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;; => <!doctype html>...<div id=\"sx-app-root\">...</div>...\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
"<!doctype html>\n<html>\n<head>\n <link rel=\"stylesheet\" href=\"/static/app.css\">\n <script src=\"/static/scripts/sx-browser.js\"></script>\n</head>\n<body>\n <div id=\"sx-app-root\"></div>\n <script>\n SX.boot(function(sx) {\n sx.appBoot(/* app config */)\n })\n </script>\n</body>\n</html>"
"html"))

View File

@@ -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"

View File

@@ -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 \"<style>\" (join \"\" rules) \"</style>\")))" "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 \"<style>\" (join \"\" rules) \"</style>\")))" "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."))

View File

@@ -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"

View File

@@ -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 <content>) → passes content through\n;; 3. Wrap in (~layouts/doc :path \"...\" <content>)"
"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.<sx:item_id>)))))\"\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 <ast>) → 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))\" <content>)\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"))

View File

@@ -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 ("

View File

@@ -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"