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 <noreply@anthropic.com>
This commit is contained in:
2026-03-06 00:41:28 +00:00
parent 0693586e6f
commit 12fe93bb55
5 changed files with 546 additions and 3 deletions

245
shared/sx/ref/callcc.sx Normal file
View File

@@ -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).
;;
;; --------------------------------------------------------------------------

View File

@@ -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).
;;
;; --------------------------------------------------------------------------