ADR-193: A Description Says Whether It Threads, And The Describer Proposes The Answer

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)

1. Context

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.

2. Decisions

D1 — 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.

D2 — 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.

D3 — The describer proposes and never claims, and the asymmetry is the design.

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.

D4 — What the describer reads, and what each step is worth.

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(…)

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.

D5 — The describer flags 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.

3. Consequences

4. What this does not decide

5. What is built

Nothing. The order of work, and each step is usable without the next:

  1. the column — its value, its parse, its render, and NK2502 reading it (D1, D2);
  2. the signature scan and the note it writes, on the scraper as it stands (D3), which already carries the safe half;
  3. syn and cargo metadata under it (D4), which is what makes step 2 reliable rather than lucky;
  4. unsafe impl Send/Sync flagged (D5) — cheap, sound, and independent of 3;
  5. the intra-crate call graph and the use table (D4), for the unchecked row.

[#195](https://github.com/Nikaia-Language/Nikaia/issues/195) carries it.

This site is open source. Improve this page.