#Requirements and Suspension
Status: language specification draft.
This chapter defines static dependency requirements, provider injection, and one-shot suspension. The mechanisms are related but deliberately separate. The grammar and semantics in this chapter are part of hd-lang; additional runtime and library policies are tracked as future work.
#Decomposed Model
hd-lang decomposes concerns often grouped under algebraic effects into three mechanisms:
| Mechanism | What it does |
|---|---|
| One-shot suspension | fn!, Suspend[T], bang calls, polling, and cancellation describe a resumable computation. |
| Requirement checking | $ rows let the compiler prove that every dependency used by a function is available. |
| Dependency injection | Contexts bind requirement keys to ordinary values that implement those requirement traits. |
- req.model.one-shotSuspension is not a general or multi-shot continuation system.
- req.model.not-capabilityA requirement is not necessarily a host capability.
- req.model.errorsNormal errors are values represented by
Result[T, E]. - req.model.lookup-no-suspendDependency lookup does not suspend.
- req.model.suspend-no-errorsSuspension does not raise ordinary errors.
- req.model.no-provider-choiceDeclaring a requirement does not by itself choose a provider.
- req.model.runtimesProduction, test, sandbox, and replay runtimes may bind different providers or drivers at explicit requirement and suspension boundaries.
- req.model.never-reinterpretedCode outside those boundaries is never reinterpreted: every runtime runs it the same way.
trait Store:
fn get(self) -> string
fn read() -> string $ Store:
$.use(Store).get()#Requirement Rows
This section defines requirement rows, the $ clause that writes them, and
how the compiler checks them.
- req.row.definitionA requirement row is the normalized unordered set of requirement keys on a callable signature.
- req.row.parameterA row parameter is a generic parameter whose values are requirement rows.
These are the only terms used below for the concrete and generic forms.
trait Logger:
fn log(self, text: string) -> void
fn work() -> void $ Logger:
$.use(Logger).log("done")#Row Syntax
A function signature may end with $ and a list of requirement keys:
fn load_user!(id: UserId) -> Result[User?, DbError] $ Database + Cache:
...Requirement Clauses gives the
grammar of requirement_clause, requirement_row, and requirement_key.
- req.row.syntax.signatureA function signature may end with
$and an unordered list of requirement traits. - req.row.syntax.plus-listEach key names a separate injected value, and several keys are joined with
+, as in$ Database + Cache. - req.row.syntax.single-bareA single key may be bare, as in
$ Console. - req.row.syntax.same-formA header and a type write a row the same way, as in
fn(UserId) -> User $ Database + Cache. - req.row.syntax.emptyThe clause
$()writes the empty row explicitly. - req.row.syntax.no-mutA key has no
mutprefix; a trait's methods decide the provider's access, as Mutable Providers describes.
See also: Mutable Providers, Types.
#Callable Forms
Function declarations, closure expressions, and function types use the same requirement clause:
Functions, Types, and
Closures give the productions function_decl,
function_type, and closure_expression.
- req.row.callable.same-clauseFunction declarations, closure expressions, and function types use the same requirement clause.
- req.row.callable.methodsThe same callable name and optional requirement clause apply to trait methods and functions inside
implblocks. - req.row.callable.impl-agreesA trait requirement and its implementation must agree on suspension and normalized requirement row behavior.
trait Store:
fn get(self) -> string
fn read() -> string $ Store:
$.use(Store).get()
fn run(read: fn() -> string $ Store) -> string $ Store:
read()#Omitted Requirement Clauses
- req.row.omitted.empty-pubA public function, a public inherent method, a trait method, or a method of a trait implementation without a requirement clause has the empty row.
- req.row.omitted.empty.bodyThe body of such a callable may use only requirements satisfied by a
$.withblock in that body. - req.row.omitted.inferred-privateA non-public function, a non-public inherent method, or a local
fndeclaration without a requirement clause has an inferred row. It is computed by the same rule as a closure's below. - req.row.omitted.cycleInferred rows of functions that call each other in a cycle are the least rows that satisfy every member of the cycle.
- req.row.omitted.closure-rowWhen a closure omits its requirement clause, the compiler infers a row for it. The row is the least one containing every requirement its body uses outside the
$.withblocks of that body. - req.row.omitted.outer-scopeA
$.withblock around a closure or localfndeclaration never satisfies that callable's requirements, so each key its body uses goes into its row. - req.row.omitted.parameter-callsCalls through function parameters contribute their normalized rows.
- req.row.omitted.expected-parameterIf an expected function type contains a row parameter, the inferred concrete row is unified with that parameter.
- req.row.omitted.not-emptyOmission never means an empty row merely because the expected row is generic.
- req.row.omitted.writtenA written
$clause is explicit and must entail the same body requirements.
trait Logger:
fn write(self, message: string) -> void
fn log(message: string) -> void $ Logger:
logger := $.use(Logger)
logger.write(message)
trait Job:
fn run(self) -> void
struct Nightly: pass
impl Job for Nightly:
fn run(self) -> void:
log("nightly") # error: missing-requirementWhy. A public row is the reader's summary of what a function reaches, so every package writes it. A row alias shortens a long public row instead of inference.
#Row Sets
- req.row.set.orderRows are sets: order does not affect type identity.
- req.row.set.onceA key occurs at most once after normalization.
- req.row.set.unionThe row denoted by a list is the union of its keys.
- req.row.set.parameterA row parameter listed beside other keys contributes every key of its row.
- req.row.set.duplicate
$ Logger + Clock + Loggeris the row$ Clock + Logger. - req.row.set.callA function may call another required function only when its own row includes those requirements or a lexical provider scope satisfies them. Error:
missing-requirement.
trait Database
fn query() -> void $ Database:
pass
pub fn invalid() -> void:
query() # error: missing-requirement#Entailment
- req.row.entail.setsRequirement checking uses set entailment after alias expansion.
- req.row.entail.availableFor a body,
availableis the union of the declared row and the lexical keys. - req.row.entail.memberEvery required key must be a member of
available. - req.row.entail.membershipRows contain only unions, so entailment reduces to membership.
- req.row.entail.concreteA concrete key
Kis entailed by a row exactly when the row listsK. - req.row.entail.concrete.parameterAn unknown row parameter listed in the row never entails
K. - req.row.entail.parameterAn unknown row parameter
Ris entailed by a row exactly when that row listsRitself. - req.row.entail.parameter.concreteNo set of concrete keys entails
R. - req.row.entail.genericA generic key such as
Repo[T]is entailed only by a key that is identical to it after alias expansion.
trait Clock
fn read() -> i32 $ Clock: 1
pub fn value() -> i32: read() # error: missing-requirement
fn tick() -> i32 $ Clock: read() # ok: the row entails Clock#Least Row Solutions
- req.row.least.solutionInference for a parameter pattern that lists a row parameter beside concrete keys chooses the least row solution.
- req.row.least.examplesMatching
$ R + Kagainst the row$ Kinfers the empty row forR, and matching it against$ K + Clockinfers$ Clock. - req.row.least.absent-keyWhen the matched row lacks
K, the pattern still matches, andRis the whole matched row. - req.row.least.removalThis least-solution rule is also how a callee removes a key from a callback row.
- req.row.least.one-unknownA row pattern may list at most one row parameter that is still unknown when the pattern is solved.
- req.row.least.fixedA parameter's row pattern fixes a row parameter when that parameter is the only one in the pattern not already fixed. Patterns are solved one at a time in that order.
- req.row.least.ambiguousA declaration is an error when a row pattern in a parameter type lists two or more row parameters that no other parameter's pattern fixes. Error:
ambiguous-row-pattern. - req.row.least.ambiguous.reportedThe error is reported on the parameter whose type holds that pattern.
trait Db
trait Clock
fn both[$R1, $R2](first: fn() -> void $ R1, second: fn() -> void $ R1 + R2) -> void $ R1 + R2:
first()
second()
fn split[$R1, $R2](f: fn() -> void $ R1 + R2) -> void $ R1: # error: ambiguous-row-pattern
passIn both, first fixes R1, so R2 is the one unknown in the pattern of
second. For a callback row $ Db + Clock, split could take R1 as
$ Db or as $(), and the two choices give split different rows.
Why. A callback that needs fewer keys than its pattern allows is always safe to call where the pattern's keys are provided. A pattern with one unknown has one least solution. Koka and Links state the same limit in their syntax, where a row ends in at most one variable.
See also: Requirement Polymorphism.
#Row Subsumption
A function value fits a function type whose row is wider than its own:
trait Db
trait Clock
fn health() -> string $ Clock:
"ok"
fn orders() -> string $ Db + Clock:
"orders"
fn routes() -> List[fn() -> string $ Db + Clock]:
[health, orders]- req.row.subsumeA function value fits an expected function type when the expected row entails every key of the value's row and the two types otherwise match.
- req.row.subsume.sitesRow subsumption applies wherever a function value is checked against an expected function type. That includes an argument, an assignment, a declared binding, a return value, a field, and a list element.
- req.row.subsume.missingA function value whose row lists a key the expected row does not entail does not fit. Error:
type-mismatch. - req.row.subsume.adaptThe compiler may adapt such a value, so that a call through the wider type passes the value only the providers of its own row.
- req.row.subsume.closureA closure whose inferred row is narrower than an expected row therefore fits that row without taking it.
trait Db
trait Metrics
fn record() -> void $ Db + Metrics:
pass
fn store(callback: fn() -> void $ Db) -> void $ Db:
callback()
fn run() -> void $ Db + Metrics:
store(record) # error: type-mismatchWhy. Calling a function where more providers are available than it needs is always safe. A table of handlers can then share one wide row, while each handler keeps its own least row.
Note. Row subsumption converts a function value, not a container of function values. Variance still keeps a function type's row invariant (Variance), so a
List[fn() -> void $ Db]value does not convert toList[fn() -> void $ Db + Clock]. A wider list is built by an explicit copy, such as a spread into a list with the wider element type:let wide: List[fn() -> void $ Db + Clock] = [narrow...].
See also: Assignability And Coercion, Least Row Solutions.
#Row Union In Literals
A list or map literal with no expected type gives its function values the union of their rows, and so does every other least-common-type site:
trait Db
trait Clock
fn health() -> string $ Clock:
"ok"
fn orders() -> string $ Db:
"orders"
fn serve() -> string $ Db + Clock:
handlers := [health, orders]
let text = ""
for handler in handlers:
text = text + handler()
text
fn serve_one(admin: bool) -> string $ Db + Clock:
handler := if admin: orders else: health
handler()- req.row.union.literalWhen a list literal has no expected type and its elements are function values, its element row is the union of the elements' rows.
- req.row.union.literal.mapThe same holds for the values of a map literal with no expected type.
- req.row.union.literal.typeThe element type is the least common type of the elements' types after each function type's row is widened to that union.
- req.row.union.literal.fitEach element then fits the element type by row subsumption, so a call through the collection passes each function only its own row's providers.
- req.row.union.literal.spreadA spread contributes the row of its list's element type, as it contributes that type (
expr.list.spread.inferred). - req.row.union.literal.other-partsWhen the widened types still have no common type, as for
fn() -> string $ Dbandfn() -> i32 $ Clock, the literal is an error. Error:no-common-type. - req.row.union.literal.directOnly function elements take the union. An element that holds function values, such as a list, keeps its own type, so
[[health], [orders]]has no common type. Error:no-common-type. - req.row.union.literal.expectedWith an expected type, each element is checked against the expected element type (
expr.collection.expected), and a key outside the expected row istype-mismatch(req.row.subsume.missing). - req.row.union.literal.invariantThe inferred collection keeps the union row, and it converts to no list or map with a wider row, as the Note above states.
- req.row.union.literal.diagnosticsA diagnostic prints an inferred union row with each key once, in a stable order.
- req.row.union.literal.diagnostics.expandedA
missing-requirementortype-mismatchdiagnostic on that row also lists its expanded keys and names the missing key, asreq.row.alias.diagnostics.expandedstates. - req.row.union.sitesThe other least-common-type sites take the union the same way. They are the branches of a value-producing
ifand the arms of a value-producingmatch. They also include the final value andreturnoperands of a closure or non-public function whose result type is inferred. - req.row.union.sites.typeAt each such site, the inferred type is the least common type of the values' types. Before that, each function type's row is widened to the union of their rows. Each value then fits that type by row subsumption.
- req.row.union.sites.directAt every site, only function values take the union. A value that holds function values keeps its own type, so
if admin: [orders] else: [health]has no common type. Error:no-common-type.
trait Db
trait Clock
trait Log
fn health() -> string $ Clock:
"ok"
fn orders() -> string $ Db:
"orders"
fn serve_all(handlers: List[fn() -> string $ Db + Clock + Log]) -> void:
pass
fn wire() -> void:
handlers := [health, orders]
serve_all(handlers) # error: type-mismatch
fn group(admin: bool) -> void:
groups := if admin: [orders] else: [health] # error: no-common-typeWhy. Without the union,
[health, orders]has no common type. A handler table would then need a written element type even when every row is known. The union is the least row that every element fits. One rule for every least-common-type site is simpler than a literal-only case.
#Row Parameters
A row parameter is marked with $ where it is declared:
fn map[T, U, $R](items: List[T], f: fn(T) -> U $ R) -> List[U] $ R:
let mut result: List[U] = []
for item in items:
result.push(f(item))
result- req.row.param.markedA generic parameter written with
$before its name, as$Rinfn map[T, U, $R], is a row parameter. - req.row.param.marked.ownersA function, a method, an implementation, and a row alias may declare row parameters.
- req.row.param.marked.useInside a row, a row parameter is written by its name, as in
$ Ror$ R + Log. - req.row.param.no-inferenceA parameter's kind comes only from its declaration. A parameter written without
$is type-kinded, whatever its uses. - req.row.param.unmarkedA parameter written without
$and used in a row is an error whose fix-it adds$to its declaration. Error:generic-kind-mismatch. - req.row.param.as-typeA row parameter used as a type, as in
value: Rafter$R, is an error. Error:generic-kind-mismatch. - req.row.param.one-kindOne parameter cannot be used as both an ordinary type and a requirement row.
- req.row.param.no-dataA struct, an enum, or a trait declares no row parameter: each of its own generic parameters is type-kinded.
- req.row.param.no-data.markedA
$on a generic parameter of a struct, an enum, a trait, or a newtype, as instruct Job[$R], is an error. Error:generic-kind-mismatch. - req.row.param.no-data.errorUsing such a parameter in requirement position, as in a field of type
fn() -> void $ R, is an error. Error:generic-kind-mismatch. - req.row.param.no-newtypeA newtype declares no row parameter either, as a struct does not. So
type Job[R](fn() -> void $ R)is an error. Error:generic-kind-mismatch. - req.row.param.context
$.Context[...]takes only a concrete row, so a row parameter in its row is an error. Error:row-parameter-in-context.
fn run[R](job: fn() -> void $ R) -> void $ R: # error: generic-kind-mismatch
job()
fn keep[$R](value: R) -> void: # error: generic-kind-mismatch
passstruct Job[R]:
run: fn() -> void $ R # error: generic-kind-mismatch
type Task[R](fn() -> void $ R) # error: generic-kind-mismatch
fn run_job[$R](providers: $.Context[$ R], job: fn() -> void $ R) -> void: # error: row-parameter-in-context
$.with(providers...):
job()Why. In
Job[Db], nothing shows whetherDbis a requirement row or an ordinary type argument. The marker answers that at the declaration, and no use of the parameter changes its kind.
Note. Handlers stored in one table share a concrete row, often a row alias, as in
List[fn(Request) -> Response $ AppRow]. Row Subsumption lets each handler keep a narrower row.
#Row Slots
A row written inside a type's brackets starts with $, even for one key:
use std.function.Fn
trait Db
trait Cache
type Job[$R] = fn() -> void $ R
fn schedule(first: Job[$ Db], second: Job[$ Db + Cache], ctx: $.Context[$ Db]) -> void:
pass
fn wrap(callback: Fn[(), void, $ Db]) -> Job[$ Db]:
callback- req.row.slotA row slot is a place in a type or a type argument list that takes a requirement row.
- req.row.slot.listThe row slots are the brackets of
$.Context[...]and the row argument ofFnandSuspendFn. They also include an argument for a row parameter of an alias. An explicit type argument for a row parameter of a function or method is one too. - req.row.slot.dollarEvery row in a row slot is written after
$, including one key and a row alias, as inJob[$ Db],$.Context[$ AppRow], andFn[(), void, $ AppRow]. - req.row.slot.emptyThe empty row in a row slot is
$(). - req.row.slot.bareA bare key or a bare row alias in a row slot is an error whose fix-it adds
$. Error:generic-kind-mismatch.
trait Db
type Job[$R] = fn() -> void $ R
fn schedule(job: Job[Db]) -> void: # error: generic-kind-mismatch
pass
fn install(ctx: $.Context[Db]) -> void: # error: generic-kind-mismatch
passWhy. A bare name in brackets reads as a type argument. With
$on every row, a reader tells a row from a type at each use, without looking up the parameter's kind.
#Row Aliases
A row alias names a set of requirement keys with an ordinary transparent
alias. Its right side is a row, written after $ like every other row:
trait Db
trait Cache
trait Log
trait Clock
type AppRow = $ Db + Cache + Log
fn get_order() -> string $ AppRow + Clock:
"order"- req.row.alias.dollarA row alias is a transparent alias whose right side is a row after
$, as intype AppRow = $ Db + Cache + Logortype Web = $ AppRow. - req.row.alias.empty
type NoRow = $()declares a row alias for the empty row. - req.row.alias.expandA row alias written in a row stands for its keys, so
$ AppRow + Clockis the row$ Db + Cache + Log + Clock. - req.row.alias.expand.firstExpansion comes before normalization, entailment, least-row solving, row subsumption, and the check for generic key collisions.
- req.row.alias.nestedA row alias may name another row alias, and expansion flattens every level into one set.
- req.row.alias.dollar.missingA right side that joins keys with
+without$, as intype AppRow = Db + Cache, is an error. So is one that names one row alias without$, as intype Web = AppRow. The fix-it adds$. Error:generic-kind-mismatch. - req.row.alias.duplicateA key reached twice, directly or through aliases, occurs once in the row, as
req.row.set.duplicatestates, and is not diagnosed. - req.row.alias.in-rowsA row alias is written only in a row after
$: in a requirement clause, or in a row slot, as in$.Context[$ AppRow]. - req.row.alias.type-or-keyA row alias is row-kinded. Using one as a value type, a bound, a type-kinded argument, or a single key, as in
$.use(AppRow)orAppRow=value, is an error. Error:generic-kind-mismatch. - req.row.alias.type-alias.reference-valuesA right side without
$declares a type alias, never a key, as intype Users = &dyn Store[Item = User]. It is usable only where a type is, as inx: Users. - req.row.alias.type-alias.keyA type alias names a type, so writing one where a trait goes, as in
$ Users,$.use(Users),Users=value, or the boundT < Users, is an error. Error:unknown-trait. - req.row.alias.bare-trait.reference-valuesA bare trait on the right side, as in
type Store = Db, is a trait in a type position, sotypes.trait.value.dyn-required.reference-valuesrejects it. Error:trait-used-as-type. - req.row.alias.named-keyA key or a set of keys gets a name only through a row alias, as in
type UserRow = $ Store[Item = User], written$ UserRowin a row. - req.row.alias.bound-keyA row alias may list a bound key, as in
type UserRow = $ Store[Item = User] + Log. - req.row.alias.key-written
$.use, a binding in$.with, and a bound write the trait itself, as in$.use(Store[Item = User]). - req.row.alias.accessA key reached through an alias is its trait, so its provider's access follows
req.mut.trait-access. - req.row.alias.generic.markedA row alias may declare generic parameters, and a row parameter among them is marked
$, as intype WithLog[$R] = $ R + Log. - req.row.alias.generic.use
$ WithLog[$ Clock]is the row$ Clock + Log, and in a function with row parameterR,$ WithLog[$ R]extendsRwithLog. - req.row.alias.generic.kindA row argument for a type-kinded alias parameter, as in
$ Only[$ AppRow]aftertype Only[T] = T, is an error. Error:generic-kind-mismatch. - req.row.alias.cycleA row alias that expands to itself is an alias cycle. Error:
alias-cycle.
use std.function.Fn
trait Db
trait Cache
trait Log
type AppRow = $ Db + Cache
type WithLog[$R] = $ R + Log
fn install(ctx: $.Context[$ AppRow], job: Fn[(), void, $ AppRow]) -> void:
$.with(ctx...):
job()
fn logged() -> void $ WithLog[$ AppRow]:
pass
fn provide_log[$R](callback: fn() -> void $ R + Log) -> void $ R:
pass
fn run() -> void $ AppRow:
provide_log::[$ AppRow](logged)trait Db
trait Cache
type AppRow = $ Db + Cache
type Bare = Db + Cache # error: generic-kind-mismatch
type Web = AppRow # error: generic-kind-mismatch
fn load(rows: AppRow) -> void: pass # error: generic-kind-mismatch
fn read() -> void $ AppRow:
_ := $.use(AppRow) # error: generic-kind-mismatch
type Users = &dyn Db
fn count() -> i32 $ Users: # error: unknown-trait
0trait Db
trait Cache
type Front = $ Back + Db # error: alias-cycle
type Back = $ Front + CacheWhy. Koka, Scala, Effect-TS, and Haskell name a set of requirements with the language's ordinary alias. A row alias works the same way, so it adds no new kind of declaration.
Why. With
$on every row, the declaration shows that a row alias is a row, as each use does. A right side without$is always a type, sotypenever declares a trait alias. A key is written as its trait or reached through a row alias, never through a type alias.
Note. A
pubrow alias names only public traits, asmodule.vis.signature.coveragerequires of every alias target.
#Aliases In Diagnostics
- req.row.alias.diagnosticsA diagnostic prints a row as it is written, with alias names kept.
- req.row.alias.diagnostics.expandedA
missing-requirementortype-mismatchdiagnostic on a row that uses an alias also lists the row's expanded keys and names the missing key. The expansion may be part of the message text.
For example, a missing Metrics under $ WebRow + Auth might read:
error[missing-requirement]: `respond` requires `Metrics`
--> handlers/orders.hd:12:5
| pub fn get_order!(req: Request) -> Response $ WebRow + Auth
| ------ WebRow = Db + Cache + Clock + Log
See also: Transparent Aliases And Newtypes, Type Declarations.
#Requirement Keys
- req.key.traitsRequirement keys are traits, including interfaces such as
Databaseand host capabilities such asClockorNetwork. - req.key.no-effect-syntaxThe language does not introduce a separate effect-declaration syntax.
- req.key.in-scopeA requirement key is an ordinary trait name, so its trait must be in scope where the key is written, as in
$.with(Mailer=mailer). Error:unknown-trait. - req.key.inspectableA trait that is
std.inspect.Inspectableor has it as a direct or transitive supertrait is never a requirement key. - req.key.inspectable.errorWriting one as a key is an error reported on the key. That covers a requirement clause, a provider scope, and any other place a key is named. Error:
inspectable-requirement. - req.key.inspectable.boundSuch a trait remains valid as a bound and as a value type.
- req.key.any-trait-members.reference-valuesA requirement key may name any trait. A member unavailable on the trait value reference that
$.useyields is unavailable through$.usetoo.
use std.inspect.Inspectable
trait Storage < Inspectable:
fn get(self, key: string) -> string?
fn load() -> void $ Storage: # error: inspectable-requirement
pass
trait Factory:
fn create() -> Self
fn name(self) -> string
fn build() -> string $ Factory: # valid: create is only unavailable on the provider
$.use(Factory).name()Why. The rule keeps provider values attenuated: a provider reached through a key can never be tested for its concrete type.
See also: Runtime Type Identity.
#Bound Requirement Keys
A requirement key may bind the trait's associated types, as a bound does:
trait Store:
type Item
fn load(self, id: string) -> Self::Item
struct User:
name: string
struct Post:
title: string
fn find(id: string) -> User $ Store[Item = User]:
$.use(Store[Item = User]).load(id)
type UserRow = $ Store[Item = User]
fn greet(id: string) -> string $ UserRow:
find(id).name
fn wrong(id: string) -> User $ Store[Item = Post]:
find(id) # error: missing-requirement- req.key.bindingA requirement key may bind associated types after its positional arguments, as in
$ Store[Item = User]. - req.key.binding.namesThe rules of Binding Names apply to a key's bindings. So a binding may name a supertrait's associated type. An unknown name is an error. Error:
unknown-associated-type. An ambiguous name is an error. Error:ambiguous-associated-type. - req.key.binding.identityTwo keys are the same key when they name the same trait instantiation and bind the same associated types to the same types. Order does not matter.
- req.key.binding.identity.exampleSo
Store[Item = User]andStore[Item = Post]are two keys, asRepo[User]andRepo[Post]are. - req.key.binding.identity.superA binding that the key's trait fixes through its supertrait list counts as written, as for a trait value (
trait.dyn.binding.identity). So undertrait PriceFeed < Feed[Item = i32],$ PriceFeedand$ PriceFeed[Item = i32]are one key: a provider given for one covers a function that names the other, and$.useof either has the same provider value type. - req.key.binding.rowsRow sets, entailment, least row solutions, and row subsumption compare keys by that identity. A row that lists
Store[Item = Post]therefore does not entailStore[Item = User]. - req.key.binding.value.reference-valuesThe provider value of a bound key has the bound trait value type, so
$.use(Store[Item = User])has type&dyn Store[Item = User]and itsloadreturnsUser. - req.key.binding.completeA requirement key must bind every associated type of its trait, including those that its supertraits declare.
- req.key.binding.complete.errorA key that leaves one unbound, such as
$ Storefor theStoreabove, is an error, as its trait value type is. Error:trait-not-dynamically-safe.
trait Store:
type Item
fn load(self, id: string) -> Self::Item
fn count(id: string) -> i32 $ Store: # error: trait-not-dynamically-safe
0Why. A key names the type of its provider value (
req.use.value.ordinary-type.reference-values), and a trait value type may bind associated types (Bound Associated Types). The key takes the same form, so a provider's methods have concrete types.
See also: Associated Type Bindings.
#Provider Access
$.use retrieves providers from the statically known current context:
db := $.use(Database)
let (db, cache) = $.use(Database, Cache)- req.use.context
$.useretrieves providers from the statically known current context. - req.use.orderThe result order matches the requested key order.
- req.use.missingA missing provider is a compile-time error at every call site below an entry point. Error:
missing-requirement. - req.use.entry-rowEntry-point rows may contain only host capability traits declared by the selected runtime profile. Error:
nonhost-entry-requirement. - req.use.registered-profile-setFor a registered boundary, the registration contract's explicit bindable-trait set is that boundary's profile.
- req.use.host-configurationFailure to configure one of those host providers is a pre-execution host configuration error.
- req.use.no-dynamic-search
$.useis non-suspending and performs no dynamic handler search that can fail at runtime below that boundary.
pub fn main() -> void: _ := $.use(Clock) # error: missing-requirement
trait ClockOpen in playground#Provider Values
- req.use.value.ordinary-type.reference-valuesA provider returned by
$.use(K)has type&mut dyn Kfor a mutable requirement trait, and&dyn Kotherwise. - req.use.value.localThat provider is a local reference, so it is never stored, returned, or captured.
- req.use.value.countedWhere a counted reference is expected, as for a
*mut dyn Kfield,$.use(K)yields the counted*mut dyn Kor*dyn Kinstead. - req.use.value.access-by-traitIts access follows from its trait, as Mutable Providers describes: writable for a mutable requirement trait, not writable otherwise.
- req.use.value.flowA counted provider value may flow anywhere an ordinary value of that type may flow, including fields, collections, closure captures, return values, and suspension frames.
- req.use.value.outlives-scopeA provider value remains usable after its provider scope ends.
- req.use.value.row-meaningA requirement row therefore records unresolved provider lookup, not every authority a callable can exercise through values it holds.
trait Store:
fn get(self) -> string
fn keep() -> *dyn Store $ Store:
$.use(Store) # counted: the result type expects *dyn Store#Context Namespace
- req.use.namespace
$is a special context namespace, not an ordinary value. - req.use.namespace.keysRequirement keys in its operations are type-level keys rather than named argument labels.
trait Store:
fn get(self) -> string
fn read() -> string $ Store:
$.use(Store).get() # $ is the context namespace, not a value#Provider Scopes
$.with binds providers for one lexical trailing block:
fn demo!() -> Result[User?, DbError] $ Cache:
$.with(Database=mock_db):
load_user!(UserId("user_123"))- req.with.block
$.withbinds providers for one lexical trailing block. - req.with.evaluatedEach provider expression is evaluated before entering the block.
- req.with.type-or-refEach provider expression must have a type implementing its named requirement trait, or be a reference to such a type, as Providers Behind References states.
- req.with.type.binding.reference-valuesFor a bound key, the provider's implementation must also bind each associated type to the type the key states. A value converting to that trait value type must do the same (
trait.dyn.binding.reference-convert). A provider whose implementation binds another type is an error. Error:type-mismatch. - req.with.nestedNested scopes may replace an outer provider for the same key within the nested block.
- req.with.nearestA call receives, for each key of its row, the nearest provider in effect at the call.
- req.with.nearest.whichThe nearest provider is that of the innermost enclosing
$.withthat binds the key, or else the one the caller received. - req.with.nearest.suspendingA suspending call receives its providers when its suspension is constructed (
req.bind.construction). - req.with.nearest.row-parameterThis holds when a key reaches a callback both through a row parameter and through a nearer scope.
- req.with.nearest.forcedSo when a caller fixes
Rto a row listing a key the callee installs, the callback's lookups of that key use the callee's provider. - req.with.nearest.not-errorSuch an overlap is not an error.
trait Tag:
fn name(self) -> string
struct Named:
label: string
impl Tag for Named:
fn name(self) -> string: self.label
fn read_tag() -> string $ Tag:
$.use(Tag).name()
fn with_tag[$R](callback: fn() -> string $ R + Tag) -> string $ R:
$.with(Tag=Named { label: "inner" }):
callback()
fn run() -> string:
$.with(Tag=Named { label: "outer" }):
with_tag::[$ Tag](read_tag) # "inner": the callee installs the nearer TagWhy. One rule finds every provider: the nearest scope that binds the key, as for nested
$.withscopes.
req.with.nearest.forcedis intended. A callback that keeps a key in its row asks its call site to supply that key. Zhang and Myers (POPL 2019) name the unintended case accidental handling. They prevent it with lexically scoped handlers. The language keeps one dynamic rule, and a closure gets lexical scope by an explicit capture (Lexical And Dynamic Providers).
#Lexical And Dynamic Providers
A closure gets a dynamic provider at each call when its row keeps the
key. It fixes a lexical provider where it is written by capturing the
value of $.use. The dynamic form keeps Clock in the callback's row, so
the callee's $.with supplies it:
trait Clock:
fn now(self) -> i32
struct Fixed:
hour: i32
impl Clock for Fixed:
fn now(self) -> i32: self.hour
fn at_noon[$R](callback: fn() -> i32 $ R + Clock) -> i32 $ R:
$.with(Clock=Fixed { hour: 12 }):
callback()
fn dynamic() -> i32:
$.with(Clock=Fixed { hour: 9 }):
at_noon(fn(): $.use(Clock).now()) # 12: the callee's ClockThe lexical form captures the provider value when the closure is written:
fn lexical() -> i32:
$.with(Clock=Fixed { hour: 9 }):
clock := $.use(Clock)
at_noon(fn(): clock.now()) # 9: the captured Clock- req.with.dynamicA closure's row keys are resolved at each call, so a callee's
$.withmay supply a key the closure keeps in its row. - req.with.lexicalA closure fixes a provider where it is written only by capturing the value of
$.use, as inclock := $.use(Clock)and thenfn(): clock.now(). - req.with.lexical.rowSuch a closure's row omits that key, so no callee's
$.withchanges the provider it uses.
Why. Dynamic scope costs nothing to write, and lexical scope is one binding. A closure that captured providers implicitly would hide which of the two a reader is looking at.
See also: Omitted Requirement Clauses, Provider Values, Captures.
#Generic Key Collisions
- req.with.collision.comparedAt each
$.with, the compiler compares every newly bound key with every other new key. It also compares each with every declared-row or lexical key visible in the block. - req.with.collision.closureInside a closure, the visible keys are only those a lookup in the closure body can select. Those are the closure's declared or inferred row, and the keys of
$.withblocks inside the closure. - req.with.collision.closure.outerThe keys of a
$.withblock around the closure are therefore not compared. - req.with.collisionTwo distinct generic key expressions that can become identical under any valid type-argument substitution are an error. Error:
generic-requirement-key-collision. - req.with.collision.contextThe same check applies among entries of a
$.contextexpression. - req.with.collision.exactAn exact replacement written with the same key expression remains the ordinary nested-scope override described above.
- req.with.collision.before-erasureThis check is performed before erasure or specialization, so compilation strategy cannot change which provider a lookup selects.
- req.with.collision.concreteDistinct concrete keys such as
Repo[User]andRepo[Post]remain valid. - req.with.collision.bindingsA key's bindings participate in the comparison as its type arguments do, so
Store[Item = T]andStore[Item = User]collide.
For example, a generic body must not make Repo[T] and Repo[U] concurrently
visible, because an instantiation can choose T = U. It likewise must not
combine a declared Repo[T] with a lexical Repo[User], because T can be
User:
trait Repo[T]
struct User: pass
fn choose[T, U](first: *dyn Repo[T], second: *dyn Repo[U]) -> void:
$.with(Repo[T]=first):
$.with(Repo[U]=second): # error: generic-requirement-key-collision
pass
fn mixed[T](local: *dyn Repo[User]) -> void $ Repo[T]:
$.with(Repo[User]=local): # error: generic-requirement-key-collision
passInside a closure, only the closure's own row and blocks count:
trait Repo[T]
struct User: pass
fn outer[T](generic: *dyn Repo[T], local: *dyn Repo[User]) -> void:
$.with(Repo[T]=generic):
run := fn() -> void:
$.with(Repo[User]=local): # valid: the outer block is not compared
pass
run()
fn declared[T](local: *dyn Repo[User]) -> void:
_ := fn() -> void $ Repo[T]:
$.with(Repo[User]=local): # error: generic-requirement-key-collision
passWhy. No closure takes a provider from a
$.withblock around it (req.row.omitted.outer-scope), so no lookup in its body can select that block's key.
#Reusable Contexts
Reusable provider maps use $.Context[$ Row]:
fn prod_context() -> $.Context[$ Metrics + Cache]:
$.context(Metrics=metrics, Cache=cache)
$.with(Database=db, Logger=logger, prod_context()...):
...- req.context.row
$.Context[$ A + B]is indexed by one unordered, duplicate-free requirement row; it is not a variadic generic. - req.context.dollarIts brackets are a row slot, so one key or a row alias is written after
$too, as in$.Context[$ Clock]and$.Context[$ AppRow]. - req.context.empty
$.Context[$()]is the empty context. - req.context.create
$.contextcreates a context value. - req.context.spreadAn entry
ctx...spreads the providers of the context valuectx. - req.context.spread.suffixLike every spread, a context spread is written with a suffix
..., and a prefix...ctxis a syntax error. Error:syntax-error. - req.context.orderContext spreads and explicit bindings are applied left to right, and the later binding wins when the same key appears more than once.
- req.context.one-per-keyThe resulting context still has one provider per key.
trait Tag:
fn name(self) -> string
fn run(ctx: $.Context[$ Tag]) -> void:
$.with(...ctx): # error: syntax-error
passWhy. A prefix
...means copy.
See also: Data Expressions.
#Provider Grammar
Requirements And Provider Contexts
gives the productions context_use, context_create, context_type,
context_scope, and context_entries.
- req.context.grammar.categories
context_useandcontext_createare primary expressions;context_typeis a reference type; andcontext_scopeis a suite expression. - req.context.ordinary-valuesProvider values are ordinary values and use ordinary trait implementations.
- req.context.no-handlerThere is no separate
handlerdeclaration.
trait Logger:
fn log(self, text: string) -> void
struct Quiet:
silent: bool
impl Logger for Quiet:
fn log(self, text: string) -> void: pass
fn demo() -> void:
$.with(Logger=Quiet { silent: true }):
$.use(Logger).log("done")#Mutable Providers
A requirement trait with a mut self or &mut self method is always
provided with mutable access, so a provider written in hd can change its own
state:
trait Counter:
fn count(self) -> i32
fn bump(&mut self) -> void
struct MemoryCounter:
value: i32
impl Counter for MemoryCounter:
fn count(self) -> i32:
self.value
fn bump(&mut self) -> void:
self.value = self.value + 1
fn tick() -> i32 $ Counter:
$.use(Counter).bump()
$.use(Counter).count()
fn demo() -> i32:
let mut counter = MemoryCounter { value: 0 }
$.with(Counter=&mut counter):
_ := tick()
tick() # 2- req.mut.trait-changingA mutable requirement trait is a trait that declares or inherits at least one
mut selfor&mut selfmethod. - req.mut.trait-accessA provider's access follows from its trait alone: writable for a mutable requirement trait, not writable for any other trait.
- req.mut.no-spellingRequirement syntax never writes
mut: a row keymut K,$.use(mut K), and a bindingmut K=expressionare errors. Error:syntax-error.
trait Counter:
fn bump(&mut self) -> void
fn tick() -> void $ mut Counter: # error: syntax-error
$.use(Counter).bump()Why.
mutmarks where code may change a value. A requirement is one provider shared by its whole call tree, so its trait already says whether the provider can change.
Note. Adding a
mut selfor&mut selfmethod, even one with a default, to a published requirement trait that had none is a breaking change.$.usethen yields a writable provider, and installing a&Treference becomesmutable-upgrade. Adding a first&selfor&mut selfmethod changes the type of$.usetoo (req.ref.use.reference-values).
Note. Two tasks in one provider scope, such as the children of
std.task.all!, share one mutable provider. Each sees the other's changes between its!calls, which are the only yield points (Cooperative Scheduling). Code that needs isolation installs a provider per task with$.with.
#Installing
- req.mut.install-writable-refFor a mutable requirement trait
K, a reference in a bindingK=expressionin$.withor$.contextmust be&mut Tfor a typeTimplementingK. A&Treference is an error. Error:mutable-upgrade. - req.mut.install-readonly-traitFor any other trait, the reference may be
&Tor&mut T, and the provider is installed without write access.
pub fn main() -> void:
counter := MemoryCounter { value: 0 }
$.with(Counter=&counter): # error: mutable-upgrade
_ := $.use(Counter).count()Open in playgroundNote. A new object is installed as a reference to it, as in
$.with(Counter=&mut MemoryCounter { value: 0 }), byreq.ref.install-ref.
#Retrieving
- req.mut.use-writable.reference-valuesFor a mutable requirement trait
K,$.use(K)yields a writable provider,&mut dyn Kbyreq.ref.use.reference-values. The methods ofKthat change their receiver may be called on it. - req.mut.use-readonly.reference-valuesFor a trait without a mutable receiver,
$.use(K)yields&dyn K. - req.mut.use.shared-referenceA read-only
&dyn Kprovider reference cannot call a&mut selfmethod, regardless of its binding spelling. Error:not-writable.
trait Counter:
fn count(self) -> i32
fn bump(&mut self) -> void
fn tick() -> void $ Counter:
let counter: &dyn Counter = $.use(Counter)
counter.bump() # error: not-writableNote. A
&mut dyn Kprovider (req.ref.use.reference-values) stays writable in a:=binding, as every&mutreference does.
#Access In Rows
- req.mut.row.plainA row entry is always the plain key
K, and it requires the provider forKwith its trait's access. - req.mut.row.no-access-rulesRows,
$.Context[$ Row]rows, and removal by extension therefore compare and remove keys by trait alone. - req.mut.row.missing-keyA required key with no available provider is an error, whatever its trait's access. Error:
missing-requirement.
trait Counter:
fn bump(&mut self) -> void
fn demo() -> void $ Counter:
$.use(Counter).bump()#Entry-Point Access
- req.mut.entry.trait-accessA runtime profile binds the host provider for a mutable requirement trait with write access, and every other host provider without it.
- req.mut.entry.registered-accessA registration contract binds each trait it lists the same way.
- req.mut.entry.no-markingNeither a runtime profile nor a registration contract marks a trait mutable.
pub fn main!() -> void $ Console:
println("hi")Open in playgroundSee also: Wasm Boundary.
#Captured Access
- req.mut.capture-traitA cold suspension captures each provider with its trait's access.
- req.mut.capture.fixedConstruction-Time Requirement Binding therefore also fixes the access a stored computation later uses.
trait Store:
fn get(self) -> string
fn fetch!() -> string $ Store:
$.use(Store).get() # a stored suspension keeps the Store access#Providers Behind References
A provider may be a reference, so the state a provider changes is in a place the caller reads after the scope:
trait Counter:
fn bump(&mut self) -> void
fn total(&self) -> i32
struct Tally:
n: i32
impl Counter for Tally:
fn bump(&mut self) -> void:
self.n += 1
fn total(&self) -> i32:
self.n
fn tick() -> i32 $ Counter:
counter := $.use(Counter) # a `&mut dyn Counter`
counter.bump()
counter.total()
fn demo() -> i32:
let mut tally = Tally { n: 0 }
$.with(Counter=&mut tally):
_ := tick()
_ := tick()
tally.n # 2: the provider changed `tally`#Keys Of Reference Receiver Traits
- req.ref.key.reference-valuesEvery requirement provider is a trait value reference.
- req.ref.use.reference-values
$.use(K)yields&mut dyn Kfor a mutable requirement trait, and&dyn Kotherwise. - req.ref.install-refThe expression of a binding
K=expressionfor such a key converts to that type as any value does, by Trait Value References. So&Sand&mut Sshare the place they refer to, and a value ofSis an error whose fix-it writes&or&mut. Error:type-mismatch. - req.ref.same-placeEvery
$.use(K)that reaches one provider refers to the place that provider refers to. Soisbetween two of them is true, and so isiswith a reference converted from the one installed. - req.ref.host.reference-valuesA host provider is a trait value reference to a new object holding the host implementation; every
$.use(K)below that entry refers to the same place.
trait Counter:
fn bump(&mut self) -> void
struct Tally:
n: i32
impl Counter for Tally:
fn bump(&mut self) -> void:
self.n += 1
fn shared(tally: *Tally) -> void:
$.with(Counter=tally): # error: mutable-upgrade
passWhy. Every provider is a reference. Reference-receiver traits require a written reference so their writes reach the chosen place.
#References To Other Providers
- req.ref.share.reference-valuesA provider for a trait without reference receivers accepts
&Sor&mut S, sharing its place, or a value ofS, copied into a new object. - req.ref.share.implA reference to a type that does not implement
Kexplicitly is an error. Error:type-mismatch. - req.ref.share.write-throughA method that changes its receiver, called on that provider through
$.use(K), changes that place.
trait Inventory:
fn stock(self, sku: string) -> i32
struct Shelf:
apples: i32
impl Inventory for Shelf:
fn stock(self, sku: string) -> i32:
self.apples
fn count() -> i32 $ Inventory:
$.use(Inventory).stock("apple") # a `&dyn Inventory` that refers to `shelf`
fn demo() -> i32:
let shelf = Shelf { apples: 10 }
$.with(Inventory=&shelf):
count() # 10Why. Existing references share their place; a value provider is copied into a new object. Both yield the same reference type from
$.use.
See also: Mutable Providers.
#Suspending Functions
A suspending declaration places ! after its name:
fn fetch_user!(id: UserId) -> Result[User, DbError]:
...It introduces two call forms:
let pending: mut Suspend[Result[User, DbError]] = fetch_user(id)
result := fetch_user!(id) # Result[User, DbError]- req.suspend.markerA suspending declaration places
!after its name. - req.suspend.two-formsA suspending declaration introduces two call forms: the ordinary call and the bang call.
- req.suspend.coldThe ordinary call constructs a cold suspension.
- req.suspend.cold.capturesThe ordinary call evaluates and captures arguments and required providers but does not begin executing the body.
- req.suspend.bangThe bang call constructs that suspension and drives it as a child of the current suspending computation.
- req.suspend.bang.pointA bang call is a possible suspension point.
#Suspending Function Types
The surface type of fetch_user is fn!(UserId) -> Result[User, DbError].
A suspending function type is definitionally equivalent to an ordinary
constructor function returning a suspension:
fn!(A, B) -> T $ R
fn(A, B) -> mut Suspend[T] $ R- req.suspend.type.equivalentA suspending function type is definitionally equivalent to an ordinary constructor function returning a suspension.
- req.suspend.type.spellingsThe first spelling preserves the source-level bang-call operation; the second is its lowered callable type.
- req.suspend.type.conversionAssignment or argument checking may convert the first to the second, but not back.
- req.suspend.type.plain-callCalling either form without
!constructs the cold mutableSuspend[T]. - req.suspend.type.bangOnly the first form supports
callee!(...)directly. - req.suspend.type.not-suspendingA bang call whose callee is neither a suspending function or function value nor a
Suspend[T]value is an error. Error:not-suspending. - req.suspend.type.readonly-bindingA
:=binding weakens that result to readonlySuspend[T], which cannot callpollorcancel.
fn plain() -> i32: 42
fn run!() -> i32: plain!() # error: not-suspendingNote. Store the result with
let pending: mut Suspend[T] = callee(...)when it must be driven later.
#Suspending Closures
Anonymous suspending callables use the same marker after fn:
loader := fn!(id: UserId) -> Result[User, DbError] $ Database:
db := $.use(Database)
db.load_user!(id)- req.suspend.closure.markerAnonymous suspending callables use the same marker after
fn. - req.suspend.closure.requirementsRequirement-bearing non-suspending closures omit
!and place$after their result type. - req.suspend.closure.row-inferenceClosure inference may infer a requirement row from an expected function type.
- req.suspend.closure.trailing-onlyInference makes a closure suspending only for a trailing block passed for a suspending parameter, whose
fn!type is the visible marker (Trailing Callback Blocks). Every other suspending closure is writtenfn!.
#Bang Calls And Driver Contexts
A bang call ends in the suspension_call_suffix of
Expressions: !, optional type arguments, and an argument clause.
- req.bang.suffixThis suffix is part of
postfix_suffixat ordinary call precedence. - req.bang.not-negationIt follows a completed operand, so it never collides with prefix logical
!at the start of an operand.!fetch!(id)is a legal negation of a bang call'sboolresult. - req.bang.driver-formsA bang call is valid only in a driver context. A driver context is exactly one of: a suspending function or closure body, the host executor driving
main!, or the top level of a script or of a REPL input. A test body is a suspending closure. - req.bang.top-level-moduleThe top level of any other module is not a driver context. A bang call outside a driver context is an error. Error:
bang-call-outside-suspension. - req.bang.activeA driver is active while its executor is evaluating or polling that driver context on the current program-instance call stack.
- req.bang.pending-not-activeA pending invocation retained by the host between polls is unfinished but not active.
- req.bang.test-drivenThe test runner drives each test body as a suspension, so a driver is active throughout the test, including calls through non-suspending helpers.
- req.bang.requirements-not-suspendingMerely using requirements does not make a function suspending; a non-suspending function may have a
$row.
fn work!() -> i32: 42
fn main() -> i32: work!() # error: bang-call-outside-suspensionOpen in playground#Driving A Stored Suspension
- req.drive.bangFor any expression
sof typemut Suspend[T],s!()drives that stored suspension to completion and has typeT. - req.drive.onceThe expression
sis evaluated once. - req.drive.not-methodThis is the same postfix bang suffix used for a direct
fn!call; it is not a method lookup. - req.drive.block-on.signatureNon-suspending code imports
use std.task.block_onand calls the standard functionblock_on[T](s: mut Suspend[T]) -> BlockOn[T], which owns the driver loop until the drive ends or panics.BlockOnis an enum ofstd.task. - req.drive.block-on.completedWhen the suspension completes,
block_onreturns.Completedwith its value. That holds also when a dispose was cut short after an ordinary exit, byreq.abandon.dependency.completed. - req.drive.block-on.cancelledWhen the suspension turns out cancelled,
block_ongoes on driving its cleanup until the cleanup has ended. It then returns.Cancelledwhen the cleanup ran to its end, and.Abandonedwhen a part of it was cut short. - req.drive.block-on.cancelled.whenThat holds for a suspension cancelled before the call, and for one that code inside the drive cancels, as an await of a suspension cancelled elsewhere does by
req.cancel.await. - req.drive.block-on.forbidden-contexts
block_onis forbidden in a default expression, adefersuite, or non-entry module initialization; those contexts cannot start suspension work. - req.drive.block-on.fact-contexts
block_onis also forbidden in a fact or metadata expression of typed derivation. - req.drive.block-on.directA
block_oncall written directly in a forbidden context is an error. A call inside a closure or local function declared there is not direct. Error:suspension-forbidden-context. - req.drive.block-on.direct.syntaxThe check of a direct call reads only the forbidden context's own source, never the body of a function it calls.
- req.drive.block-on.indirectA
block_oncall reached through a call while a forbidden context runs panics when it is reached. Panic:suspension-forbidden-context. - req.drive.block-on.indirect.nestedA forbidden context runs from its start to its end, including every call it makes, even while another driver is active.
- req.drive.block-on.printlnA
printlncall follows the same rules: a direct call in a forbidden context is an error, and an indirect one panics, bymodule.console.println-block-on. Error:suspension-forbidden-context. Panic:suspension-forbidden-context. - req.drive.block-on.under-driverOutside a forbidden context, a
block_oncall is valid while another driver is active, as inmain!or a test body. That holds whether it is reached directly or through non-suspending helpers. - req.drive.block-on.inner-onlyThat call drives only its own argument to completion, synchronously, and never polls or cancels a suspension of the outer driver.
- req.drive.block-on.deadlockWhen an inner
block_oncall's argument is pending and no host operation, timer, or wake can resume it, the call panics. Panic:suspension-deadlock.
use std.task.block_on
fn ready!() -> i32: 42
fn finish(pending: mut Suspend[i32]) -> void:
_ := block_on(pending) # panics with suspension-forbidden-context when a defer suite calls it
fn main() -> void:
let pending: mut Suspend[i32] = ready()
defer:
_ := block_on(pending) # error: suspension-forbidden-contextOpen in playgroundWhy. A direct call is found from the source of the context alone, so checking never reads another function's body. An indirect call is caught when it runs, as Rust's tokio panics on a nested
block_on.
Why. The caller of
block_onis synchronous code that expects a value, and nothing can cancel it. Soblock_onnames each way the drive can end instead of panicking, as a task awaiting the suspension would be cancelled.
Note. Only code that holds another reference to the suspension can cancel it: code before the call, or code inside the drive. A caller that passes its only reference never gets
.Cancelledor.Abandoned, unless the suspension cancels its own work.
Note. The outer driver makes no progress while an inner
block_oncall runs. An inner suspension that can progress only when the outer driver runs never completes. When nothing else can wake it, the call panics withsuspension-deadlock; while a host operation or timer is pending, it waits.
#Entry Driver
- req.entry.driverThe host executor is the driver for
main!. - req.entry.waker-drivenThis entry driver is waker-driven.
- req.entry.pendingWhen a poll returns
Pendingbecause a host provider operation is pending, the driver returns control to the host. It polls again only after a waker for that suspension is invoked. - req.entry.busy-pollA driver that keeps polling a pending host operation without returning to the host is not conforming.
- req.entry.deadlockWhen the entry suspension is pending and no host operation, timer, or wake can resume it, the program panics instead of waiting. Panic:
suspension-deadlock.
pub fn main!() -> void:
pass # the host executor drives this entry pointOpen in playground#Cancelling The Entry
The host may cancel the entry suspension, as a Ctrl-C does. It then waits
for the entry's cleanup, as cancel!() does, under a bound of its own:
use std.ops.Disposable
use std.time.{Clock, ms, s, sleep}
struct Conn:
closing: mut Suspend[void]
impl Disposable for Conn:
fn dispose!(&mut self) -> void:
let mut wait = self.closing
wait!() # the host waits here, up to its cleanup bound
pub fn main!() -> void $ Clock:
using conn = &mut Conn { closing: sleep(ms(50)) }
sleep!(s(60)) # a cancel by the host stops the entry hereOpen in playground- req.entry.cancelThe host may cancel the entry suspension while it waits on the host. It cancels it as
cancel!()does, byreq.cancel.steps, with the host as the waiter. - req.entry.cancel.waitThe host then goes on driving the entry until its cleanup has ended, by
req.cancel.wait. - req.entry.cancel.boundThe host waits under a cleanup bound, a host setting. The bound's deadline is set once, at the first cancel, and no later cancel moves it.
- req.entry.cancel.bound.abandonWhen the deadline passes before the cleanup has ended, the host abandons the entry, by
req.abandon.unwind. - req.entry.cancel.bound.cooperativeThe host abandons it at the first moment at or after the deadline when the entry waits on the host. Code that computes is never interrupted for the bound.
- req.entry.cancel.bound.timerWhile the host bounds a cleanup, the deadline counts as a pending timer for
req.entry.deadlock. - req.entry.cancel.abandonThe host may also abandon a cancelled entry at once, before the deadline.
- req.entry.cancelledAn entry suspension that turns out cancelled ends the run once its cleanup has ended. That holds whoever cancelled it: the host, or the entry's own code, as an await of a suspension cancelled elsewhere does by
req.cancel.await. - req.entry.cancelled.statusA run that ends this way does not panic. It exits with the cancellation status that the runtime profile selects, and reports how the cleanup ended.
Why. A Ctrl-C asks a program to stop, not to drop its cleanup, so the host waits as Kotlin's
cancelAndJoindoes. A bound keeps a close that never ends from holding the program, as Trio's shielded deadline does, and only the host's ownabandon()cuts the cleanup short.
Note. The default profile's host is
hd. Its bound, its handling of a second Ctrl-C, and its cancellation status are in Cancelling A Run.
#Host Waits
- req.host-wait.waitThe runtime-provided leaf
std.task.host_wait -> Tsuspends until an opaque host wait operation completes, then returns its result. - req.host-wait.abiHow a host wait operation crosses the host boundary is an implementation detail, not part of the language.
- req.host-wait.sourceUser code obtains
HostWait[T]values only from host providers; the type has no public constructor.
fn fetch!(key: string) -> string: "value of $key"
fn demo!() -> string:
fetch!("k") # a host wait inside resolves through the provider; user code never builds one#Suspend[T] Protocol
Suspend[T] is the stateful one-shot computation. Poll[T] describes one poll
result:
enum Poll[T]:
Pending
Ready(T)
impl[T] Suspend[T]:
fn poll(mut self, context: PollContext) -> Poll[T]
fn cancel!(mut self) -> CleanupEnd
fn abandon(mut self) -> void- req.protocol.builtin
Suspend[T]is a built-in type that no package implements; code writes it withoutdyn, asmut Suspend[i32]. Its values come only from compiler-generated frames and fromstd.task. Ordinary packages may consume them, but animplofSuspend[T]outside the standard library is an error, becauseSuspendnames a built-in type and no trait. Error:unknown-trait. - req.protocol.no-otherThere is no separate
Pollableor publicContinuation[T]abstraction.
Why. A built-in type makes the one-shot runtime checks part of the protocol rather than an unenforceable user convention.
#Suspension Values
| Rule | A Suspend[T] value is |
|---|---|
| req.value.coldCold | cold until first polled |
| req.value.singleSingle execution | single-execution, not a reusable plan or memoized result |
| req.value.exclusiveExclusive | exclusively driven at runtime |
| req.value.statefulStateful | stateful across normal successive polls while pending |
fn fetch!(key: string) -> string: "value of $key"
fn demo() -> void:
pending := fetch("k") # cold: the plain call only constructs, nothing runs#Poll Context
Standard-library suspension implementations access the waker of a
PollContext through this sealed surface:
struct PollContext: pass
impl PollContext:
fn waker(self) -> *dyn Waker
trait Waker:
fn wake(self) -> void- req.poll-context.construction
PollContextis constructed only by the host runtime andstd.task; user code cannot construct one. Its field is private and not shown above, so a construction in user code is an error. Error:private-member. - req.poll-context.wakerStandard-library suspension implementations access its waker through this sealed surface.
- req.poll-context.userOrdinary user packages may receive a
PollContextonly inside APIs explicitly provided bystd.task; they cannot use it to implementSuspend[T], a built-in type.
#Wakers
- req.waker.arrangeAn operation returning
Pendingmust arrange for the waker to be invoked when another poll may make progress. - req.waker.retainedA waker may be retained after
pollreturns, invoked from a host callback, and invoked more than once. - req.waker.coalescedRedundant wakes are coalesced and never poll concurrently.
- req.waker.no-resultThe waker carries no result; state remains in the suspension frame.
fn fetch!(key: string) -> string: "value of $key"
fn demo!() -> void:
_ := fetch!("k") # a pending poll arranges its own waker; the driver polls again on wake#Runtime Checks
- req.check.panicsCompeting drivers, reentrant polling, polling after
Ready, or attempting a second execution cause a runtime panic. - req.check.cancel-activeCancelling a suspension while it or one of its descendants is active on the current poll stack causes
suspension-reentrant-poll. - req.check.cancel-active.no-effectThat cancellation performs no cleanup or state transition.
- req.check.cancel-idempotentCancellation is otherwise idempotent: cancelling an already cancelled or completed suspension has no further effect.
- req.check.driver-cancelled.awaitA task that awaits a cancelled suspension is cancelled, by
req.cancel.await, but a driver cannot be cancelled.block_onreturns how the cleanup ended instead, byreq.drive.block-on.cancelled. - req.check.driver-cancelled.entry-endsThe entry driver is a driver too. Its suspension, once cancelled, ends the run instead of a panic, by
req.entry.cancelled. - req.check.runtime-onlyThese are runtime checks rather than ownership rules in the type system.
- req.check.code.competingCompeting drivers report
suspension-competing-driver. - req.check.code.reentrantRecursive polling and active-stack cancellation report
suspension-reentrant-poll. - req.check.code.after-endPolling after completion and attempting a second execution report
suspension-invalid-state.
fn fetch!(key: string) -> string: "value of $key"
fn demo!() -> string:
first := fetch!("k")
first # one driver only: a second driver on this suspension would panic#Discarding A Suspension
- req.discard.coldDiscarding a cold suspension that has never been polled has no cleanup work to perform.
- req.discard.startedOnce polling has begun, a host or driver that stops owning the suspension must cancel it before discarding it.
- req.discard.droppedDropping a started suspension without cancelling or abandoning it does not run its registered
defersuites.
fn fetch!(key: string) -> string: "value of $key"
fn demo() -> void:
_ := fetch("k") # discarded cold: no cleanup to run#Cooperative Scheduling
- req.schedule.one-threadEach program instance executes cooperatively on one thread.
- req.schedule.yield-pointsBang calls are the only language-level yield points; code between them does not interleave with a sibling suspension in that instance.
- req.schedule.all-unfinished
std.task.all!polls its children in argument order on its initial poll. After every wake it polls again, in argument order, only the children that have not completed. - req.schedule.all-completed
all!never polls a completed child again; it keeps that child's result.
fn first!() -> i32: +1
fn second!() -> i32: +2
fn demo!() -> i32:
first!() + second!() # one thread: no interleaving between bang calls#Compilation Strategy
fn name!(args) -> T is source sugar for a compiler-generated cold state
machine implementing Suspend[T].
- req.lowering.sugar
fn name!(args) -> Tis source sugar for a compiler-generated cold state machine implementingSuspend[T]. - req.lowering.frameThe frame stores captured arguments, construction-time providers, locals live across suspension points, and a state discriminant.
- req.lowering.pollThe frame's
pollmethod advances until it returnsPendingor completes withReady(value). - req.lowering.representationThe compiler may use a different representation, but it must preserve cold construction, provider binding, suspension points, and single-execution behavior.
Conceptually, not as normative source code, a suspending function lowers to a constructor:
fn adjusted(base: i32) -> mut Suspend[i32] $ Counter:
counter := $.use(Counter)
AdjustedFrame {
state: AdjustedState.New(base=base, counter=counter),
}and to a frame whose poll method advances its state:
enum AdjustedState:
New(base: i32, counter: Counter)
Waiting(child: mut Suspend[i32], offset: i32)
Complete
impl Suspend[i32] for AdjustedFrame:
fn poll(mut self, context: PollContext) -> Poll[i32]:
while true:
match self.state:
AdjustedState.New(base, counter) =>
let child: mut Suspend[i32] = counter.next()
self.state = AdjustedState.Waiting(child, offset=base)
AdjustedState.Waiting(child, offset) =>
match child.poll(context):
Poll.Pending => return Poll.Pending
Poll.Ready(value) =>
self.state = AdjustedState.Complete
return Poll.Ready(offset + value)
AdjustedState.Complete => panic("already completed")#Construction-Time Requirement Binding
Requirements of a suspending function are resolved when its cold suspension is constructed:
let pending: mut Suspend[Result[User?, DbError]] = $.with(Database=mock_db):
load_user(id)
# Driving pending later still uses mock_db.- req.bind.constructionRequirements of a suspending function are resolved when its cold suspension is constructed, not when a later driver first polls it.
- req.bind.capturedThe chosen providers are captured by the generated frame.
- req.bind.callerA caller constructing a required suspension must itself satisfy that requirement even if it does not immediately bang-call the suspension. Error:
missing-requirement.
trait Tag:
fn name(self) -> string
fn tagged!() -> string $ Tag:
$.use(Tag).name()
pub fn rejected_construction() -> void:
let pending: mut Suspend[string] = tagged() # error: missing-requirement
_ := pending.cancel()Why. Binding at construction prevents a stored computation from silently changing dependencies when it moves between drivers.
#Cancellation
This section defines cancel!, which stops a suspension and waits until
its cleanup has ended, abandon, which stops it and cuts its cleanup
short, and how the standard combinators cancel their children.
use std.ops.Disposable
use std.task.{all, race}
use std.time.{Clock, ms, s, sleep}
struct Conn:
log: *mut List[string]
closing: mut Suspend[void]
impl Disposable for Conn:
fn dispose!(&mut self) -> void:
self.log.push("closing")
let mut wait = self.closing
wait!()
self.log.push("closed")
fn session!(log: *mut List[string]) -> string $ Clock:
using conn = &mut Conn { log: log, closing: sleep(ms(2)) }
sleep!(s(60))
"late"
fn ready!() -> string:
"won"
fn stopper!(job: mut Suspend[string], log: &mut List[string]) -> string:
end := job.cancel!() # returns once the dispose! of conn has finished
match end:
.Finished => log.push("stopper finished")
.Abandoned => log.push("stopper abandoned")
"stopped"
pub fn main!() -> void $ Console + Clock:
let mut log: List[string] = []
let job: mut Suspend[string] = session(&mut log)
let (winner, stopped) = all!(race(job, ready()), stopper(job, &mut log))
println("${winner} ${stopped}") # won stopped
println(log.join(", ")) # closing, closed, stopper finishedOpen in playground- req.cancel.bang
Suspend[T]has the suspending methodcancel!(mut self), which returns aCleanupEndofstd.task. - req.cancel.stepsIt marks the suspension cancelled, synchronously propagates cancellation to an unfinished child, and asks unfinished external operations to abort through their provider/runtime contracts.
- req.cancel.coldLike every suspending call,
job.cancel()without the bang builds a cold suspension of the cancel.jobkeeps its state until that suspension is driven. - req.cancel.cold.discardA cold cancel that is discarded is an error, as any discarded suspension is. Error:
discarded-must-use-value. - req.cancel.deferAfter an unfinished child has completed its own cancellation cleanup, registered
defersuites in the suspension's unfinished frames run synchronously. They run in last-in, first-out order, from the innermost frame outward. - req.cancel.disposeA
usingbinding's dispose may suspend, so cancellation that reaches it follows Using Bindings instead of running synchronously. - req.cancel.waitThe cancel completes only once the suspension's whole cleanup has ended, every dispose that suspends included.
- req.cancel.resultIt then returns how the cleanup ended. A cleanup that ran to its end gives
.Finished. - req.cancel.cold-jobCancelling a cold suspension marks it cancelled without running its body, and returns
.Finishedat once. - req.cancel.completedCancelling a completed suspension changes nothing. It returns how the suspension's own cleanup ended.
- req.cancel.sharedA suspension has at most one cleanup, and the runtime runs it. A cancel made while that cleanup runs waits for it and starts no other, so each dispose and
defersuite runs once. - req.cancel.shared.resultEvery cancel that waits on one cleanup returns the same result.
- req.cancel.keptA cancel made after the cleanup ended returns at once how it ended.
- req.cancel.takeoverA cancel that reaches a suspension already disposing, after
return,?, or the end of a block, takes over that dispose. The dispose goes on from where it is, and the code after the block never runs. - req.cancel.nestedCancelling the task that waits in a cancel stops only that wait. The cleanup goes on as part of the task's own cleanup, which ends after it, by
flow.using.cancel.wait. - req.cancel.awaitA task that awaits the value of a cancelled suspension, by a bang call or as a child of
all!, is cancelled at that await. - req.cancel.await.unwindThe task unwinds as any cancellation does, and its cleanup folds the awaited suspension's cleanup. No value is produced, and nothing panics.
- req.cancel.await.kindsThe rule holds for every suspension: a compiler-generated frame, a
std.taskcombinator or cancel, and a pending host operation. - req.cancel.await.propagateA task that awaits a task cancelled this way is cancelled the same way, up to the first task where the chain ends.
- req.cancel.await.disposeInside a
dispose!, the await unwinds that dispose instead, byreq.abandon.dependency, and the chain ends there. - req.cancel.await.driverThe chain also ends at a driver, by
req.check.driver-cancelled.await. - req.cancel.cooperativeCancellation is cooperative at suspension boundaries for language code.
- req.cancel.external-abortA runtime must not leave a known active HTTP request or equivalent external operation running when its provider supports abort.
- req.cancel.no-ownershipCancellation runs already registered synchronous
defersuites, but it does not establish ownership or stop aliases from escaping.
fn ready!() -> i32: 42
fn stop!(job: mut Suspend[i32]) -> void:
job.cancel() # error: discarded-must-use-valueWhy. Kotlin's
cancelAndJoinalso cancels a job and then waits for its cleanup. Becausecancel!()waits inside the call, no cleanup outlives the code that asked for it. Because a suspension has one cleanup, a close handshake never runs twice, however many tasks cancel it. A value that a cancelled suspension owes never comes, so its awaiter is cancelled, as Python's asyncio raisesCancelledErrorat such anawait.
Note. A cancel that waits keeps the code after it from running. In the example,
stopperlogs only after the close has finished.
#Abandoning
abandon() stops a suspension and unwinds it at once, cutting short every
dispose! that waits:
use std.ops.Disposable
use std.task.race
use std.time.{Clock, ms, s, sleep}
struct Release:
log: *mut List[string]
closing: mut Suspend[void]
impl Disposable for Release:
fn dispose!(&mut self) -> void:
defer: self.log.push("dispose defer")
self.log.push("dispose start")
let mut wait = self.closing
wait!() # abandon() stops the dispose here
self.log.push("dispose end")
fn ordered!(res: &mut Release, log: &mut List[string]) -> string $ Clock:
defer: log.push("outer")
using r = res
defer: log.push("tail")
sleep!(s(60))
"late"
fn stop!(job: mut Suspend[string]) -> string:
job.abandon() # returns once the unwinding has ended
"stopped"
pub fn main!() -> void $ Console + Clock:
let mut log: List[string] = []
let job: mut Suspend[string] = ordered(&mut Release { log: &mut log, closing: sleep(ms(5)) }, &mut log)
println(race!(job, stop(job))) # stopped
println(log.join(", ")) # tail, dispose start, dispose defer, outerOpen in playground- req.abandon.method
Suspend[T]has the methodabandon(mut self) -> void. It does not suspend, so any function may call it. - req.abandon.unwindIt marks the suspension cancelled and unwinds it once, synchronously. The unwinding has ended when the call returns, before any other task runs.
- req.abandon.orderThe unwinding runs the cleanup as one last-in, first-out list, from the innermost frame outward.
defersuites and disposes keep the order offlow.using.order. - req.abandon.deferEvery registered
defersuite runs once, including those of adispose!that is running. Adefersuite never registered does not run. - req.abandon.disposeA
dispose!that waits is stopped at its suspension point, after its own registereddefersuites run. The code after that point never runs. - req.abandon.dispose.unstartedA dispose that has not started yet starts. It runs to its first suspension point and is stopped there the same way.
- req.abandon.hostA pending host operation is aborted as that operation's own cancellation rule says. Abandoning closes no handle: one that a dispose borrows, or that the operation uses, stays open.
- req.abandon.foldedEvery cleanup that cancellation folds into the suspension is abandoned the same way. That includes an unfinished child's cleanup, the children's of
all!andrace!, and a cleanup the suspension waits on incancel!(). - req.abandon.resultEvery
cancel!()waiting on the suspension's cleanup returns.Abandoned, and so does every latercancel!()of it. - req.abandon.carryA cleanup that folds an abandoned cleanup ends
.Abandonedtoo, even when its own part ran to its end. - req.abandon.coldAbandoning a cold suspension marks it cancelled without running its body.
- req.abandon.endedAbandoning a suspension that has ended does nothing: one that completed, or one whose cleanup ended. Abandoning it twice is the same as once.
- req.abandon.activeAbandoning a suspension while it or one of its descendants is active on the current poll stack panics, as cancelling it does by
req.check.cancel-active. Panic:suspension-reentrant-poll. - req.abandon.dependencyA
dispose!that awaits a suspension cancelled elsewhere is unwound from that await as an abandon would unwind it. Its registereddefersuites run, and the code after the await never runs. - req.abandon.dependency.resultThe cleanup that the dispose belongs to then ends
.Abandoned. The rest of that cleanup goes on as usual. - req.abandon.dependency.completedIf that dispose ran after an ordinary exit, the suspension still completes with its value. Only its kept cleanup result is
.Abandoned.
Why. asyncpg pairs a graceful
close()with a synchronousterminate();cancel!()andabandon()are that pair. A killed wait closes nothing, because a dispose may wait on a handle it only borrows; a dispose that owns a handle registers adeferthat closes it.
Note. A
dispose!is shielded from the cancellation of its task, but no shield can produce the value of a suspension that was cancelled elsewhere, so that one await ends its dispose early.
#Standard Combinators
- req.combinator.cancel-childrenThe
std.taskcombinatorsall!andrace!cancel their children when the parent is cancelled. - req.combinator.race-losers-disposed
race!also cancels losing children before returning the first completed value. It returns that value once every loser's disposes have finished, byflow.using.cancel.wait. - req.combinator.intrinsicThe standard polling combinators in
std.task, includingall!andrace!, are compiler intrinsics. - req.combinator.bang-calledEach is imported and bang-called like an ordinary
fn!function, but the compiler supplies its frame and polling behavior. - req.combinator.not-syntaxThey are not first-class control-flow syntax.
- req.combinator.userBecause
Suspend[T]is a built-in type, user code cannot define an equivalent polling combinator; it composes the standard intrinsics instead. - req.combinator.race-signature
race!has the plain vararg signaturefn race -> T, with no typing rule of its own. - req.combinator.race-joinIts children's result types therefore meet only by permission weakening, as
types.generic.infer.joinstates, so children of unrelated types are an inference error. - req.combinator.race-emptyA
race!call written with no task argument, asrace!::[i32](), is an error. Error:argument-count. - req.combinator.race-empty-literalA
race!call that passes an empty list literal by name, asrace!::[i32](tasks=[]), is the same error. Error:argument-count. - req.combinator.race-empty-runA
race!call whose task list is empty at run time, passed by a spread or as a list value, panics when it is called. Panic:explicit-panic. - req.combinator.library-restThe other concrete signatures, and the complete intrinsic set, remain standard-library API design.
use std.task.all
fn load_user!(id: i64) -> string:
"user $id"
fn load_orders!(id: i64) -> List[i64]:
[id]
fn page!(id: i64) -> string:
let (user, orders) = all!(load_user(id), load_orders(id))
"$user: ${orders.len()}"#Cancelling The Children
A cancelled all! or race! stops every unfinished child before it waits
for any, so a server shuts down without a deadlock:
use std.ops.Disposable
use std.task.{all, race}
use std.time.{Clock, ms, s, sleep}
struct Server:
accepting: bool
log: *mut List[string]
struct Connections:
server: *mut Server
clock: *mut dyn Clock
impl Disposable for Connections:
fn dispose!(&mut self) -> void:
self.server.log.push("connections draining")
while self.server.accepting:
let mut tick = self.clock.sleep(ms(1))
tick!()
self.server.log.push("connections drained")
fn connections!(server: *mut Server) -> void $ Clock:
using open = &mut Connections { server: server, clock: $.use(Clock) }
sleep!(s(60))
fn acceptor!(server: &mut Server) -> void $ Clock:
defer:
server.accepting = false
server.log.push("acceptor stops accepting")
sleep!(s(60))
fn serve!(server: *mut Server) -> string $ Clock:
defer: server.log.push("serve defer")
_ := all!(connections(server), acceptor(server))
"served"
fn shutdown!() -> string $ Clock:
sleep!(ms(1))
"shutdown"
pub fn main!() -> void $ Console + Clock:
let server: *mut Server = &mut Server { accepting: true, log: &mut [] }
println(race!(serve(server), shutdown()))
println(server.log.join(", "))Open in playground- req.combinator.cancel-foldA cancelled combinator never abandons a child's cleanup. Each unfinished child's cleanup becomes part of the combinator's own.
- req.combinator.cancel-orderIt starts every unfinished child's cleanup, in argument order, before it waits for any of them.
- req.combinator.cancel-parent-lastThe task that awaits the combinator runs its own cleanup only after every child's cleanup has ended, by
flow.using.cancel.wait. - req.combinator.race-outer-cancelA cancel that reaches
race!while a loser's cleanup runs continues that cleanup as part of the race's own cleanup. - req.combinator.all-cancelledAn
all!child that turns out cancelled cancels the task that awaits theall!, byreq.cancel.await. - req.combinator.race-child-cancelledA
race!child that turns out cancelled cancels the task that awaits therace!at once, as anall!child does byreq.cancel.await. A child that completed earlier on the same poll wins instead.
Note. In the example, the connections' dispose waits until the acceptor stops accepting, and the acceptor's
deferis what stops it. A parent that waited out the connections before it cancelled the acceptor would never reach thatdefer. A dispose that waits for its own parent'sdeferstill never ends, since the parent cleans up last.
Why. A child cancelled from outside means the structure around it is being torn down, so
race!stops asall!does. Otherwisetimeout!, a race of work and a sleep, would wait out its limit and reportTimedOutfor work that was cancelled.
#Typing all!
all! awaits children of different result types and returns their results
as one tuple:
use std.task.all
struct User:
name: string
fn load_user!(id: i64) -> User:
User { name: "Ada" }
fn load_orders!(id: i64) -> List[i64]:
[id]
fn page!(id: i64) -> string:
let (user, orders) = all!(load_user(id), load_orders(id))
"${user.name}: ${orders.len()}"- req.combinator.all-typing
all!has no written signature. A callall!(e_1, ..., e_n)in which eache_ihas typemut Suspend[X_i]has type(X_1, ..., X_n). - req.combinator.all-argumentAn argument of any other type is an error. Error:
type-mismatch. - req.combinator.all-direct
allmust be the callee of a direct call with positional arguments. A spread, a named argument, an explicit type argument list such asall!::[i32, string], or a use as a function value is an error. Error:type-mismatch. - req.combinator.all-bang-childA child written as a bang call, as in
all!(load_user!(id)), is an ordinary bang call. The caller awaits it beforeall!starts, so its value is not aSuspend.
use std.task.all
fn ready!() -> i32: 1
fn eager!() -> (i32, i32):
all!(ready!(), ready()) # error: type-mismatch
fn spread!(tasks: List[mut Suspend[i32]]) -> void:
_ := all!(tasks...) # error: type-mismatch
fn explicit!() -> (i32, i32):
all!::[i32, i32](ready(), ready()) # error: type-mismatchNote. A plain call of a suspending function, such as
load_user(id), builds the coldmut Suspend[User]thatall!polls (req.suspend.type.plain-call). Scheduling, cancellation, and failure follow the rules above and Cooperative Scheduling.
Why. A written signature for
all!would need a type-level map over a tuple, fromSuspend[X_i]toX_i. The language has no higher-kinded types, so one written typing rule replaces the signature.
Note. The
retry!combinator ofstd.taskis a plain library loop over afn!attempt, not an intrinsic, so the stdlib tier specifies it (Retry). Its cancellation follows the rules above.
#Requirement Polymorphism
Higher-order code preserves callback requirements with a row parameter:
fn transform[T, U, $R](items: List[T], f: fn(T) -> U $ R) -> List[U] $ R:
...A local provider removes one key from a callback row by extension:
fn provide_logger[$R](callback: fn(string) -> void $ R + Logger) -> void $ R:
$.with(Logger=logger):
callback("message")- req.poly.row-parameterHigher-order code preserves callback requirements with a row parameter.
- req.poly.extensionA local provider removes one key from a callback row by extension.
- req.poly.extension.formThe parameter row lists the row parameter beside the removed key, and the callee's own row is the plain row parameter.
- req.poly.kind.marked
Ris a row parameter because its declaration writes$R, not because of its use after$or the case of its name. - req.poly.namingRow parameters follow the ordinary uppercase convention for generic parameters.
- req.poly.leastAt a call, the compiler infers
Ras the least row solution of the callback pattern. - req.poly.least.examplePassing a callback with row
$ Logger + ClockinfersRas$ Clock, so the call requires onlyClock. - req.poly.absent-matchesPassing a callback whose row lacks
Loggeris valid, andRis the callback's own row. - req.poly.bodyInside the body, calling
callbackrequiresRandLogger; the declared row suppliesR, and the$.withscope suppliesLogger. - req.poly.one-bodyA row parameter's providers are passed as one bundle, so a row-polymorphic body is compiled once and never specialized per row.
- req.poly.no-subtractionRows have no subtraction operator.
- req.poly.rows-onlyThis mechanism does not quantify over arbitrary type-level expressions; it is specific to requirement rows.
trait Logger:
fn write(self, message: string) -> void
trait Clock
struct SilentLogger: pass
impl Logger for SilentLogger:
fn write(self, message: string) -> void: pass
fn provide_logger[$R](callback: fn() -> void $ R + Logger) -> void $ R:
$.with(Logger=SilentLogger {}):
callback()
fn tick() -> void $ Clock: pass
fn run() -> void $ Clock:
provide_logger(tick) # valid: R is ClockNote. Extension in the input and the plain row variable in the output is the established form for handling one effect. Koka writes
<console, exn | e>, Unison writes{g, Exception}, and Effekt writes/ { Console, Exc }.
See also: Least Row Solutions.
#Runtime Boundary
This section defines how a stored suspension reaches a driver, where providers come from, and what a program instance's behavior depends on.
- req.runtime.driverA stored suspension is driven through a runtime or standard-library driver; no additional source keyword is required.
- req.runtime.exclusiveDrivers enforce exclusive access and the panic rules above without exposing a way to upgrade an arbitrary readonly reference.
- req.runtime.explicit-providerProvider selection is never implicit: a provider comes from an enclosing
$.withscope or from the host configuration of an entry point. - req.runtime.libraryScheduling APIs, durable replay storage and runners, and affine resource ownership are runtime or library concerns.
- req.runtime.cancel-cleanupCancellation participates in synchronous
defercleanup but does not replace an ownership or resource-lifetime design.
fn fetch!(key: string) -> string: "value of $key"
fn demo!() -> string:
fetch!("k") # driven by a runtime driver; providers come from $.with or the host#Determinism
- req.determinism.inputsA program instance is deterministic in its inputs.
- req.determinism.same-programTwo builds are the same compiled program when the compiler's outputs for them are byte-identical.
- req.determinism.dependsIts observable behavior depends only on which compiled program it runs, its runtime profile, and its entry arguments. It also depends on the ordered sequence of host-call results and waker and cancellation deliveries it receives.
- req.determinism.no-other-sourceCode between host calls has no other source of nondeterminism.
- req.determinism.replayA runtime may therefore reproduce an instance by supplying the same inputs in the same order.
- req.determinism.limitsFailures caused by host stack or memory limits are outside this guarantee.
- req.determinism.limits-profileThe host's stack and memory limits are part of the runtime profile.
- req.determinism.weakGarbage collection timing is not an input: user code cannot observe a weak reference clearing or a finalizer running.
fn demo(seed: u64) -> u64:
seed * 2 # same inputs, same outputs: nothing else varies between host callsNote. A host-call result that is a float arrives as its raw IEEE 754 value, by
module.profile.host-float.raw. A runtime that records results as text for replay writes each float bymodule.boundary.float-bits, so a replayed NaN, infinity, or-0.0is the recorded value.
See also: Comparison Traits, Representation And Garbage Collection.