Suspension handlers#
A suspension handler connects suspension-aware calls to a host's event loop. A root task function installs the host's handler, then ordinary functions beneath it can park without accepting a scheduler parameter:
frame.handler is an ordinary value. It is not a keyword, a global scheduler, or a handler built into Nupp. The application function defines its dynamic scope. printCompilerVersion uses it because application called that function inside the region.
The host supplies scheduling policy#
A command-line program needs no handler. Its suspension-aware calls drive the registered readiness sources and block the current operating-system thread.
A host supplies a handler when blocking the thread would stop unrelated work:
- A game engine parks a loading coroutine and renders the next frame.
- A server parks one request and serves other connections.
- A UI runtime parks a task and continues processing input.
- A test scheduler controls exactly when a suspended operation resumes.
Most application code consumes that handler through handle suspension. Framework authors and scheduler integrations implement one. The all, gather, race, and batch combinators also use a private handler to interleave their branches.
A wait parks one coroutine#
When child:communicate() cannot finish immediately, control moves through seven steps:
- The process library registers its readiness source and cancellation function.
- The suspension runtime calls
frame.handler.parkwith the pending wait. - The handler records the current coroutine and yields it to the event loop.
- The event loop runs another coroutine, request, or frame.
suspension.poll()discovers that the child process has completed.- The library resumes the wait, and the handler queues its coroutine again.
- The coroutine runs, and
communicate()returns itsprocess.Result.
The handler never supplies the result. The library's guarded resume function does that. The handler decides when the coroutine runs again.
Scope follows the coroutine#
A handler is dynamically scoped per coroutine, not process-wide. A host often wraps its root application task, which makes that handler application-wide in practice:
suspension.create inherits the installation in force at creation. A stock coroutine.create inherits none. A nested handle suspension temporarily replaces the current handler and restores the outer one when its region ends. Different coroutines may therefore use different handlers at the same time.
Writing a frame handler#
This scheduler keeps a queue of runnable coroutines. Its event loop calls tick once per frame to poll readiness sources and resume the tasks they woke:
The as suspension.Handler cast accepts a trusted runtime contract. The checker verifies the function bodies and their annotations, but only the scheduler author can guarantee that park eventually resumes or cancels every wait.
The three members divide the work:
parkregisters a waker that enqueues the current coroutine, then yields until the wait is ready.canParkreturns false inside a host barrier where yielding would violate a runtime invariant.shutdowndrains work queued while the handled extent is ending.
waiting:onResume(wake) is a notification, not value delivery. The readiness source supplies the value through resume; the waker makes the coroutine runnable after that value exists. run creates the root task used in the opening application. Its first resume reaches park and yields. Each tick polls completion sources and resumes tasks placed on runnable. A game host calls the same tick function once per frame instead of using this standalone loop.
Cancellation unwinds the parked stack#
handle suspension lowers to an owned handler installation. When its extent ends, the runtime restores the previous handler, cancels outstanding subscriptions, wakes their coroutines, and invokes shutdown. A cancelled suspend raises inside its parked coroutine, so lexical resource drops run as the stack unwinds.
The region currently refuses a return or an unbound break that would cross its boundary:
error: NUPP2706: control cannot leave a `handle suspension` region yetStore the result outside the region and return after the previous handler has been restored:
Diagnostics#
- NUPP2706 reports a
returnor unboundbreakthat leaves ahandle suspensionregion.
Next#
- Suspension explains blocking, parking, effects, raw coroutines, and concurrent combinators.
- Ownership explains the obligations cancellation unwinds.
- The
nupp.suspensionAPI reference lists the handler, waiting, installation, source, and subscription types.