All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 4m49s
Three-layer architecture:
spec/ — Core language (19 files): evaluator, parser, primitives,
CEK machine, types, continuations. Host-independent.
web/ — Web framework (20 files): signals, adapters, engine,
orchestration, boot, router, CSSX. Built on core spec.
sx/ — Application (sx-docs website). Built on web framework.
Split boundary.sx into boundary-core.sx (type-of, make-env, identical?)
and boundary-web.sx (IO primitives, signals, spreads, page helpers).
Bootstrappers search spec/ → web/ → shared/sx/ref/ for .sx files.
Original files remain in shared/sx/ref/ as fallback during transition.
All 63 tests pass.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
246 lines
9.3 KiB
Plaintext
246 lines
9.3 KiB
Plaintext
;; ==========================================================================
|
|
;; 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).
|
|
;;
|
|
;; --------------------------------------------------------------------------
|