syncStatus: 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)
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.
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.
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.
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.
examples/fortunes.nika’s route handler, .route("/fortunes") fn {
fortunes(db) }, is writable: route’s parameter allows pausing, and the
lambda is lowered to the shape that can.sync on the
parameter, which is what the word already promises.What it costs, measured (D3): 15.1 ns per call against 0.33 ns for
the plain closure — a factor of about 45, reproduced over three runs, with the
control tying (benches/handler). The future is driven by the executor a
generated program is driven by, and the plain closure inlines away, which is
what makes 0.33 ns the honest floor rather than a rigged one.
The relative number is large and the absolute one is small, and which of the
two matters is the whole of D1’s argument. A handler answering an HTTP
request — the case this record exists for — spends microseconds; 15 ns is
nothing it can see. A lambda run a million times over a list is the other
case, and it is the one sync is the door out of: the word that already
promises this never pauses is the word that buys the plain closure. A reader
can tell which they are getting by reading the signature, which is D1.
async closure, should the language below stabilise one,
replaces the boxed future shape; the surface would not change.sync as a performance word. It is not one here: it lowers to the plain
closure because that is what never pauses means.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.