hd-lang

#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.
  1. req.model.one-shotSuspension is not a general or multi-shot continuation system.
  2. req.model.not-capabilityA requirement is not necessarily a host capability.
  3. req.model.errorsNormal errors are values represented by Result[T, E].
  4. req.model.lookup-no-suspendDependency lookup does not suspend.
  5. req.model.suspend-no-errorsSuspension does not raise ordinary errors.
  6. req.model.no-provider-choiceDeclaring a requirement does not by itself choose a provider.
  7. req.model.runtimesProduction, test, sandbox, and replay runtimes may bind different providers or drivers at explicit requirement and suspension boundaries.
  8. 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.

  1. req.row.definitionA requirement row is the normalized unordered set of requirement keys on a callable signature.
  2. 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.

  1. req.row.syntax.signatureA function signature may end with $ and an unordered list of requirement traits.
  2. req.row.syntax.plus-listEach key names a separate injected value, and several keys are joined with +, as in $ Database + Cache.
  3. req.row.syntax.single-bareA single key may be bare, as in $ Console.
  4. req.row.syntax.same-formA header and a type write a row the same way, as in fn(UserId) -> User $ Database + Cache.
  5. req.row.syntax.emptyThe clause $() writes the empty row explicitly.
  6. req.row.syntax.no-mutA key has no mut prefix; 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.

  1. req.row.callable.same-clauseFunction declarations, closure expressions, and function types use the same requirement clause.
  2. req.row.callable.methodsThe same callable name and optional requirement clause apply to trait methods and functions inside impl blocks.
  3. 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

  1. 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.
  2. req.row.omitted.empty.bodyThe body of such a callable may use only requirements satisfied by a $.with block in that body.
  3. req.row.omitted.inferred-privateA non-public function, a non-public inherent method, or a local fn declaration without a requirement clause has an inferred row. It is computed by the same rule as a closure's below.
  4. 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.
  5. 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 $.with blocks of that body.
  6. req.row.omitted.outer-scopeA $.with block around a closure or local fn declaration never satisfies that callable's requirements, so each key its body uses goes into its row.
  7. req.row.omitted.parameter-callsCalls through function parameters contribute their normalized rows.
  8. req.row.omitted.expected-parameterIf an expected function type contains a row parameter, the inferred concrete row is unified with that parameter.
  9. req.row.omitted.not-emptyOmission never means an empty row merely because the expected row is generic.
  10. req.row.omitted.writtenA written $ clause is explicit and must entail the same body requirements.
Error example
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-requirement

Why. 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

  1. req.row.set.orderRows are sets: order does not affect type identity.
  2. req.row.set.onceA key occurs at most once after normalization.
  3. req.row.set.unionThe row denoted by a list is the union of its keys.
  4. req.row.set.parameterA row parameter listed beside other keys contributes every key of its row.
  5. req.row.set.duplicate$ Logger + Clock + Logger is the row $ Clock + Logger.
  6. 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.
Error example
trait Database

fn query() -> void $ Database:
    pass

pub fn invalid() -> void:
    query()  # error: missing-requirement

#Entailment

  1. req.row.entail.setsRequirement checking uses set entailment after alias expansion.
  2. req.row.entail.availableFor a body, available is the union of the declared row and the lexical keys.
  3. req.row.entail.memberEvery required key must be a member of available.
  4. req.row.entail.membershipRows contain only unions, so entailment reduces to membership.
  5. req.row.entail.concreteA concrete key K is entailed by a row exactly when the row lists K.
  6. req.row.entail.concrete.parameterAn unknown row parameter listed in the row never entails K.
  7. req.row.entail.parameterAn unknown row parameter R is entailed by a row exactly when that row lists R itself.
  8. req.row.entail.parameter.concreteNo set of concrete keys entails R.
  9. req.row.entail.genericA generic key such as Repo[T] is entailed only by a key that is identical to it after alias expansion.
Error example
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

  1. req.row.least.solutionInference for a parameter pattern that lists a row parameter beside concrete keys chooses the least row solution.
  2. req.row.least.examplesMatching $ R + K against the row $ K infers the empty row for R, and matching it against $ K + Clock infers $ Clock.
  3. req.row.least.absent-keyWhen the matched row lacks K, the pattern still matches, and R is the whole matched row.
  4. req.row.least.removalThis least-solution rule is also how a callee removes a key from a callback row.
  5. req.row.least.one-unknownA row pattern may list at most one row parameter that is still unknown when the pattern is solved.
  6. 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.
  7. 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.
  8. req.row.least.ambiguous.reportedThe error is reported on the parameter whose type holds that pattern.
Error example
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
    pass

In 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]
  1. 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.
  2. 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.
  3. req.row.subsume.missingA function value whose row lists a key the expected row does not entail does not fit. Error: type-mismatch.
  4. 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.
  5. req.row.subsume.closureA closure whose inferred row is narrower than an expected row therefore fits that row without taking it.
Error example
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-mismatch

