Status: Superseded by ADR-290 — its D1-D4 are D16-D19 there
Date: September 22, 2026
Target Version: Nikaia 0.0.156
Narrows: ADR-195 D4 — the two std pieces are still wanted
and are no longer on the reading half’s critical path
Related: ADR-002 D4 (the pre-lowering this is), ADR-014
D1 (the first Nikaia module inside a Rust crate), ADR-195 D1 and D2
(why describe is written in Nikaia, and that it ships this way),
ADR-038 D6 (the HTTP/1.1 parser moving into Nikaia),
ADR-104 D4 (an absence of a claim is never a claim)
ADR-195 decided that nikaia describe is written in Nikaia. It did
not say how the result reaches the Rust program that calls it, and there are
three ways a piece written in one language reaches a program written in another:
cdylib, the caller declares extern "C",
and every type between them is reduced to what C can name;The first two are what a reader expects, and both are worse here than they look: a process needs a second binary to exist, to be found and to be installed, and the C ABI throws away every type either side has in exchange for a boundary neither needs.
The owner named the third, and the tree already runs it.
crates/nikaia-std/src/text.nika is lowered by the Stage 0 compiler to
text.rs, committed, and include!d into nikaia_std::text. A Rust caller
reaches nikaia_std::text::digit_value with a plain call and plain types, and
nothing on either side knows a boundary was crossed.
Not a process, not the C ABI, not a dynamic library. The .nika is the source;
ADR-002 D4’s 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.
What this buys is that there is no interface to design. A boundary whose
types are Rust’s own is a boundary nothing has to agree about — no wire format,
no serialisation, no unsafe, no second process to keep alive, and no version
skew between two halves that ship together because they are one artefact.
describe’s parser and the HTTP/1.1 parser.The owner asked for the principle rather than the instance, and the instance it was found on is not the only one.
describe’s reading half (ADR-195 D3) — a grammar over Rust
signatures, called by the nikaia binary, which is Rust.And the second half of the owner’s point is the one worth keeping. 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. One implementation, two ecosystems — which is the strongest argument this project has for writing anything in its own language at all.
What qualifies: a piece whose input is text or bytes and whose output is data, and which has a caller on each side. What does not: anything the compiler needs in order to compile itself, which is ADR-195 D2’s cycle and stays out.
nikaia-std as an ordinary dependency, and the graph stays acyclic.crates/nikaia has nikaia-std as a dev-dependency today; a lowered grammar
needs it at run time, so the edge becomes a real one.
There is no cycle, and the reason is written down already: nikaia-std’s own
module header says this crate has no build dependency on the compiler and must
not grow one (ADR-002 D4). So nikaia → nikaia-std is a plain
edge, and what must not happen — nikaia-std linking the compiler to lower its
own sources, which once built the compiler a second time inside every project’s
target/ — is already refused by the release step’s shape.
std pieces are no longer on the reading half’s critical path, and the split they leave behind is a staging and not a boundary.ADR-195 D4 gave std a directory walk and a subprocess because a
Nikaia describe has to read a crate. On this route it does not have to, yet:
the Rust half keeps the I/O and hands the Nikaia half the text, which is
what a parser wants anyway. So the grammar ships first and alone, and both std
pieces keep D4’s own argument and none of its urgency.
This is the risk this decision carries, and it is named rather than left to be
discovered. ADR-195 D1’s reason is that a language whose
toolchain is written in another has no user forced to live with its gaps. A
route that makes a partial move cheap makes a permanent partial move cheap
too, and a describe whose parser is Nikaia and whose everything else is Rust
would satisfy the letter of D1 and none of it.
So the split is a staging with a written end: the walk and the subprocess
land in std, and the rest of the command crosses after them
(#124). The sign that the staging has
stalled is concrete — fs has a directory walk and the Rust half is still doing
the walking — and it is a thing to look for rather than a gate, because a gate
on it would be a gate on work nobody has started.
examples/rust-signatures.nika, compiled and run at both settings by
crates/nikaia/tests/examples.rs.describe is a rewrite and not a rearchitecture. The scanner comes out,
the grammar goes in, and translate — the half that turns a Rust type into a
ledger type — is untouched, because the grammar hands it the same text the
scanner did..rs is committed, so a change to the .nika that is not
re-lowered is a silent divergence. crates/nikaia/tests/sysroot.rs already
fails on exactly that for std, and anything joining this route joins that
test.describe’s .nika lives. The sysroot’s std is where the release
step looks today; a module that belongs to the compiler rather than to std
may want its own place and its own step. That is a question for the commit
that wires it in.describe able to go first is that nothing bootstraps through it.std::text, since ADR-014.examples/rust-signatures.nika — which reports
no item that is not one, where the scanner reports four on the same text.Not built: the edge in crates/nikaia/Cargo.toml, the .nika in the compiler’s
own tree, and the release step that lowers it.
#124 carries the order.