overlap, Which Keeps Every Result and Every Failure, or select, Which Keeps the FirstStatus: 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
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.
| 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.
| 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.
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.
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.
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).
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
overlap, which the compiler checks.overlap.nikaia explain.overlap or a select is not sync, so neither can
appear in a par_iter lambda, an access or an update.std’s vehicles per arity (overlap<n>, race<n>, combine<n>), the list on
the error carrier, and one method on the task handle.overlap’s first failure — a
measurable question that would trade D9’s complete list for a faster failure;
an option on the block, if it comes.let x = async e binds one name per task. Refused
(D2); the tuple is the smaller form and admits the other later.overlap of branches none of which can pause going through no vehicle
at all, which would make it sync again — unmeasured, and it changes what
--overlaps reports.select with a default arm, a poll rather than a race, which no page
writes.overlap
branch.par_iter chain, whose lambdas may pause): the same operation over many elements is
par_iter (ADR-235), not overlap.