Why. 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 to List[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()
  1. 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.
  2. req.row.union.literal.mapThe same holds for the values of a map literal with no expected type.
  3. 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.
  4. 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.
  5. req.row.union.literal.spreadA spread contributes the row of its list's element type, as it contributes that type (expr.list.spread.inferred).
  6. req.row.union.literal.other-partsWhen the widened types still have no common type, as for fn() -> string $ Db and fn() -> i32 $ Clock, the literal is an error. Error: no-common-type.
  7. 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.
  8. 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 is type-mismatch (req.row.subsume.missing).
  9. 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.
  10. req.row.union.literal.diagnosticsA diagnostic prints an inferred union row with each key once, in a stable order.
  11. req.row.union.literal.diagnostics.expandedA missing-requirement or type-mismatch diagnostic on that row also lists its expanded keys and names the missing key, as req.row.alias.diagnostics.expanded states.
  12. req.row.union.sitesThe other least-common-type sites take the union the same way. They are the branches of a value-producing if and the arms of a value-producing match. They also include the final value and return operands of a closure or non-public function whose result type is inferred.
  13. 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.
  14. 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.
Error example
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-type

Why. 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
  1. req.row.param.markedA generic parameter written with $ before its name, as $R in fn map[T, U, $R], is a row parameter.
  2. req.row.param.marked.ownersA function, a method, an implementation, and a row alias may declare row parameters.
  3. req.row.param.marked.useInside a row, a row parameter is written by its name, as in $ R or $ R + Log.
  4. req.row.param.no-inferenceA parameter's kind comes only from its declaration. A parameter written without $ is type-kinded, whatever its uses.
  5. 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.
  6. req.row.param.as-typeA row parameter used as a type, as in value: R after $R, is an error. Error: generic-kind-mismatch.
  7. req.row.param.one-kindOne parameter cannot be used as both an ordinary type and a requirement row.
  8. req.row.param.no-dataA struct, an enum, or a trait declares no row parameter: each of its own generic parameters is type-kinded.
  9. req.row.param.no-data.markedA $ on a generic parameter of a struct, an enum, a trait, or a newtype, as in struct Job[$R], is an error. Error: generic-kind-mismatch.
  10. 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.
  11. 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.
  12. 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.
Error example
fn run[R](job: fn() -> void $ R) -> void $ R:  # error: generic-kind-mismatch
    job()

fn keep[$R](value: R) -> void:  # error: generic-kind-mismatch
    pass
Error example
struct 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 whether Db is 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
  1. req.row.slotA row slot is a place in a type or a type argument list that takes a requirement row.
  2. req.row.slot.listThe row slots are the brackets of $.Context[...] and the row argument of Fn and SuspendFn. 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.
  3. req.row.slot.dollarEvery row in a row slot is written after $, including one key and a row alias, as in Job[$ Db], $.Context[$ AppRow], and Fn[(), void, $ AppRow].
  4. req.row.slot.emptyThe empty row in a row slot is $().
  5. 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.
Error example
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
    pass

Why. 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"
  1. req.row.alias.dollarA row alias is a transparent alias whose right side is a row after $, as in type AppRow = $ Db + Cache + Log or type Web = $ AppRow.
  2. req.row.alias.emptytype NoRow = $() declares a row alias for the empty row.
  3. req.row.alias.expandA row alias written in a row stands for its keys, so $ AppRow + Clock is the row $ Db + Cache + Log + Clock.
  4. req.row.alias.expand.firstExpansion comes before normalization, entailment, least-row solving, row subsumption, and the check for generic key collisions.
  5. req.row.alias.nestedA row alias may name another row alias, and expansion flattens every level into one set.
  6. req.row.alias.dollar.missingA right side that joins keys with + without $, as in type AppRow = Db + Cache, is an error. So is one that names one row alias without $, as in type Web = AppRow. The fix-it adds $. Error: generic-kind-mismatch.
  7. req.row.alias.duplicateA key reached twice, directly or through aliases, occurs once in the row, as req.row.set.duplicate states, and is not diagnosed.
  8. 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].
  9. 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) or AppRow=value, is an error. Error: generic-kind-mismatch.
  10. req.row.alias.type-alias.reference-valuesA right side without $ declares a type alias, never a key, as in type Users = &dyn Store[Item = User]. It is usable only where a type is, as in x: Users.
  11. 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 bound T < Users, is an error. Error: unknown-trait.
  12. 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, so types.trait.value.dyn-required.reference-values rejects it. Error: trait-used-as-type.
  13. 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 $ UserRow in a row.
  14. req.row.alias.bound-keyA row alias may list a bound key, as in type UserRow = $ Store[Item = User] + Log.
  15. req.row.alias.key-written$.use, a binding in $.with, and a bound write the trait itself, as in $.use(Store[Item = User]).
  16. req.row.alias.accessA key reached through an alias is its trait, so its provider's access follows req.mut.trait-access.
  17. req.row.alias.generic.markedA row alias may declare generic parameters, and a row parameter among them is marked $, as in type WithLog[$R] = $ R + Log.
  18. req.row.alias.generic.use$ WithLog[$ Clock] is the row $ Clock + Log, and in a function with row parameter R, $ WithLog[$ R] extends R with Log.
  19. req.row.alias.generic.kindA row argument for a type-kinded alias parameter, as in $ Only[$ AppRow] after type Only[T] = T, is an error. Error: generic-kind-mismatch.
  20. 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)
