select { … } Keeps the First Branch to Finish, and a Task Handle Has cancel()Status: Superseded by ADR-292 — its D1-D4 are D12-D15 there
Date: September 19, 2026
Target Version: Nikaia 0.0.31
Related: ADR-050 (overlap { … }, the sibling that keeps
every branch), ADR-141 D2 (which marked the page unspecified),
ADR-129 (the C cancel, which leans on the same race),
ADR-006 D3 (a parked cleanup and the deadline),
ADR-084 (what a keyword costs),
ADR-126 D1 (_ where a value arrives), Part II 12.4, 12.7
Part II 12.4 writes a select block and
ADR-141 D2 marked it unspecified: a construct with no record.
The semantics are not missing — the loser stops at its pause point, its
cleanup is adopted and finished in the background, the cleanup-deadline
bounds it (ADR-006 D3) — and the runtime’s race is what
ADR-129’s C cancel already leans on. What is missing is the
construct.
And a word. Today nothing but losing a select cancels a task, so a
program cannot say stop to something it started — which is a gap the C
direction has already had to work around.
select is a block construct, and its arms bind.select {
result = heavy_math() => { return result }
_ = sleep(5.seconds()) => { throw Timeout::TooSlow }
}
An arm is pattern = expr => { … }: the expression is started, the first one
to finish wins, its value is bound to the pattern and its block runs. _ is
the ignore pattern where the value is not wanted — a value arrives and is
ignored, which is ADR-126 D1’s own position and not the
catch-all arm ADR-145 took away from it.
Syntax and not a function, which is the whole of why it costs a keyword: a branch binds a name and then runs a block, and no function parameter can be given that shape.
A loser stops at its current pause point; its values are torn down; a
cleanup that pauses is adopted by the runtime and finished in the
background, bounded by the cleanup-deadline. The program does not wait for
the loser’s teardown, because it should not pay for it.
None of that is new here — it is ADR-006 D3 written down again where it is now reachable.
cancel(), with exactly those semantics.spawn hands back a TaskHandle; it gains cancel(), which does to that task
what losing a select does to a loser. One mechanism, so that a program’s own
cancel, a select’s loser and ADR-129’s C cancel are the same
thing said three ways rather than three things.
It costs nothing new in the runtime, which already cancels a loser.
overlap and select are a pair.ADR-050’s overlap { … } runs its branches at once and keeps
every result; select runs them at once and keeps the first. A
language with one and not the other has half a pair, and the second is the
argument for the keyword: it is not a new idea, it is the missing half of one
already taken.
select with a default arm (if nothing is ready yet), which is a
poll and not a race, and which no page writes.cancel() is a request the runtime honours at a pause point; what the
handle says afterwards is a question with no program behind it.overlap branch — which is the same mechanism one scope over and waits for
a program.All of it.
select is reserved and has
its construct in the same change, which is
ADR-051 D3’s one ground that holds — and the number that
allowed it is zero: nothing in examples/, in tests/ or in the three pages
wrote it as a name. _ and a binding name are the two patterns.match over which arm won: std gains
race2..race8 and a Race<n> per arity, beside the overlap<n> it
already had. An arm binding a name and then running a block is a match arm
in the language below, so nothing had to be invented for it. A block of one
arm is refused with its reason, as an overlap of one branch is.cleanup that pauses is adopted and bounded by the
cleanup-deadline (ADR-006 D3). D2’s “that is the one
mechanism” is a fact about the emitted code rather than a rule this record
added.cancel() on the handle, over a flag the task reads at its pause point
and the waker that makes it notice now rather than when whatever it was
waiting for answers.cancel() takes the handle, the way join does — and that is what became
of §4’s whether a cancelled task can be observed to have been cancelled: it is
a question no program can ask, because after the call there is no handle to ask
with. It also means a cancelled task is never joined, so nothing waits
for a value that is not coming.
An arm that can fail propagates from the arm that won and not from the
race: every arm is wrapped in Ok so the vehicle sees one shape, and the ? is
the first line of the winning arm’s body. overlap’s D5 puts its ?s on the
tuple instead, because there every branch has a value.