async Closure, And A Premise Under Twelve Sentences Is FalseStatus: Superseded by ADR-277 — its D1 is D13 there and D2 is D16 (its third point displaced by ADR-233); D3 (answered by ADR-192 D1) and D4 are not carried Date: September 22, 2026 Target Version: Nikaia 0.0.138 Corrects the premise of: ADR-055 §6 step 3, ADR-033 D10’s coda, ADR-122 §1 and D1 — none of which is rewritten, because a record says what was decided when Related: ADR-009 D4 (measure before choosing a shape), ADR-102 D2 (the type says what the code may do)
Twelve live sentences in this tree — a std module header, five emitter
comments, three test doc comments, the roadmap, spec-promises.md and
#187 — say some form of:
Rust has no stable
asyncclosure.
It is false, and it was false when each of them was written. Measured on
this repository’s own toolchain, rustc 1.94.1, edition 2021, stable, with no
feature gate and no RUSTC_BOOTSTRAP:
async fn twice<F: AsyncFn(i32) -> i32>(f: F) -> i32 { f(1).await + f(2).await }
let f = async |x: i32| -> i32 { x * 10 };
compiles and runs, as do AsyncFnMut, AsyncFnOnce and async move |…|.
A false premise is worse than a wrong decision, because a decision is argued against and a premise is inherited. Each of the twelve sentences is someone’s reason for a shape, and every reader who takes the shape on that reason takes the falsehood with it. That is what this record is for.
And it is not a fact that expired. The async closure has been on stable
since well before the earliest of these records. Nothing here was overtaken by
the language below; it was never checked against it — which is
ADR-009 D4’s rule (measure before choosing a shape) not applied
to the shape’s own premise.
benches/handler gains a row. Against a plain closure as the floor, on this
machine, best of five, control tying:
| row | ns/call | × |
|---|---|---|
plain closure (impl Fn) |
0.31 | 1.0 |
boxed future (impl Fn(A) -> Pin<Box<dyn Future>>) — ADR-122 D1’s shape |
11.99 | 38.3 |
async closure (impl AsyncFn(A) -> R) — the shape said not to exist |
1.37 | 4.4 |
| plain closure, twice (the control) | 0.29 | — |
The shape chosen because it did not exist costs 8.7× what it does.
Three of the sentences justify a shape that is right for a different reason, and the correction is to say that reason:
overlap branch are async blocks. A body is a
block, not a callable; wrapping it in a closure to call it once adds a call
and takes nothing away. spawn fn { … } and overlap { … } lower as they
did.task::interleave and the overlap/race families take futures, because
a branch is that same block — and because rayon::join, the pool vehicle
ADR-033 D10 names, takes synchronous closures and cannot
await inside one. That is a fact about rayon and not about Rust.std entry is still refused at the build,
because the entry’s own Rust signature takes a synchronous closure:
Iterator::map takes FnMut, and an async closure handed to it yields an
iterator of futures, which is a different program. That is a fact about
those entries and not about Rust.ADR-122 D1 — a parameter whose type may pause lowers to a closure returning a boxed future — has no surviving reason of the kind D2 supplies, and D1’s measurement is what makes that matter: 38× against a floor, where the alternative is 4.4×.
It is not this record’s to overturn, and the reason is D1’s own second half:
a reader can tell what a signature costs by reading it, one shape for a run
parameter and a kept one alike. impl AsyncFn is a bound and not a type, so
a kept parameter — one stored in a struct — still needs a box or a type
parameter of its own, and taking the cheap shape for the run case splits the two
apart again. That is a trade between coherence and 8.7×, which is the owner’s,
so it goes to open-decisions.md in that page’s
shape.
ADR-055, ADR-033 and ADR-122 are not
edited. A record says what was decided when, and a premise quietly corrected
inside one is a record that no longer explains the code it produced. What is
corrected is every living sentence — source comments, test doc comments, the
roadmap, spec-promises.md — plus a row in the index’s superseded and
narrowed table, which is where this tree already says this part of that record
no longer holds.
rayon::join is synchronous, Iterator::map
is synchronous.benches/handler now prints the
alternative beside the shape, so the reopened question is answered with a
measurement whichever way it goes.open-decisions.md.AsyncFn. Nothing in
the surface language changes here; the question is about a lowering.std’s lambda-taking entries. They are Rust with
hand-written signatures (ADR-122 D3), and a synchronous one stays
synchronous whatever the closure spelling is.At 0.0.138.
benches/handler gains the async closure row and the paragraph saying why
the bench outlived its record.crates/nikaia-std/src/task.rs,
crates/nikaia/src/emit/mod.rs (five), crates/nikaia/tests/pausing.rs,
crates/nikaia/tests/tasks.rs, crates/nikaia/tests/function_types.rs,
docs/project_status_and_roadmap.md, docs/spec-promises.md and
#187 — the one the owner pointed at.open-decisions.md.