Error example
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
    0
Error example
trait Db

trait Cache

type Front = $ Back + Db  # error: alias-cycle

type Back = $ Front + Cache

Why. 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, so type never declares a trait alias. A key is written as its trait or reached through a row alias, never through a type alias.

Note. A pub row alias names only public traits, as module.vis.signature.coverage requires of every alias target.

#Aliases In Diagnostics

  1. req.row.alias.diagnosticsA diagnostic prints a row as it is written, with alias names kept.
  2. req.row.alias.diagnostics.expandedA missing-requirement or type-mismatch diagnostic 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

  1. req.key.traitsRequirement keys are traits, including interfaces such as Database and host capabilities such as Clock or Network.
  2. req.key.no-effect-syntaxThe language does not introduce a separate effect-declaration syntax.
  3. 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.
  4. req.key.inspectableA trait that is std.inspect.Inspectable or has it as a direct or transitive supertrait is never a requirement key.
  5. 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.
  6. req.key.inspectable.boundSuch a trait remains valid as a bound and as a value type.
  7. req.key.any-trait-members.reference-valuesA requirement key may name any trait. A member unavailable on the trait value reference that $.use yields is unavailable through $.use too.
Error example
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:

Error example
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
  1. req.key.bindingA requirement key may bind associated types after its positional arguments, as in $ Store[Item = User].
  2. 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.
  3. 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.
  4. req.key.binding.identity.exampleSo Store[Item = User] and Store[Item = Post] are two keys, as Repo[User] and Repo[Post] are.
  5. 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 under trait PriceFeed < Feed[Item = i32], $ PriceFeed and $ PriceFeed[Item = i32] are one key: a provider given for one covers a function that names the other, and $.use of either has the same provider value type.
  6. 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 entail Store[Item = User].
  7. 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 its load returns User.
  8. req.key.binding.completeA requirement key must bind every associated type of its trait, including those that its supertraits declare.
  9. req.key.binding.complete.errorA key that leaves one unbound, such as $ Store for the Store above, is an error, as its trait value type is. Error: trait-not-dynamically-safe.
Error example
trait Store:
    type Item
    fn load(self, id: string) -> Self::Item

fn count(id: string) -> i32 $ Store:  # error: trait-not-dynamically-safe
    0

Why. 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)
  1. req.use.context$.use retrieves providers from the statically known current context.
  2. req.use.orderThe result order matches the requested key order.
  3. req.use.missingA missing provider is a compile-time error at every call site below an entry point. Error: missing-requirement.
  4. req.use.entry-rowEntry-point rows may contain only host capability traits declared by the selected runtime profile. Error: nonhost-entry-requirement.
  5. req.use.registered-profile-setFor a registered boundary, the registration contract's explicit bindable-trait set is that boundary's profile.
  6. req.use.host-configurationFailure to configure one of those host providers is a pre-execution host configuration error.
  7. req.use.no-dynamic-search$.use is non-suspending and performs no dynamic handler search that can fail at runtime below that boundary.
Error example
pub fn main() -> void: _ := $.use(Clock)  # error: missing-requirement

trait Clock
Open in playground

#Provider Values

  1. req.use.value.ordinary-type.reference-valuesA provider returned by $.use(K) has type &mut dyn K for a mutable requirement trait, and &dyn K otherwise.
  2. req.use.value.localThat provider is a local reference, so it is never stored, returned, or captured.
  3. req.use.value.countedWhere a counted reference is expected, as for a *mut dyn K field, $.use(K) yields the counted *mut dyn K or *dyn K instead.
  4. req.use.value.access-by-traitIts access follows from its trait, as Mutable Providers describes: writable for a mutable requirement trait, not writable otherwise.
  5. 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.
  6. req.use.value.outlives-scopeA provider value remains usable after its provider scope ends.
  7. 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

  1. req.use.namespace$ is a special context namespace, not an ordinary value.
  2. 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"))
  1. req.with.block$.with binds providers for one lexical trailing block.
  2. req.with.evaluatedEach provider expression is evaluated before entering the block.
  3. 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.
  4. 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.
  5. req.with.nestedNested scopes may replace an outer provider for the same key within the nested block.
  6. req.with.nearestA call receives, for each key of its row, the nearest provider in effect at the call.
  7. req.with.nearest.whichThe nearest provider is that of the innermost enclosing $.with that binds the key, or else the one the caller received.
  8. req.with.nearest.suspendingA suspending call receives its providers when its suspension is constructed (req.bind.construction).
  9. req.with.nearest.row-parameterThis holds when a key reaches a callback both through a row parameter and through a nearer scope.
  10. req.with.nearest.forcedSo when a caller fixes R to a row listing a key the callee installs, the callback's lookups of that key use the callee's provider.
  11. 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 Tag

Why. One rule finds every provider: the nearest scope that binds the key, as for nested $.with scopes.

req.with.nearest.forced is 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 Clock

