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:
@@ -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();
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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"];
|
||||
"""
|
||||
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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))"
|
||||
|
||||
@@ -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
661
sx/sx/plans/foundations.sx
Normal 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.")))
|
||||
444
sx/sx/plans/scoped-effects.sx
Normal file
444
sx/sx/plans/scoped-effects.sx
Normal 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.")))))
|
||||
126
sx/sx/spreads.sx
126
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 \"<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."))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user