Add Continuations essay to SX docs
Some checks failed
Build and Deploy / build-and-deploy (push) Has been cancelled

Covers server-side (suspendable rendering, streaming, error boundaries),
client-side (linear async flows, wizard forms, cooperative scheduling,
undo), and implementation path from the existing TCO trampoline. Updates
TCO essay's continuations section to link to the new essay instead of
dismissing the idea. Fixes "What sx is not" to acknowledge macros + TCO.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-04 10:41:22 +00:00
parent 5069072715
commit fd67f202c2
2 changed files with 302 additions and 0 deletions

View File

@@ -73,6 +73,8 @@ ESSAYS_NAV = [
("Client Reactivity", "/essays/client-reactivity"),
("SX Native", "/essays/sx-native"),
("The SX Manifesto", "/essays/sx-manifesto"),
("Tail-Call Optimization", "/essays/tail-call-optimization"),
("Continuations", "/essays/continuations"),
]
MAIN_NAV = [

View File

@@ -1878,6 +1878,8 @@ def _essay_content_sx(slug: str) -> str:
"client-reactivity": _essay_client_reactivity,
"sx-native": _essay_sx_native,
"sx-manifesto": _essay_sx_manifesto,
"tail-call-optimization": _essay_tail_call_optimization,
"continuations": _essay_continuations,
}
return builders.get(slug, _essay_sx_sucks)()
@@ -2651,6 +2653,304 @@ def _essay_sx_manifesto() -> str:
)
def _essay_tail_call_optimization() -> str:
p = '(p :class "text-stone-600"'
code = '(~doc-code :lang "lisp" :code'
return (
'(~doc-page :title "Tail-Call Optimization in SX"'
' (p :class "text-stone-500 text-sm italic mb-8"'
' "How SX eliminates stack overflow for recursive functions '
'using trampolining — across Python server and JavaScript client.")'
' (~doc-section :title "The problem" :id "problem"'
f' {p}'
' "Every language built on a host runtime inherits the host\'s stack limits. '
'Python defaults to 1,000 frames. JavaScript engines vary — Chrome gives ~10,000, '
'Safari sometimes less. A naive recursive function blows the stack:")'
f' {code}'
' "(define factorial (fn (n)\\n'
' (if (= n 0)\\n'
' 1\\n'
' (* n (factorial (- n 1))))))\\n\\n'
';; (factorial 50000) → stack overflow")'
f' {p}'
' "This isn\'t just academic. Tree traversals, state machines, '
'interpreters, and accumulating loops all naturally express as recursion. '
'A general-purpose language that can\'t recurse deeply isn\'t general-purpose."))'
' (~doc-section :title "Tail position" :id "tail-position"'
f' {p}'
' "A function call is in tail position when its result IS the result of the '
'enclosing function — nothing more happens after it returns. '
'The call doesn\'t need to come back to finish work:")'
f' {code}'
' ";; Tail-recursive — the recursive call IS the return value\\n'
'(define count-down (fn (n)\\n'
' (if (= n 0) \\"done\\" (count-down (- n 1)))))\\n\\n'
';; NOT tail-recursive — multiplication happens AFTER the recursive call\\n'
'(define factorial (fn (n)\\n'
' (if (= n 0) 1 (* n (factorial (- n 1))))))")'
f' {p}'
' "SX identifies tail positions in: if/when branches, the last expression in '
'let/begin/do bodies, cond/case result branches, lambda/component bodies, '
'and macro expansions."))'
' (~doc-section :title "Trampolining" :id "trampolining"'
f' {p}'
' "Instead of recursing, tail calls return a thunk — a deferred '
'(expression, environment) pair. The evaluator\'s trampoline loop unwraps '
'thunks iteratively:")'
f' {code}'
' ";; Conceptually:\\n'
'evaluate(expr, env):\\n'
' result = eval(expr, env)\\n'
' while result is Thunk:\\n'
' result = eval(thunk.expr, thunk.env)\\n'
' return result")'
f' {p}'
' "One stack frame. Always. The trampoline replaces recursive stack growth '
'with an iterative loop. Non-tail calls still use the stack normally — only tail '
'positions get the thunk treatment."))'
' (~doc-section :title "What this enables" :id "enables"'
f' {p}'
' "Tail-recursive accumulator pattern — the natural loop construct for '
'a language without for/while:")'
f' {code}'
' ";; Sum 1 to n without stack overflow\\n'
'(define sum (fn (n acc)\\n'
' (if (= n 0) acc (sum (- n 1) (+ acc n)))))\\n\\n'
'(sum 100000 0) ;; → 5000050000")'
f' {p}'
' "Mutual recursion:")'
f' {code}'
' "(define is-even (fn (n) (if (= n 0) true (is-odd (- n 1)))))\\n'
'(define is-odd (fn (n) (if (= n 0) false (is-even (- n 1)))))\\n\\n'
'(is-even 100000) ;; → true")'
f' {p}'
' "State machines:")'
f' {code}'
' "(define state-a (fn (input)\\n'
' (cond\\n'
' (= (first input) \\"x\\") (state-b (rest input))\\n'
' (= (first input) \\"y\\") (state-a (rest input))\\n'
' :else \\"rejected\\")))\\n\\n'
'(define state-b (fn (input)\\n'
' (if (empty? input) \\"accepted\\"\\n'
' (state-a (rest input)))))")'
f' {p}'
' "All three patterns recurse arbitrarily deep with constant stack usage."))'
' (~doc-section :title "Implementation" :id "implementation"'
f' {p}'
' "TCO is implemented identically across all three SX evaluators:")'
' (ul :class "list-disc pl-6 space-y-1 text-stone-600"'
' (li (span :class "font-semibold" "Python sync evaluator") " — shared/sx/evaluator.py")'
' (li (span :class "font-semibold" "Python async evaluator") " — shared/sx/async_eval.py (planned)")'
' (li (span :class "font-semibold" "JavaScript client evaluator") " — sx.js"))'
f' {p}'
' "The pattern is the same everywhere: a Thunk type with (expr, env) slots, '
'a trampoline loop in the public evaluate() entry point, and thunk returns '
'from tail positions in the internal evaluator. External consumers '
'(HTML renderer, resolver, higher-order forms) trampoline all eval results.")'
f' {p}'
' "The key insight: callers that already work don\'t need to change. '
'The public sxEval/evaluate API always returns values, never thunks. '
'Only the internal evaluator and special forms know about thunks."))'
' (~doc-section :title "What about continuations?" :id "continuations"'
f' {p}'
' "TCO handles the immediate need: recursive algorithms that don\'t blow the stack. '
'Continuations (call/cc, delimited continuations) are a separate, larger primitive — '
'they capture the entire evaluation context as a first-class value.")'
f' {p}'
' "Having the primitive available doesn\'t add complexity unless it\'s invoked. '
'See " (a :href "/essays/continuations" :class "text-violet-600 hover:underline" '
' "the continuations essay") " for what they would enable in SX.")))'
')'
)
def _essay_continuations() -> str:
p = '(p :class "text-stone-600"'
code = '(~doc-code :lang "lisp" :code'
return (
'(~doc-page :title "Continuations and call/cc"'
' (p :class "text-stone-500 text-sm italic mb-8"'
' "What first-class continuations would enable in SX — '
'on both the server (Python) and client (JavaScript).")'
# --- What is a continuation ---
' (~doc-section :title "What is a continuation?" :id "what"'
f' {p}'
' "A continuation is the rest of a computation. At any point during evaluation, '
'the continuation is everything that would happen next. call/cc (call-with-current-continuation) '
'captures that \\\"rest of the computation\\\" as a first-class function that you can store, '
'pass around, and invoke later — possibly multiple times.")'
f' {code}'
' ";; call/cc captures \\\"what happens next\\\" as k\\n'
'(+ 1 (call/cc (fn (k)\\n'
' (k 41)))) ;; → 42\\n\\n'
';; k is \\\"add 1 to this and return it\\\"\\n'
';; (k 41) jumps back to that point with 41")'
f' {p}'
' "The key property: invoking a continuation abandons the current computation '
'and resumes from where the continuation was captured. It is a controlled, first-class goto."))'
# --- Server-side uses ---
' (~doc-section :title "Server-side: suspendable rendering" :id "server"'
f' {p}'
' "The strongest case for continuations on the server is suspendable rendering — '
'the ability for a component to pause mid-render while waiting for data, '
'then resume exactly where it left off.")'
f' {code}'
' ";; Hypothetical: component suspends at a data boundary\\n'
'(defcomp ~user-profile (&key user-id)\\n'
' (let ((user (suspend (query :user user-id))))\\n'
' (div :class \\\"p-4\\\"\\n'
' (h2 (get user \\\"name\\\"))\\n'
' (p (get user \\\"bio\\\")))))")'
f' {p}'
' "Today, all data must be fetched before render_to_sx is called — Python awaits '
'every query, assembles a complete data dict, then passes it to the evaluator. '
'With continuations, the evaluator could yield at (suspend ...), '
'the server flushes what it has so far, and resumes when the data arrives. '
'This is React Suspense, but for server-side s-expressions.")'
f' {p}'
' "Streaming follows naturally. The server renders the page shell immediately, '
'captures continuations at slow data boundaries, and flushes partial SX responses '
'as each resolves. The client receives a stream of s-expression chunks '
'and incrementally builds the DOM.")'
f' {p}'
' "Error boundaries also become first-class. Capture a continuation at a component boundary. '
'If any child fails, invoke the continuation with fallback content instead of '
'letting the exception propagate up through Python. The evaluator handles it, '
'not the host language."))'
# --- Client-side uses ---
' (~doc-section :title "Client-side: linear async flows" :id "client"'
f' {p}'
' "On the client, continuations eliminate callback nesting for interactive flows. '
'A confirmation dialog becomes a synchronous-looking expression:")'
f' {code}'
' "(let ((answer (call/cc show-confirm-dialog)))\\n'
' (if answer\\n'
' (delete-item item-id)\\n'
' (noop)))")'
f' {p}'
' "show-confirm-dialog receives the continuation, renders a modal, '
'and wires the Yes/No buttons to invoke the continuation with true or false. '
'The let binding reads top-to-bottom. No promises, no callbacks, no state machine.")'
f' {p}'
' "Multi-step forms — wizard-style UIs where each step captures a continuation. '
'The back button literally invokes a saved continuation, restoring the exact evaluation state:")'
f' {code}'
' "(define wizard\\n'
' (fn ()\\n'
' (let* ((name (call/cc (fn (k) (render-step-1 k))))\\n'
' (email (call/cc (fn (k) (render-step-2 k name))))\\n'
' (plan (call/cc (fn (k) (render-step-3 k name email)))))\\n'
' (submit-registration name email plan))))")'
f' {p}'
' "Each render-step-N shows a form and wires the \\\"Next\\\" button to invoke k '
'with the form value. The \\\"Back\\\" button invokes the previous step\\\'s continuation. '
'The wizard logic is a straight-line let* binding, not a state machine."))'
# --- Cooperative scheduling ---
' (~doc-section :title "Cooperative scheduling" :id "scheduling"'
f' {p}'
' "Delimited continuations (shift/reset rather than full call/cc) enable '
'cooperative multitasking within the evaluator. A long render can yield control:")'
f' {code}'
' ";; Render a large list, yielding every 100 items\\n'
'(define render-chunk\\n'
' (fn (items n)\\n'
' (when (> n 100)\\n'
' (yield) ;; delimited continuation — suspends, resumes next frame\\n'
' (set! n 0))\\n'
' (when (not (empty? items))\\n'
' (render-item (first items))\\n'
' (render-chunk (rest items) (+ n 1)))))")'
f' {p}'
' "This is cooperative concurrency without threads, without promises, '
'without requestAnimationFrame callbacks. The evaluator\'s trampoline loop '
'already has the right shape — it just needs to be able to park a thunk and '
'resume it later instead of immediately."))'
# --- Undo ---
' (~doc-section :title "Undo as continuation" :id "undo"'
f' {p}'
' "If you capture a continuation before a state mutation, '
'the continuation IS the undo operation. Invoking it restores the computation '
'to exactly the state it was in before the mutation happened.")'
f' {code}'
' "(define with-undo\\n'
' (fn (action)\\n'
' (let ((restore (call/cc (fn (k) k))))\\n'
' (action)\\n'
' restore)))\\n\\n'
';; Usage:\\n'
'(let ((undo (with-undo (fn () (delete-item 42)))))\\n'
' ;; later...\\n'
' (undo \\\"anything\\\")) ;; item 42 is back")'
f' {p}'
' "No command pattern, no reverse operations, no state snapshots. '
'The continuation captures the entire computation state. '
'This is the most elegant undo mechanism possible — '
'and the most expensive in memory, which is the trade-off."))'
# --- Implementation cost ---
' (~doc-section :title "Implementation" :id "implementation"'
f' {p}'
' "SX already has the foundation. The TCO trampoline returns thunks '
'from tail positions — a continuation is a thunk that can be stored and resumed later '
'instead of being immediately trampolined.")'
f' {p}'
' "The minimal implementation: delimited continuations via shift/reset. '
'These are strictly less powerful than full call/cc but cover the practical use cases '
'(suspense, cooperative scheduling, linear async flows) without the footguns '
'(capturing continuations across async boundaries, re-entering completed computations).")'
f' {p}'
' "Full call/cc is also possible. The evaluator is already '
'continuation-passing-style-adjacent — the thunk IS a continuation, '
'just one that\'s always immediately invoked. Making it first-class means '
'letting user code hold a reference to it.")'
f' {p}'
' "The key insight: having the primitive available doesn\'t make the evaluator '
'harder to reason about. Only code that calls call/cc pays the complexity cost. '
'Components that don\'t use continuations behave exactly as they do today."))'
# --- What this means for SX ---
' (~doc-section :title "What this means for SX" :id "meaning"'
f' {p}'
' "SX started as a rendering language. TCO made it capable of arbitrary recursion. '
'Macros made it extensible. Continuations would make it a full computational substrate — '
'a language where control flow itself is a first-class value.")'
f' {p}'
' "The practical benefits are real: streaming server rendering, '
'linear client-side interaction flows, cooperative scheduling, '
'and elegant undo. These aren\'t theoretical — they\'re patterns that '
'React, Clojure, and Scheme have proven work.")'
f' {p}'
' "The question isn\'t whether continuations are useful. '
'It\'s whether SX needs them now or whether async/await and HTMX cover enough. '
'The answer, for now, is that they\'re worth building — '
'because the evaluator is already 90%% of the way there, '
'and the remaining 10%% unlocks an entirely new class of UI patterns."))'
')'
)
# ---------------------------------------------------------------------------
# Wire-format partials (for sx-get requests)
# ---------------------------------------------------------------------------