The 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
  1. req.with.dynamicA closure's row keys are resolved at each call, so a callee's $.with may supply a key the closure keeps in its row.
  2. req.with.lexicalA closure fixes a provider where it is written only by capturing the value of $.use, as in clock := $.use(Clock) and then fn(): clock.now().
  3. req.with.lexical.rowSuch a closure's row omits that key, so no callee's $.with changes 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

  1. 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.
  2. 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 $.with blocks inside the closure.
  3. req.with.collision.closure.outerThe keys of a $.with block around the closure are therefore not compared.
  4. 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.
  5. req.with.collision.contextThe same check applies among entries of a $.context expression.
  6. req.with.collision.exactAn exact replacement written with the same key expression remains the ordinary nested-scope override described above.
  7. req.with.collision.before-erasureThis check is performed before erasure or specialization, so compilation strategy cannot change which provider a lookup selects.
  8. req.with.collision.concreteDistinct concrete keys such as Repo[User] and Repo[Post] remain valid.
  9. req.with.collision.bindingsA key's bindings participate in the comparison as its type arguments do, so Store[Item = T] and Store[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:

Error example
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
        pass

Inside a closure, only the closure's own row and blocks count:

Error example
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
            pass

Why. No closure takes a provider from a $.with block 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()...):
    ...
  1. req.context.row$.Context[$ A + B] is indexed by one unordered, duplicate-free requirement row; it is not a variadic generic.
  2. 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].
  3. req.context.empty$.Context[$()] is the empty context.
  4. req.context.create$.context creates a context value.
  5. req.context.spreadAn entry ctx... spreads the providers of the context value ctx.
  6. req.context.spread.suffixLike every spread, a context spread is written with a suffix ..., and a prefix ...ctx is a syntax error. Error: syntax-error.
  7. 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.
  8. req.context.one-per-keyThe resulting context still has one provider per key.
Error example
trait Tag:
    fn name(self) -> string

fn run(ctx: $.Context[$ Tag]) -> void:
    $.with(...ctx):  # error: syntax-error
        pass

Why. 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.

  1. req.context.grammar.categoriescontext_use and context_create are primary expressions; context_type is a reference type; and context_scope is a suite expression.
  2. req.context.ordinary-valuesProvider values are ordinary values and use ordinary trait implementations.
  3. req.context.no-handlerThere is no separate handler declaration.
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
  1. req.mut.trait-changingA mutable requirement trait is a trait that declares or inherits at least one mut self or &mut self method.
  2. req.mut.trait-accessA provider's access follows from its trait alone: writable for a mutable requirement trait, not writable for any other trait.
  3. req.mut.no-spellingRequirement syntax never writes mut: a row key mut K, $.use(mut K), and a binding mut K=expression are errors. Error: syntax-error.
Error example
trait Counter:
    fn bump(&mut self) -> void

fn tick() -> void $ mut Counter:  # error: syntax-error
    $.use(Counter).bump()

Why. mut marks 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 self or &mut self method, even one with a default, to a published requirement trait that had none is a breaking change. $.use then yields a writable provider, and installing a &T reference becomes mutable-upgrade. Adding a first &self or &mut self method changes the type of $.use too (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

  1. req.mut.install-writable-refFor a mutable requirement trait K, a reference in a binding K=expression in $.with or $.context must be &mut T for a type T implementing K. A &T reference is an error. Error: mutable-upgrade.
  2. req.mut.install-readonly-traitFor any other trait, the reference may be &T or &mut T, and the provider is installed without write access.
Error example
pub fn main() -> void:
    counter := MemoryCounter { value: 0 }
    $.with(Counter=&counter):  # error: mutable-upgrade
        _ := $.use(Counter).count()
Open in playground

Note. A new object is installed as a reference to it, as in $.with(Counter=&mut MemoryCounter { value: 0 }), by req.ref.install-ref.

#Retrieving

  1. req.mut.use-writable.reference-valuesFor a mutable requirement trait K, $.use(K) yields a writable provider, &mut dyn K by req.ref.use.reference-values. The methods of K that change their receiver may be called on it.
  2. req.mut.use-readonly.reference-valuesFor a trait without a mutable receiver, $.use(K) yields &dyn K.
  3. req.mut.use.shared-referenceA read-only &dyn K provider reference cannot call a &mut self method, regardless of its binding spelling. Error: not-writable.
Error example
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-writable

Note. A &mut dyn K provider (req.ref.use.reference-values) stays writable in a := binding, as every &mut reference does.

#Access In Rows

  1. req.mut.row.plainA row entry is always the plain key K, and it requires the provider for K with its trait's access.
  2. req.mut.row.no-access-rulesRows, $.Context[$ Row] rows, and removal by extension therefore compare and remove keys by trait alone.
  3. 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

  1. 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.
  2. req.mut.entry.registered-accessA registration contract binds each trait it lists the same way.
  3. 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 playground

See also: Wasm Boundary.

#Captured Access

  1. req.mut.capture-traitA cold suspension captures each provider with its trait's access.
  2. 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

  1. req.ref.key.reference-valuesEvery requirement provider is a trait value reference.
  2. req.ref.use.reference-values$.use(K) yields &mut dyn K for a mutable requirement trait, and &dyn K otherwise.
  3. req.ref.install-refThe expression of a binding K=expression for such a key converts to that type as any value does, by Trait Value References. So &S and &mut S share the place they refer to, and a value of S is an error whose fix-it writes & or &mut. Error: type-mismatch.
  4. req.ref.same-placeEvery $.use(K) that reaches one provider refers to the place that provider refers to. So is between two of them is true, and so is is with a reference converted from the one installed.
  5. 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.
Error example
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
        pass

Why. Every provider is a reference. Reference-receiver traits require a written reference so their writes reach the chosen place.

#References To Other Providers

  1. req.ref.share.reference-valuesA provider for a trait without reference receivers accepts &S or &mut S, sharing its place, or a value of S, copied into a new object.
  2. req.ref.share.implA reference to a type that does not implement K explicitly is an error. Error: type-mismatch.
  3. 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()  # 10

Why. 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]
  1. req.suspend.markerA suspending declaration places ! after its name.
  2. req.suspend.two-formsA suspending declaration introduces two call forms: the ordinary call and the bang call.
  3. req.suspend.coldThe ordinary call constructs a cold suspension.
  4. req.suspend.cold.capturesThe ordinary call evaluates and captures arguments and required providers but does not begin executing the body.
  5. req.suspend.bangThe bang call constructs that suspension and drives it as a child of the current suspending computation.
  6. 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
  1. req.suspend.type.equivalentA suspending function type is definitionally equivalent to an ordinary constructor function returning a suspension.
  2. req.suspend.type.spellingsThe first spelling preserves the source-level bang-call operation; the second is its lowered callable type.
  3. req.suspend.type.conversionAssignment or argument checking may convert the first to the second, but not back.
  4. req.suspend.type.plain-callCalling either form without ! constructs the cold mutable Suspend[T].
  5. req.suspend.type.bangOnly the first form supports callee!(...) directly.
  6. 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.
  7. req.suspend.type.readonly-bindingA := binding weakens that result to readonly Suspend[T], which cannot call poll or cancel.
