# 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` | |