ADR-163: A Block That Joins on the Executor Pauses, and Its Branches Travel in the Function’s Own Channel

Status: Superseded by ADR-292 — its D1-D2 are D16-D17 there; D3 named a defect ADR-164 D1 closed (D18 there) Date: September 20, 2026 Target Version: Nikaia 0.0.81 Related: ADR-050 D2 and D5 (overlap waits for all its branches; the first failure in written order wins), ADR-148 D1 (select), ADR-055 (a pause is a suspension point, and the executor is the only place a program parks), ADR-027 D2 and D4 (sync inferred restrictively; an assertion is never overwritten by it), ADR-157 D1 (the channel is the error type), ADR-159 D2 (a library’s error travels bare), ADR-160 D1 (a set of two is a generated sum), Part III C.1

1. Context

overlap { … } and select { … } are the two constructs that hand several branches to the executor and wait there. Both were built before ADR-055 made the lowering implicitly async, and before ADR-157 and the three records after it gave the failure channel a type. Neither was asked again afterwards, and each had a correct program that rustc refused — in a file the author never wrote, which is Part III C.1’s class of defect.

Neither was reachable from this repository. No program in examples/, in benches/ or in tests/samples/ writes an overlap block or a select at all; the constructs are held by their own tests, and those tests happened to write the two shapes that still worked. Ten lines were enough for the first:

fn twice(n: i64) -> i64 {
    return n * 2
}

fn main() {
    let pair = overlap {
        twice(3)
        twice(4)
    }
    println(f"{pair.0} {pair.1}")
}

error[E0728]: await is only allowed inside async functions and blocks.

2. Decisions

D1 — An overlap and a select are suspension points, whatever their branches do.

Both lower to an .await on a vehicle in nikaia_std::task — overlap<n> and race<n> — because a block that runs several things at once is a future that polls them. That .await is written whether or not any branch can pause, so a function holding one parks on the executor and is not sync (ADR-055: the executor is the only place a program parks).

Neither construct is a call, so the shared resolution both sync analyses use has nothing to answer about them and each has to ask separately. The inference loses the claim, as it does for everything it cannot see the end of (ADR-027 D2); and the check contradicts a written one with NK2202, because D4 of that record says an assertion is never overwritten by the inference — so fixing the derivation alone would leave a hand-written sync on a body that pauses, and the language below would be the one to say so.

A construct has no ledger entry to name, so the diagnostic says where the pause is instead: a select block hands its branches to the executor and waits there, which is a pause.

throws deliberately does not ask the same question. A block’s failures are its branches’, which that walk already reaches by descending into them; treating the construct as opaque there would put a "?" in the set of every function that writes one — a claim about failures made to fix a fact about pausing.

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

Both vehicles wrap a fallible branch in Ok::<_, E>(…), and the type has to be written out rather than inferred: an async block with a ? in it and nothing to infer from is type annotations needed about a generated file. E was the box, which was right while every channel was one. Since ADR-157 a channel is Thrown<E> for a type the program declares, the type bare for a library’s (ADR-159 D2), or a generated sum for a set of two (ADR-160 D1) — and the ? under the block converts a branch’s error into whichever it is, which it cannot do from a box.

So the channel travels with the flow the body is emitted in, exactly as the function’s ledger key already does, and both vehicles read it. It is the string the signature wrote, not a second derivation of it, because two answers that have to agree are one answer or a bug.

D3 — An overlap whose failure is handled at the block is not built, and is named as a defect rather than decided here.

overlap { … } catch { … } — ADR-115 D4’s own written example — does not lower. Where the enclosing function is not throws, no branch is wrapped and no ? is written, so the catch emits a match over a tuple as though it were a Result.

It is not fixed here, and the reason is that fixing it is ADR-115’s own first step rather than this record’s last: combining the branches at the block means looking at every outcome instead of returning at the first Err, which is exactly what that record’s secondary list needs. Fixing it twice would be writing the same loop twice. #136 carries it with the program above as its reproducer.

3. Consequences

4. What this does not decide

5. What is built

All of D1 and D2, and D3 is written down rather than built. crates/nikaia/tests/joining.rs holds seven: each construct over branches that cannot pause, lowered, compiled and run; a branch that pauses still making the block pause; the written sync refused with NK2202 and the construct’s own note; the channel named for a type this program declares and for a library’s; and ADR-050 D5’s first-in-written-order failure, with an assertion that the second one is nowhere — which is the line that changes the day ADR-115 is built.

This site is open source. Improve this page.