Error example
fn plain() -> i32: 42
fn run!() -> i32: plain!()  # error: not-suspending

Note. 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)
  1. req.suspend.closure.markerAnonymous suspending callables use the same marker after fn.
  2. req.suspend.closure.requirementsRequirement-bearing non-suspending closures omit ! and place $ after their result type.
  3. req.suspend.closure.row-inferenceClosure inference may infer a requirement row from an expected function type.
  4. 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 written fn!.

#Bang Calls And Driver Contexts

A bang call ends in the suspension_call_suffix of Expressions: !, optional type arguments, and an argument clause.

  1. req.bang.suffixThis suffix is part of postfix_suffix at ordinary call precedence.
  2. 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's bool result.
  3. 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.
  4. 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.
  5. req.bang.activeA driver is active while its executor is evaluating or polling that driver context on the current program-instance call stack.
  6. req.bang.pending-not-activeA pending invocation retained by the host between polls is unfinished but not active.
  7. 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.
  8. req.bang.requirements-not-suspendingMerely using requirements does not make a function suspending; a non-suspending function may have a $ row.
Error example
fn work!() -> i32: 42
fn main() -> i32: work!()  # error: bang-call-outside-suspension
Open in playground

#Driving A Stored Suspension

  1. req.drive.bangFor any expression s of type mut Suspend[T], s!() drives that stored suspension to completion and has type T.
  2. req.drive.onceThe expression s is evaluated once.
  3. req.drive.not-methodThis is the same postfix bang suffix used for a direct fn! call; it is not a method lookup.
  4. req.drive.block-on.signatureNon-suspending code imports use std.task.block_on and calls the standard function block_on[T](s: mut Suspend[T]) -> BlockOn[T], which owns the driver loop until the drive ends or panics. BlockOn is an enum of std.task.
  5. req.drive.block-on.completedWhen the suspension completes, block_on returns .Completed with its value. That holds also when a dispose was cut short after an ordinary exit, by req.abandon.dependency.completed.
  6. req.drive.block-on.cancelledWhen the suspension turns out cancelled, block_on goes on driving its cleanup until the cleanup has ended. It then returns .Cancelled when the cleanup ran to its end, and .Abandoned when a part of it was cut short.
  7. 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.
  8. req.drive.block-on.forbidden-contextsblock_on is forbidden in a default expression, a defer suite, or non-entry module initialization; those contexts cannot start suspension work.
  9. req.drive.block-on.fact-contextsblock_on is also forbidden in a fact or metadata expression of typed derivation.
  10. req.drive.block-on.directA block_on call 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.
  11. 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.
  12. req.drive.block-on.indirectA block_on call reached through a call while a forbidden context runs panics when it is reached. Panic: suspension-forbidden-context.
  13. 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.
  14. req.drive.block-on.printlnA println call follows the same rules: a direct call in a forbidden context is an error, and an indirect one panics, by module.console.println-block-on. Error: suspension-forbidden-context. Panic: suspension-forbidden-context.
  15. req.drive.block-on.under-driverOutside a forbidden context, a block_on call is valid while another driver is active, as in main! or a test body. That holds whether it is reached directly or through non-suspending helpers.
  16. 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.
  17. req.drive.block-on.deadlockWhen an inner block_on call's argument is pending and no host operation, timer, or wake can resume it, the call panics. Panic: suspension-deadlock.
