Continuations & Preemption
Delimited continuations and preemptive scheduling — the foundation for fibers, coroutines, generators, async/await, and algebraic effects.
Overview
A continuation represents “the rest of the computation” — everything that would happen after a certain point. Zym provides delimited continuations that capture only up to a prompt boundary, making them composable and easy to reason about.
Continuations in Zym are one-shot — each can be resumed exactly once. After resuming, it becomes invalid.
Key Features
- Tagged Prompts — Multiple independent control flow boundaries
- One-Shot Semantics — Each continuation can be resumed exactly once
- Delimited Capture — Only captures state up to a prompt, not the entire stack
- GC Integration — Continuations are properly garbage collected
- Zero Cost When Unused — No overhead if you don't use these features
- Preemption Support — Optional time-slicing for fair scheduling
| Module | Purpose |
|---|---|
Cont | Delimited continuation primitives (prompts, capture, resume, abort, shift) |
Preempt | Preemptive scheduling support (timeslices, yield requests) |
Understanding Continuations
What is a Continuation?
A continuation represents "the rest of the computation" — everything that would happen after a certain point in your program. In Zym, you can capture this as a first-class value and resume it later with a chosen result.
Think of it like a bookmark in your program's execution that you can jump back to.
Delimited vs Undelimited
Undelimited continuations (like Scheme's call/cc) capture the entire program state — everything from the current point to program termination. This is powerful but unwieldy.
Delimited continuations (what Zym provides) capture only up to a prompt boundary. This makes them composable and much easier to reason about.
┌─────────────────────────────────────────────┐ │ Main Program │ │ ┌───────────────────────────────────┐ │ │ │ Prompt Boundary (tag: myTag) │ │ │ │ ┌─────────────────────────┐ │ │ │ │ │ Code that calls │ │ │ │ │ │ Cont.capture(myTag) │ │ │ │ │ │ ← CAPTURED PORTION → │ │ │ │ │ └─────────────────────────┘ │ │ │ └───────────────────────────────────┘ │ │ ← NOT captured (outside prompt) │ └─────────────────────────────────────────────┘
Prompt Tags
A prompt tag is a unique identifier that marks a control flow boundary. You create tags with Cont.newPrompt() and use them to:
- Install prompts — Mark where continuations can be captured to
- Capture — Specify which prompt to capture up to
- Abort — Specify which prompt to unwind to
Multiple tags can coexist, allowing independent libraries to use continuations without interfering with each other.
The shift Operator
When you use Cont.capture(tag) inside Cont.withPrompt, the captured continuation returns to the withPrompt call site.
The caller then has to inspect the result, check if it's a continuation with Cont.isContinuation(), and decide what to do.
This boilerplate is needed at every withPrompt call site that might encounter a capture.
Cont.shift(tag, handler) eliminates this pattern. Instead of returning the continuation to the caller,
it passes it directly to a handler function you provide. The handler runs at the prompt boundary and whatever it returns becomes the withPrompt result — no ambiguity, no isContinuation checks.
Getting Started
Basic Capture and Resume
Important: Always call Cont.capture() inside a helper function. If you capture directly at the prompt level, the variable holding the continuation gets overwritten when you resume!
// 1. Create a prompt tag var tag = Cont.newPrompt() // 2. Define a function that captures // This is CRITICAL - capture must be inside a function! func pauseAndGetValue() { print("About to pause...") var received = Cont.capture(tag) // Pause here print("Resumed with: " + str(received)) return received * 2 // Continue computation } // 3. Install prompt and call the function Cont.pushPrompt(tag) var k = pauseAndGetValue() // k receives the continuation // 4. k is now a continuation object print("Got continuation: " + str(Cont.isContinuation(k))) // 5. Resume the continuation with a value var result = Cont.resume(k, 21) // 21 becomes 'received' in the function print("Final result: " + str(result)) // 42 (21 * 2)
Cont Module
Prompt Tags
Creates a new, unique prompt tag.
Creates a prompt tag with a debug name (shown in error messages).
var tag = Cont.newPrompt("fiber")
Returns true if value is a prompt tag.
Installing Prompts
Executes fn (zero-argument function) within a prompt boundary. The prompt is automatically installed before fn runs and removed when it returns. This is the recommended way to install prompts.
Returns: fn’s return value, a captured continuation, or an abort value.
var tag = Cont.newPrompt("work") var result = Cont.withPrompt(tag, func() { print("Inside prompt scope") return 42 }) print(result) // 42
Manually installs a prompt boundary. You must call Cont.popPrompt() when done.
Removes the topmost prompt from the stack.
Capture & Resume
Captures the continuation from the current point up to the specified prompt, then transfers control back to the prompt location.
capture() inside a helper function. If you capture directly at the prompt level, the variable holding the continuation gets overwritten when resumed.var tag = Cont.newPrompt() // CORRECT: Capture inside a function func pauseHere() { var received = Cont.capture(tag) return received * 2 } Cont.pushPrompt(tag) var k = pauseHere() // k is the continuation var result = Cont.resume(k, 21) // result is 42
Resumes a captured continuation with the provided value. The continuation is consumed after this call (one-shot).
continuation— the continuation to resumevalue— value to inject (becomes the result ofcapture)
Returns: the eventual return value from the resumed computation.
Returns true if value is a continuation object.
Returns true if the continuation has not been consumed yet.
Cont.isValid(k) // true Cont.resume(k, null) Cont.isValid(k) // false — already consumed
Abort
Aborts to a prompt without capturing a continuation. Unwinds to the prompt and provides a value — like an early return.
var tag = Cont.newPrompt("bail") var result = Cont.withPrompt(tag, func() { Cont.abort(tag, "early exit") print("This never runs") }) print(result) // "early exit"
Shift
Captures the continuation and passes it to handler(k). The handler runs at the prompt boundary and its return value becomes the withPrompt result. Eliminates the isContinuation boilerplate needed with raw capture.
tag— the prompt tag to capture up tohandler— one-argument function that receives the continuation
var tag = Cont.newPrompt("demo") var result = Cont.withPrompt(tag, func() { var x = Cont.shift(tag, func(k) { return Cont.resume(k, 42) }) print("Got: " + str(x)) // "Got: 42" return "done" }) print(result) // "done"
Examples
Generator Pattern
var tag = Cont.newPrompt("gen") func yield(value) { Cont.shift(tag, func(k) { return [value, k] }) } var pair = Cont.withPrompt(tag, func() { yield(1) yield(2) yield(3) return null }) // pair = [1, <continuation>] print(pair[0]) // 1 pair = Cont.resume(pair[1], null) print(pair[0]) // 2
Early Return with Abort
var RETURN_TAG = Cont.newPrompt("return") func earlyReturn(value) { Cont.abort(RETURN_TAG, value) } func withEarlyReturn(fn) { Cont.pushPrompt(RETURN_TAG) var result = fn() Cont.popPrompt() return result } var result = withEarlyReturn(func() { var numbers = [1, 3, 5, 4, 7] var i = 0 while (i < length(numbers)) { if (numbers[i] % 2 == 0) { earlyReturn(numbers[i]) } i = i + 1 } return null }) print("First even: " + str(result)) // "First even: 4"
Preempt Module
Provides time-sliced execution control for fair scheduling of multiple fibers. When the instruction budget expires, preemption triggers — either calling a registered callback or suspending the VM to the host.
For a comprehensive guide with patterns, scheduler examples, and best practices, see the CLI Preemption page.
Enable / Disable
Enables preemption. The VM will begin counting instructions against the yield budget.
Disables preemption globally. Instructions are no longer counted.
Returns true if preemption is currently active (enabled and disable depth is zero).
Timeslice Control
Sets the number of instructions per timeslice (minimum 1).
Preempt.setTimeslice(10000) // 10,000 instructions per slice Preempt.setTimeslice(100) // Very short — for untrusted code
Returns the current timeslice setting (number of instructions).
Returns the number of instructions remaining in the current timeslice.
Resets the yield budget to a full timeslice and clears any pending request.
Yield Control
Requests preemption at the next dispatch. The budget is not zeroed — preemption fires on the very next instruction.
Immediately forces a yield. Sets the remaining budget to zero and sets the request flag, so the next instruction dispatches as a preemption event.
Callback
Registers a zero-argument function as the preemption handler. When the budget expires, the VM pushes a new frame for fn and calls it inside the current execution — no C-stack unwinding occurs.
Preempt.setCallback(func() { // Preemption is disabled here automatically print("Timeslice expired!") // You can capture a continuation, yield, or just return })
If no callback is set and preemption fires, the VM suspends to the host with ZYM_STATUS_YIELD.
Scoped Disable
For critical sections that must not be interrupted, use the disable depth (a counter, not a boolean). This allows nested disabling without accidentally re-enabling preemption too early.
Increments the disable depth. Preemption is suppressed while the depth is greater than zero, even if Preempt.enable() has been called.
Decrements the disable depth. Preemption resumes when the depth reaches zero (and preemption is enabled).
Returns the current disable depth as a number.
Executes fn (zero-argument function) with preemption disabled. Equivalent to pushDisable(), calling fn, then popDisable() — but the depth is correctly balanced even if the function exits via a continuation capture or abort.
Preempt.withDisabled(func() { // This code will never be preempted updateSharedState() commitTransaction() })
Preemption & Continuations
The disable depth is saved and restored when continuations are captured and resumed. A fiber that was running with depth 0 will resume with depth 0, regardless of the scheduler’s own disable state.
Cont.pushPrompt/Cont.popPrompt (not withPrompt) in your scheduler loop to avoid frame accumulation. See the Preemption & Continuations section in the CLI docs for a complete round-robin scheduler example.Error Reference
| Error | Cause |
|---|---|
prompt tag not found | capture or abort with a tag that has no active prompt |
continuation already consumed | Attempting to resume a one-shot continuation twice |
prompt stack overflow | Too many nested prompts (max 32) |
maximum nesting depth exceeded | Too many nested withPrompt calls (max 16) |
Best Practices
- Always capture inside a function — never directly at the prompt level.
- Prefer
withPromptover manualpushPrompt/popPromptto avoid leaked prompts. - Check
Cont.isValid(k)before resuming. - Use named tags for easier debugging:
Cont.newPrompt("myLib.fiber"). - Keep prompt scopes small — capture only the computation you need.
- Disable preemption around critical sections.
See also: GC API — Error Handling — Language Guide