sync — and the Language Below Is Rust 1.75 or NewerStatus: Superseded by ADR-288 — its D1-D4 are D24-D27 there
Superseded in part by: ADR-219 — the floor is 1.88, measured; D4’s mechanism stays
Date: September 16, 2026
Target Version: Nikaia 0.0.8
Related: ADR-102 D2 (the same reading for a function type),
ADR-078 D4 (a trait’s method in the ledger), ADR-080
D1 (NK1129), ADR-106 (a bound across a package, and the trait
table), ADR-027 (sync is an assertion; NK2202),
ADR-045 (whether a value may cross a thread is the same at both
settings), ADR-001 D1 (the channel is stable, and one file names
it), Part I 4.7, Part III 13.2
The README’s first claim is that functions have no colour. A trait’s methods
had one: every method a trait declares was sync, because a declaration has
no body to read a pause from, and because the language below had no way to
write async fn inside a trait. So no trait could describe I/O — not a
Handler, not a Repository, not a Source backed by a file — and a
language that removed the sync/async split from functions kept it, inverted,
in its traits. http cannot be hosted on that.
The second reason stopped being true with Rust 1.75, which stabilised async
fn in traits and return-position impl Trait in traits. The first was never
a reason: a declaration without a body is exactly what a function type is,
and ADR-102 D2 already decided how one reads — without sync it
may pause, without throws it cannot fail, and a body that does less fits a
type that allows more. A trait method is that signature with a name in front.
sync it may pause, without throws it cannot fail.trait Source {
fn load(&self) -> String throws // may pause, may fail
fn name(&self) -> String sync // never pauses, cannot fail
}
Nothing new is written: sync and throws stand where a declaration already
puts them. The ledger’s trait entry (ADR-106) records the words
as written, and sync::infer reads a call through a bound as one that may
pause unless the declaration says sync — so a body that calls s.load()
under [S: Source] pauses where load may, and is sync where every call in
it is.
A body that pauses under a declaration that says sync is NK1129, and the
message is the comparison: Disk::load pauses, and Source declares load
as sync. A body that fails under a declaration without throws is refused
the same way, as NK1140. Both are ADR-027’s NK2202 asked of
somebody else’s signature, which is what ADR-102 D2 does for a
lambda.
The other direction fits, as it does for a lambda: a body that never pauses
under a declaration that may, or one that cannot fail under throws, is
correct and says nothing.
Send on the future follows the executor.In the trait, a method that may pause is lowered as
fn load(&self) -> impl Future<Output = String>; the impl writes async
fn, which satisfies that declaration. The sugar async fn in the trait is
not written, because the async_fn_in_trait lint warns on a public trait
whose future carries no Send bound, and a warning about the generated file
is a defect in this project.
The bound is + Send where user_parallelism = yes and absent at no: at
yes a task crosses threads and the spawn needs a Send future; at no
nothing crosses, the counts are plain (ADR-037 D7) and a
Send demand would refuse them. This is a requirement of the executor the
program is built for, written by the emitter, and not a claim about a type:
the answer ADR-045 gives about whether a value may cross a
thread stays the same at both settings, and no switch reaches it.
Every generated Cargo.toml carries rust-version = "1.75", from one
constant in the emitter. Before the compiler hands a build to cargo it
compares rustc --version to that constant and refuses in its own words
where the toolchain is older — Cargo’s package requires rustc 1.75 or newer
is a message about a generated file, which Part III C.1
forbids.
The floor is a different fact from the channel. rust-toolchain.toml names
the channel and stays the only place that does (ADR-001 D1);
the floor is a version the emitted code needs, and it lives in the
emitter. It rises only by a record — a later feature of the language below
that some lowering needs — and never silently.
The compiler’s own floor is not claimed in Cargo, because it has not been
measured: 1.75 cannot build the compiler, whose lockfile already carries
edition-2024 crates and so needs Cargo 1.85 or newer, and nothing between
that and the current stable has been tried. An operator builds the compiler
with the stable that rust-toolchain.toml names, which is newer than both.
http::Handler can be declared
with a method that pauses, and Cleanup (Part I 6.4), whose whole point is
that tearing a resource down is I/O, is the first trait in std that reads
this way.NK1129 changes meaning, not shape: from a trait’s method cannot
pause to this one says it does not. A program refused today is refused
tomorrow only if its declaration says sync.sync = true where the word stands and nothing where it does not, which is
the rule every function entry already follows.Send by setting;
two comparisons at the impl; a constant, a line in the generated manifest
and one version check before cargo.dyn was lost, because there was none: a trait with an async
method is not object-safe in the language below, and this language has a
bound and nothing else (ADR-106 §4).sync written once on the trait for all its methods — a spelling, and not
one this record adds.All four decisions. A trait method carries the word it was written with, an
implementation is compared against it (NK1129 and NK1140), the declaration
is lowered -> impl Future<Output = …> with + Send at
user_parallelism = yes, and every generated Cargo.toml says
rust-version = "1.75" with the comparison made before Cargo is handed
anything.
What NK1129 became. It used to refuse every pausing implementation,
because sync was asserted for every trait method
(ADR-078 D4) — a plain fn was the only thing the emitter could
write in a trait. D3 takes that cause away, so what is left is a comparison:
Disk::load pauses and Source declares load as sync. The other
direction fits and says nothing.
Three things the record had not said, each found by a test:
async fn when its declaration may pause, whether
or not its own body does — the declaration is the wider claim, so a body that
never pauses still has to hand back a future. It is written as an edge in
sync::infer’s graph rather than as a correction afterwards, so the fixpoint
carries it the rest of the way: the main that calls t.go() is async for
the same reason t.go() is.impl Error for ConfigError names a trait the
compiler reads rather than one a .nika file wrote, and an edge to a name the
graph has no entry for is read as pauses, which would have made every error
type’s methods async.handler::Handler an app implements
is declared just as much as one written here (ADR-100 D1: a
consumer reads a dependency’s contracts), and a test across a package boundary
is what said so.What moved in the corpus. Two tests in crates/nikaia/tests/package_graph.rs
wrote a trait without sync and meant cannot pause; the one that holds the
refusal says the word now, and the one that holds the program is a program
either way. crates/nikaia/tests/traits.rs carries the rest — twenty tests,
including docs/language-review.md §1.3’s probe compiled and run.
What is not built: nothing of this record. Send on the future is the
emitter’s, written per setting, and is not a claim about a type — which stays
contracts::send’s and the same at both settings
(ADR-045, ADR-005 Group B).