diff --git a/sx/content/pages.py b/sx/content/pages.py index 7662878..ba1bdd3 100644 --- a/sx/content/pages.py +++ b/sx/content/pages.py @@ -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 = [ diff --git a/sx/sxc/sx_components.py b/sx/sxc/sx_components.py index b5f17d0..e54fbb3 100644 --- a/sx/sxc/sx_components.py +++ b/sx/sxc/sx_components.py @@ -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) # ---------------------------------------------------------------------------