# `nupp.tasks` An application task scope. `nupp.suspension`'s combinators own a family that is complete when the call is written: `all` cannot be handed a fourth body once it is running. `create` makes a coroutine that inherits a handler and says nothing about its result, failure or lifetime. Neither is what a server, a loading pipeline or a scene needs, which is a place to put children discovered over time and one answer to what happens when the body returns, a child fails, or the whole thing is cancelled. `nupp.workers` has that shape already for CPU work, and its terminal cleanup cannot suspend, so leaving its scope blocks the thread until unawaited children finish. Correct for a worker scope, wrong inside a frame. A task scope owns one lazily and closes it through the suspension-aware path before the task scope returns. The scheduling contract is with the host, not with each child. A host sees one aggregate and decides when it runs; this decides which of its children run then, in FIFO order, up to a bounded number of activations per host turn. Nesting divides that bound rather than multiplying it, because the token belongs to the turn rather than to a scope. ```nupp local greeting = "" with scope = nupp.tasks.open() do const hello = scope:spawn(function(): string return "hello" end) const world = scope:spawn(function(): string return "world" end) greeting = hello:await() .. " " .. world:await() end assert(greeting == "hello world") ``` A scope is opened with `open`, held by a `with`, and settled when the block ends: every child has run, been cancelled, or unwound by then. `open` takes a `limit`, which parks `spawn` and `fork` while that many children are live, and a `deadline`. A running child cooperates by calling `checkpoint`, so it can stop promptly when the scope is cancelled. ```nupp local total = 0 with scope = nupp.tasks.open(deadline = 1000) do const sum = scope:spawnNamed("sum values", function(): integer local answer = 0 for value = 1, 1000 do nupp.tasks.checkpoint() answer = answer + value end return answer end) total = sum:await() end assert(total == 500500) ``` ## Types ### `Cancellation` _record_ ```nupp record tasks.Cancellation operation: string reason: string? end ``` What a cancelled task raises. Nominal rather than a string, so `isCancelled` recognizes it without matching text, and so a program that catches everything still sees something it can ask about. `tostring` renders it because an uncaught one reaches a human. #### Fields ##### `operation` ```nupp operation: string ``` What was cancelled, as it was named when it was started. ##### `reason` ```nupp reason: string? ``` Why, where a caller supplied a reason. ### `Scope` _record_ ```nupp record tasks.Scope spawn: function( borrows self: tasks.Scope, takes body: F, ...: unpackof Parameters(F) ): tasks.Task borrows (self) spawnNamed: function(borrows self: tasks.Scope, name: string, takes body: F): tasks.Task borrows (self) fork: function( borrows self: tasks.Scope, F, ...: unpackof nupp.runtime.services.workers.Submitted(F) ): tasks.Task borrows (self) cancel: function(borrows self: tasks.Scope, reason: string?): nil end ``` The scope a `run` body is handed. Not affine, and deliberately: `run` owns the extent, so there is no obligation for a body to discharge and no way for one to end the scope early. What the body can do is add children to it and reach the worker scope it owns. #### Methods ##### `spawn` ```nupp spawn: function( borrows self: tasks.Scope, takes body: F, ...: unpackof Parameters(F) ): tasks.Task borrows (self) ``` Starts a child under this scope: `scope:spawn(arguments..., f)`. ###### Arguments | Name | Type | Description | | --- | --- | --- | | `borrows self` | `tasks.Scope` | | | `takes body` | `F` | | | `...` | `unpackof Parameters(F)` | | ###### Returns | Type | Description | | --- | --- | | `tasks.Task\ borrows (self)` | | ##### `spawnNamed` ```nupp spawnNamed: function(borrows self: tasks.Scope, name: string, takes body: F): tasks.Task borrows (self) ``` Starts a named child under this scope. ###### Arguments | Name | Type | Description | | --- | --- | --- | | `borrows self` | `tasks.Scope` | | | `name` | `string` | | | `takes body` | `F` | | ###### Returns | Type | Description | | --- | --- | | `tasks.Task\ borrows (self)` | | ##### `fork` ```nupp fork: function( borrows self: tasks.Scope, F, ...: unpackof nupp.runtime.services.workers.Submitted(F) ): tasks.Task borrows (self) ``` Starts a child on a worker lane: `scope:fork(arguments..., f)`. ###### Arguments | Name | Type | Description | | --- | --- | --- | | `borrows self` | `tasks.Scope` | | | `?` | `F` | | | `...` | `unpackof nupp.runtime.services.workers.Submitted(F)` | | ###### Returns | Type | Description | | --- | --- | | `tasks.Task\ borrows (self)` | | ##### `cancel` ```nupp cancel: function(borrows self: tasks.Scope, reason: string?): nil ``` Requests cancellation of every child. ###### Arguments | Name | Type | Description | | --- | --- | --- | | `borrows self` | `tasks.Scope` | | | `reason` | `string?` | | ###### Returns | Type | Description | | --- | --- | | `nil` | | ### `Task` _type_ ```nupp type tasks.Task = TaskType(F) ``` The handle one `spawn` answers. Its `await` result pack is the body's, which is why this is derived from the function type rather than declared once over `any`. #### Type parameters | Name | Description | | --- | --- | | `F` | | ## Functions ### `ForkMT.await` _function_ ```nupp function ForkMT.await(self: any) ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `any` | | ### `ForkMT.cancel` _function_ ```nupp function ForkMT.cancel(self: any, reason: string?): boolean ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `any` | | | `reason` | `string?` | | #### Returns | Type | Description | | --- | --- | | `boolean` | | ### `ForkMT.isDone` _function_ ```nupp function ForkMT.isDone(self: any): boolean ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `any` | | #### Returns | Type | Description | | --- | --- | | `boolean` | | ### `ForkMT.status` _function_ ```nupp function ForkMT.status(self: any): string ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `any` | | #### Returns | Type | Description | | --- | --- | | `string` | | ### `TaskMT.await` _function_ ```nupp function TaskMT.await(self: any) ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `any` | | ### `TaskMT.cancel` _function_ ```nupp function TaskMT.cancel(self: any, reason: string?): boolean ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `any` | | | `reason` | `string?` | | #### Returns | Type | Description | | --- | --- | | `boolean` | | ### `TaskMT.isDone` _function_ ```nupp function TaskMT.isDone(self: any): boolean ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `any` | | #### Returns | Type | Description | | --- | --- | | `boolean` | | ### `TaskMT.status` _function_ ```nupp function TaskMT.status(self: any): string ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `any` | | #### Returns | Type | Description | | --- | --- | | `string` | | ### `tasks.checkpoint` _function_ ```nupp function tasks.checkpoint(): nil ``` Raises where the current task has been cancelled or has run out of time. The one authored cancellation point, and the only thing that reaches a body computing without parking. It never suspends, so a `nosuspend` region and a worker lane can both call it, and outside any task it does nothing. ```nupp for index = 1, #items do tasks.checkpoint() consume(items[index]) end ``` #### Returns | Type | Description | | --- | --- | | `nil` | | #### Raises - the cancellation, where one has been requested ### `tasks.deadline` _function_ ```nupp function tasks.deadline(): number? ``` The effective deadline of the current task, or nil where there is none. So a body can size its work rather than discover the bound by being cancelled part way through it. #### Returns | Type | Description | | --- | --- | | `number?` | an absolute monotonic reading, as `nupp.time.now` answers | ### `tasks.gather` _function_ ```nupp function tasks.gather(bodies: {function(): T}): {T?}, {any} ``` Runs every body concurrently and answers what each of them did, failures included. The fail-soft family. Both arrays are indexed as `bodies` was, and exactly one of them holds an entry per branch, so a caller who has to see every outcome sees them beside each other. A scope is the fail-fast answer to the same question: use one where the first failure should end the rest. ```nupp const values, errors = nupp.tasks.gather({ function(): string return fetch(primary) end, function(): string return fetch(mirror) end, }) ``` #### 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 | | `{any}` | each body's error, where it raised | #### Raises - the enclosing deadline, where one passed before the family settled ### `tasks.isCancelled` _function_ ```nupp function tasks.isCancelled(value: any): boolean ``` Whether a caught value is a task cancellation. The one question a `pcall` around task work has to be able to ask, because cancellation is not a failure and should usually be re-raised rather than reported. ```nupp const ok, problem = pcall(work) if not ok and not tasks.isCancelled(problem) then report(problem) end ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `value` | `any` | whatever was caught | #### Returns | Type | Description | | --- | --- | | `boolean` | whether this is a cancellation | ### `tasks.open` _function_ ```nupp function tasks.open(limit: integer?, deadline: number?): affine(tasks.Scope, tasks.settle) ``` Opens a scope, to be held by a `with`. The block is the scope's body. Children started in it with `spawn` and `fork` are its family, and leaving the block -- normally, by `break` or `return`, or by an error -- settles them: every one has run, been cancelled, or unwound before the block is left. A child's failure is the scope's from the moment it happens, cancels its siblings, and is raised where the block is left, whether or not anything awaited that child. With a `limit`, `spawn` and `fork` park while that many children are live, so a loop that fans out over a source is bounded by the loop itself: nothing is pulled from the source until there is room to run it. With a `deadline`, in milliseconds of the monotonic clock, expiry requests ordinary cancellation. A scope opened inside another takes the earlier of the two deadlines: a child may bound itself more tightly than its parent did, and may not extend what its parent already promised. Both are named arguments: `open()`, `open(limit = 8)`, `open(deadline = 500)`, or `open(limit = 8, deadline = 500)`. ```nupp const sizes: {integer} = {} with scope = nupp.tasks.open(limit = 8) do for index, url in ipairs(urls) do scope:spawn(sizes, index, url, storeSize) end end ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `limit` | `integer?` | how many children may be live at once, or nil for no bound | | `deadline` | `number?` | how long the whole scope may take, in milliseconds, or nil | #### Returns | Type | Description | | --- | --- | | `affine(tasks.Scope, tasks.settle)` | the scope, settled when its `with` ends | #### Raises - where the limit is not a positive integer or the deadline is not a finite non-negative number ### `tasks.settle` _function_ ```nupp function tasks.settle(takes scope: tasks.Scope): nil ``` Settles an opened scope, which is what leaving its `with` does. Every child has run, been cancelled, or unwound when this returns, and the worker scope, where one was opened, has been closed through the suspension-aware path. It is a settling terminal: it parks until that is so, and so is refused inside a `nosuspend` region. Idempotent, so a scope settled by hand before its block ends settles once. A failure the block itself raises does not cancel the children: they run to completion before it propagates. Call `cancel` first where that is not wanted. ```nupp with scope = nupp.tasks.open() do scope:spawn(function(): nil work() end) end ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `takes scope` | `tasks.Scope` | the scope `open` answered | #### Returns | Type | Description | | --- | --- | | `nil` | | #### Raises - the first failure a child had, or the cancellation a deadline caused