Unicode escapes, variadic infix fix, spreads demos, scoped-effects + foundations plans

- 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 <noreply@anthropic.com>
This commit is contained in:
2026-03-13 12:03:58 +00:00
parent 28a6560963
commit 214963ea6a
15 changed files with 1323 additions and 38 deletions

View File

@@ -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();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

661
sx/sx/plans/foundations.sx Normal file
View File

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

View File

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

View File

@@ -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 \"<style>\" (join \"\" rules) \"</style>\")))" "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 \"<style>\" (join \"\" rules) \"</style>\")))" "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 "<style>") " tag. "
@@ -42,12 +125,15 @@
(p "Inside an island, when a spread's value depends on signals, "
(code "reactive-spread") " tracks signal dependencies and surgically "
"updates the parent element's attributes when signals change.")
(~docs/code :code (highlight "(defisland ~themed-card ()\n (let ((theme (signal \"violet\")))\n (div (~cssx/tw :tokens (str \"bg-\" (deref theme) \"-500 p-4\"))\n (button :on-click (fn (e) (reset! theme \"rose\"))\n \"change theme\"))))" "lisp"))
(p "When " (code "theme") " changes from " (code "\"violet\"") " to "
(code "\"rose\"") ":")
(~geography/demo-example
:demo (~geography/demo-reactive-spread)
:code (highlight "(defisland ~themed-card ()\n (let ((theme (signal \"violet\")))\n (div (~cssx/tw :tokens\n (str \"bg-\" (deref theme)\n \"-100 p-4\"))\n (button\n :on-click\n (fn (e) (reset! theme \"rose\"))\n \"change theme\"))))" "lisp"))
(p "When " (code "theme") " changes:")
(ul :class "list-disc pl-5 space-y-1 text-stone-600"
(li "Old classes (" (code "sx-bg-violet-500") ") are removed from the element")
(li "New classes (" (code "sx-bg-rose-500") ") are added")
(li "Old classes are removed from the element")
(li "New classes are added")
(li "New CSS rules are JIT-generated and flushed to the live stylesheet")
(li "No re-render. No VDOM. No diffing. Just attr surgery."))))
@@ -98,11 +184,15 @@
(~docs/section :title "CSSX: the first application" :id "cssx"
(p (code "~cssx/tw") " is a component that uses all three primitives:")
(~docs/code :code (highlight "(defcomp ~cssx/tw (tokens)\n (let ((token-list (filter (fn (t) (not (= t \"\")))\n (split (or tokens \"\") \" \")))\n (results (map cssx-process-token token-list))\n (valid (filter (fn (r) (not (nil? r))) results))\n (classes (map (fn (r) (get r \"cls\")) valid))\n (rules (map (fn (r) (get r \"rule\")) valid))\n (_ (for-each (fn (rule) (collect! \"cssx\" rule)) rules)))\n (if (empty? classes)\n nil\n (make-spread {\"class\" (join \" \" classes)\n \"data-tw\" (or tokens \"\")}))))" "lisp"))
(p "It's a regular " (code "defcomp") ". It uses " (code "make-spread") " to "
"inject classes onto its parent, " (code "collect!") " to accumulate CSS rules "
"for batch flushing, and when called inside an island with signal-dependent "
"tokens, " (code "reactive-spread") " makes it live.")
(~geography/demo-example
:demo (~geography/demo-cssx-tw)
:code (highlight "(defcomp ~cssx/tw (tokens)\n (let ((token-list\n (filter (fn (t) (not (= t \"\")))\n (split (or tokens \"\") \" \")))\n (results\n (map cssx-process-token\n token-list))\n (classes (map (fn (r)\n (get r \"cls\")) results))\n (rules (map (fn (r)\n (get r \"rule\")) results)))\n (for-each (fn (rule)\n (collect! \"cssx\" rule)) rules)\n (make-spread\n {\"class\" (join \" \" classes)})))" "lisp"))
(p "It uses " (code "make-spread") " to inject classes, "
(code "collect!") " to accumulate CSS rules for batch flushing, and "
"when called inside an island with signal-dependent tokens, "
(code "reactive-spread") " makes it live.")
(p "But " (code "~cssx/tw") " is just one instance. The same primitives enable:")
(ul :class "list-disc pl-5 space-y-1 text-stone-600"
(li (code "~aria") " — reactive accessibility attributes driven by UI state")
@@ -117,9 +207,13 @@
(~docs/section :title "Semantic style variables" :id "variables"
(p "Because " (code "~cssx/tw") " returns a spread, and spreads are values, "
"you can bind them to names:")
(~docs/code :code (highlight ";; Define once\n(define heading-style (~cssx/tw :tokens \"text-violet-700 text-2xl font-bold\"))\n(define nav-link (~cssx/tw :tokens \"text-stone-500 text-sm\"))\n(define card-base (~cssx/tw :tokens \"bg-stone-50 rounded-lg p-4\"))\n\n;; Use everywhere\n(div card-base\n (h1 heading-style \"Title\")\n (a nav-link :href \"/\" \"Home\"))" "lisp"))
(~geography/demo-example
:demo (~geography/demo-semantic-vars)
:code (highlight "(let ((card (~cssx/tw :tokens\n \"bg-stone-50 rounded-lg p-4\n shadow-sm border\"))\n (heading (~cssx/tw :tokens\n \"text-violet-700 text-lg\n font-bold\"))\n (body (~cssx/tw :tokens\n \"text-stone-600 text-sm\")))\n ;; Reuse everywhere:\n (div card\n (h4 heading \"First Card\")\n (p body \"Same variables.\"))\n (div card\n (h4 heading \"Second Card\")\n (p body \"Consistent look.\")))" "lisp"))
(p "These are semantic names wrapping utility tokens. Change the definition, "
"every use updates. No build step, no CSS-in-JS runtime. Just " (code "define") ".")
"every use updates. No build step, no CSS-in-JS runtime. Just " (code "let") ".")
(p "Namespacing prevents clashes — " (code "~app/heading") " vs "
(code "~admin/heading") " are different components in different namespaces."))