From 214963ea6a4a0b14b8c2d3a9e72d857322a3a9ec Mon Sep 17 00:00:00 2001 From: giles Date: Fri, 13 Mar 2026 12:03:58 +0000 Subject: [PATCH] Unicode escapes, variadic infix fix, spreads demos, scoped-effects + foundations plans MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add \uXXXX unicode escape support to parser.py and parser.sx spec - Add char-from-code primitive (Python chr(), JS String.fromCharCode()) - Fix variadic infix operators in both bootstrappers (js.sx, py.sx) — (+ a b c d) was silently dropping terms, now left-folds correctly - Rebootstrap sx_ref.py and sx-browser.js with all fixes - Fix 3 pre-existing map-dict test failures in shared/sx/tests/run.py - Add live demos alongside examples in spreads essay (side-by-side layout) - Add scoped-effects plan: algebraic effects as unified foundation for spread/collect/island/lake/signal/context - Add foundations plan: CEK machine, the computational floor, three-axis model (depth/topology/linearity), Curry-Howard correspondence - Route both plans in page-functions.sx and nav-data.sx Co-Authored-By: Claude Opus 4.6 --- shared/static/scripts/sx-browser.js | 19 +- shared/sx/parser.py | 8 + shared/sx/ref/js.sx | 20 +- shared/sx/ref/parser.sx | 34 +- shared/sx/ref/platform_js.py | 2 + shared/sx/ref/platform_py.py | 1 + shared/sx/ref/primitives.sx | 5 + shared/sx/ref/py.sx | 20 +- shared/sx/ref/sx_ref.py | 1 + shared/sx/tests/run.py | 12 + sx/sx/nav-data.sx | 6 +- sx/sx/page-functions.sx | 2 + sx/sx/plans/foundations.sx | 661 ++++++++++++++++++++++++++++ sx/sx/plans/scoped-effects.sx | 444 +++++++++++++++++++ sx/sx/spreads.sx | 126 +++++- 15 files changed, 1323 insertions(+), 38 deletions(-) create mode 100644 sx/sx/plans/foundations.sx create mode 100644 sx/sx/plans/scoped-effects.sx diff --git a/shared/static/scripts/sx-browser.js b/shared/static/scripts/sx-browser.js index 5827d98..aff569d 100644 --- a/shared/static/scripts/sx-browser.js +++ b/shared/static/scripts/sx-browser.js @@ -14,7 +14,7 @@ // ========================================================================= var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } }); - var SX_VERSION = "2026-03-13T05:11:12Z"; + var SX_VERSION = "2026-03-13T11:02:33Z"; function isNil(x) { return x === NIL || x === null || x === undefined; } function isSxTruthy(x) { return x !== false && !isNil(x); } @@ -410,6 +410,7 @@ PRIMITIVES["ends-with?"] = function(s, p) { var str = String(s); return str.indexOf(p, str.length - p.length) !== -1; }; PRIMITIVES["slice"] = function(c, a, b) { if (!c || typeof c.slice !== "function") { console.error("[sx-debug] slice called on non-sliceable:", typeof c, c, "a=", a, "b=", b, new Error().stack); return []; } return b !== undefined ? c.slice(a, b) : c.slice(a); }; PRIMITIVES["substring"] = function(s, a, b) { return String(s).substring(a, b); }; + PRIMITIVES["char-from-code"] = function(n) { return String.fromCharCode(n); }; PRIMITIVES["string-length"] = function(s) { return String(s).length; }; PRIMITIVES["string-contains?"] = function(s, sub) { return String(s).indexOf(String(sub)) !== -1; }; PRIMITIVES["concat"] = function() { @@ -774,6 +775,7 @@ return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\t/g, "\\t"); } function sxExprSource(e) { return typeof e === "string" ? e : String(e); } + var charFromCode = PRIMITIVES["char-from-code"]; // === Transpiled from eval === @@ -1382,6 +1384,7 @@ if (isSxTruthy(sxOr((ch == " "), (ch == "\t"), (ch == "\n"), (ch == "\r")))) { p continue; } else if (isSxTruthy((ch == ";"))) { pos = (pos + 1); skipComment(); continue; } else { return NIL; } } } else { return NIL; } } }; + var hexDigitValue = function(ch) { return indexOf_("0123456789abcdef", lower(ch)); }; var readString = function() { pos = (pos + 1); return (function() { var buf = ""; @@ -1389,9 +1392,19 @@ return (function() { if (isSxTruthy((ch == "\""))) { pos = (pos + 1); return NIL; } else if (isSxTruthy((ch == "\\"))) { pos = (pos + 1); { var esc = nth(source, pos); -buf = (String(buf) + String((isSxTruthy((esc == "n")) ? "\n" : (isSxTruthy((esc == "t")) ? "\t" : (isSxTruthy((esc == "r")) ? "\r" : esc))))); +if (isSxTruthy((esc == "u"))) { pos = (pos + 1); +{ var d0 = hexDigitValue(nth(source, pos)); +var _ = (pos = (pos + 1)); +var d1 = hexDigitValue(nth(source, pos)); +var _ = (pos = (pos + 1)); +var d2 = hexDigitValue(nth(source, pos)); +var _ = (pos = (pos + 1)); +var d3 = hexDigitValue(nth(source, pos)); +var _ = (pos = (pos + 1)); +buf = (String(buf) + String(charFromCode(((((d0 * 4096) + (d1 * 256)) + (d2 * 16)) + d3)))); +continue; } } else { buf = (String(buf) + String((isSxTruthy((esc == "n")) ? "\n" : (isSxTruthy((esc == "t")) ? "\t" : (isSxTruthy((esc == "r")) ? "\r" : esc))))); pos = (pos + 1); -continue; } } else { buf = (String(buf) + String(ch)); +continue; } } } else { buf = (String(buf) + String(ch)); pos = (pos + 1); continue; } } } } }; readStrLoop(); diff --git a/shared/sx/parser.py b/shared/sx/parser.py index f409293..ed5eaad 100644 --- a/shared/sx/parser.py +++ b/shared/sx/parser.py @@ -106,6 +106,14 @@ def _unescape_string(s: str) -> str: while i < len(s): if s[i] == "\\" and i + 1 < len(s): nxt = s[i + 1] + if nxt == "u" and i + 5 < len(s): + hex_str = s[i + 2:i + 6] + try: + out.append(chr(int(hex_str, 16))) + i += 6 + continue + except ValueError: + pass # fall through to default handling out.append(_ESCAPE_MAP.get(nxt, nxt)) i += 2 else: diff --git a/shared/sx/ref/js.sx b/shared/sx/ref/js.sx index 7a4c296..d5cd2bd 100644 --- a/shared/sx/ref/js.sx +++ b/shared/sx/ref/js.sx @@ -1259,11 +1259,21 @@ (define js-emit-infix (fn ((op :as string) (args :as list)) - (let ((js-op (js-op-symbol op))) - (if (and (= (len args) 1) (= op "-")) - (str "(-" (js-expr (first args)) ")") - (str "(" (js-expr (first args)) - " " js-op " " (js-expr (nth args 1)) ")"))))) + (let ((js-op (js-op-symbol op)) + (n (len args))) + (cond + (and (= n 1) (= op "-")) + (str "(-" (js-expr (first args)) ")") + (= n 2) + (str "(" (js-expr (first args)) + " " js-op " " (js-expr (nth args 1)) ")") + ;; Variadic: left-fold (a op b op c op d ...) + :else + (let ((result (js-expr (first args)))) + (for-each (fn (arg) + (set! result (str "(" result " " js-op " " (js-expr arg) ")"))) + (rest args)) + result))))) ;; -------------------------------------------------------------------------- diff --git a/shared/sx/ref/parser.sx b/shared/sx/ref/parser.sx index 919806b..b224963 100644 --- a/shared/sx/ref/parser.sx +++ b/shared/sx/ref/parser.sx @@ -80,6 +80,9 @@ ;; -- Atom readers -- + (define hex-digit-value :effects [] + (fn (ch) (index-of "0123456789abcdef" (lower ch)))) + (define read-string :effects [] (fn () (set! pos (inc pos)) ;; skip opening " @@ -95,14 +98,29 @@ (= ch "\\") (do (set! pos (inc pos)) (let ((esc (nth source pos))) - (set! buf (str buf - (cond - (= esc "n") "\n" - (= esc "t") "\t" - (= esc "r") "\r" - :else esc))) - (set! pos (inc pos)) - (read-str-loop))) + (if (= esc "u") + ;; Unicode escape: \uXXXX → char + (do (set! pos (inc pos)) + (let ((d0 (hex-digit-value (nth source pos))) + (_ (set! pos (inc pos))) + (d1 (hex-digit-value (nth source pos))) + (_ (set! pos (inc pos))) + (d2 (hex-digit-value (nth source pos))) + (_ (set! pos (inc pos))) + (d3 (hex-digit-value (nth source pos))) + (_ (set! pos (inc pos)))) + (set! buf (str buf (char-from-code + (+ (* d0 4096) (* d1 256) (* d2 16) d3)))) + (read-str-loop))) + ;; Standard escapes: \n \t \r or literal + (do (set! buf (str buf + (cond + (= esc "n") "\n" + (= esc "t") "\t" + (= esc "r") "\r" + :else esc))) + (set! pos (inc pos)) + (read-str-loop))))) :else (do (set! buf (str buf ch)) (set! pos (inc pos)) diff --git a/shared/sx/ref/platform_js.py b/shared/sx/ref/platform_js.py index 2c5e9b8..9e10be3 100644 --- a/shared/sx/ref/platform_js.py +++ b/shared/sx/ref/platform_js.py @@ -983,6 +983,7 @@ PRIMITIVES_JS_MODULES: dict[str, str] = { PRIMITIVES["ends-with?"] = function(s, p) { var str = String(s); return str.indexOf(p, str.length - p.length) !== -1; }; PRIMITIVES["slice"] = function(c, a, b) { if (!c || typeof c.slice !== "function") { console.error("[sx-debug] slice called on non-sliceable:", typeof c, c, "a=", a, "b=", b, new Error().stack); return []; } return b !== undefined ? c.slice(a, b) : c.slice(a); }; PRIMITIVES["substring"] = function(s, a, b) { return String(s).substring(a, b); }; + PRIMITIVES["char-from-code"] = function(n) { return String.fromCharCode(n); }; PRIMITIVES["string-length"] = function(s) { return String(s).length; }; PRIMITIVES["string-contains?"] = function(s, sub) { return String(s).indexOf(String(sub)) !== -1; }; PRIMITIVES["concat"] = function() { @@ -1600,6 +1601,7 @@ PLATFORM_PARSER_JS = r""" return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\t/g, "\\t"); } function sxExprSource(e) { return typeof e === "string" ? e : String(e); } + var charFromCode = PRIMITIVES["char-from-code"]; """ diff --git a/shared/sx/ref/platform_py.py b/shared/sx/ref/platform_py.py index 8aaf74a..0744671 100644 --- a/shared/sx/ref/platform_py.py +++ b/shared/sx/ref/platform_py.py @@ -881,6 +881,7 @@ PRIMITIVES["zero?"] = lambda n: n == 0 "core.strings": ''' # core.strings PRIMITIVES["str"] = sx_str +PRIMITIVES["char-from-code"] = lambda n: chr(_b_int(n)) PRIMITIVES["upper"] = lambda s: str(s).upper() PRIMITIVES["lower"] = lambda s: str(s).lower() PRIMITIVES["trim"] = lambda s: str(s).strip() diff --git a/shared/sx/ref/primitives.sx b/shared/sx/ref/primitives.sx index 3687243..ec5a7cb 100644 --- a/shared/sx/ref/primitives.sx +++ b/shared/sx/ref/primitives.sx @@ -323,6 +323,11 @@ :returns "number" :doc "Length of string in characters.") +(define-primitive "char-from-code" + :params ((n :as number)) + :returns "string" + :doc "Convert Unicode code point to single-character string.") + (define-primitive "substring" :params ((s :as string) (start :as number) (end :as number)) :returns "string" diff --git a/shared/sx/ref/py.sx b/shared/sx/ref/py.sx index 2083e00..9a28375 100644 --- a/shared/sx/ref/py.sx +++ b/shared/sx/ref/py.sx @@ -830,11 +830,21 @@ (define py-emit-infix (fn ((op :as string) (args :as list) (cell-vars :as list)) - (let ((py-op (py-op-symbol op))) - (if (and (= (len args) 1) (= op "-")) - (str "(-" (py-expr-with-cells (first args) cell-vars) ")") - (str "(" (py-expr-with-cells (first args) cell-vars) - " " py-op " " (py-expr-with-cells (nth args 1) cell-vars) ")"))))) + (let ((py-op (py-op-symbol op)) + (n (len args))) + (cond + (and (= n 1) (= op "-")) + (str "(-" (py-expr-with-cells (first args) cell-vars) ")") + (= n 2) + (str "(" (py-expr-with-cells (first args) cell-vars) + " " py-op " " (py-expr-with-cells (nth args 1) cell-vars) ")") + ;; Variadic: left-fold (a op b op c op d ...) + :else + (let ((result (py-expr-with-cells (first args) cell-vars))) + (for-each (fn (arg) + (set! result (str "(" result " " py-op " " (py-expr-with-cells arg cell-vars) ")"))) + (rest args)) + result))))) ;; -------------------------------------------------------------------------- diff --git a/shared/sx/ref/sx_ref.py b/shared/sx/ref/sx_ref.py index 1c75517..367a9fb 100644 --- a/shared/sx/ref/sx_ref.py +++ b/shared/sx/ref/sx_ref.py @@ -851,6 +851,7 @@ PRIMITIVES["zero?"] = lambda n: n == 0 # core.strings PRIMITIVES["str"] = sx_str +PRIMITIVES["char-from-code"] = lambda n: chr(_b_int(n)) PRIMITIVES["upper"] = lambda s: str(s).upper() PRIMITIVES["lower"] = lambda s: str(s).lower() PRIMITIVES["trim"] = lambda s: str(s).strip() diff --git a/shared/sx/tests/run.py b/shared/sx/tests/run.py index 21791dc..b8e9c7a 100644 --- a/shared/sx/tests/run.py +++ b/shared/sx/tests/run.py @@ -940,6 +940,18 @@ def _load_types(env): # types.sx uses component-has-children (no ?), test runner has component-has-children? if "component-has-children" not in env: env["component-has-children"] = lambda c: getattr(c, 'has_children', False) + # types.sx uses map-dict for record type resolution + if "map-dict" not in env: + from shared.sx.types import Lambda as _Lambda + def _map_dict(fn, d): + result = {} + for k, v in d.items(): + if isinstance(fn, _Lambda): + result[k] = _trampoline(_eval([fn, k, v], env)) + else: + result[k] = fn(k, v) + return result + env["map-dict"] = _map_dict # Try bootstrapped types first, fall back to eval try: diff --git a/sx/sx/nav-data.sx b/sx/sx/nav-data.sx index 20c9fd3..d743beb 100644 --- a/sx/sx/nav-data.sx +++ b/sx/sx/nav-data.sx @@ -220,7 +220,11 @@ (dict :label "Spec Explorer" :href "/sx/(etc.(plan.spec-explorer))" :summary "The fifth ring — SX exploring itself. Per-function cards showing source, Python/JS/Z3 translations, platform dependencies, tests, proofs, and usage examples.") (dict :label "SX Protocol" :href "/sx/(etc.(plan.sx-protocol))" - :summary "S-expressions as a universal protocol for networked hypermedia — replacing URLs, HTTP verbs, query languages, and rendering with one evaluable format."))) + :summary "S-expressions as a universal protocol for networked hypermedia — replacing URLs, HTTP verbs, query languages, and rendering with one evaluable format.") + (dict :label "Scoped Effects" :href "/sx/(etc.(plan.scoped-effects))" + :summary "Algebraic effects as the unified foundation — spreads, islands, lakes, signals, and context are all instances of one primitive: a named scope with downward value, upward accumulation, and a propagation mode.") + (dict :label "Foundations" :href "/sx/(etc.(plan.foundations))" + :summary "The computational floor — from scoped effects through algebraic effects and delimited continuations to the CEK machine. Why three registers are irreducible, and the three-axis model (depth, topology, linearity)."))) (define reactive-islands-nav-items (list (dict :label "Overview" :href "/sx/(geography.(reactive))" diff --git a/sx/sx/page-functions.sx b/sx/sx/page-functions.sx index a46fc90..e8c67a6 100644 --- a/sx/sx/page-functions.sx +++ b/sx/sx/page-functions.sx @@ -528,4 +528,6 @@ "spec-explorer" '(~plans/spec-explorer/plan-spec-explorer-content) "sx-urls" '(~plans/sx-urls/plan-sx-urls-content) "sx-protocol" '(~plans/sx-protocol/plan-sx-protocol-content) + "scoped-effects" '(~plans/scoped-effects/plan-scoped-effects-content) + "foundations" '(~plans/foundations/plan-foundations-content) :else '(~plans/index/plans-index-content))))) diff --git a/sx/sx/plans/foundations.sx b/sx/sx/plans/foundations.sx new file mode 100644 index 0000000..be5a4c6 --- /dev/null +++ b/sx/sx/plans/foundations.sx @@ -0,0 +1,661 @@ +;; Foundations — The Computational Floor +;; From scoped effects to CEK: what's beneath algebraic effects and why it's the limit. + +(defcomp ~plans/foundations/plan-foundations-content () + (~docs/page :title "Foundations \u2014 The Computational Floor" + + (p :class "text-stone-500 text-sm italic mb-8" + "Scoped effects unify everything SX does. But they are not the bottom. " + "Beneath them is a hierarchy of increasingly fundamental primitives, " + "terminating at three irreducible things: an expression, an environment, and a continuation. " + "This document traces that hierarchy, proves where the floor is, " + "and maps the path from where SX stands to the deepest layer.") + + ;; ----------------------------------------------------------------------- + ;; The hierarchy + ;; ----------------------------------------------------------------------- + + (h2 :class "text-xl font-bold mt-12 mb-4" "The Hierarchy") + + (p "Every layer is definable in terms of the one below. " + "No layer can be decomposed without the layer beneath it.") + + (~docs/code-block :code + (str "Layer 0: CEK machine (expression + environment + continuation)\n" + "Layer 1: Continuations (shift / reset \u2014 delimited capture)\n" + "Layer 2: Algebraic effects (operations + handlers)\n" + "Layer 3: Scoped effects (+ region delimitation)\n" + "Layer 4: SX patterns (spread, provide, island, lake, signal, collect)")) + + (p "SX currently has layers 0, 1, and 4. " + "Layer 3 is the scoped-effects plan (provide/context/emit!). " + "Layer 2 falls out of layers 1 and 3 and doesn't need its own representation. " + "This document is about layers 0 through 2 \u2014 the machinery beneath scoped effects.") + + ;; ----------------------------------------------------------------------- + ;; Layer 0: The CEK machine + ;; ----------------------------------------------------------------------- + + (h2 :class "text-xl font-bold mt-12 mb-4" "Layer 0: The CEK Machine") + + (p "The CEK machine (Felleisen & Friedman, 1986) is a small-step evaluator with three registers:") + + (div :class "overflow-x-auto mb-6" + (table :class "min-w-full text-sm" + (thead (tr + (th :class "text-left pr-4 pb-2 font-semibold" "Register") + (th :class "text-left pr-4 pb-2 font-semibold" "Name") + (th :class "text-left pb-2 font-semibold" "Meaning"))) + (tbody + (tr (td :class "pr-4 py-1 font-mono" "C") (td :class "pr-4" "Control") (td "The expression being evaluated")) + (tr (td :class "pr-4 py-1 font-mono" "E") (td :class "pr-4" "Environment") (td "The bindings in scope \u2014 names to values")) + (tr (td :class "pr-4 py-1 font-mono" "K") (td :class "pr-4" "Kontinuation") (td "What to do with the result"))))) + + (p "Every step of evaluation is a transition: take the current (C, E, K), " + "produce a new (C\u2032, E\u2032, K\u2032). That's it. That's all computation is.") + + (p "Can you remove any register?") + + (ul :class "list-disc pl-6 mb-4 space-y-1" + (li (strong "Remove C") " \u2014 nothing to evaluate. No computation.") + (li (strong "Remove E") " \u2014 names are meaningless. Lose abstraction. " + "Every value must be literal, every function must be closed. No variables, no closures, no reuse.") + (li (strong "Remove K") " \u2014 no sequencing. The result of an expression goes nowhere. " + "Lose composition \u2014 you can evaluate one thing but never connect it to the next.")) + + (p "Three things, all necessary, none decomposable further.") + + (h3 :class "text-lg font-semibold mt-8 mb-3" "CEK in eval.sx") + + (p "SX already implements CEK. It just doesn't name it:") + + (~docs/code-block :code + (str ";; eval-expr IS the CEK transition function\n" + ";; C = expr, E = env, K = implicit (call stack / trampoline)\n" + "(define eval-expr\n" + " (fn (expr env)\n" + " (cond\n" + " (number? expr) expr ;; literal: C \u2192 value, K unchanged\n" + " (string? expr) expr\n" + " (symbol? expr) (env-get env expr) ;; variable: C + E \u2192 value\n" + " (list? expr) ;; compound: modify K (push frame)\n" + " (let ((head (first expr)))\n" + " ...))))\n" + "\n" + ";; The trampoline IS the K register made explicit:\n" + ";; instead of growing the call stack, thunks are continuations\n" + "(define trampoline\n" + " (fn (val)\n" + " (let loop ((v val))\n" + " (if (thunk? v)\n" + " (loop (eval-expr (thunk-expr v) (thunk-env v)))\n" + " v))))")) + + (p "The trampoline is the K register. Thunks are suspended continuations. " + "Tail-call optimization is replacing K instead of extending it. " + "SX's evaluation model is already a CEK machine \u2014 the plan is to make this explicit, " + "not to build something new.") + + ;; ----------------------------------------------------------------------- + ;; Layer 1: Delimited continuations + ;; ----------------------------------------------------------------------- + + (h2 :class "text-xl font-bold mt-12 mb-4" "Layer 1: Delimited Continuations") + + (p "Delimited continuations (Felleisen 1988, Danvy & Filinski 1990) " + "expose the K register as a first-class value:") + + (~docs/code-block :code + (str ";; reset marks a point in the continuation\n" + "(reset\n" + " (+ 1 (shift k ;; k = \"the rest up to reset\"\n" + " (k (k 10)))) ;; invoke it twice: 1 + (1 + 10) = 12\n" + ")")) + + (p (code "reset") " says: \"here's a boundary.\" " + (code "shift") " says: \"give me everything between here and that boundary as a callable function.\" " + "The captured continuation " (em "is") " a slice of the K register.") + + (p "This is already specced in SX (continuations.sx). What it gives us beyond CEK:") + + (ul :class "list-disc pl-6 mb-4 space-y-1" + (li (strong "Suspendable computation") " \u2014 capture where you are, resume later") + (li (strong "Backtracking") " \u2014 capture a choice point, try alternatives") + (li (strong "Coroutines") " \u2014 two computations yielding to each other") + (li (strong "Async as a library") " \u2014 async/await is shift/reset with a scheduler")) + + (h3 :class "text-lg font-semibold mt-8 mb-3" "The Filinski Embedding") + + (p "Filinski (1994) proved that " (code "shift/reset") " can encode " + (em "any") " monadic effect. State, exceptions, nondeterminism, I/O, " + "continuations themselves \u2014 all expressible as shift/reset patterns. " + "This means layer 1 is already computationally complete for effects. " + "Everything above is structure, not power.") + + (p "This is the key insight: " + (strong "layers 2\u20134 add no computational power. ") "They add " (em "structure") " \u2014 " + "they make effects composable, nameable, handleable. " + "But anything you can do with scoped effects, " + "you can do with raw shift/reset. You'd just hate writing it.") + + ;; ----------------------------------------------------------------------- + ;; Layer 2: Algebraic effects + ;; ----------------------------------------------------------------------- + + (h2 :class "text-xl font-bold mt-12 mb-4" "Layer 2: Algebraic Effects") + + (p "Plotkin & Pretnar (2009) observed that most effects have algebraic structure: " + "an operation (\"perform this effect\") and a handler (\"here's what that effect means\"). " + "The handler receives the operation's argument and a continuation to resume the program.") + + (~docs/code-block :code + (str ";; Pseudocode \u2014 algebraic effect style\n" + "(handle\n" + " (fn () (+ 1 (perform :ask \"what number?\")))\n" + " {:ask (fn (prompt resume)\n" + " (resume 41))})\n" + ";; => 42")) + + (p (code "perform") " is shift. " (code "handle") " is reset. " + "But with names and types. The handler pattern gives you:") + + (ul :class "list-disc pl-6 mb-4 space-y-1" + (li (strong "Named effects") " \u2014 not just \"capture here\" but \"I need state / logging / auth\"") + (li (strong "Composable handlers") " \u2014 stack handlers, each handling different effects") + (li (strong "Effect signatures") " \u2014 a function declares what effects it needs; " + "the type system ensures all effects are handled")) + + (p "Plotkin & Power (2003) proved that this captures: " + "state, exceptions, nondeterminism, I/O, cooperative concurrency, " + "probability, and backtracking. All as instances of one algebraic structure.") + + (h3 :class "text-lg font-semibold mt-8 mb-3" "What algebraic effects cannot express") + + (p "Standard algebraic effects have a limitation: their operations are " (em "first-order") ". " + "An operation takes a value and produces a value. But some effects need operations that " + "take " (em "computations") " as arguments:") + + (ul :class "list-disc pl-6 mb-4 space-y-1" + (li (code "catch") " \u2014 takes a computation that might throw, runs it with a handler") + (li (code "local") " \u2014 takes a computation, runs it with modified state") + (li (code "once") " \u2014 takes a nondeterministic computation, commits to its first result") + (li (code "scope") " \u2014 takes a computation, runs it within a delimited region")) + + (p "These are " (strong "higher-order effects") ". They need computations as arguments, " + "not just values. This is precisely what the scoped-effects plan addresses.") + + ;; ----------------------------------------------------------------------- + ;; Layer 3: Scoped effects (the bridge) + ;; ----------------------------------------------------------------------- + + (h2 :class "text-xl font-bold mt-12 mb-4" "Layer 3: Scoped and Higher-Order Effects") + + (p "Wu, Schrijvers, and Hinze (2014) introduced " (em "scoped effects") " \u2014 " + "algebraic effects extended with operations that delimit regions. " + "Pirog, Polesiuk, and Sieczkowski (2018) proved these are " + (strong "strictly more expressive") " than standard algebraic effects.") + + (p "Bach Poulsen and van der Rest (2023) generalized further with " + (em "hefty algebras") " \u2014 a framework that captures " (em "all") " known higher-order effects, " + "with scoped effects as a special case. This is the current state of the art.") + + (p "SX's " (code "provide") " is a scoped effect. It creates a region (the body), " + "makes a value available within it (context), and collects contributions from within it (emit/emitted). " + "This is why it can express things that plain algebraic effects can't: " + "the region boundary is part of the effect, not an accident of the call stack.") + + ;; ----------------------------------------------------------------------- + ;; The floor proof + ;; ----------------------------------------------------------------------- + + (h2 :class "text-xl font-bold mt-12 mb-4" "Why Layer 0 Is the Floor") + + (p "Two independent arguments:") + + (h3 :class "text-lg font-semibold mt-8 mb-3" "1. Church\u2013Turing") + + (p "The Church\u2013Turing thesis: any effectively computable function can be computed by " + "a Turing machine (equivalently, by the lambda calculus, equivalently, by the CEK machine). " + "This has withstood 90 years of attempts to disprove it. " + "No formal system has ever been shown to compute more.") + + (p "This means CEK captures " (em "all") " computation. Adding more primitives doesn't let you " + "compute anything new. It only changes what's " (em "convenient") " to express.") + + (h3 :class "text-lg font-semibold mt-8 mb-3" "2. Irreducibility of C, E, K") + + (p "The three registers are independently necessary:") + + (ul :class "list-disc pl-6 mb-4 space-y-1" + (li (strong "C without E") " = combinatory logic. Turing-complete but inhumane. " + "No named bindings \u2014 everything via S, K, I combinators. " + "Proves you can drop E in theory, but the resulting system " + "can't express abstraction (which is what E provides).") + (li (strong "C without K") " = single expression evaluation. " + "You can compute one thing but can't compose it with anything. " + "Technically you can encode K into C (CPS transform), " + "but this transforms the expression to include the continuation explicitly \u2014 " + "K hasn't been removed, just moved into C.") + (li (strong "E without C") " = a phone book with no one to call.") + (li (strong "K without C") " = a to-do list with nothing on it.")) + + (p "You can " (em "encode") " any register into another (CPS eliminates K, " + "De Bruijn indices eliminate E), but encoding isn't elimination. " + "The information is still there, just hidden in a different representation. " + "Three independent concerns; three registers.") + + ;; ----------------------------------------------------------------------- + ;; Open questions + ;; ----------------------------------------------------------------------- + + (h2 :class "text-xl font-bold mt-12 mb-4" "Open Questions") + + (p "The hierarchy above is well-established for " (em "sequential") " computation. " + "But there are orthogonal axes where the story is incomplete:") + + (h3 :class "text-lg font-semibold mt-8 mb-3" "Concurrency") + + (p "Scoped effects assume tree-shaped execution: one thing happens, then the next. " + "But real computation forks:") + + (ul :class "list-disc pl-6 mb-4 space-y-1" + (li "Video processing \u2014 pipeline stages run in parallel, frames are processed concurrently") + (li "CI/CD \u2014 test suites fork, builds parallelize, deployment is staged") + (li "Web rendering \u2014 async I/O, streaming SSE, suspense boundaries") + (li "Art DAG \u2014 the entire engine is a DAG of dependent transforms")) + + (p "The \u03c0-calculus (Milner 1999) handles concurrency well but not effects. " + "Effect systems handle effects well but not concurrency. " + "Combining them is an open problem. " + "Brachth\u00e4user, Schuster, and Ostermann (2020) have partial results for " + "algebraic effects with multi-shot handlers (where the continuation can be invoked " + "on multiple concurrent threads), but a full synthesis doesn't exist yet.") + + (p "For SX, this matters because the Art DAG is fundamentally a concurrent execution engine. " + "If SX ever specifies DAG execution natively, it'll need something beyond scoped effects.") + + (h3 :class "text-lg font-semibold mt-8 mb-3" "Linearity") + + (p "Can an effect handler be invoked more than once? Must a scope be entered? " + "Must every emitted value be consumed?") + + (ul :class "list-disc pl-6 mb-4 space-y-1" + (li (strong "Unrestricted") " \u2014 use as many times as you want (current SX)") + (li (strong "Affine") " \u2014 use at most once (Rust's ownership model)") + (li (strong "Linear") " \u2014 use exactly once (quantum no-cloning, exactly-once delivery)")) + + (p "Linear types constrain the continuation: if a handler is linear, " + "it " (em "must") " resume the computation exactly once. No dropping (resource leak), " + "no duplicating (nondeterminism). This connects to:") + + (ul :class "list-disc pl-6 mb-4 space-y-1" + (li "Resource management \u2014 file handles that " (em "must") " be closed") + (li "Protocol correctness \u2014 a session type that must complete") + (li "Transaction semantics \u2014 exactly-once commit/rollback") + (li "Quantum computing \u2014 no-cloning theorem as a type constraint")) + + (p "Benton (1994) established the connection between linear logic and computation. " + "Adding linear effects to SX would constrain what handlers can do, " + "enabling stronger guarantees about resource safety. " + "This is orthogonal to depth \u2014 it's about " (em "discipline") ", not " (em "power") ".") + + (h3 :class "text-lg font-semibold mt-8 mb-3" "Higher-order effects beyond hefty algebras") + + (p "Nobody has proved that hefty algebras capture " (em "all possible") " higher-order effects. " + "The hierarchy might continue. This is active research with no clear terminus. " + "The question \"is there a structured effect that no framework can express?\" " + "is analogous to G\u00f6del's incompleteness \u2014 it may be that every effect framework " + "has blind spots, and the only \"complete\" system is raw continuations (layer 1), " + "which are universal but unstructured.") + + ;; ----------------------------------------------------------------------- + ;; The three-axis model + ;; ----------------------------------------------------------------------- + + (h2 :class "text-xl font-bold mt-12 mb-4" "The Three-Axis Model") + + (p "The deepest primitive is not a single thing. " + "It's a point in a three-dimensional space:") + + (~docs/code-block :code + (str "depth: CEK \u2192 continuations \u2192 algebraic effects \u2192 scoped effects\n" + "topology: sequential \u2192 concurrent \u2192 distributed\n" + "linearity: unrestricted \u2192 affine \u2192 linear")) + + (p "Each axis is independent. You can have:") + + (ul :class "list-disc pl-6 mb-4 space-y-1" + (li "Scoped effects + sequential + unrestricted \u2014 " (strong "SX today")) + (li "Scoped effects + concurrent + unrestricted \u2014 the Art DAG integration") + (li "Scoped effects + sequential + linear \u2014 resource-safe SX") + (li "Algebraic effects + concurrent + linear \u2014 something like Rust + Tokio + effect handlers") + (li "CEK + distributed + unrestricted \u2014 raw Erlang/BEAM")) + + (p "SX's current position and trajectory:") + + (div :class "overflow-x-auto mb-6" + (table :class "min-w-full text-sm" + (thead (tr + (th :class "text-left pr-4 pb-2 font-semibold" "Axis") + (th :class "text-left pr-4 pb-2 font-semibold" "Current") + (th :class "text-left pr-4 pb-2 font-semibold" "Next") + (th :class "text-left pb-2 font-semibold" "Eventual"))) + (tbody + (tr (td :class "pr-4 py-1" "Depth") + (td :class "pr-4" "Layer 4 (patterns) + Layer 1 (continuations)") + (td :class "pr-4" "Layer 3 (scoped effects)") + (td "Layer 0 (explicit CEK)")) + (tr (td :class "pr-4 py-1" "Topology") + (td :class "pr-4" "Sequential") + (td :class "pr-4" "Async I/O (partial concurrency)") + (td "DAG execution (Art DAG)")) + (tr (td :class "pr-4 py-1" "Linearity") + (td :class "pr-4" "Unrestricted") + (td :class "pr-4" "Unrestricted") + (td "Affine (resource safety)"))))) + + ;; ----------------------------------------------------------------------- + ;; What explicit CEK gives SX + ;; ----------------------------------------------------------------------- + + (h2 :class "text-xl font-bold mt-12 mb-4" "What Explicit CEK Gives SX") + + (p "Making the CEK machine explicit in the spec (rather than implicit in eval-expr) enables:") + + (h3 :class "text-lg font-semibold mt-8 mb-3" "Stepping") + + (p "A CEK machine transitions one step at a time. " + "If the transition function is explicit, you can:") + + (ul :class "list-disc pl-6 mb-4 space-y-1" + (li "Single-step through evaluation (debugger)") + (li "Pause and serialize mid-evaluation (suspend to disk, resume on another machine)") + (li "Instrument each step (profiling, tracing, time-travel debugging)") + (li "Interleave steps from multiple computations (cooperative scheduling without OS threads)")) + + (h3 :class "text-lg font-semibold mt-8 mb-3" "Serializable computation") + + (p "If C, E, and K are all data structures (not host stack frames), " + "the entire computation state is serializable:") + + (~docs/code-block :code + (str ";; Freeze a computation mid-flight\n" + "(let ((state (capture-cek)))\n" + " (send-to-worker state) ;; ship to another machine\n" + " ;; or: (store state) ;; persist to disk\n" + " ;; or: (fork state) ;; run the same computation twice\n" + " )")) + + (p "This connects to content-addressed computation: " + "a CID identifying a CEK state is a pointer to a computation in progress. " + "Resume it anywhere. Verify it. Cache it. Share it.") + + (h3 :class "text-lg font-semibold mt-8 mb-3" "Formal verification") + + (p "An explicit CEK machine is a state machine. State machines are verifiable. " + "You can prove properties about all possible execution paths: " + "termination, resource bounds, effect safety. " + "The theorem prover (prove.sx) could verify CEK transitions directly.") + + ;; ----------------------------------------------------------------------- + ;; SX's existing layers + ;; ----------------------------------------------------------------------- + + (h2 :class "text-xl font-bold mt-12 mb-4" "SX's Existing Layers") + + (p "SX already has most of the hierarchy, specced or planned:") + + (div :class "overflow-x-auto mb-6" + (table :class "min-w-full text-sm" + (thead (tr + (th :class "text-left pr-4 pb-2 font-semibold" "Layer") + (th :class "text-left pr-4 pb-2 font-semibold" "SX has") + (th :class "text-left pr-4 pb-2 font-semibold" "Where") + (th :class "text-left pb-2 font-semibold" "Status"))) + (tbody + (tr (td :class "pr-4 py-1" "0 \u2014 CEK") + (td :class "pr-4" "eval-expr + trampoline") + (td :class "pr-4" "eval.sx") + (td "Implicit. Works, but CEK is hidden in the host call stack.")) + (tr (td :class "pr-4 py-1" "1 \u2014 Continuations") + (td :class "pr-4" "shift / reset") + (td :class "pr-4" "continuations.sx") + (td "Specced. Bootstraps to Python and JavaScript.")) + (tr (td :class "pr-4 py-1" "2 \u2014 Algebraic effects") + (td :class "pr-4" "\u2014") + (td :class "pr-4" "\u2014") + (td "Falls out of layers 1 + 3. No dedicated spec needed.")) + (tr (td :class "pr-4 py-1" "3 \u2014 Scoped effects") + (td :class "pr-4" "provide / context / emit!") + (td :class "pr-4" "scoped-effects plan") + (td "Planned. Implements over eval.sx + adapters.")) + (tr (td :class "pr-4 py-1" "4 \u2014 Patterns") + (td :class "pr-4" "spread, collect, island, lake, signal, store") + (td :class "pr-4" "various .sx files") + (td "Implemented. Will be redefined in terms of layer 3."))))) + + ;; ----------------------------------------------------------------------- + ;; Implementation path + ;; ----------------------------------------------------------------------- + + (h2 :class "text-xl font-bold mt-12 mb-4" "Implementation Path") + + (h3 :class "text-lg font-semibold mt-8 mb-3" "Phase 1: Scoped effects (Layer 3)") + + (p "This is the scoped-effects plan. Immediate next step.") + + (ul :class "list-disc pl-6 mb-4 space-y-1" + (li "Spec " (code "provide") ", " (code "context") ", " (code "emit!") ", " (code "emitted") " in eval.sx") + (li "Implement in all adapters (HTML, DOM, SX wire, async)") + (li "Redefine spread, collect, and reactive-spread as instances") + (li "Prove existing tests still pass")) + + (h3 :class "text-lg font-semibold mt-8 mb-3" "Phase 2: Effect signatures (Layer 2)") + + (p "Add optional effect annotations to function definitions:") + + (~docs/code-block :code + (str ";; Declare what effects a function uses\n" + "(define fetch-user :effects [io auth]\n" + " (fn (id) ...))\n" + "\n" + ";; Pure function \u2014 no effects\n" + "(define add :effects []\n" + " (fn (a b) (+ a b)))\n" + "\n" + ";; Scoped effect \u2014 uses context + emit\n" + "(define themed-heading :effects [context]\n" + " (fn (text)\n" + " (h1 :style (str \"color:\" (get (context \"theme\") :primary))\n" + " text)))")) + + (p "Effect signatures are checked at registration time. " + "A function that declares " (code ":effects []") " cannot call " (code "emit!") " or " (code "context") ". " + "This is layer 2 \u2014 algebraic effect structure \u2014 applied as a type discipline.") + + (h3 :class "text-lg font-semibold mt-8 mb-3" "Phase 3: Explicit CEK (Layer 0)") + + (p "Refactor eval.sx to expose the CEK registers as data:") + + (~docs/code-block :code + (str ";; The CEK state is a value\n" + "(define-record CEK\n" + " :control expr ;; the expression\n" + " :env env ;; the bindings\n" + " :kont kont) ;; the continuation stack\n" + "\n" + ";; One step\n" + "(define step :effects []\n" + " (fn (cek)\n" + " (case (type-of (get cek :control))\n" + " :literal (apply-kont (get cek :kont) (get cek :control))\n" + " :symbol (apply-kont (get cek :kont)\n" + " (env-get (get cek :env) (get cek :control)))\n" + " :list (let ((head (first (get cek :control))))\n" + " ...))))\n" + "\n" + ";; Run to completion\n" + "(define run :effects []\n" + " (fn (cek)\n" + " (if (final? cek)\n" + " (get cek :control)\n" + " (run (step cek)))))")) + + (p "This makes computation " (em "inspectable") ". A CEK state can be:") + + (ul :class "list-disc pl-6 mb-4 space-y-1" + (li "Serialized to a CID (content-addressed frozen computation)") + (li "Single-stepped by a debugger") + (li "Forked (run the same state with different inputs)") + (li "Migrated (ship to another machine, resume there)") + (li "Verified (prove properties about all reachable states)")) + + (h3 :class "text-lg font-semibold mt-8 mb-3" "Phase 4: Concurrent effects (topology axis)") + + (p "Extend the CEK machine to support multiple concurrent computations:") + + (~docs/code-block :code + (str ";; Fork: create two CEK states from one\n" + "(define fork :effects [concurrency]\n" + " (fn (cek)\n" + " (list (step cek) (step cek))))\n" + "\n" + ";; Join: merge results from two computations\n" + "(define join :effects [concurrency]\n" + " (fn (cek-a cek-b combine)\n" + " (combine (run cek-a) (run cek-b))))\n" + "\n" + ";; Channel: typed communication between concurrent computations\n" + "(define channel :effects [concurrency]\n" + " (fn (name)\n" + " {:send (fn (v) (emit! name v))\n" + " :recv (fn () (shift k (handle-recv name k)))}))")) + + (p "This is where scoped effects meet the \u03c0-calculus. " + "The Art DAG is the natural first consumer \u2014 " + "its execution model is already a DAG of dependent computations.") + + (h3 :class "text-lg font-semibold mt-8 mb-3" "Phase 5: Linear effects (linearity axis)") + + (p "Add resource-safety constraints:") + + (~docs/code-block :code + (str ";; Linear scope: must be entered, must complete\n" + "(define-linear open-file :effects [io linear]\n" + " (fn (path)\n" + " (provide \"file\" (fs-open path)\n" + " ;; body MUST consume the file handle exactly once\n" + " ;; compiler error if handle is dropped or duplicated\n" + " (yield (file-read (context \"file\")))\n" + " ;; cleanup runs unconditionally\n" + " )))")) + + (p "This is the furthest horizon. " + "Linear effects connect SX to session types, protocol verification, " + "and the kind of safety guarantees that Rust provides at the type level.") + + ;; ----------------------------------------------------------------------- + ;; The Curry-Howard correspondence + ;; ----------------------------------------------------------------------- + + (h2 :class "text-xl font-bold mt-12 mb-4" "The Curry\u2013Howard Correspondence") + + (p "At the deepest level, computation and logic are the same thing:") + + (div :class "overflow-x-auto mb-6" + (table :class "min-w-full text-sm" + (thead (tr + (th :class "text-left pr-4 pb-2 font-semibold" "Logic") + (th :class "text-left pr-4 pb-2 font-semibold" "Computation") + (th :class "text-left pb-2 font-semibold" "SX"))) + (tbody + (tr (td :class "pr-4 py-1" "Proposition") + (td :class "pr-4" "Type") + (td "Effect signature")) + (tr (td :class "pr-4 py-1" "Proof") + (td :class "pr-4" "Program") + (td "Implementation")) + (tr (td :class "pr-4 py-1" "Proof normalization") + (td :class "pr-4" "Evaluation") + (td "CEK transitions")) + (tr (td :class "pr-4 py-1" "Hypothesis") + (td :class "pr-4" "Variable binding") + (td "Environment (E register)")) + (tr (td :class "pr-4 py-1" "Implication (A \u2192 B)") + (td :class "pr-4" "Function type") + (td "Lambda")) + (tr (td :class "pr-4 py-1" "Conjunction (A \u2227 B)") + (td :class "pr-4" "Product type") + (td "Dict / record")) + (tr (td :class "pr-4 py-1" "Disjunction (A \u2228 B)") + (td :class "pr-4" "Sum type / union") + (td "Case dispatch")) + (tr (td :class "pr-4 py-1" "Universal (\u2200x.P)") + (td :class "pr-4" "Polymorphism") + (td "Generic components")) + (tr (td :class "pr-4 py-1" "Existential (\u2203x.P)") + (td :class "pr-4" "Abstract type") + (td "Opaque scope")) + (tr (td :class "pr-4 py-1" "Double negation (\u00ac\u00acA)") + (td :class "pr-4" "Continuation") + (td "shift/reset"))))) + + (p "A program is a proof that a computation is possible. " + "An effect signature is a proposition about what the program does to the world. " + "A handler is a proof that the effect can be realized. " + "Evaluation is proof normalization \u2014 the systematic simplification of a proof to its normal form.") + + (p "This is why CEK is the floor: it " (em "is") " logic. " + "Expression = proposition, environment = hypotheses, continuation = proof context. " + "You can't go beneath logic and still be doing computation. " + "The Curry\u2013Howard correspondence is not a metaphor. It's an isomorphism.") + + ;; ----------------------------------------------------------------------- + ;; Summary + ;; ----------------------------------------------------------------------- + + (h2 :class "text-xl font-bold mt-12 mb-4" "Summary") + + (p "The path from where SX stands to the computational floor:") + + (div :class "overflow-x-auto mb-6" + (table :class "min-w-full text-sm" + (thead (tr + (th :class "text-left pr-4 pb-2 font-semibold" "Step") + (th :class "text-left pr-4 pb-2 font-semibold" "What") + (th :class "text-left pr-4 pb-2 font-semibold" "Enables") + (th :class "text-left pb-2 font-semibold" "Depends on"))) + (tbody + (tr (td :class "pr-4 py-1" "1") + (td :class "pr-4" "Scoped effects") + (td :class "pr-4" "Unify spread/collect/island/lake/context") + (td "eval.sx + adapters")) + (tr (td :class "pr-4 py-1" "2") + (td :class "pr-4" "Effect signatures") + (td :class "pr-4" "Static effect checking, pure/IO boundary") + (td "types.sx + scoped effects")) + (tr (td :class "pr-4 py-1" "3") + (td :class "pr-4" "Explicit CEK") + (td :class "pr-4" "Stepping, serialization, migration, verification") + (td "eval.sx refactor")) + (tr (td :class "pr-4 py-1" "4") + (td :class "pr-4" "Concurrent effects") + (td :class "pr-4" "DAG execution, parallel pipelines") + (td "CEK + channels")) + (tr (td :class "pr-4 py-1" "5") + (td :class "pr-4" "Linear effects") + (td :class "pr-4" "Resource safety, protocol verification") + (td "Effect signatures + linear types"))))) + + (p "Each step is independently valuable. The first is immediate. " + "The last may be years out or may never arrive. " + "The hierarchy exists whether we traverse it or not \u2014 " + "it's the structure of computation itself, " + "and SX is better for knowing where it sits within it.") + + (p :class "text-stone-500 text-sm italic mt-12" + "The true foundation of any language is not its syntax or its runtime " + "but the mathematical structure it participates in. " + "SX is an s-expression language, which makes it a notation for the lambda calculus, " + "which is a notation for logic, which is the structure of thought. " + "The floor is thought itself. We can't go deeper, because there's no one left to dig."))) diff --git a/sx/sx/plans/scoped-effects.sx b/sx/sx/plans/scoped-effects.sx new file mode 100644 index 0000000..d326fac --- /dev/null +++ b/sx/sx/plans/scoped-effects.sx @@ -0,0 +1,444 @@ +;; Scoped Effects — The Deepest Primitive +;; Algebraic effects as the unified foundation for spreads, islands, lakes, and context. + +(defcomp ~plans/scoped-effects/plan-scoped-effects-content () + (~docs/page :title "Scoped Effects — The Deepest Primitive" + + (p :class "text-stone-500 text-sm italic mb-8" + "Everything SX does — spreads, signals, islands, lakes, morph, context, collect — " + "is an instance of one pattern: a named scope with a value flowing down, " + "contributions flowing up, and a propagation mode determining when effects are realised. " + "This plan traces that pattern to its foundation in algebraic effects " + "and proposes " (code "scope") " as the single primitive underneath everything.") + + ;; ===================================================================== + ;; I. The Observation + ;; ===================================================================== + + (~docs/section :title "The observation" :id "observation" + + (p "SX has accumulated several mechanisms that all do variations of the same thing:") + + (table :class "w-full text-sm border-collapse mb-6" + (thead + (tr :class "border-b border-stone-300" + (th :class "text-left py-2 pr-4" "Mechanism") + (th :class "text-left py-2 pr-4" "Direction") + (th :class "text-left py-2 pr-4" "When") + (th :class "text-left py-2" "Boundary"))) + (tbody + (tr :class "border-b border-stone-100" + (td :class "py-2 pr-4 font-mono text-xs" "spread") + (td :class "py-2 pr-4" "child → parent") + (td :class "py-2 pr-4" "render time") + (td :class "py-2" "element")) + (tr :class "border-b border-stone-100" + (td :class "py-2 pr-4 font-mono text-xs" "collect! / collected") + (td :class "py-2 pr-4" "child → ancestor") + (td :class "py-2 pr-4" "render time") + (td :class "py-2" "render tree")) + (tr :class "border-b border-stone-100" + (td :class "py-2 pr-4 font-mono text-xs" "provide / context") + (td :class "py-2 pr-4" "ancestor → child") + (td :class "py-2 pr-4" "render time") + (td :class "py-2" "render subtree")) + (tr :class "border-b border-stone-100" + (td :class "py-2 pr-4 font-mono text-xs" "signal / effect") + (td :class "py-2 pr-4" "value → subscribers") + (td :class "py-2 pr-4" "signal change") + (td :class "py-2" "reactive scope")) + (tr :class "border-b border-stone-100" + (td :class "py-2 pr-4 font-mono text-xs" "defisland") + (td :class "py-2 pr-4" "server → client") + (td :class "py-2 pr-4" "hydration") + (td :class "py-2" "server/client")) + (tr :class "border-b border-stone-100" + (td :class "py-2 pr-4 font-mono text-xs" "lake") + (td :class "py-2 pr-4" "server → client slot") + (td :class "py-2 pr-4" "navigation/morph") + (td :class "py-2" "client/server")) + (tr :class "border-b border-stone-100" + (td :class "py-2 pr-4 font-mono text-xs" "reactive-spread") + (td :class "py-2 pr-4" "child → parent") + (td :class "py-2 pr-4" "signal change") + (td :class "py-2" "element")) + (tr + (td :class "py-2 pr-4 font-mono text-xs" "def-store / use-store") + (td :class "py-2 pr-4" "island ↔ island") + (td :class "py-2 pr-4" "signal change") + (td :class "py-2" "cross-island")))) + + (p "Eight mechanisms. Each invented separately for a real use case. " + "Each with its own API surface, its own implementation path, its own section in the spec.") + + (p "But look at the table. Every row is: " (em "a named region, " + "a direction of data flow, a moment when effects are realised, " + "and a boundary that determines what crosses") ". They are all the same shape.")) + + + ;; ===================================================================== + ;; II. Three propagation modes + ;; ===================================================================== + + (~docs/section :title "Three propagation modes" :id "propagation" + + (p "The \"when\" column has exactly three values. These are the three " + "propagation modes of the render tree:") + + (~docs/subsection :title "Render propagation" + (p "Effects are realised " (strong "during the render pass") ". " + "Synchronous, one-shot, deterministic. The renderer walks the tree, " + "encounters effects, handles them immediately.") + (p "Instances: " (code "provide/context/emit!") ", " (code "spread") ", " + (code "collect!/collected") ".") + (p "Characteristic: by the time rendering finishes, all render-time effects " + "are fully resolved. The output is static HTML with everything baked in.")) + + (~docs/subsection :title "Reactive propagation" + (p "Effects are realised " (strong "when signals change") ". " + "Fine-grained, incremental, potentially infinite. A signal notifies " + "its subscribers, each subscriber updates its piece of the DOM.") + (p "Instances: " (code "signal/effect/computed") ", " (code "reactive-spread") ", " + (code "defisland") ", " (code "def-store/use-store") ".") + (p "Characteristic: the initial render produces a snapshot. " + "Subsequent changes propagate through the signal graph without re-rendering. " + "Each signal write touches only the DOM nodes that actually depend on it.")) + + (~docs/subsection :title "Morph propagation" + (p "Effects are realised " (strong "when the server sends new HTML") ". " + "Network-bound, server-initiated, structural. The morph algorithm " + "compares old and new DOM trees and surgically updates.") + (p "Instances: " (code "lake") ", full-page morph, " (code "sx-get/sx-post") " responses.") + (p "Characteristic: the server is the source of truth for content. " + "The client has reactive state that must be " (em "protected") " during morph. " + "Lakes mark the boundaries: morph enters islands, finds lake elements by ID, " + "updates their children, leaves reactive attrs untouched."))) + + + ;; ===================================================================== + ;; III. The scope primitive + ;; ===================================================================== + + (~docs/section :title "The scope primitive" :id "scope" + + (p "A " (code "scope") " is a named region of the render tree with three properties:") + (ol :class "space-y-2 text-stone-600" + (li (strong "Value") " — data flowing " (em "downward") " to descendants (readable via " (code "context") ")") + (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")) + + (p "Every existing mechanism is a scope with a specific configuration:") + + (table :class "w-full text-sm border-collapse mb-6" + (thead + (tr :class "border-b border-stone-300" + (th :class "text-left py-2 pr-4" "Mechanism") + (th :class "text-left py-2 pr-4" "Scope equivalent") + (th :class "text-left py-2" "Propagation"))) + (tbody + (tr :class "border-b border-stone-100" + (td :class "py-2 pr-4 font-mono text-xs" "provide") + (td :class "py-2 pr-4 font-mono text-xs" "(scope name :value v body)") + (td :class "py-2" ":render")) + (tr :class "border-b border-stone-100" + (td :class "py-2 pr-4 font-mono text-xs" "defisland") + (td :class "py-2 pr-4 font-mono text-xs" "(scope name :propagation :reactive body)") + (td :class "py-2" ":reactive")) + (tr :class "border-b border-stone-100" + (td :class "py-2 pr-4 font-mono text-xs" "lake") + (td :class "py-2 pr-4 font-mono text-xs" "(scope name :propagation :morph body)") + (td :class "py-2" ":morph")) + (tr :class "border-b border-stone-100" + (td :class "py-2 pr-4 font-mono text-xs" "spread") + (td :class "py-2 pr-4 font-mono text-xs" "(emit! :element-attrs dict)") + (td :class "py-2" ":render (implicit)")) + (tr :class "border-b border-stone-100" + (td :class "py-2 pr-4 font-mono text-xs" "collect!") + (td :class "py-2 pr-4 font-mono text-xs" "(emit! name value)") + (td :class "py-2" ":render")) + (tr + (td :class "py-2 pr-4 font-mono text-xs" "def-store") + (td :class "py-2 pr-4 font-mono text-xs" "(scope name :value (signal v) :propagation :reactive)") + (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")) + (p "Three scopes, three propagation modes, nested naturally. " + "The reactive scope manages signal lifecycle. " + "The render scope provides downward context. " + "The morph scope marks where server content flows in. " + "Each scope handles its own effects without knowing about the others."))) + + + ;; ===================================================================== + ;; IV. The 2×3 matrix + ;; ===================================================================== + + (~docs/section :title "The 2×3 matrix" :id "matrix" + + (p "Direction (up/down) × propagation mode (render/reactive/morph) gives six cells. " + "Every mechanism in SX occupies one:") + + (table :class "w-full text-sm border-collapse mb-6" + (thead + (tr :class "border-b border-stone-300" + (th :class "text-left py-2 pr-4" "") + (th :class "text-left py-2 pr-4" "Render-time") + (th :class "text-left py-2 pr-4" "Reactive") + (th :class "text-left py-2" "Morph"))) + (tbody + (tr :class "border-b border-stone-100" + (td :class "py-2 pr-4 font-semibold" "Down ↓") + (td :class "py-2 pr-4 font-mono text-xs" "context") + (td :class "py-2 pr-4 font-mono text-xs" "reactive context") + (td :class "py-2 font-mono text-xs" "server props")) + (tr :class "border-b border-stone-100" + (td :class "py-2 pr-4 font-semibold" "Up ↑") + (td :class "py-2 pr-4 font-mono text-xs" "emit! / spread") + (td :class "py-2 pr-4 font-mono text-xs" "reactive-spread") + (td :class "py-2 font-mono text-xs" "lake contributions")) + (tr + (td :class "py-2 pr-4 font-semibold" "Both ↕") + (td :class "py-2 pr-4 font-mono text-xs" "provide") + (td :class "py-2 pr-4 font-mono text-xs" "island") + (td :class "py-2 font-mono text-xs" "full morph")))) + + (p "Some cells are already implemented. Some are new. " + "The point is that " (code "scope") " fills them all from " (strong "one primitive") ". " + "You don't need to invent a new mechanism for each cell — you configure the scope.") + + (~docs/subsection :title "The new cell: reactive context" + (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")) + (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."))) + + + ;; ===================================================================== + ;; V. Algebraic effects + ;; ===================================================================== + + (~docs/section :title "Algebraic effects: the deepest layer" :id "algebraic-effects" + + (p "The scope primitive has a name in programming language theory: " + (strong "algebraic effects with handlers") ".") + + (~docs/subsection :title "The correspondence" + (table :class "w-full text-sm border-collapse mb-6" + (thead + (tr :class "border-b border-stone-300" + (th :class "text-left py-2 pr-4" "Algebraic effects") + (th :class "text-left py-2 pr-4" "SX") + (th :class "text-left py-2" "What it does"))) + (tbody + (tr :class "border-b border-stone-100" + (td :class "py-2 pr-4 font-mono text-xs" "perform effect") + (td :class "py-2 pr-4 font-mono text-xs" "emit!") + (td :class "py-2" "Raise an effect upward through the tree")) + (tr :class "border-b border-stone-100" + (td :class "py-2 pr-4 font-mono text-xs" "handle effect") + (td :class "py-2 pr-4 font-mono text-xs" "scope / provide") + (td :class "py-2" "Catch and interpret the effect")) + (tr :class "border-b border-stone-100" + (td :class "py-2 pr-4 font-mono text-xs" "ask") + (td :class "py-2 pr-4 font-mono text-xs" "context") + (td :class "py-2" "Read the nearest handler's value")) + (tr :class "border-b border-stone-100" + (td :class "py-2 pr-4 font-mono text-xs" "handler nesting") + (td :class "py-2 pr-4 font-mono text-xs" "scope nesting") + (td :class "py-2" "Inner handlers shadow outer ones")) + (tr + (td :class "py-2 pr-4 font-mono text-xs" "resumption") + (td :class "py-2 pr-4 font-mono text-xs" "reactive propagation") + (td :class "py-2" "Handler can re-invoke the continuation")))) + + (p "The last row is the deep one. In algebraic effect theory, a handler can " + (strong "resume") " the computation that performed the effect — optionally with " + "a different value. This is exactly what reactive propagation does: when a signal " + "changes, the effect handler (the island scope) re-invokes the subscribed " + "continuations (the effect callbacks) with the new value.")) + + (~docs/subsection :title "Why this matters" + (p "Algebraic effects are " (strong "the most general control flow abstraction known") ". " + "Exceptions, generators, async/await, coroutines, backtracking, and " + "nondeterminism are all instances of algebraic effects with different handler strategies.") + (p "SX's three propagation modes are three handler strategies:") + (ul :class "list-disc pl-5 space-y-2 text-stone-600" + (li (strong "Render") " = " (em "one-shot") " handler — handle the effect immediately, " + "don't resume. Like " (code "try/catch") " for rendering.") + (li (strong "Reactive") " = " (em "multi-shot") " handler — handle the effect, " + "then re-handle it every time the value changes. Like a generator that yields " + "whenever a signal fires.") + (li (strong "Morph") " = " (em "external resumption") " handler — the server decides " + "when to resume, sending new content over the network. Like an async/await " + "where the promise is resolved by a different machine."))) + + (~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")) + (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."))) + + + ;; ===================================================================== + ;; VI. What scope enables + ;; ===================================================================== + + (~docs/section :title "What scope enables" :id "enables" + + (~docs/subsection :title "1. New propagation modes" + (p "The three modes (render, reactive, morph) are not hardcoded — they are " + "handler strategies. New strategies can be added without new primitives:") + (ul :class "list-disc pl-5 space-y-2 text-stone-600" + (li (strong ":animation") " — effects realised on requestAnimationFrame, " + "batched per frame, interruptible by higher-priority updates") + (li (strong ":idle") " — effects deferred to requestIdleCallback, " + "used for non-urgent background updates (the transition pattern)") + (li (strong ":stream") " — effects realised as server-sent events arrive, " + "for live data (scores, prices, notifications)") + (li (strong ":worker") " — effects computed in a Web Worker, " + "results propagated back to the main thread"))) + + (~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")) + (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.")) + + (~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")) + (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. " + "The three scope types naturally produce the islands-and-lakes architecture — " + "not as a special case, but as the " (em "only") " case.")) + + (~docs/subsection :title "4. Cross-scope communication" + (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")) + (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."))) + + + ;; ===================================================================== + ;; VII. Implementation path + ;; ===================================================================== + + (~docs/section :title "Implementation path" :id "implementation" + + (p "The path from current SX to the scope primitive follows the existing plan " + "and adds two phases:") + + (~docs/subsection :title "Phase 1: provide/context/emit! (immediate)" + (p "Already planned. Implement render-time dynamic scope. Four primitives: " + (code "provide") " (special form), " (code "context") ", " (code "emit!") ", " + (code "emitted") ". Platform provides " (code "provide-push!/provide-pop!") ".") + (p "This is " (code "scope") " with " (code ":propagation :render") " only. " + "No change to islands or lakes. Pure addition.") + (p (strong "Delivers: ") "render-time context, scoped accumulation, " + "spread and collect reimplemented as sugar over provide/emit.")) + + (~docs/subsection :title "Phase 2: scope as the common form (next)" + (p "Introduce " (code "scope") " as the general form. " + (code "provide") " becomes sugar for " (code "(scope ... :propagation :render)") ". " + (code "defisland") " becomes sugar for " (code "(scope ... :propagation :reactive)") ". " + (code "lake") " becomes sugar for " (code "(scope ... :propagation :morph)") ".") + (p "The sugar forms remain — nobody writes " (code "scope") " directly in page code. " + "But the evaluator, adapters, and bootstrappers all dispatch through one mechanism.") + (p (strong "Delivers: ") "unified internal representation, reactive context (the new cell), " + "simplified adapter code (one scope handler instead of three separate paths).")) + + (~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")) + (p (strong "Delivers: ") "user-defined propagation modes (animation, debounce, throttle, " + "worker, stream), effect composition by nesting, " + "the full algebraic effects model."))) + + + ;; ===================================================================== + ;; VIII. What this means for the spec + ;; ===================================================================== + + (~docs/section :title "What this means for the spec" :id "spec" + + (p "The self-hosting spec currently has separate code paths for each mechanism. " + "Under the scope model, they converge:") + + (table :class "w-full text-sm border-collapse mb-6" + (thead + (tr :class "border-b border-stone-300" + (th :class "text-left py-2 pr-4" "Spec file") + (th :class "text-left py-2 pr-4" "Current") + (th :class "text-left py-2" "After scope"))) + (tbody + (tr :class "border-b border-stone-100" + (td :class "py-2 pr-4 font-mono text-xs" "eval.sx") + (td :class "py-2 pr-4" "provide + defisland as separate special forms") + (td :class "py-2" "scope as one special form, sugar for provide/defisland/lake")) + (tr :class "border-b border-stone-100" + (td :class "py-2 pr-4 font-mono text-xs" "adapter-html.sx") + (td :class "py-2 pr-4" "provide, island, lake as separate dispatch cases") + (td :class "py-2" "one scope dispatch, mode determines serialisation")) + (tr :class "border-b border-stone-100" + (td :class "py-2 pr-4 font-mono text-xs" "adapter-dom.sx") + (td :class "py-2 pr-4" "render-dom-island, render-dom-lake, reactive-spread") + (td :class "py-2" "one render-dom-scope, mode determines lifecycle")) + (tr :class "border-b border-stone-100" + (td :class "py-2 pr-4 font-mono text-xs" "engine.sx") + (td :class "py-2 pr-4" "morph-island-children, sync-attrs, data-sx-reactive-attrs") + (td :class "py-2" "morph-scope (scope boundary determines skip/update)")) + (tr + (td :class "py-2 pr-4 font-mono text-xs" "signals.sx") + (td :class "py-2 pr-4" "standalone signal runtime") + (td :class "py-2" "unchanged — signals are the value layer, scopes are the structure layer")))) + + (p "Signals remain orthogonal. A scope's " (code ":value") " can be a signal or a plain " + "value — the scope doesn't care. The propagation mode determines whether the scope " + "re-runs effects when the value changes, not whether the value " (em "can") " change.")) + + + ;; ===================================================================== + ;; IX. The deepest thing + ;; ===================================================================== + + (~docs/section :title "The deepest thing" :id "deepest" + + (p "Go deep enough and there is one operation:") + + (blockquote :class "border-l-4 border-violet-300 pl-4 my-6" + (p :class "text-lg font-semibold text-stone-700" + "Evaluate this expression in a scope. Handle effects that cross scope boundaries.")) + + (p "Rendering is evaluating expressions in a scope where " (code "emit!") " effects " + "are handled by accumulating HTML. Reactivity is re-evaluating expressions " + "when signals fire. Morphing is receiving new expressions from the server " + "and re-evaluating them in existing scopes.") + + (p "Every SX mechanism — every one — is a specific answer to three questions:") + (ol :class "space-y-2 text-stone-600" + (li (strong "What scope") " am I in? (element / subtree / island / lake / global)") + (li (strong "What direction") " does data flow? (down via context, up via emit)") + (li (strong "When") " are effects realised? (render / signal change / network)")) + + (p (code "scope") " is the primitive that makes all three questions explicit " + "and composable. It's the last primitive SX needs.") + + (~docs/note + (p (strong "Status: ") "Phase 1 (" (code "provide/context/emit!") ") is specced and " + "ready to build. Phase 2 (" (code "scope") " unification) follows naturally once " + "provide is working. Phase 3 (extensible handlers) is the research frontier — " + "it may turn out that three modes are sufficient, or it may turn out that " + "user-defined modes unlock something unexpected."))))) diff --git a/sx/sx/spreads.sx b/sx/sx/spreads.sx index f9d91a6..ab9c5f9 100644 --- a/sx/sx/spreads.sx +++ b/sx/sx/spreads.sx @@ -2,6 +2,87 @@ ;; Spreads — child-to-parent communication across render boundaries ;; --------------------------------------------------------------------------- + +;; ---- Layout helper ---- + +(defcomp ~geography/demo-example (&key demo code) + (div :class "grid grid-cols-1 lg:grid-cols-2 gap-4 my-6 items-start" + (div :class "border border-dashed border-stone-300 rounded-lg p-4 bg-stone-50 min-h-[80px]" + demo) + (div :class "not-prose bg-stone-100 rounded-lg p-4 overflow-x-auto" + (pre :class "text-sm leading-relaxed whitespace-pre-wrap break-words" (code code))))) + + +;; ---- Demo components ---- + +(defcomp ~geography/demo-callout (&key type) + (make-spread + (cond + (= type "info") {"style" "border-left:4px solid #3b82f6;padding:0.5rem 0.75rem;background:#eff6ff;border-radius:0.25rem" + "data-callout" "info"} + (= type "warning") {"style" "border-left:4px solid #f59e0b;padding:0.5rem 0.75rem;background:#fffbeb;border-radius:0.25rem" + "data-callout" "warning"} + (= type "success") {"style" "border-left:4px solid #10b981;padding:0.5rem 0.75rem;background:#ecfdf5;border-radius:0.25rem" + "data-callout" "success"} + :else {"style" "border-left:4px solid #78716c;padding:0.5rem 0.75rem;background:#fafaf9;border-radius:0.25rem" + "data-callout" "default"}))) + +(defcomp ~geography/demo-spread-basic () + (div :class "space-y-3" + (div (~geography/demo-callout :type "info") + (p :class "text-sm" "Info — styled by its child.")) + (div (~geography/demo-callout :type "warning") + (p :class "text-sm" "Warning — same component, different type.")) + (div (~geography/demo-callout :type "success") + (p :class "text-sm" "Success — child tells parent how to look.")))) + +(defcomp ~geography/demo-cssx-tw () + (div :class "space-y-3" + (div (~cssx/tw :tokens "bg-violet-100 rounded-lg p-4") + (h4 (~cssx/tw :tokens "text-violet-800 font-semibold text-lg") "Styled via ~cssx/tw") + (p (~cssx/tw :tokens "text-stone-600 text-sm mt-1") + "Classes injected from spread child.")) + (div (~cssx/tw :tokens "bg-rose-50 rounded-lg p-4 border border-rose-200") + (h4 (~cssx/tw :tokens "text-rose-700 font-semibold text-lg") "Different tokens") + (p (~cssx/tw :tokens "text-stone-600 text-sm mt-1") + "CSS rules JIT-generated. No build step.")))) + +(defcomp ~geography/demo-semantic-vars () + (let ((card (~cssx/tw :tokens "bg-stone-50 rounded-lg p-4 shadow-sm border border-stone-200")) + (heading (~cssx/tw :tokens "text-violet-700 text-lg font-bold")) + (body-text (~cssx/tw :tokens "text-stone-600 text-sm mt-1"))) + (div :class "space-y-3" + (div card + (h4 heading "First Card") + (p body-text "Named spreads bound with let.")) + (div card + (h4 heading "Second Card") + (p body-text "Same variables, consistent look."))))) + +(defisland ~geography/demo-reactive-spread () + (let ((colour (signal "violet"))) + (div (~cssx/tw :tokens (str "rounded-lg p-4 transition-all duration-300 bg-" (deref colour) "-100 border border-" (deref colour) "-300")) + (h4 (~cssx/tw :tokens (str "text-" (deref colour) "-800 font-semibold text-lg")) + (str "Theme: " (deref colour))) + (p (~cssx/tw :tokens "text-stone-600 text-sm mt-2 mb-3") + "Click to change theme. Reactive spreads surgically update classes.") + (div :class "flex gap-2 flex-wrap" + (button :on-click (fn (e) (reset! colour "violet")) + (~cssx/tw :tokens "bg-violet-500 text-white px-3 py-1.5 rounded text-sm font-medium") + "Violet") + (button :on-click (fn (e) (reset! colour "rose")) + (~cssx/tw :tokens "bg-rose-500 text-white px-3 py-1.5 rounded text-sm font-medium") + "Rose") + (button :on-click (fn (e) (reset! colour "amber")) + (~cssx/tw :tokens "bg-amber-500 text-white px-3 py-1.5 rounded text-sm font-medium") + "Amber") + (button :on-click (fn (e) (reset! colour "emerald")) + (~cssx/tw :tokens "bg-emerald-500 text-white px-3 py-1.5 rounded text-sm font-medium") + "Emerald"))))) + + +;; ---- Page content ---- + (defcomp ~geography/spreads-content () (~docs/page :title "Spreads" @@ -22,9 +103,11 @@ (p "A spread is a value type. " (code "make-spread") " creates one from a dict of " "attributes. When the renderer encounters a spread as a child of an element, " "it merges the attrs onto the parent element instead of appending a DOM node.") - (~docs/code :code (highlight "(defcomp ~highlight (&key colour)\n (make-spread {\"class\" (str \"highlight-\" colour)\n \"data-highlight\" colour}))" "lisp")) - (p "Use it as a child of any element:") - (~docs/code :code (highlight "(div (~highlight :colour \"yellow\")\n \"This div gets class=highlight-yellow\")" "lisp")) + + (~geography/demo-example + :demo (~geography/demo-spread-basic) + :code (highlight "(defcomp ~callout (&key type)\n (make-spread\n (cond\n (= type \"info\")\n {\"style\" \"border-left:4px solid\n #3b82f6; background:#eff6ff\"}\n (= type \"warning\")\n {\"style\" \"border-left:4px solid\n #f59e0b; background:#fffbeb\"}\n (= type \"success\")\n {\"style\" \"border-left:4px solid\n #10b981; background:#ecfdf5\"})))\n\n;; Child injects attrs onto parent:\n(div (~callout :type \"info\")\n \"This div gets the callout style.\")" "lisp")) + (p (code "class") " values are appended (space-joined). " (code "style") " values are appended (semicolon-joined). " "All other attributes overwrite.")) @@ -32,7 +115,7 @@ (~docs/subsection :title "2. collect! / collected / clear-collected!" (p "Render-time accumulators. Values are collected into named buckets " "during rendering and retrieved at flush points. Deduplication is automatic.") - (~docs/code :code (highlight ";; Deep inside a component tree:\n(collect! \"cssx\" \".sx-bg-red-500{background-color:hsl(0,72%,53%)}\")\n\n;; At the flush point (once, in the layout):\n(let ((rules (collected \"cssx\")))\n (clear-collected! \"cssx\")\n (raw! (str \"\")))" "lisp")) + (~docs/code :code (highlight ";; Deep inside a component tree:\n(collect! \"cssx\" \".sx-bg-red-500{background:red}\")\n\n;; At the flush point (once, in the layout):\n(let ((rules (collected \"cssx\")))\n (clear-collected! \"cssx\")\n (raw! (str \"\")))" "lisp")) (p "This is upward communication through the render tree: " "a deeply nested component contributes a CSS rule, and the layout " "emits all accumulated rules as a single " (code "