ADR-292: Statement Order Is the Written Order — a Program Asks for Concurrency with overlap, Which Keeps Every Result and Every Failure, or select, Which Keeps the First

Status: Accepted Date: October 3, 2026 History: the records this one replaces are archived in docs/history/adr; read them only for how the decision was reached Answers: what order statements run in, and how a program asks for two things at once (the open-decisions.md entry on reordering statements by their touch sets); what becomes of an overlap’s other failures (#199); what Part II 12.4’s select is (ADR-141 D2 marked it unspecified); and why a correct program with a joining block reached rustc (#136) Related: ADR-020 (the ledger the touches column joins), ADR-005 §1 Group B (the crossing check an overlap does not meet), ADR-297 D5 and D4 (a parked cleanup and its deadline; a cleanup error attached, not substituted), ADR-021 D5 (the cache dimension ordering was, and no longer is), ADR-023 D6 and D8 (what an error carries; how a failure propagates), ADR-288 D1, D2, D4 (sync, inferred restrictively, an assertion never overwritten), ADR-037 D2 and ADR-303 D3, D4 (the user in user_parallelism; I/O the kernel performs carries no user code), ADR-055 (a pause is a suspension point, and the executor is the only place a program parks), ADR-276 (what a keyword costs), ADR-291 D6 (_ where a value arrives), ADR-284 (the C cancel, the same mechanism as D14), ADR-280 D13, D15 (the four channels a failure can travel in, an envelope on a bare channel where a body joins) and D21 (the secondary list is a diagnostic, not a value), ADR-277 D16 (an overlap branch is a block, not a callable), ADR-149 (channels) and ADR-235 (par_iter), the other two concurrency forms, decided apart, Part I 8.1.1 and 8.1.2, Part II 12.4

1. Question

Within one task, may the compiler run two statements in an order other than the written one — and if not, how does a program ask for two things at once, what does such a block hand back when its branches fail, and what does a block that keeps only the first branch to finish look like?

Without an answer a reader cannot tell what a line of Nikaia means: whether let a = fs::read(…) followed by let b = fs::read(…) costs the sum of the two or the larger, and whether that depends on a contract somebody else wrote.

2. How others do it

Language / tool Its answer What it costs there Source
A CPU reorders independent loads freely, and a program writes a fence where order matters affordable only because the reordering is provably unobservable; I/O reordering is not D1
Swift async let at the definition, joined at first use the fast path is the one a program asks for §3 B, from memory
Rust / tokio try_join! runs futures together and cancels the others at the first error which of the others had failed depends on the clock D9, from memory
Every mainstream run these together form takes the programmer’s word that the branches are independent an independence claim nobody checks D3

What does not transfer: the CPU’s freedom, because nothing about I/O reordering is provably equivalent; try_join’s cancellation, because it makes the list of failures a race (D9). What does: a form a program writes to ask for overlap, which every language above offers first — and which this language can do one better on, because its ledger already knows what each call touches.

3. Options, weighed

Option What a program writes Why it lost, or won
A — reorder by touch sets (D1, D7) nothing; seq { … } to force an order the analysis cannot see; ordering = "strict" to turn it off lost. Two escapes admit that the analysis is incomplete and corrected by hand after somebody noticed; the wins reached only adjacent statements, one line between two reads removed the pair unexamined, and where they did not arrive a program had no way to ask
B — mark each binding (async let) a word on every value to be overlapped lost as a per-binding mark: a block that waits for all of them is one form, smaller, and lets the branches be checked as a set
C — mark the barriers a fence where order matters lost: every sequence would have to be audited for an order that is not there
D — overlap { … }, checked a block whose branches run at once and are proved to meet on nothing won (D2-D3): the overlap is asked for, and the claim is checked rather than trusted
E — cancel the other branches at the first failure the same block, failing faster open: it trades a complete, reproducible failure list for a faster failure (§6)
F — select as a function select(a, b) lost: an arm binds a name and then runs a block, and no parameter has that shape (D12)
G — combine branch failures with a ? per branch at the call nothing different lost: a ? leaves the function, so a catch on the block could not handle it (D18)
H — error.secondary as a value throw LoadFailed(error, error.secondary) lost (ADR-280 D21): the list is a diagnostic that travels with the failure

No number decided Option A: the corpus was the designers’ own programs, so a number off it describes those programs rather than the language.

4. Decision

D1 — Statement order is the written order, without qualification.

Two statements run in the order they are written, whether or not they meet on anything, and no analysis stands between the source and the schedule. Nothing is started early, so no rule is needed about what may be started early: a branch, a diverting catch, a break or a loop condition between two statements are not cases to name.

D2 — overlap { … } runs its branches together and waits for all of them.

Changed by ADR-async-5 D5-D9, D24: overlap is removed; async e starts a task whose value is waited for where it is first needed, async { … } is the group, and fan-out over any number of elements is the par_iter chain.

let (user, rights, prefs) = overlap {
    db::load_user(id)
    db::load_rights(id)
    cache::load_prefs(id)
}

Each statement in the block is one branch, and a branch is an expression; several steps in one branch are a block expression inside it. The block starts every branch, waits for every branch, and its value is the tuple of their results in written order. A branch that binds a name is refused — the block’s value already carries every result. A block has two to eight branches; one has nothing to overlap with, and more is refused naming the limit.

D3 — The branches must meet on nothing, and the compiler checks it.

Changed by ADR-async-5 D7: the check runs between a task and its window and between the tasks of a group; a binding is no longer refused.

Every call has a touch set in the ledger: what it reaches, and whether it reads or writes it — file(path), stdout, a lock, an endpoint. Two reads of one resource do not collide; a write and anything else on the same resource do. This is the borrow checker extended past memory to the world: & and &mut are read and write, and a file or stdout joins what can be pointed at.

The touch sets do not decide whether the compiler may overlap two statements; they decide whether the programmer was right to. A pair of branches that meets on a resource is refused with NK2104, and the message is the verdict’s own words — both reach stdout, and one writes it.

A branch this compiler cannot account for — an unknown callee, a non-literal argument, a catch whose handler can leave the function, which the walk does not look through — is not refused: reading an absence as “they meet” would refuse a correct program on nothing (Part III C.4).

--overlaps reports on the blocks a program writes: which branches it wrote, and which of them are started first (D6).

D4 — An overlap block is not a task, and nothing outlives it.

Changed by ADR-async-5 D2: it holds for a child task and its group.

The block ends before the function continues. Nothing is moved into a branch, borrowing works inside one, and the structural crossing check has nothing to do, because nothing crosses out. That is what makes the form lighter than two spawns.

D5 — A branch that fails fails the block, and the first in written order wins.

A branch whose failure is uncaught makes the block fail. Where more than one fails, the block’s error is the one from the first branch in written order — the only order the source has, so the only one that makes the outcome reproducible — whatever failed first in time. Per-branch handling is a catch inside the branch.

D6 — Every branch is started up to its first pause before any branch that cannot pause is run.

I/O the runtime performs is not user code, so any number of operations may be in flight at either setting of user_parallelism; what no bounds is the program’s own computation, one piece at a time. A block of two reads and one computation therefore costs max(reads, computation) at both settings, and the setting changes only whether two computations may also overlap.

Reaching that bound needs an order of starting: the branches that can pause go first, read off ADR-288 D1’s sync property, then the computation runs while they are in flight. This changes nothing observable, because D3 has established that the branches meet on nothing.

D7 — There is no seq { … } and no ordering switch.

Both would force or restore the written order against a reordering D1 rules out. seq is not a reserved word, a manifest that carries ordering fails the build, and Part I 1.2’s build switches are two.

D8 — An error carries a list, secondary, and it is empty until something joins.

Changed by ADR-280 D21: the list is a diagnostic a program does not read, with no type and no error.secondary accessor.

Every error has a list of further errors, in the order they joined, each with its own raise site. It costs nothing where nothing fails; where something does, it is what an uncaught failure’s printout and nikaia explain show, indented under the primary. It is a diagnostic: it travels with the failure and throw error hands it on; a program does not read it, and there is no type for a list of errors. A catch still catches one error and chooses by its type.

D9 — In an overlap, the other branches’ failures are the winner’s secondary, in written order.

Changed by ADR-async-5 D21: the list is filled at the group’s end, in written order; where the group is left by a failure, that failure leads.

The block waits for every branch (D2), so when it ends every outcome is known. Every later failing branch is appended to the winner’s list in written order. Running to completion is what makes the list a fact and not a race. A branch that hangs holds the block; a timeout is the branch’s own, written as a select against a deadline inside it.

D10 — A cleanup error attaches the same way, and the result is a tree.

A cleanup that fails while a branch is already failing joins that branch’s error, which then joins the winner’s with its own list. ADR-297 D4’s secondary is this list: cleanup’s attachment and overlap’s are one mechanism.

D11 — A branch handles its own failure; the block is where failures are combined.

A handler inside a branch that yields a value makes the branch succeed; one that throws makes the branch fail with what it threw. A branch cannot see another’s failure, because branches meet on nothing and run at once. Combining is a catch on the block, whose handler has the winner; re-thrown with throw error, the others go with it.

D12 — select is a block construct, and its arms bind.

select {
    result = heavy_math()              => { return result }
    _      = time::sleep(5.seconds()) => { throw Timeout::TooSlow }
}

An arm is pattern = expr => { … }: every expression is started, the first to finish wins, its value is bound to the pattern and its block runs. _ is a value that arrives and is ignored. A block has two to eight arms. It is syntax and not a function because an arm binds a name and then runs a block.

D13 — A select’s losers are cancelled, and that is the one mechanism.

A loser stops at its current pause point and its values are torn down; a cleanup that pauses is adopted by the runtime and finished in the background, bounded by cleanup-deadline. The program does not wait for a loser’s teardown.

D14 — A task handle has cancel(), with exactly those semantics.

cancel() does to a task what losing a select does to a loser, and it takes the handle, as join does — so a cancelled task is never joined and cannot be asked afterwards whether it was cancelled. A program’s cancel, a select’s loser and ADR-284’s C cancel are one mechanism.

D15 — overlap and select are a pair.

Changed by ADR-async-5 D4: the pair is async and select, two policies of one kind of task.

Both start every branch at once; overlap keeps every result, select keeps the first. That is the argument for the keyword ADR-276 says is the most expensive thing a language adds: it is the missing half of an idea already taken.

D16 — An overlap and a select pause, whatever their branches do.

Changed by ADR-async-5 D20: it holds for a body holding any async form.

Both hand their branches to the executor and wait there, so a function holding one parks on the executor and is not sync, even where no branch can pause. Neither is a call, so each sync analysis asks about them separately; the inference loses the claim, and a written sync on such a body is refused with NK2202, whose note says where the pause is. The throws walk does not treat the block as opaque: a block’s failures are its branches’, which it already reaches.

D17 — A branch’s Ok names the enclosing function’s channel.

Where a block’s failure leaves the function, each fallible branch is wrapped in the function’s own channel — Thrown<E>, a library’s type bare, a generated sum or the box — read from the string the signature wrote, not derived twice, so the ? under the block converts into whichever it is.

D18 — A joining block’s branches become one outcome, in std.

overlap combines its branches’ results in written order into one Result whose Err is the first failure (D5); where the failure leaves the function, the ? is one, on the block. That combining function is the only code that holds every outcome, which is where D9’s list is filled. A select with a handler on the block does the same in its shape: the winning arm takes its own failure apart and its value becomes the Ok half.

D19 — A handler on a joining block binds the set the branches throw.

A joining block has an error set of its own — the union of its branches’, in the ledger’s order — and a catch on it binds that set, in the channel the same derivation gives a function’s key. Both the branches’ Ok and the handler read it, so they cannot disagree. A branch whose callee no ledger knows makes the set the box: the safe direction.

D20 — rustc says nothing about the file it is handed.

A warning about a generated file is Part III C.1 one severity down. A handler’s envelope is opened as its first statement, not as a block around it; a tail throw in a throws body is not wrapped in Ok(, while a tail return is.

5. Consequences

6. What this does not decide

This site is open source. Improve this page.