ADR-290: A Rust Crate Is Described Before It Is Called, the Description Is Reviewed Like Code, and nikaia describe Drafts It as a Nikaia Program Reached From Rust as an Ordinary Module

Status: Accepted Date: October 3, 2026 History: the records this one replaces are archived in docs/history/adr; read them only for how the decision was reached Answers: what a Nikaia program knows about a Rust crate it calls (#100); the former open-decisions.md’s does a described foreign function say whether it puts its argument on a thread? (A with D, the owner’s choice) and is nikaia describe written in Nikaia? (option A, for the owner’s reason) (#195, #124); how a piece written in Nikaia reaches the Rust program that calls it; and the boundary translation for a hand-edited hash, ADR-100 D6 (#194) Related: ADR-020 D5 (entries a compiler cannot infer are written and reviewed like code — this is that rule for every crate); ADR-288 D11 (an entry is written and reviewed, not copied from Rust); ADR-100 D3 and D6 (a ledger believed while its hashes hold; a backend mismatch at a package boundary named); ADR-251 D1 (what a contract was derived from is a file beside it); ADR-123 (crosses, three values, filled by the describer from a type’s fields); ADR-303 D6 and D7 (the HTTP/1.1 parser moving into Nikaia; NK2502’s wording); ADR-312 D8, D9 (what a foreign call may be handed); ADR-001 D1 (stable toolchain only); ADR-002 D4 (the committed pre-lowering); ADR-005 D7 (the crossing message this closes the second half of); ADR-010 D1 (absence is never a claim); ADR-014 D1 (the first Nikaia module inside a Rust crate); ADR-289 D2 and D6 (an operating-system resource belongs in std), D15 (HTTP/1.1’s parser); ADR-296 D12 (measure before choosing a shape, and where it does not apply); ADR-243 (std::process, the subprocess D14 asks for); ADR-294 (the road on which the compiler itself moves to Nikaia); Part III 15.2 (the type mapping) and C.1, C.4

1. Question

What does a Nikaia program know about a Rust crate it calls, who writes it down, and what is the tool that writes the draft?

A call into a Rust crate is a call no ledger describes: the compiler reads no Rust, hands the program to rustc, and whatever rustc refuses arrives in Rust’s words about a file nobody wrote. That is the one place Part III C.1’s promise — an untranslated backend error is a compiler bug — could not be kept, and the place where every analysis (sync, throws, keeps, touches, the crossing verdict) fails closed at once: safe, and blind.

The answer has five parts:

  1. a call into an undescribed crate, and what a description holds (D1-D5);
  2. whether a description says that a call threads, and how the describer helps (D6-D10);
  3. what language the describer is written in (D11-D15);
  4. how that Nikaia code reaches the Rust compiler that calls it (D16-D19);
  5. what the emitter and the diagnostics do with a described call (D20-D21).

2. How others do it

The comparison is of tools and routes rather than of other languages.

Tool / route Its answer What it costs Source
rustdoc-JSON the crate’s public surface as data, from the compiler unstable: reading it gives up the stable-only toolchain D4
syn a real Rust parser a Rust dependency, and still blind to what a macro generates D9, D13
-Zunpretty=expanded macro expansion nightly — the same ADR-001 D1 wall D9
cargo metadata the resolved version’s own sources a subprocess D9
a process (one language calls the other’s binary) a wire format between them a second binary to build, find and install D16
the C ABI (cdylib, extern "C") a boundary every language can call every type reduced to what C names, and unsafe D16
the same compilation (lower ahead of time, link as a module) plain calls with plain types a committed .rs that must not drift from its .nika D16, ADR-014

What does not transfer: none of these says what a call may do — block, fail, put its argument on a thread. That is a claim a person makes, which is why the description is reviewed (ADR-020 D5).

3. Options, weighed

Option Rejected because Was
An undescribed foreign call compiles, and the analyses fail closed a silent third answer; every analysis is blind exactly at the boundary, and C.1 cannot be kept there D1
Describe the crate’s whole surface the review would be proportional to the crate, not to what the program uses D2
Wait for rustdoc-JSON it is unstable; the stable-only toolchain is not given up for it D4
On a hash mismatch, derive the description again (a ledger’s rule) a description holds a reviewer’s judgement — a crosses read off a field, a lying signature corrected — which a derivation would erase D5
threads with two values a hopeful false is a false silence, ADR-010 D1’s vulnerability generator D6
The describer fills threads, as it fills crosses its licence for crosses is soundness; a Send + 'static bound only says may, and as a claim it refuses correct programs (C.4) D8
syn as the parser the command is a Nikaia program, so its parser is a grammar; the macro limit survives either D13 (narrowing D9)
Write the grammar first as a measurement that settles it the choice rests on a project principle, which no number moves D15
The describer as a separate process, or behind the C ABI a second binary, or every type thrown away, for a boundary neither side needs D16
std’s walk and subprocess before the grammar ships the Rust half can do the I/O and hand the grammar text D19
Merge the descriptions into std’s ledger for the emitter a described sync = false means may block; a std entry’s means await it D20
Leave the backend’s words at a boundary mismatch C.1: the mismatch is a stale description or ledger, and the build can name which D21

The number that decided something: on one crate the hand-written scanner reported four items that do not exist, and the grammar reports none on the same text (D13).

4. Decision

D1 — A call into a crate no description covers is refused, and the message names the command.

There is no silent third answer for a foreign call. A call — or a written type — reaching into a crate the manifest declares with type = "rust" and no contracts/<crate>.contracts describes is NK2504: regex is not described; run nikaia describe regex. Three things about its shape are the rule: it fires only where the manifest declared the crate (a name nobody declared keeps whatever message it had, C.4); once per crate, with the caret on the first call; and the crate is named as a program writes it (hyper-shim in the manifest is hyper_shim).

What every analysis then reads at the boundary is an entry, never an absence: the signature types the call and what it hands back, throws makes it a place that can fail, sync makes it one a sync function may not make. A described crate is a package by the spelling rule, not one of std’s modules, so nothing about it is imported.

D2 — nikaia describe <crate> writes a draft for the functions the program uses.

The command finds the crate’s calls in the program’s sources, reads the crate’s pub signatures, and writes an entry for each called function and for the types those signatures name — not the whole surface, because every entry is a claim a person reviews (ADR-288 D11), and a review is proportional to use. A later call to an undescribed function meets D1 and the same command.

D3 — The draft is translated by Part III 15.2’s table, and what a signature does not say fails closed.

Rust the entry
&T a view; T by value is kept
&mut T changed in place (mut)
Result<T, E> throws; the error type where 15.2 can name it
async fn may pause; a plain fn is sync
Option<T> T?
Send on a type may cross a thread
touches, locks absent — touches everything (ADR-292) and the column’s third answer — because the signature cannot say

crosses itself is ADR-123’s column: the describer reads a pub struct’s fields — false for an Rc or a raw pointer, true where every field is plainly sendable, nothing where it cannot tell.

D4 — rustdoc-JSON is read where the toolchain offers it, and the crate’s sources where it does not.

Both produce the same draft, and the file records which. A stable-only toolchain (ADR-001 D1) is not given up for the JSON, so the source reader is the one that runs. Whatever neither can read — a macro-generated function, a signature over a trait object — is written ? and left to D5: the absence of a claim, never a guess. A version dependency, whose sources are in Cargo’s registry cache under a layout ADR-002 D1 leaves to Cargo, is refused with that reason and the hand-written file named instead.

D5 — The description is committed and reviewed like code, believed while its sources hash as recorded, and reviewed again — never derived again — when they do not.

contracts/<crate>.contracts, beside the program’s own ledger, with contracts/<crate>.derived beside it holding the crate’s name, version and source hashes (ADR-251 D1). A hand edit is expected: a Rust signature that lies — unsafe impl Send on a type that is not — is caught by the reviewer or by nobody, and the file is where the correction lives.

Where the files a description was derived from hash differently, the build refuses with NK2505, once per crate, with the command in the message. That is ADR-100 D3’s second row turned into a refusal, because what a description says is a person’s judgement and cannot be derived again. It fires only where a recorded hash disagrees: a crate declared by version and a description that recorded no sources answer nothing moved, as ADR-281 D34 keeps an absence from refusing.

D6 — threads is a column of a description, hand-written, with three values.

true, false, and absent, like crosses (ADR-123 D1). The absence is nobody said, never it does not: 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.

D7 — NK2502 asks a described call too, and fires on the claim, never on its absence.

A description that does not say was not asked, and the program keeps the answer it has otherwise — rustc’s Send bound, reported on the .nika line by ADR-005 D7’s translation. So the crossing message moves into this compiler’s words only where somebody wrote the word.

D8 — The describer proposes threads and never claims it; it may propose true, never false.

nikaia describe writes what it saw as a reviewable comment above the entry, naming the evidence — the parameter f is bound Send + 'static; does this put it on a thread? — and the person writes threads, or does not. Nothing a signature shows entails does not thread (a function may spawn something it built itself), so where it cannot see it stays silent. A note, not the column, because a bound says may, and only soundness licenses the describer to fill a column (ADR-123 D2).

D9 — What the describer reads: the signature scan for safe Rust, a call graph and a use table for the unchecked row, cargo metadata for the files, a grammar for the parse, and no cache.

A sink reached through a call says this function threads something, never this function threads your argument; connecting the two is dataflow, which this does not do, and that is the second reason the output is a note.

D10 — 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 most useful sentence the tool can write about a foreign crate — this crate makes a promise the toolchain cannot check — and the note says what no tool can do: see whether the promise is true. That is why the column stays a person’s.

D11 — nikaia describe is written in Nikaia, because this language has to be written in.

A language whose own toolchain is written in another one has no user forced to live with its gaps, and every gap this tree has found was found by running something. describe is the first tool that could be written in Nikaia without anything else moving, so it goes first. This is a decision about the project, not the shape, which is why no number stands behind it.

D12 — Nothing bootstraps through it, and it ships by the committed pre-lowering.

The compiler compiles without describe: NK2504 names the command and a person runs it, so there is no cycle. It ships by ADR-002 D4’s route — the .nika kept beside the .rs, lowered at release by nikaia lower-std — so a binary install needs no compiler to build it.

D13 — The reading half is a grammar.

The parser is a Nikaia grammar over the subset of Rust describe needs (pub fn, pub struct, impl headers and the type grammar under them), and it replaces the syn step of D9. The grammar removes the class of the scanner’s errors, not the instance; the one limit that survives — an item a macro generates — survives syn too.

D14 — std gains a directory walk and a subprocess, and neither is on the reading half’s critical path.

Both are operating-system resources of the kind ADR-289 D6 put the socket in std for, so ADR-289 D2’s subtraction covers them. On D16’s route the Rust half may keep the I/O and hand the grammar the text, so the grammar waits on neither. The subprocess is ADR-243’s std::process; the walk is fs::walk.

D15 — A measurement decides between options; where the choice rests on a project principle, a number is a diagnosis.

ADR-296 D12’s measure before choosing a shape applies where two shapes are genuinely on the table. Here the choice is made on a reason about the project, so if the Nikaia describe is slower than the scanner, that is a finding about this compiler and work owed — not a reason to go back. A measurement that cannot change the decision is still worth taking; calling it the reason is what this rule refuses.

D16 — A Nikaia piece reaches a Rust program by the committed pre-lowering, and the call across is an ordinary Rust call.

Not a process, not the C ABI, not a dynamic library. The .nika is the source, the release step writes the .rs beside it, the .rs joins whatever crate wants it as a module, and every call is a plain Rust call with plain Rust types — no wire format, no serialisation, no unsafe, no second binary, and no interface to design, because a boundary whose types are Rust’s own is one nothing has to agree about.

D17 — It holds wherever a piece has callers on both sides.

Two such pieces: describe’s reading half, and HTTP/1.1’s text half (ADR-303 D6, ADR-289 D15). Because the lowered form is an ordinary Cargo module, a thing written in Nikaia is available to Rust programs that have never heard of Nikaia. What qualifies: a piece whose input is text or bytes and whose output is data, with a caller on each side. What does not: anything the compiler needs in order to compile itself, which stays out (D12).

D18 — The compiler takes nikaia-std as an ordinary dependency, and the graph stays acyclic.

A lowered grammar is needed at run time, so crates/nikaia’s dev-dependency on nikaia-std becomes a real one. There is no cycle: nikaia-std has no build dependency on the compiler and must not grow one (ADR-002 D4).

D19 — The split D14 leaves is a staging with a written end, and its stalled state is named.

A route that makes a partial move cheap makes a permanent one cheap too, and a describe whose parser is Nikaia and whose everything else is Rust would meet the letter of D11 and none of it. So the walk and the subprocess land in std, and the rest of the command crosses after them. The sign that the staging has stalled: fs has a directory walk and the Rust half is still doing the walking. It is a thing to look for, not a gate.

D20 — The emitter reads the descriptions, for throws.

The described crates’ ledger reaches the emitter beside the program’s, and a call the description says can fail gets its ?. It is read for that question only and not merged into std’s ledger: a described sync = false means may block, and a std entry’s means await it.

D21 — A boundary the backend disagrees with is named as a stale description or ledger.

A program never meets a Result or a future; both are this compiler’s lowering of throws and of a pause. So where the backend says is not a future, that ? needs a Try, or names a Result, the build names the boundary — the one the line calls, else a described crate whose sources moved, else the only one the file calls:

The backend’s words are kept as a note. Where no boundary answers, the message is left as the backend said it, and it is this compiler’s defect. D21 covers what D5 cannot see: a description edited to say something the crate does not do.

D22 — A crate may ship its reviewed description, and describe adopts it.

A crate may keep its reviewed description as nikaia.contracts beside its Cargo.toml, with its [sources] in nikaia.derived. For a path dependency, nikaia describe adopts that file instead of drafting: it checks the crate version the file names and the hash of every source, and copies both files into the project’s contracts/. Without a shipped file it drafts, as before.

The description is a judgement about the crate, so the crate’s authors are the ones placed to make it once: they know which unsafe use is sound, which function is cfg-gated, what a signature hides. Every project that calls the crate would otherwise repeat a review of code it did not write, and each repeat is another chance to get it wrong. The checks are D5’s rule applied at the copy: a shipped file that names another version or hashes differently describes code that is no longer there, and is refused rather than believed.

The project keeps its own copy, and the build believes only that copy. The build stays a function of the project’s tree, a description changes in the project’s diff where a reviewer sees it, and the lockfile rule is unchanged.

Rejected:

5. Consequences

6. What this does not decide

Not open here: whether the compiler itself is ever written in Nikaia is ADR-294’s.

This site is open source. Improve this page.