From 12fe93bb553b56676466e83d72d9595a81fac49c Mon Sep 17 00:00:00 2001 From: giles Date: Fri, 6 Mar 2026 00:41:28 +0000 Subject: [PATCH] Add continuation specs: delimited (shift/reset) and full (call/cc) Optional bolt-on extensions to the SX spec. continuations.sx defines delimited continuations for all targets. callcc.sx defines full call/cc for targets where it's native (Scheme, Haskell). Shared continuation type if both are loaded. Wired into specs section of sx-docs. Co-Authored-By: Claude Opus 4.6 --- shared/sx/ref/callcc.sx | 245 ++++++++++++++++++++++++++++++++ shared/sx/ref/continuations.sx | 248 +++++++++++++++++++++++++++++++++ sx/sx/nav-data.sx | 14 +- sx/sx/specs.sx | 34 ++++- sx/sxc/pages/docs.sx | 8 ++ 5 files changed, 546 insertions(+), 3 deletions(-) create mode 100644 shared/sx/ref/callcc.sx create mode 100644 shared/sx/ref/continuations.sx diff --git a/shared/sx/ref/callcc.sx b/shared/sx/ref/callcc.sx new file mode 100644 index 0000000..6fe6716 --- /dev/null +++ b/shared/sx/ref/callcc.sx @@ -0,0 +1,245 @@ +;; ========================================================================== +;; callcc.sx — Full first-class continuations (call/cc) +;; +;; OPTIONAL EXTENSION — not required by the core evaluator. +;; Bootstrappers include this only when the target supports it naturally. +;; +;; Full call/cc (call-with-current-continuation) captures the ENTIRE +;; remaining computation as a first-class function — not just up to a +;; delimiter, but all the way to the top level. Invoking a continuation +;; captured by call/cc abandons the current computation entirely and +;; resumes from where the continuation was captured. +;; +;; This is strictly more powerful than delimited continuations (shift/reset) +;; but harder to implement in targets that don't support it natively. +;; Recommended only for targets where it's natural: +;; - Scheme/Racket (native call/cc) +;; - Haskell (ContT monad transformer) +;; +;; For targets like Python, JavaScript, and Rust, delimited continuations +;; (continuations.sx) are more practical and cover the same use cases +;; without requiring a global CPS transform. +;; +;; One new special form: +;; (call/cc f) — call f with the current continuation +;; +;; One new type: +;; continuation — same type as in continuations.sx +;; +;; If both extensions are loaded, the continuation type is shared. +;; Delimited and undelimited continuations are the same type — +;; the difference is in how they are captured, not what they are. +;; +;; Platform requirements: +;; (make-continuation fn) — wrap a function as a continuation value +;; (continuation? x) — type predicate +;; (type-of continuation) → "continuation" +;; (call-with-cc f env) — target-specific call/cc implementation +;; ========================================================================== + + +;; -------------------------------------------------------------------------- +;; 1. Semantics +;; -------------------------------------------------------------------------- +;; +;; (call/cc f) +;; +;; Evaluates f (which must be a function of one argument), passing it the +;; current continuation as a continuation value. f can: +;; +;; a) Return normally — call/cc returns whatever f returns +;; b) Invoke the continuation — abandons f's computation, call/cc +;; "returns" the value passed to the continuation +;; c) Store the continuation — invoke it later, possibly multiple times +;; +;; Key difference from shift/reset: invoking an undelimited continuation +;; NEVER RETURNS to the caller. It abandons the current computation and +;; jumps back to where call/cc was originally called. +;; +;; ;; Delimited (shift/reset) — k returns a value: +;; (reset (+ 1 (shift k (+ (k 10) (k 20))))) +;; ;; (k 10) → 11, returns to the (+ ... (k 20)) expression +;; ;; (k 20) → 21, returns to the (+ 11 ...) expression +;; ;; result: 32 +;; +;; ;; Undelimited (call/cc) — k does NOT return: +;; (+ 1 (call/cc (fn (k) +;; (+ (k 10) (k 20))))) +;; ;; (k 10) abandons (+ (k 10) (k 20)) entirely +;; ;; jumps back to (+ 1 _) with 10 +;; ;; result: 11 +;; ;; (k 20) is never reached +;; +;; -------------------------------------------------------------------------- + + +;; -------------------------------------------------------------------------- +;; 2. call/cc — call with current continuation +;; -------------------------------------------------------------------------- + +(define sf-callcc + (fn (args env) + ;; Single argument: a function to call with the current continuation. + (let ((f-expr (first args)) + (f (trampoline (eval-expr f-expr env)))) + (call-with-cc f env)))) + + +;; -------------------------------------------------------------------------- +;; 3. Derived forms +;; -------------------------------------------------------------------------- +;; +;; With call/cc available, several patterns become expressible: +;; +;; --- Early return --- +;; +;; (define find-first +;; (fn (pred items) +;; (call/cc (fn (return) +;; (for-each (fn (item) +;; (when (pred item) +;; (return item))) +;; items) +;; nil)))) +;; +;; --- Exception-like flow --- +;; +;; (define try-catch +;; (fn (body handler) +;; (call/cc (fn (throw) +;; (body throw))))) +;; +;; (try-catch +;; (fn (throw) +;; (let ((result (dangerous-operation))) +;; (when (not result) (throw "failed")) +;; result)) +;; (fn (error) (str "Caught: " error))) +;; +;; --- Coroutines --- +;; +;; Two call/cc captures that alternate control between two +;; computations. Each captures its own continuation, then invokes +;; the other's. This gives cooperative multitasking without threads. +;; +;; --- Undo --- +;; +;; (define with-undo +;; (fn (action) +;; (call/cc (fn (restore) +;; (action) +;; restore)))) +;; +;; ;; (let ((undo (with-undo (fn () (delete-item 42))))) +;; ;; (undo "anything")) → item 42 is back +;; +;; -------------------------------------------------------------------------- + + +;; -------------------------------------------------------------------------- +;; 4. Interaction with delimited continuations +;; -------------------------------------------------------------------------- +;; +;; If both callcc.sx and continuations.sx are loaded: +;; +;; - The continuation type is shared. (continuation? k) returns true +;; for both delimited and undelimited continuations. +;; +;; - shift inside a call/cc body captures up to the nearest reset, +;; not up to the call/cc. The two mechanisms compose. +;; +;; - call/cc inside a reset body captures the entire continuation +;; (past the reset). This is the expected behavior — call/cc is +;; undelimited by definition. +;; +;; - A delimited continuation (from shift) returns a value when invoked. +;; An undelimited continuation (from call/cc) does not return. +;; Both are callable with the same syntax: (k value). +;; The caller cannot distinguish them by type — only by behavior. +;; +;; -------------------------------------------------------------------------- + + +;; -------------------------------------------------------------------------- +;; 5. Interaction with I/O and state +;; -------------------------------------------------------------------------- +;; +;; Full call/cc has well-known interactions with side effects: +;; +;; Re-entry: +;; Invoking a saved continuation re-enters a completed computation. +;; If that computation mutated state (set!, I/O writes), the mutations +;; are NOT undone. The continuation resumes in the current state, +;; not the state at the time of capture. +;; +;; I/O: +;; Same as delimited continuations — I/O executes at invocation time. +;; A continuation containing (current-user) will call current-user +;; when invoked, in whatever request context exists then. +;; +;; Dynamic extent: +;; call/cc captures the continuation, not the dynamic environment. +;; Host-language context (Python's Quart request context, JavaScript's +;; async context) may not be valid when a saved continuation is invoked +;; later. Typed targets can enforce this; dynamic targets fail at runtime. +;; +;; Recommendation: +;; Use call/cc for pure control flow (early return, coroutines, +;; backtracking). Use delimited continuations for effectful patterns +;; (suspense, cooperative scheduling) where the delimiter provides +;; a natural boundary. +;; +;; -------------------------------------------------------------------------- + + +;; -------------------------------------------------------------------------- +;; 6. Implementation notes per target +;; -------------------------------------------------------------------------- +;; +;; Scheme / Racket: +;; Native call/cc. Zero implementation effort. +;; +;; Haskell: +;; ContT monad transformer. The evaluator runs in ContT, and call/cc +;; is callCC from Control.Monad.Cont. Natural and type-safe. +;; +;; Python: +;; Requires full CPS transform of the evaluator, or greenlet-based +;; stack capture. Significantly more invasive than delimited +;; continuations. NOT RECOMMENDED — use continuations.sx instead. +;; +;; JavaScript: +;; Requires full CPS transform. Cannot be implemented with generators +;; alone (generators only support delimited yield, not full escape). +;; NOT RECOMMENDED — use continuations.sx instead. +;; +;; Rust: +;; Full CPS transform at compile time. Possible but adds significant +;; complexity. Delimited continuations are more natural (enum-based). +;; Consider only if the target genuinely needs undelimited escape. +;; +;; -------------------------------------------------------------------------- + + +;; -------------------------------------------------------------------------- +;; 7. Platform interface — what each target must provide +;; -------------------------------------------------------------------------- +;; +;; (call-with-cc f env) +;; Call f with the current continuation. f is a function of one +;; argument (the continuation). If f returns normally, call-with-cc +;; returns f's result. If f invokes the continuation, the computation +;; jumps to the call-with-cc call site with the provided value. +;; +;; (make-continuation fn) +;; Wrap a native function as a continuation value. +;; (Shared with continuations.sx if both are loaded.) +;; +;; (continuation? x) +;; Type predicate. +;; (Shared with continuations.sx if both are loaded.) +;; +;; Continuations must be callable via the standard function-call +;; dispatch in eval-list (same path as lambda calls). +;; +;; -------------------------------------------------------------------------- diff --git a/shared/sx/ref/continuations.sx b/shared/sx/ref/continuations.sx new file mode 100644 index 0000000..94d501c --- /dev/null +++ b/shared/sx/ref/continuations.sx @@ -0,0 +1,248 @@ +;; ========================================================================== +;; continuations.sx — Delimited continuations (shift/reset) +;; +;; OPTIONAL EXTENSION — not required by the core evaluator. +;; Bootstrappers include this only when the target requests it. +;; +;; Delimited continuations capture "the rest of the computation up to +;; a delimiter." They are strictly less powerful than full call/cc but +;; cover the practical use cases: suspendable rendering, cooperative +;; scheduling, linear async flows, wizard forms, and undo. +;; +;; Two new special forms: +;; (reset body) — establish a delimiter +;; (shift k body) — capture the continuation to the nearest reset +;; +;; One new type: +;; continuation — a captured delimited continuation, callable +;; +;; The captured continuation is a function of one argument. Invoking it +;; provides the value that the shift expression "returns" within the +;; delimited context, then completes the rest of the reset body. +;; +;; Continuations are composable — invoking a continuation returns a +;; value (the result of the reset body), which can be used normally. +;; This is the key difference from undelimited call/cc, where invoking +;; a continuation never returns. +;; +;; Platform requirements: +;; (make-continuation fn) — wrap a function as a continuation value +;; (continuation? x) — type predicate +;; (type-of continuation) → "continuation" +;; Continuations are callable (same dispatch as lambda). +;; ========================================================================== + + +;; -------------------------------------------------------------------------- +;; 1. Type +;; -------------------------------------------------------------------------- +;; +;; A continuation is a callable value of one argument. +;; +;; (continuation? k) → true if k is a captured continuation +;; (type-of k) → "continuation" +;; (k value) → invoke: resume the captured computation with value +;; +;; Continuations are first-class: they can be stored in variables, passed +;; as arguments, returned from functions, and put in data structures. +;; +;; Invoking a delimited continuation RETURNS a value — the result of the +;; reset body. This makes them composable: +;; +;; (+ 1 (reset (+ 10 (shift k (k 5))))) +;; ;; k is "add 10 to _ and return from reset" +;; ;; (k 5) → 15, which is returned from reset +;; ;; (+ 1 15) → 16 +;; +;; -------------------------------------------------------------------------- + + +;; -------------------------------------------------------------------------- +;; 2. reset — establish a continuation delimiter +;; -------------------------------------------------------------------------- +;; +;; (reset body) +;; +;; Evaluates body in the current environment. If no shift occurs during +;; evaluation of body, reset simply returns the value of body. +;; +;; If shift occurs, reset is the boundary — the continuation captured by +;; shift extends from the shift point back to (and including) this reset. +;; +;; reset is the "prompt" — it marks where the continuation stops. +;; +;; Semantics: +;; (reset expr) where expr contains no shift +;; → (eval expr env) ;; just evaluates normally +;; +;; (reset ... (shift k body) ...) +;; → captures continuation, evaluates shift's body +;; → the result of the shift body is the result of the reset +;; +;; -------------------------------------------------------------------------- + +(define sf-reset + (fn (args env) + ;; Single argument: the body expression. + ;; Install a continuation delimiter, then evaluate body. + ;; The implementation is target-specific: + ;; - In Scheme: native reset/shift + ;; - In Haskell: Control.Monad.CC or delimited continuations library + ;; - In Python: coroutine/generator-based (see implementation notes) + ;; - In JavaScript: generator-based or CPS transform + ;; - In Rust: CPS transform at compile time + (let ((body (first args))) + (eval-with-delimiter body env)))) + + +;; -------------------------------------------------------------------------- +;; 3. shift — capture the continuation to the nearest reset +;; -------------------------------------------------------------------------- +;; +;; (shift k body) +;; +;; Captures the continuation from this point back to the nearest enclosing +;; reset and binds it to k. Then evaluates body in the current environment +;; extended with k. The result of body becomes the result of the enclosing +;; reset. +;; +;; k is a function of one argument. Calling (k value) resumes the captured +;; computation with value standing in for the shift expression. +;; +;; The continuation k is composable: (k value) returns a value (the result +;; of the reset body when resumed with value). This means k can be called +;; multiple times, and its result can be used in further computation. +;; +;; Examples: +;; +;; ;; Basic: shift provides a value to the surrounding computation +;; (reset (+ 1 (shift k (k 41)))) +;; ;; k = "add 1 to _", (k 41) → 42, reset returns 42 +;; +;; ;; Abort: shift can discard the continuation entirely +;; (reset (+ 1 (shift k "aborted"))) +;; ;; k is never called, reset returns "aborted" +;; +;; ;; Multiple invocations: k can be called more than once +;; (reset (+ 1 (shift k (list (k 10) (k 20))))) +;; ;; (k 10) → 11, (k 20) → 21, reset returns (11 21) +;; +;; ;; Stored for later: k can be saved and invoked outside reset +;; (define saved nil) +;; (reset (+ 1 (shift k (set! saved k) 0))) +;; ;; reset returns 0, saved holds the continuation +;; (saved 99) ;; → 100 +;; +;; -------------------------------------------------------------------------- + +(define sf-shift + (fn (args env) + ;; Two arguments: the continuation variable name, and the body. + (let ((k-name (symbol-name (first args))) + (body (second args))) + ;; Capture the current continuation up to the nearest reset. + ;; Bind it to k-name in the environment, then evaluate body. + ;; The result of body is returned to the reset. + (capture-continuation k-name body env)))) + + +;; -------------------------------------------------------------------------- +;; 4. Interaction with other features +;; -------------------------------------------------------------------------- +;; +;; TCO (trampoline): +;; Continuations interact naturally with the trampoline. A shift inside +;; a tail-call position captures the continuation including the pending +;; return. The trampoline resolves thunks before the continuation is +;; delimited. +;; +;; Macros: +;; shift/reset are special forms, not macros. Macros expand before +;; evaluation, so shift inside a macro-expanded form works correctly — +;; it captures the continuation of the expanded code. +;; +;; Components: +;; shift inside a component body captures the continuation of that +;; component's render. The enclosing reset determines the delimiter. +;; This is the foundation for suspendable rendering — a component can +;; shift to suspend, and the server resumes it when data arrives. +;; +;; I/O primitives: +;; I/O primitives execute at invocation time, in whatever context +;; exists then. A continuation that captures a computation containing +;; I/O will re-execute that I/O when invoked. If the I/O requires +;; request context (e.g. current-user), invoking the continuation +;; outside a request will fail — same as calling the I/O directly. +;; This is consistent, not a restriction. +;; +;; In typed targets (Haskell, Rust), the type system can enforce that +;; continuations containing I/O are only invoked in appropriate contexts. +;; In dynamic targets (Python, JS), it fails at runtime. +;; +;; Lexical scope: +;; Continuations capture the dynamic extent (what happens next) but +;; close over the lexical environment at the point of capture. Variable +;; bindings in the continuation refer to the same environment — mutations +;; via set! are visible. +;; +;; -------------------------------------------------------------------------- + + +;; -------------------------------------------------------------------------- +;; 5. Implementation notes per target +;; -------------------------------------------------------------------------- +;; +;; The bootstrapper emits target-specific continuation machinery. +;; The spec defines semantics; each target chooses representation. +;; +;; Scheme / Racket: +;; Native shift/reset. No transformation needed. The bootstrapper +;; emits (require racket/control) or equivalent. +;; +;; Haskell: +;; Control.Monad.CC provides delimited continuations in the CC monad. +;; Alternatively, the evaluator can be CPS-transformed at compile time. +;; Continuations become first-class functions naturally. +;; +;; Python: +;; Generator-based: reset creates a generator, shift yields from it. +;; The trampoline loop drives the generator. Each yield is a shift +;; point, and send() provides the resume value. +;; Alternative: greenlet-based (stackful coroutines). +;; +;; JavaScript: +;; Generator-based (function* / yield). Similar to Python. +;; Alternative: CPS transform at bootstrap time — the bootstrapper +;; rewrites the evaluator into continuation-passing style, making +;; shift/reset explicit function arguments. +;; +;; Rust: +;; CPS transform at compile time. Continuations become enum variants +;; or boxed closures. The type system ensures continuations are used +;; linearly if desired (affine types via ownership). +;; +;; -------------------------------------------------------------------------- + + +;; -------------------------------------------------------------------------- +;; 6. Platform interface — what each target must provide +;; -------------------------------------------------------------------------- +;; +;; (eval-with-delimiter expr env) +;; Install a reset delimiter, evaluate expr, return result. +;; If expr calls shift, the continuation is captured up to here. +;; +;; (capture-continuation k-name body env) +;; Capture the current continuation up to the nearest delimiter. +;; Bind it to k-name in env, evaluate body, return result to delimiter. +;; +;; (make-continuation fn) +;; Wrap a native function as a continuation value. +;; +;; (continuation? x) +;; Type predicate. +;; +;; Continuations must be callable via the standard function-call +;; dispatch in eval-list (same path as lambda calls). +;; +;; -------------------------------------------------------------------------- diff --git a/sx/sx/nav-data.sx b/sx/sx/nav-data.sx index 14536bb..407589e 100644 --- a/sx/sx/nav-data.sx +++ b/sx/sx/nav-data.sx @@ -95,7 +95,9 @@ (dict :label "SxEngine" :href "/specs/engine") (dict :label "Orchestration" :href "/specs/orchestration") (dict :label "Boot" :href "/specs/boot") - (dict :label "CSSX" :href "/specs/cssx"))) + (dict :label "CSSX" :href "/specs/cssx") + (dict :label "Continuations" :href "/specs/continuations") + (dict :label "call/cc" :href "/specs/callcc"))) (define bootstrappers-nav-items (list (dict :label "Overview" :href "/bootstrappers/") @@ -146,7 +148,15 @@ :desc "On-demand CSS: style dictionary, keyword resolution, rule injection." :prose "CSSX is the on-demand CSS system. It resolves keyword atoms (:flex, :gap-4, :hover:bg-sky-200) into StyleValue objects with content-addressed class names, injecting CSS rules into the document on first use. The style dictionary is a JSON blob containing: atoms (keyword to CSS declarations), pseudo-variants (hover:, focus:, etc.), responsive breakpoints (md:, lg:, etc.), keyframe animations, arbitrary value patterns, and child selector prefixes (space-x-, space-y-). Classes are only emitted when used, keeping the CSS payload minimal. The dictionary is typically served inline in a