Error example
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-context
Open in playground

Why. 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_on is synchronous code that expects a value, and nothing can cancel it. So block_on names 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 .Cancelled or .Abandoned, unless the suspension cancels its own work.

Note. The outer driver makes no progress while an inner block_on call runs. An inner suspension that can progress only when the outer driver runs never completes. When nothing else can wake it, the call panics with suspension-deadlock; while a host operation or timer is pending, it waits.

#Entry Driver

  1. req.entry.driverThe host executor is the driver for main!.
  2. req.entry.waker-drivenThis entry driver is waker-driven.
  3. req.entry.pendingWhen a poll returns Pending because 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.
  4. req.entry.busy-pollA driver that keeps polling a pending host operation without returning to the host is not conforming.
  5. 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 point
Open 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 here
Open in playground
  1. req.entry.cancelThe host may cancel the entry suspension while it waits on the host. It cancels it as cancel!() does, by req.cancel.steps, with the host as the waiter.
  2. req.entry.cancel.waitThe host then goes on driving the entry until its cleanup has ended, by req.cancel.wait.
  3. 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.
  4. req.entry.cancel.bound.abandonWhen the deadline passes before the cleanup has ended, the host abandons the entry, by req.abandon.unwind.
  5. 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.
  6. req.entry.cancel.bound.timerWhile the host bounds a cleanup, the deadline counts as a pending timer for req.entry.deadlock.
  7. req.entry.cancel.abandonThe host may also abandon a cancelled entry at once, before the deadline.
  8. 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.
  9. 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 cancelAndJoin does. A bound keeps a close that never ends from holding the program, as Trio's shielded deadline does, and only the host's own abandon() 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

  1. req.host-wait.waitThe runtime-provided leaf std.task.host_wait![T](operation: std.task.HostWait[T]) -> T suspends until an opaque host wait operation completes, then returns its result.
  2. req.host-wait.abiHow a host wait operation crosses the host boundary is an implementation detail, not part of the language.
  3. 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
  1. req.protocol.builtinSuspend[T] is a built-in type that no package implements; code writes it without dyn, as mut Suspend[i32]. Its values come only from compiler-generated frames and from std.task. Ordinary packages may consume them, but an impl of Suspend[T] outside the standard library is an error, because Suspend names a built-in type and no trait. Error: unknown-trait.
  2. req.protocol.no-otherThere is no separate Pollable or public Continuation[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
  1. req.poll-context.constructionPollContext is constructed only by the host runtime and std.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.
  2. req.poll-context.wakerStandard-library suspension implementations access its waker through this sealed surface.
  3. req.poll-context.userOrdinary user packages may receive a PollContext only inside APIs explicitly provided by std.task; they cannot use it to implement Suspend[T], a built-in type.

#Wakers

  1. req.waker.arrangeAn operation returning Pending must arrange for the waker to be invoked when another poll may make progress.
  2. req.waker.retainedA waker may be retained after poll returns, invoked from a host callback, and invoked more than once.
  3. req.waker.coalescedRedundant wakes are coalesced and never poll concurrently.
  4. 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

  1. req.check.panicsCompeting drivers, reentrant polling, polling after Ready, or attempting a second execution cause a runtime panic.
  2. req.check.cancel-activeCancelling a suspension while it or one of its descendants is active on the current poll stack causes suspension-reentrant-poll.
  3. req.check.cancel-active.no-effectThat cancellation performs no cleanup or state transition.
  4. req.check.cancel-idempotentCancellation is otherwise idempotent: cancelling an already cancelled or completed suspension has no further effect.
  5. req.check.driver-cancelled.awaitA task that awaits a cancelled suspension is cancelled, by req.cancel.await, but a driver cannot be cancelled. block_on returns how the cleanup ended instead, by req.drive.block-on.cancelled.
  6. 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.
  7. req.check.runtime-onlyThese are runtime checks rather than ownership rules in the type system.
  8. req.check.code.competingCompeting drivers report suspension-competing-driver.
  9. req.check.code.reentrantRecursive polling and active-stack cancellation report suspension-reentrant-poll.
  10. 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

  1. req.discard.coldDiscarding a cold suspension that has never been polled has no cleanup work to perform.
  2. req.discard.startedOnce polling has begun, a host or driver that stops owning the suspension must cancel it before discarding it.
  3. req.discard.droppedDropping a started suspension without cancelling or abandoning it does not run its registered defer suites.
fn fetch!(key: string) -> string: "value of $key"

fn demo() -> void:
    _ := fetch("k")   # discarded cold: no cleanup to run

#Cooperative Scheduling

  1. req.schedule.one-threadEach program instance executes cooperatively on one thread.
  2. 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.
  3. req.schedule.all-unfinishedstd.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.
  4. req.schedule.all-completedall! 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].

  1. req.lowering.sugarfn name!(args) -> T is source sugar for a compiler-generated cold state machine implementing Suspend[T].
  2. req.lowering.frameThe frame stores captured arguments, construction-time providers, locals live across suspension points, and a state discriminant.
  3. req.lowering.pollThe frame's poll method advances until it returns Pending or completes with Ready(value).
  4. 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.
  1. req.bind.constructionRequirements of a suspending function are resolved when its cold suspension is constructed, not when a later driver first polls it.
  2. req.bind.capturedThe chosen providers are captured by the generated frame.
  3. 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.
