nikaia describe Drafts It as a Nikaia Program Reached From Rust as an Ordinary ModuleStatus: 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
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:
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).
| 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).
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.
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.
| 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.
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.
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.
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.
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.
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).
use table for the unchecked row, cargo metadata for the files, a grammar for the parse, and no cache.Send bound, so the answer surfaces in
the signature.unsafe impl Send took
the bound away (across_a_thread_unchecked reaches tokio::spawn only
through on_one_worker).use table per file, so use tokio::spawn; spawn(x) is the same sink
as tokio::spawn(x).cargo metadata names the files, replacing a guess with the resolved
version’s own sources.mod-path one closes, the signature one narrows, and the macro one stays —
expanding a macro needs nightly, the ADR-001 D1 wall that also
keeps rustdoc-JSON out.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.
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.
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.
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.
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.
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.
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.
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.
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).
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).
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.
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.
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:
fremd does not match the crate —
run nikaia describe fremd again and review the diff;util does not match its sources — build it again.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.
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:
keeps, sync and throws stop failing closed where a
signature answers plainly, and C.1 becomes keepable at the one place it was
not.NK2502 reaches a described call where threads says true, so the
crossing experiments under examples/foreign-runtime/ are refused by this
compiler, not rustc — including one made through unsafe impl Send, which
defeats a bound and cannot touch a line a person wrote in a description.crates/nikaia-std/src/tools/, which std
does not publish (Part III NK1186)..rs
that must not drift from its .nika — crates/nikaia/tests/sysroot.rs
fails on that, and anything on the route joins that test.Shared still do not cross into foreign code (ADR-312 D8,
D9); an entry says what the callee takes, not that the
rule bends.impl’s pub fn is at a foreign boundary;
the grammar reads methods and the draft does not write them.threads for a Nikaia function — a spawn in a .nika body is
NK2101’s — and a threads that depends on a type parameter, which
crosses also does without (ADR-123 §4).describe needs; what it
cannot translate is ? (D4).Not open here: whether the compiler itself is ever written in Nikaia is ADR-294’s.