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:
245
shared/sx/ref/callcc.sx
Normal file
245
shared/sx/ref/callcc.sx
Normal 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).
|
||||
;;
|
||||
;; --------------------------------------------------------------------------
|
||||
248
shared/sx/ref/continuations.sx
Normal file
248
shared/sx/ref/continuations.sx
Normal 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).
|
||||
;;
|
||||
;; --------------------------------------------------------------------------
|
||||
Reference in New Issue
Block a user