ADR-192: The Body’s Run-Or-Kept Answer Decides A Code Parameter’s Shape

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)

1. Context

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:

2. Decisions

D1 — A code parameter that may pause takes the shape its body’s keeps answer asks for.

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.

D2 — Run is the absence of 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.

D3 — The declaration and the call read that column the same way, including when it is absent.

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.

D4 — What D1 traded away is predictability, and the ledger is what answers for it.

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.

3. Consequences

4. What this does not decide

5. What is built

All four, at 0.0.144.

This site is open source. Improve this page.