Error example
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 finished
Open in playground
  1. req.cancel.bangSuspend[T] has the suspending method cancel!(mut self), which returns a CleanupEnd of std.task.
  2. 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.
  3. req.cancel.coldLike every suspending call, job.cancel() without the bang builds a cold suspension of the cancel. job keeps its state until that suspension is driven.
  4. req.cancel.cold.discardA cold cancel that is discarded is an error, as any discarded suspension is. Error: discarded-must-use-value.
  5. req.cancel.deferAfter an unfinished child has completed its own cancellation cleanup, registered defer suites in the suspension's unfinished frames run synchronously. They run in last-in, first-out order, from the innermost frame outward.
  6. req.cancel.disposeA using binding's dispose may suspend, so cancellation that reaches it follows Using Bindings instead of running synchronously.
  7. req.cancel.waitThe cancel completes only once the suspension's whole cleanup has ended, every dispose that suspends included.
  8. req.cancel.resultIt then returns how the cleanup ended. A cleanup that ran to its end gives .Finished.
  9. req.cancel.cold-jobCancelling a cold suspension marks it cancelled without running its body, and returns .Finished at once.
  10. req.cancel.completedCancelling a completed suspension changes nothing. It returns how the suspension's own cleanup ended.
  11. 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 defer suite runs once.
  12. req.cancel.shared.resultEvery cancel that waits on one cleanup returns the same result.
  13. req.cancel.keptA cancel made after the cleanup ended returns at once how it ended.
  14. 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.
  15. 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.
  16. 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.
  17. 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.
  18. req.cancel.await.kindsThe rule holds for every suspension: a compiler-generated frame, a std.task combinator or cancel, and a pending host operation.
  19. 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.
  20. req.cancel.await.disposeInside a dispose!, the await unwinds that dispose instead, by req.abandon.dependency, and the chain ends there.
  21. req.cancel.await.driverThe chain also ends at a driver, by req.check.driver-cancelled.await.
  22. req.cancel.cooperativeCancellation is cooperative at suspension boundaries for language code.
  23. req.cancel.external-abortA runtime must not leave a known active HTTP request or equivalent external operation running when its provider supports abort.
  24. req.cancel.no-ownershipCancellation runs already registered synchronous defer suites, but it does not establish ownership or stop aliases from escaping.
Error example
fn ready!() -> i32: 42

fn stop!(job: mut Suspend[i32]) -> void:
    job.cancel()  # error: discarded-must-use-value

Why. Kotlin's cancelAndJoin also cancels a job and then waits for its cleanup. Because cancel!() 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 raises CancelledError at such an await.

