Status: Superseded by ADR-290 — its D1-D5 are D6-D10 there
Date: September 22, 2026
Target Version: Nikaia 0.0.149
Answers: open-decisions.md’s does a described
foreign function say whether it puts its argument on a thread? — A with D,
which that page recommended and the owner took
Related: ADR-123 (crosses, the column this one is modelled
on, and D2’s describer half), ADR-038 D7 (NK2502’s own wording),
ADR-104 D4 and D5 (what the describer reads, and that its draft is
reviewed like code), ADR-010 D1 (the polarity), ADR-005
D7 (the text this closes the second half of), ADR-001 D1 (the wall
two of the limits sit behind)
NK2502 asks its question of a call nothing describes, which is
ADR-038 D7’s own wording. A described foreign call is not asked,
because no column says whether it threads.
examples/foreign-runtime/crossing is that shape exactly: a handle whose
description says crosses = false, handed to hyper_shim::across_a_thread,
which the description names. The build fails against the right .nika line —
C.1’s rule kept — in rustc’s words:
`Rc<String>` cannot be sent between threads safely
ADR-005 D7 records the text as the open half, and this is it.
The question was filed twice at the wrong altitude, and both times the
correction came from the owner. It first offered only a column or nothing,
with a third option ruled out on a reason that was false — nikaia describe
reads rustdoc JSON — when ADR-104 D4 says outright that
rustdoc-JSON is the future better half and the source parser is what runs.
And it asked the describer only to demand an answer, never to help find one.
threads is a column of a description, hand-written, with three values.Like crosses (ADR-123 D1): true, false, and absent. The
absence is nobody said, never it does not.
Three and not two, for the reason crosses has three: a threads = false
written by a hopeful hand is a false silence, and silence read as no is
the polarity ADR-010 D1 calls a vulnerability generator.
NK2502 asks a described call too, and fires on the claim.Never on its absence. A description that does not say is a description that was
not asked, and the program keeps whatever answer it has today — which is
rustc’s Send bound on the right .nika line through
ADR-005 D7’s translation.
So this record closes that record’s second half only where somebody wrote the word, and leaves the position and the translation exactly where they are everywhere else.
nikaia describe writes what it saw as a reviewable note beside the absent
column, naming the evidence:
# `spawn_worker`: the parameter `f` is bound `Send + 'static`.
# Does this put it on a thread? -> threads = true | false
and the person writes threads, or does not.
It may propose true. It must never propose false. Nothing a signature
can show entails does not thread — a function may spawn something it built
itself — and the one indicator that would ground a false, a leaf function
that calls nothing, needs the body and every callee, which is a claim this
tool cannot make. Where it cannot see, it stays silent, which is the same
absence is nobody said D1 rests on.
Why a note and not the column. ADR-123 D2 already has the
describer fill crosses — and its licence there is soundness: a field
holding an Rc entails not sendable. The indicators here do not entail. A
Send + 'static bound says the callee may send it, which is usually spawn
and is sometimes an API keeping a door open; written as a claim it can refuse a
correct program (C.4), and written as a note it is
the most actionable sentence the tool could produce.
The evidence is examples/foreign-runtime/shim, which has both shapes in it on
purpose:
across_a_thread<T: Describe + Send + 'static> → on_one_worker(move || value.describe())
across_a_thread_unchecked<T: Describe + 'static> → on_one_worker(move || smuggled.describe())
// the bound is gone: `unsafe impl Send for Smuggled<T>`
on_one_worker<F: FnOnce() -> String + Send + 'static> → tokio::spawn(…)
syn) replaces the scraper. Of its three named limits: the
mod-path one closes, the signature-translation one narrows — which
is what seeing a bound at all needs — and the macro one stays, because
expanding a macro needs -Zunpretty=expanded, which is nightly. That is the
same ADR-001 D1 wall that keeps rustdoc-JSON out: one decision,
two consequences, and no choice of parser moves either.across_a_thread_unchecked has no bound, because an unsafe impl Send took
it away, and only following the calls reaches tokio::spawn.use table per file, or use tokio::spawn; spawn(x) is a different
string from tokio::spawn(x) and the matcher misses it.nikaia describe is a command a person runs, and the committed
.contracts file is already the artifact that keeps the answer
(ADR-100). A staging decision enters this compiler only with a
measured crossover.A sink reached through a call says this function threads something, never this function threads your argument. Connecting those is dataflow through a wrapper and a closure capture, and it is not built here — which is the second reason the output is a note naming the path rather than a claim.
unsafe impl Send and unsafe impl Sync.One syntactic pattern, and sound: the item is in the text or it is not. It is the single most useful sentence this tool can write about a foreign crate — this crate makes a promise the toolchain cannot check — and the hole the shim opens on purpose is invisible to every other step in D4.
And what none of it can do, which the note has to say. A tool can see that the promise was made; it cannot see whether it is true. That is the line between a rule the toolchain enforces and a rule it inherits, and it is why the column stays a person’s.
NK2502 reaches a described call, so examples/foreign-runtime/crossing
is refused in this compiler’s words rather than the backend’s — the message
ADR-005 D7 has been carrying as its open half.Send + 'static bound
on an API that never spawns, read by someone who then stops reading the notes.
That is why a note names the parameter and the bound and never says this
might thread, and why none is written where the evidence is weaker than a
bound.threads for a Nikaia function. This is a column of a description —
what a foreign crate does. A spawn in a .nika body is NK2101’s and needs
no column.threads that depends on a type parameter. crosses took the same line
(ADR-123 §4): one value per entry is enough for everything the
corpus has.Nothing. The order of work, and each step is usable without the next:
NK2502 reading it (D1,
D2);syn and cargo metadata under it (D4), which is what makes step 2 reliable
rather than lucky;unsafe impl Send/Sync flagged (D5) — cheap, sound, and independent of 3;use table (D4), for the unchecked row.[#195](https://github.com/Nikaia-Language/Nikaia/issues/195) carries it.