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

ModulePurpose
ContDelimited continuation primitives (prompts, capture, resume, abort, shift)
PreemptPreemptive 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.

Capture Scope
┌─────────────────────────────────────────────┐
│  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:

  1. Install prompts — Mark where continuations can be captured to
  2. Capture — Specify which prompt to capture up to
  3. 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!

Basic Example
// 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

Cont.newPrompt()

Creates a new, unique prompt tag.

Cont.newPrompt(name)

Creates a prompt tag with a debug name (shown in error messages).

var tag = Cont.newPrompt("fiber")
Cont.isPromptTag(value)

Returns true if value is a prompt tag.

Installing Prompts

Cont.withPrompt(tag, fn)

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.

withPrompt
var tag = Cont.newPrompt("work")

var result = Cont.withPrompt(tag, func() {
    print("Inside prompt scope")
    return 42
})
print(result)   // 42
Cont.pushPrompt(tag)

Manually installs a prompt boundary. You must call Cont.popPrompt() when done.

Cont.popPrompt()

Removes the topmost prompt from the stack.

Capture & Resume

Cont.capture(tag)

Captures the continuation from the current point up to the specified prompt, then transfers control back to the prompt location.

Critical: Always call capture() inside a helper function. If you capture directly at the prompt level, the variable holding the continuation gets overwritten when resumed.
capture pattern
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
Cont.resume(continuation, value)

Resumes a captured continuation with the provided value. The continuation is consumed after this call (one-shot).

Returns: the eventual return value from the resumed computation.

Cont.isContinuation(value)

Returns true if value is a continuation object.

Cont.isValid(continuation)

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

Cont.abort(tag, value)

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

Cont.shift(tag, handler)

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.

shift
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

generator with shift
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

early return
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

Preempt.enable()

Enables preemption. The VM will begin counting instructions against the yield budget.

Preempt.disable()

Disables preemption globally. Instructions are no longer counted.

Preempt.isEnabled()

Returns true if preemption is currently active (enabled and disable depth is zero).

Timeslice Control

Preempt.setTimeslice(instructions)

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
Preempt.getTimeslice()

Returns the current timeslice setting (number of instructions).

Preempt.remaining()

Returns the number of instructions remaining in the current timeslice.

Preempt.reset()

Resets the yield budget to a full timeslice and clears any pending request.

Yield Control

Preempt.request()

Requests preemption at the next dispatch. The budget is not zeroed — preemption fires on the very next instruction.

Preempt.yield()

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

Preempt.setCallback(fn)

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.

Important: The callback runs with preemption automatically disabled (the VM increments the disable depth before calling it). This prevents recursive preemption. The depth is restored when the callback returns.
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.

Preempt.pushDisable()

Increments the disable depth. Preemption is suppressed while the depth is greater than zero, even if Preempt.enable() has been called.

Preempt.popDisable()

Decrements the disable depth. Preemption resumes when the depth reaches zero (and preemption is enabled).

Preempt.getDisableDepth()

Returns the current disable depth as a number.

Preempt.withDisabled(fn)

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.

Critical Section
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.

Scheduler pattern: Use 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

ErrorCause
prompt tag not foundcapture or abort with a tag that has no active prompt
continuation already consumedAttempting to resume a one-shot continuation twice
prompt stack overflowToo many nested prompts (max 32)
maximum nesting depth exceededToo many nested withPrompt calls (max 16)

Best Practices


See also: GC APIError HandlingLanguage Guide