ADR-122: A Function-Typed Parameter Lowers by Its Type — the Future Shape Unless It Says sync

Status: Superseded by ADR-277 — its D1 is D11 there as narrowed by ADR-192 D1, D2 is D10, D3 is D12 and D13 Date: September 16, 2026 Target Version: Nikaia 0.0.8 Related: ADR-102 D2, D3 and D5 (the type says what the code may do; run-or-kept is inferred; the lowering follows), ADR-009 D4 (measure before choosing a shape), ADR-055 D1 (a pausing function is an async fn)

1. Context

A lambda whose body pauses has no lowering the language below offers as a closure: Rust has no stable async closure, and the way out has been known all along — a closure that returns a future. ADR-102 gave a callee’s parameter a type that can say it takes such a thing, and said how a kept parameter lowers (a boxed closure over a boxed future). What it left between two sentences was the case the whole question exists for: a run parameter — one the callee calls and does not store — handed a lambda that pauses. A run parameter lowered “as std’s do today, a closure argument”, which is impl Fn(A) -> R, and a pausing lambda cannot be written into one.

2. Decisions

D1 — The parameter’s type decides the shape, and nothing inferred stands behind it.

A parameter whose type may pause — fn(A) -> R without sync, which is the default — lowers to the future shape whether the callee runs it or keeps it: a closure returning a boxed future, called with an .await. A parameter whose type says sync lowers to a plain closure. A reader can tell what a signature costs by reading it.

D2 — The refusal of a pausing lambda at a run parameter goes with it.

A lambda that pauses fits a parameter whose type allows pausing, run or kept (ADR-102 D2). The build-time refusal #187 carried as a lambda that pauses is refused has no case left.

D3 — The box is measured, not assumed.

A run parameter handed a lambda that does not pause now pays a heap allocation and a dynamic call it did not before. That is the cost of D1, on the common case, and it is measured on the corpus before the record is closed as built — the standard ADR-009 D4 set for choosing a shape. std’s own entries are untouched: they are Rust with hand-written signatures, so map and its kin pay nothing new.

3. Consequences

4. What this does not decide

5. What is built

D1, D2 and D3. A parameter whose type may pause is impl Fn(A) -> Pin<Box<dyn Future<Output = R>>>, a call to one in the callee’s body carries an .await, and a lambda handed to one is |a| Box::pin(async move { … }). A parameter that says sync is the plain closure it was. The measurement is §3’s.

What the emitter can answer itself and what it cannot. The declaration and the call inside the body are read off the type written in the signature it is standing in, so Flow carries the parameter names the way it already carries the mut ones. The lambda at the call site is a question about the callee’s type, which this compiler’s emitter has none of (ADR-028), so the checker records it — the arrangement lent_args and nullable_args already have.

One thing the record had not said, and the corpus said it in one line. D3’s std’s own entries are untouched is not a remark, it is a condition on the check: HashMap::and_modify is a hand-written description of a Rust signature, which takes a plain closure whatever the ledger’s sync column says. Writing the future shape for it produced expected (), found Pin<Box<…>> against examples/access-log/src/main.nika. So the shape is written only where the signature was declared in this language.

And one thing it settles that it does not name. ADR-102 D3 gave a run parameter sync = "from(f)" — the lambda decides — on ADR-029 D3’s reasoning that the lambda’s body is counted in the caller. That reasoning holds while the lambda is a plain closure and stops holding the moment the type may pause, because then the lambda is a future the callee awaits, and a function that awaits is async whatever its caller handed over. So the column follows the type here too, Sync::From is no longer an outcome of inference, and the run-kept question left contracts::sync with it. What D3 still answers is touches and throws, which are not this.

What is left is step 4, fortunes.nika as the corpus program: its route handler is writable now, and what it waits on is examples/http/ declaring route — which is ADR-102’s own consequence and needs the package rewritten rather than the compiler changed. #105 carries it.

This site is open source. Improve this page.