Suspension is an effect, not a second kind of function. Atoll has no async fn
modifier and no prefix await operator.
fn read_chunk(
socket: TcpConnection,
mut buffer: []byte
): int ! TcpError {
return socket.read_into(buffer, 4096)?
}The call looks ordinary. Its declaration or inferred effect row tells the compiler that it may pause and later continue.
Suspension sources
Current suspension sources include:
- host calls decorated with
@suspend; - joining a
Taskwith.await(); - a blocking
select; - stream producer backpressure through
send_await; - database and network operations backed by suspending host calls.
An operation may take a synchronous fast path. Awaiting an already completed
task, for example, returns without actually yielding, but its static contract
still includes Suspend.
Conversely, ordinary-looking code may suspend because a called function inherits the effect from deeper in the call graph. Read the function contract, not punctuation at the call site.
Resume behavior
At a suspension point the compiler preserves values needed later, records a resume continuation, and returns control to the scheduler. The scheduler re-enters the generated continuation after the completion source becomes ready.
This continuation machinery is compiler-managed. Application code should not construct frames, program counters, or resume functions.
The continuation resumes after the suspending operation and observes its
result exactly as an ordinary call result. Local control flow—match, ?,
loops, and deferred cleanup—continues through generated blocks rather than a
second source-language state-machine syntax.
Local state
Values live across suspension when later expressions need them:
fn total(task: Task[int]): int {
offset := 2
value := task.await()
return value + offset
}The compiler decides whether those values remain in fixed frame slots, need managed storage, or require another representation. Raw pointers and borrowed references have stricter crossing rules; see References.
Values not needed after the call need not remain live across it. Keeping scopes small can reduce captured continuation state:
request_id := request.id
response := fetch(request_id)
return response.statusAvoid retaining a large aggregate merely because one field is used after a suspension.
Atomic regions
Cooperative scheduling means straight-line code runs until it reaches a safepoint or suspension. It does not mean a large computation can never be preempted—the runtime and generated loop safepoints may still cooperate with cancellation and fairness.
Do not use this property as a replacement for an ownership contract. Pass values into child tasks deliberately, keep shared handles explicit, and use channels for cross-task communication.
Long CPU loops should still reach generated loop safepoints so cancellation and fairness can progress. Unsafe raw-pointer regions must not span those points.
Function coloring
Because call syntax is uncolored, code remains composable:
fn load_page(id: int): Page ! LoadError {
user := load_user(id)?
settings := load_settings(user.id)?
return Page { user, settings }
}Effect inference, rather than syntax at each call, determines whether
load_page needs the continuation ABI.
Cleanup
Managed locals live across suspension according to compiler liveness. Normal
resume, error propagation, and cancellation each receive a cleanup path for
owned values. Resource types with Drop can therefore release host handles
when a parked task is canceled.
Cleanup methods themselves cannot suspend. Acquire a resource through a typed
wrapper, keep it in an owned local, and let the wrapper’s synchronous Drop
close it if control leaves early.
Boundaries
Host adapters mark waiting operations with @suspend. Pure host calls such as
resource close remain synchronous. At a public API, an explicit using row can
make this distinction part of the contract; otherwise inference propagates it
automatically.