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 "