ADR-196: A Nikaia Piece Joins a Rust Program as an Ordinary Cargo Module

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)

1. Context

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:

  1. a process — the caller runs a binary and reads what it printed;
  2. the C ABI — the piece is a cdylib, the caller declares extern "C", and every type between them is reduced to what C can name;
  3. the same compilation — the piece is lowered to Rust ahead of time and the caller links it like any other module.

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.

2. Decisions

D1 — The route is the committed pre-lowering, and the call across it is an ordinary Rust call.

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.

D2 — It holds wherever a piece has callers on both sides, and the two named now are 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.

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.

D3 — The compiler takes 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.

D4 — The two 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.

3. Consequences

4. What this does not decide

5. What is built

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.

This site is open source. Improve this page.