# Module: `nupp.suspension`
Waiting, as an operation with an installable handler.
A library that must wait performs `suspend`, and does not decide how waiting
happens. Where a handler is installed -- a scheduler, a game frame -- the handler
answers; where none is, the built-in one drives the registered sources. One call
site, no `async` colouring, and no policy parameter threaded through an API that
did not want one.
The shape is tecs's, whose `taskruntime` has done this by hand inside one library
for years: subscribe for a resumption, be resumed once, be cancellable. What is
different is that the seam is the language's, so every library gets it rather
than each re-implementing the dispatch or being unusable inside a frame.
Three properties the protocol is built to keep, each because losing it is a
silent bug rather than a loud one:
- **Resumption has one path.** A handler is given no writable state. It parks,
it is woken, and it returns; the value only ever arrives through the one-shot
`resume` the runtime made, so the guard against a second resumption cannot be
walked around.
- **A park is always cancellable.** A subscription that did not resume during
the call must answer a cancellation, because a handler that has to abandon a
wait -- a cancelled task, a shutdown -- otherwise has no way to tell the
library. Only a subscription that already completed may answer nil, having
nothing left to cancel.
- **The context is available before subscribing.** A library registers its
readiness pump with whoever is handling suspensions, which means it has to
know who that is at subscription time rather than after.
Measured against tecs's own numbers (`bench/suspension-baseline.lua`), the row
that matters is the *ready* path -- an await whose subscription completes during
the call, which is most of the cost of waiting even when waiting really happens.
That path allocates no park and never wakes a handler.
See `plans/suspension.md`.
## Types
### `Context` _record_
What a library is handed while it subscribes: who is handling suspensions here, and
how to give them a readiness pump.
Available *before* the subscription runs, because registering a pump is part of
subscribing rather than something to do afterwards.
```nupp
record suspension.Context
source: function(
suspension.Context,
string,
integer,
function(): integer,
(function(integer): integer)?
): suspension.Source
uses: function(suspension.Context, suspension.Source): nil
canPark: function(suspension.Context): boolean
end
```
#### Methods
##### `source`
Registers a pump this wait needs driven. Answers the handle that stops it.
```nupp
source: function(
suspension.Context,
string,
integer,
function(): integer,
(function(integer): integer)?
): suspension.Source
```
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `?` | `suspension.Context` | |
| `?` | `string` | |
| `?` | `integer` | |
| `?` | `function(): integer` | |
| `?` | `(function(integer): integer)?` | |
###### Returns
| Type | Description |
| --- | --- |
| `suspension.Source` | |
##### `uses`
Associates a shared source with this wait without taking ownership of it.
Shared clients use this so one source may serve several simultaneous parks.
```nupp
uses: function(suspension.Context, suspension.Source): nil
```
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `?` | `suspension.Context` | |
| `?` | `suspension.Source` | |
###### Returns
| Type | Description |
| --- | --- |
| `nil` | |
##### `canPark`
Whether a suspension may happen here at all. A host with regions of its own --
tecs's barriers -- answers false inside them.
```nupp
canPark: function(suspension.Context): boolean
```
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `?` | `suspension.Context` | |
###### Returns
| Type | Description |
| --- | --- |
| `boolean` | |
### `Handler` _record_
What a host installs to answer suspensions.
```nupp
record suspension.Handler
park: function(suspension.Handler, suspension.Waiting, function(): nil): nil
canPark: function(suspension.Handler): boolean
shutdown: function(suspension.Handler): nil
end
```
#### Methods
##### `park`
Waits until `waiting:ready()`. Returning before that is a broken handler and
`suspend` reports it rather than handing back a value nobody produced.
`cancel` abandons the subscription. A handler that gives up on a park -- a
cancelled task, a shutdown -- must call it, and must then raise rather than
return.
```nupp
park: function(suspension.Handler, suspension.Waiting, function(): nil): nil
```
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `?` | `suspension.Handler` | |
| `?` | `suspension.Waiting` | |
| `?` | `function(): nil` | |
###### Returns
| Type | Description |
| --- | --- |
| `nil` | |
##### `canPark`
Whether a suspension may happen here. False inside a host's own barrier, which
is the run-time backstop for what `nosuspend` checks while compiling.
```nupp
canPark: function(suspension.Handler): boolean
```
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `?` | `suspension.Handler` | |
###### Returns
| Type | Description |
| --- | --- |
| `boolean` | |
##### `shutdown`
Told when a handled extent ends, so a handler owning parks or pumps can abandon
them deterministically rather than at collection.
```nupp
shutdown: function(suspension.Handler): nil
```
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `?` | `suspension.Handler` | |
###### Returns
| Type | Description |
| --- | --- |
| `nil` | |
### `Installed` _record_
An installed handler, and the obligation to put back what it displaced.
```nupp
record suspension.Installed
co: any
previous: any
handler: suspension.Handler
restored: boolean
released: boolean
parks: any
@drop
function release(self: suspension.Installed): nil
end
end
```
#### Methods
##### `release`
```nupp
release: function release(self: suspension.Installed): nil
```
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `self` | `suspension.Installed` | |
###### Returns
| Type | Description |
| --- | --- |
| `nil` | |
#### Fields
| Name | Type | Description |
| --- | --- | --- |
| `co` | `any` | |
| `previous` | `any` | |
| `handler` | `suspension.Handler` | |
| `restored` | `boolean` | Whether the slot this displaced has been put back. Separate from `released`, which additionally means every park it accepted has finished: a release that could not drain its parks has restored the slot and is still retryable. |
| `released` | `boolean` | |
| `parks` | `any` | Parks this extent accepted and has not seen finish. Held per installation, not per handler: one handler may be installed twice, and keying on it would let a nested extent cancel the enclosing one's parks on its way out. |
### `Source` _record_
A readiness pump, and the handle that owns its lifetime.
Returned rather than named, so two libraries registering "io" do not collide and
neither has to invent a unique string. Releasing is idempotent.
```nupp
record suspension.Source
release: function(suspension.Source): nil
name: string
priority: integer
wait: (function(integer): integer)?
end
```
#### Methods
##### `release`
Stops the pump being polled. Idempotent.
```nupp
release: function(suspension.Source): nil
```
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `?` | `suspension.Source` | |
###### Returns
| Type | Description |
| --- | --- |
| `nil` | |
#### Fields
| Name | Type | Description |
| --- | --- | --- |
| `name` | `string` | What this is, for diagnostics and for ordering ties. |
| `priority` | `integer` | Where in a pass it runs, lowest first. |
| `wait` | `(function(integer): integer)?` | Optionally blocks for at most the supplied milliseconds. Only the built-in driver calls this; a host drives `poll` and therefore never sleeps here. |
### `Waiting` _record_
What a park exposes to the handler driving it.
Deliberately no writable fields. A handler waits and is woken; it never supplies the
value, because the value has exactly one path and that path is guarded.
```nupp
record suspension.Waiting
ready: function(suspension.Waiting): boolean
onResume: function(suspension.Waiting, function(): nil): nil
operation: string
end
```
#### Methods
##### `ready`
Whether the subscription has resumed.
```nupp
ready: function(suspension.Waiting): boolean
```
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `?` | `suspension.Waiting` | |
###### Returns
| Type | Description |
| --- | --- |
| `boolean` | |
##### `onResume`
Registers what to run when it does. One waker; the handler installing a second
replaces the first, which is what re-parking means.
```nupp
onResume: function(suspension.Waiting, function(): nil): nil
```
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `?` | `suspension.Waiting` | |
| `?` | `function(): nil` | |
###### Returns
| Type | Description |
| --- | --- |
| `nil` | |
#### Fields
| Name | Type | Description |
| --- | --- | --- |
| `operation` | `string` | What is being waited for, which is what a stuck host reports. |
## Functions
### `ContextMT.canPark` _function_
```nupp
function ContextMT.canPark(self: any): boolean
```
#### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `self` | `any` | |
#### Returns
| Type | Description |
| --- | --- |
| `boolean` | |
### `ContextMT.source` _function_
```nupp
function ContextMT.source(self: any, name: string, priority: integer, poll: function(): integer, wait: (function(integer): integer)?): any
```
#### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `self` | `any` | |
| `name` | `string` | |
| `priority` | `integer` | |
| `poll` | `function(): integer` | |
| `wait` | `(function(integer): integer)?` | |
#### Returns
| Type | Description |
| --- | --- |
| `any` | |
### `ContextMT.uses` _function_
```nupp
function ContextMT.uses(self: any, source: any): nil
```
#### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `self` | `any` | |
| `source` | `any` | |
#### Returns
| Type | Description |
| --- | --- |
| `nil` | |
### `SourceMT.release` _function_
```nupp
function SourceMT.release(self: any): nil
```
#### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `self` | `any` | |
#### Returns
| Type | Description |
| --- | --- |
| `nil` | |
### `suspension.all` _function_
Runs every body concurrently and answers their values in order.
A branch that fails decides the whole call, but not until every branch has settled:
unwinding while siblings are still parked would strand their subscriptions, and a
caller who asked for all of them has no use for some of them.
```nupp
function suspension.all(bodies: {function(): T}): {T}
```
#### Type parameters
| Name | Description |
| --- | --- |
| `T` | |
#### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `bodies` | `{function(): T}` | what to run |
#### Returns
| Type | Description |
| --- | --- |
| `{T}` | each body's value, indexed as `bodies` was |
#### Raises
- the first error any branch raised
### `suspension.batch` _function_
Runs every body concurrently with at most `limit` in flight, and answers their values
in order.
`all` with a ceiling, for fanning out over more work than the thing underneath will
take at once -- child processes against cores, requests against a pool. A branch
finishing is what lets the next one start.
```nupp
function suspension.batch(bodies: {function(): T}, limit: integer): {T}
```
#### Type parameters
| Name | Description |
| --- | --- |
| `T` | |
#### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `bodies` | `{function(): T}` | what to run |
| `limit` | `integer` | how many may be in flight at once; below one is treated as one |
#### Returns
| Type | Description |
| --- | --- |
| `{T}` | each body's value, indexed as `bodies` was |
#### Raises
- the first error any branch raised
### `suspension.canSuspend` _function_
Whether a suspension performed here would be permitted, handler barriers included.
The run-time backstop for what `nosuspend` checks while compiling, for the calls
static analysis could not see.
```nupp
function suspension.canSuspend(): boolean
```
#### Returns
| Type | Description |
| --- | --- |
| `boolean` | |
### `suspension.create` _function_
Creates a coroutine that inherits the handler installed where it was created.
Stock `coroutine.create` gives the new thread no handler, so a library that spawns
one -- a worker, a pipeline stage -- would find itself blocking inside a frame that
was handling suspensions perfectly well. Inheritance is what makes a handled extent
mean the work started inside it rather than only the frames literally below it.
Inherited at creation rather than at resumption. What answers is the handler that
was in force where the coroutine was made, which is the lexical reading and the one
a reader can point at; a resumption-time rule would make a coroutine's behaviour
depend on who happened to resume it.
```nupp
function suspension.create(body: function(A...): R...): thread
```
#### Type parameters
| Name | Description |
| --- | --- |
| `A` | |
| `R` | |
#### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `body` | `function(A...): R...` | what the coroutine runs |
#### Returns
| Type | Description |
| --- | --- |
| `thread` | the coroutine |
### `suspension.gather` _function_
Runs every body concurrently and answers what each of them did, failures included.
`all` for a caller who wants the failures rather than the first of them. Both arrays
are indexed as `bodies` was, and exactly one of them holds an entry per branch.
```nupp
function suspension.gather(bodies: {function(): T}): {T?}, {any}
```
#### Type parameters
| Name | Description |
| --- | --- |
| `T` | |
#### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `bodies` | `{function(): T}` | what to run |
#### Returns
| Type | Description |
| --- | --- |
| `{T?}` | each body's value where it returned, and each body's error where it raised |
| `{any}` | |
### `suspension.handled` _function_
Whether a handler is installed here, which is what `blocking` against `cooperative`
means to a caller deciding before it commits to waiting.
```nupp
function suspension.handled(): boolean
```
#### Returns
| Type | Description |
| --- | --- |
| `boolean` | |
### `suspension.install` _function_
Installs `handler` for suspensions performed on this coroutine.
A scope rather than a callback, deliberately. Wrapping a body would make the extent
a
closure boundary -- which costs the caller its multi-value results and puts the
resource model's closure rules in the way of something that has nothing to do with
them -- so this hands back the obligation and lexical cleanup discharges it:
do
local handling = suspension.install(scheduler)
runFrame()
end
The extent is dynamic and per-coroutine: a suspension at any depth, through any
library, reaches the innermost handler installed on the coroutine performing it.
```nupp
function suspension.install(handler: suspension.Handler): suspension.Installed
```
#### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `handler` | `suspension.Handler` | what answers suspensions inside the scope |
#### Returns
| Type | Description |
| --- | --- |
| `suspension.Installed` | the installation, which must be released |
### `suspension.poll` _function_
Drives every registered pump once. Answers how many things settled.
Public because a host with its own loop drives the same pumps rather than a private
copy of them.
```nupp
function suspension.poll(): integer
```
#### Returns
| Type | Description |
| --- | --- |
| `integer` | |
### `suspension.race` _function_
Runs every body concurrently and answers the first one to settle.
The losers are abandoned rather than forgotten: each is resumed once so its park
cancels and its branch unwinds. A loser that had not started never starts.
```nupp
function suspension.race(bodies: {function(): T}): T?, integer?
```
#### Type parameters
| Name | Description |
| --- | --- |
| `T` | |
#### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `bodies` | `{function(): T}` | what to run |
#### Returns
| Type | Description |
| --- | --- |
| `T?` | the winner's value, and which body won |
| `integer?` | |
#### Raises
- the winner's error, when the first to settle settled by failing
### `suspension.source` _function_
Registers a readiness pump outside any subscription.
Inside one, prefer the context's `source`: a pump registered there belongs to the
handler answering that wait, and a handler that owns its pumps can shut them down.
```nupp
function suspension.source(name: string, priority: integer, poll: function(): integer, wait: (function(integer): integer)?): suspension.Source
```
#### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `name` | `string` | what this is, for diagnostics and ordering ties |
| `priority` | `integer` | where in a pass it runs, lowest first |
| `poll` | `function(): integer` | answers how many things it settled |
| `wait` | `(function(integer): integer)?` | |
#### Returns
| Type | Description |
| --- | --- |
| `suspension.Source` | the handle that stops it |
### `suspension.suspend` _function_
Performs a suspension.
`subscribe` is handed a one-shot `resume` and the context of whoever is handling
suspensions here, and answers a cancellation. It may answer nil only when it resumed
during the call, having nothing left to cancel; otherwise a cancellation is
required,
because a handler that has to abandon the wait needs a way to say so.
```nupp
function suspension.suspend<
T
>(operation: string, subscribe: function(function(T), suspension.Context): (function()?)): T
```
#### Type parameters
| Name | Description |
| --- | --- |
| `T` | |
#### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `operation` | `string` | what is being waited for, which is what a stuck host reports |
| `subscribe` | `function(function(T), suspension.Context): (function()?)` | hands over the resumption and the context, answers a cancellation |
#### Returns
| Type | Description |
| --- | --- |
| `T` | whatever `resume` was given |
#### Raises
- when the subscription resumes twice, answers no cancellation for a real park, suspends where the handler forbids it, or is handled by one that returns without resuming
### `WaitingMT.onResume` _function_
```nupp
function WaitingMT.onResume(self: any, waker: function(): nil): nil
```
#### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `self` | `any` | |
| `waker` | `function(): nil` | |
#### Returns
| Type | Description |
| --- | --- |
| `nil` | |
### `WaitingMT.ready` _function_
```nupp
function WaitingMT.ready(self: any): boolean
```
#### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `self` | `any` | |
#### Returns
| Type | Description |
| --- | --- |
| `boolean` | |