Note. A cancel that waits keeps the code after it from running. In the example, stopper logs 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, outer
Open in playground
  1. req.abandon.methodSuspend[T] has the method abandon(mut self) -> void. It does not suspend, so any function may call it.
  2. 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.
  3. req.abandon.orderThe unwinding runs the cleanup as one last-in, first-out list, from the innermost frame outward. defer suites and disposes keep the order of flow.using.order.
  4. req.abandon.deferEvery registered defer suite runs once, including those of a dispose! that is running. A defer suite never registered does not run.
  5. req.abandon.disposeA dispose! that waits is stopped at its suspension point, after its own registered defer suites run. The code after that point never runs.
  6. 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.
  7. 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.
  8. 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! and race!, and a cleanup the suspension waits on in cancel!().
  9. req.abandon.resultEvery cancel!() waiting on the suspension's cleanup returns .Abandoned, and so does every later cancel!() of it.
  10. req.abandon.carryA cleanup that folds an abandoned cleanup ends .Abandoned too, even when its own part ran to its end.
  11. req.abandon.coldAbandoning a cold suspension marks it cancelled without running its body.
  12. 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.
  13. 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.
  14. req.abandon.dependencyA dispose! that awaits a suspension cancelled elsewhere is unwound from that await as an abandon would unwind it. Its registered defer suites run, and the code after the await never runs.
  15. req.abandon.dependency.resultThe cleanup that the dispose belongs to then ends .Abandoned. The rest of that cleanup goes on as usual.
  16. 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 synchronous terminate(); cancel!() and abandon() 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 a defer that 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

  1. req.combinator.cancel-childrenThe std.task combinators all! and race! cancel their children when the parent is cancelled.
  2. req.combinator.race-losers-disposedrace! also cancels losing children before returning the first completed value. It returns that value once every loser's disposes have finished, by flow.using.cancel.wait.
  3. req.combinator.intrinsicThe standard polling combinators in std.task, including all! and race!, are compiler intrinsics.
  4. req.combinator.bang-calledEach is imported and bang-called like an ordinary fn! function, but the compiler supplies its frame and polling behavior.
  5. req.combinator.not-syntaxThey are not first-class control-flow syntax.
  6. req.combinator.userBecause Suspend[T] is a built-in type, user code cannot define an equivalent polling combinator; it composes the standard intrinsics instead.
  7. req.combinator.race-signaturerace! has the plain vararg signature fn race![T](tasks...: List[mut Suspend[T]]) -> T, with no typing rule of its own.
  8. req.combinator.race-joinIts children's result types therefore meet only by permission weakening, as types.generic.infer.join states, so children of unrelated types are an inference error.
  9. req.combinator.race-emptyA race! call written with no task argument, as race!::[i32](), is an error. Error: argument-count.
  10. req.combinator.race-empty-literalA race! call that passes an empty list literal by name, as race!::[i32](tasks=[]), is the same error. Error: argument-count.
  11. 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.
  12. 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
  1. req.combinator.cancel-foldA cancelled combinator never abandons a child's cleanup. Each unfinished child's cleanup becomes part of the combinator's own.
  2. req.combinator.cancel-orderIt starts every unfinished child's cleanup, in argument order, before it waits for any of them.
  3. 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.
  4. 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.
  5. req.combinator.all-cancelledAn all! child that turns out cancelled cancels the task that awaits the all!, by req.cancel.await.
  6. req.combinator.race-child-cancelledA race! child that turns out cancelled cancels the task that awaits the race! at once, as an all! child does by req.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 defer is what stops it. A parent that waited out the connections before it cancelled the acceptor would never reach that defer. A dispose that waits for its own parent's defer still 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 as all! does. Otherwise timeout!, a race of work and a sleep, would wait out its limit and report TimedOut for 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()}"
  1. req.combinator.all-typingall! has no written signature. A call all!(e_1, ..., e_n) in which each e_i has type mut Suspend[X_i] has type (X_1, ..., X_n).
  2. req.combinator.all-argumentAn argument of any other type is an error. Error: type-mismatch.
  3. req.combinator.all-directall must be the callee of a direct call with positional arguments. A spread, a named argument, an explicit type argument list such as all!::[i32, string], or a use as a function value is an error. Error: type-mismatch.
  4. 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 before all! starts, so its value is not a Suspend.
Error example
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-mismatch

Note. A plain call of a suspending function, such as load_user(id), builds the cold mut Suspend[User] that all! 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, from Suspend[X_i] to X_i. The language has no higher-kinded types, so one written typing rule replaces the signature.

Note. The retry! combinator of std.task is a plain library loop over a fn! 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")
  1. req.poly.row-parameterHigher-order code preserves callback requirements with a row parameter.
  2. req.poly.extensionA local provider removes one key from a callback row by extension.
  3. 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.
  4. req.poly.kind.markedR is a row parameter because its declaration writes $R, not because of its use after $ or the case of its name.
  5. req.poly.namingRow parameters follow the ordinary uppercase convention for generic parameters.
  6. req.poly.leastAt a call, the compiler infers R as the least row solution of the callback pattern.
  7. req.poly.least.examplePassing a callback with row $ Logger + Clock infers R as $ Clock, so the call requires only Clock.
  8. req.poly.absent-matchesPassing a callback whose row lacks Logger is valid, and R is the callback's own row.
  9. req.poly.bodyInside the body, calling callback requires R and Logger; the declared row supplies R, and the $.with scope supplies Logger.
  10. 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.
  11. req.poly.no-subtractionRows have no subtraction operator.
  12. 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 Clock

Note. 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.

  1. req.runtime.driverA stored suspension is driven through a runtime or standard-library driver; no additional source keyword is required.
  2. req.runtime.exclusiveDrivers enforce exclusive access and the panic rules above without exposing a way to upgrade an arbitrary readonly reference.
  3. req.runtime.explicit-providerProvider selection is never implicit: a provider comes from an enclosing $.with scope or from the host configuration of an entry point.
  4. req.runtime.libraryScheduling APIs, durable replay storage and runners, and affine resource ownership are runtime or library concerns.
  5. req.runtime.cancel-cleanupCancellation participates in synchronous defer cleanup 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

  1. req.determinism.inputsA program instance is deterministic in its inputs.
  2. req.determinism.same-programTwo builds are the same compiled program when the compiler's outputs for them are byte-identical.
  3. 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.
  4. req.determinism.no-other-sourceCode between host calls has no other source of nondeterminism.
  5. req.determinism.replayA runtime may therefore reproduce an instance by supplying the same inputs in the same order.
  6. req.determinism.limitsFailures caused by host stack or memory limits are outside this guarantee.
  7. req.determinism.limits-profileThe host's stack and memory limits are part of the runtime profile.
  8. 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 calls

Note. 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 by module.boundary.float-bits, so a replayed NaN, infinity, or -0.0 is the recorded value.

See also: Comparison Traits, Representation And Garbage Collection.