Status: Superseded by ADR-277 — its D1-D4 are D11, D8, D14, D15 there
Date: September 22, 2026
Target Version: Nikaia 0.0.144
Answers: open-decisions.md’s does the body’s
run-or-kept answer decide a function-typed parameter’s lowering? — option
A, which that page recommended and the owner took
Narrows: ADR-122 D1, whose whether the callee runs it or
keeps it is the half this replaces; D2 and D3 stand
Related: ADR-102 D3 (run-or-kept is inferred, and the ledger
records it), ADR-094 D2 (keeps, the column it is recorded in),
ADR-187 D1 (the premise that reopened it),
ADR-037 D7 and ADR-008 D2 (one written type, a
representation the analysis picks)
ADR-122 D1 made a parameter whose type may pause lower to a
closure returning a boxed future, whether the callee runs it or keeps it, on
the ground that Rust had no stable async closure.
ADR-187 D1 measured that ground and found it false; D3 filed what
was left as a question.
The question was first written at the wrong altitude, and the owner said so: it asked whether the Rust backend could use a new feature, when what was being decided is whether one written type may take two representations, chosen by an analysis of the callee’s body.
Read that way it is not a new rule at all. This language already answers yes:
Shared[T] is Rc<T> or Arc<T>, and which one is decided per value
(ADR-037 D7) — two values of one written type in one program,
two representations.@detached to write.&.keeps answer asks for.impl AsyncFn(A) -> R. No
box, no dynamic call.AsyncFn is a bound, and a value a field holds needs a type.sync is untouched and still writes the plain closure. The word is an
assertion about what the code may do (ADR-027); a parameter that
cannot pause has no future to hand back either way.Measured (benches/handler, best of five, control tying): the plain closure
is the 0.31 ns/call floor, the boxed future 11.99, and impl AsyncFn 1.37.
The shape a run parameter used to pay costs 8.7× the one it needed.
The lambda follows at the call: async |a| { … } where the parameter is
run, and |a| Box::pin(async move { … }) where it is kept. No move on the
run shape, which is Part I 5.4 A — a lambda handed to
a parameter the body only calls borrows what it captures, because the call
is over before the caller’s frame is.
keeps, and nothing new is derived for it.ADR-102 D3 already said where this lives: the same analysis that
decides whether a value is a view or kept, asked of a parameter that is code …
the ledger records which of the two a parameter is, from the body, the way it
records keeps. It does. fn runs(f: fn() -> String) { return f() } carries
no keeps; fn holds(f: fn() -> String) { spawn fn { f() } } carries
keeps = ["f"].
So this record adds no column. What it adds is a second reader of one that was already written.
A callee this build has no contract for reads as run, at both ends. The
direction matters less than the agreement: the declaration writes the parameter
and the call writes the argument, and the two disagreeing is one parameter with
two shapes — rustc’s words about a file nobody wrote
(Part III C.1). It is the same rule
ADR-094 D1 states for the &: one answer read twice, because the
two disagreeing is a &&T or a moved value.
A reader can no longer tell a run parameter’s cost from its signature alone. A library author who moves a body from run to kept moves every caller’s cost, which is action at a distance — what ADR-005 §3 rejected in-source annotations to avoid.
The instrument for that already exists and is used twice. A keeps change
is a nikaia.contracts diff in review (ADR-094), a tether state
is one (ADR-008 D6 — the inverse tool is inspection, not
assertion), and --locked fails a build whose contracts moved unrecorded. This
is the third use of it, not a new mechanism.
std’s own entries are untouched. They are Rust with hand-written
signatures (ADR-122 D3), and declared_here still gates the
whole question.future_lambdas,
and a reader who has to open the ledger to see a signature’s price.let are NK1142 today, so the kept branch is
written and not yet reachable from a program.AsyncFn, AsyncFnMut or AsyncFnOnce is the right bound. Fn
is what a run parameter has always been written with; a parameter called once
is a narrowing nobody has asked for.sync. It stays an assertion and stays the way to ask for
no box at all.All four, at 0.0.144.
Emitter::code_parameter_runs, set around the one ty_counted call that
writes a parameter’s type — a fact about the position, which the type
writer takes none of.Checked::run_lambdas, beside future_lambdas and read at the same key, with
arguments_given taking the callee’s contract for the one question.benches/handler’s third row, which stops being the shape D1 was written as
having no stable spelling and becomes the shape the compiler writes.crates/nikaia/tests/function_types.rs: the two shapes are read off the
declaration and the call, and the run one is compiled and run.