Status: Superseded by ADR-294 — its D1-D5 are D1-D5 there Date: September 28, 2026 Target Version: Nikaia 0.0.252 Answers: ADR-001 D4’s Stage 1 — self-hosting: Nikaia compiling Nikaia, which said where the road ends and not how it is walked Related: ADR-196 (a Nikaia piece joins a Rust program as an ordinary Cargo module), ADR-002 D4 (the committed pre-lowering), ADR-195 D1 (this language has to be written in)
The owner’s instruction is to take self-hosting as far as it goes. Five
pieces have moved by ADR-196’s route - did you mean, a dsl body’s
parameters, describe’s reader, the comptime map’s table and now the HTML
template scan - and each move found defects the test suite had not: four at
0.0.250, three at 0.0.248, four in this package. That is the argument for the
road in one line: a compiler written in its own language is tested by
compiling itself, and every module moved is a program nobody else would have
written.
Where it stands, measured (scripts/self_hosting.py): 43,842 lines of Rust
in crates/nikaia/src that say something, and 876 of Nikaia - 2.0 %.
The module graph decides the order. Read off the crate:: paths each file
uses:
| Depends on | Modules |
|---|---|
| nothing of the compiler | fixed, emit::template, manifest |
| the syntax tree only | fold, interpreter, contracts::trust, contracts::ty, contracts::send |
| the tree and the checker’s types | dsl, traits, types, contracts::{throws, locks, touch, keeps, sync, tether, keep, sharing, order}, views, text_tiers, diagnostics |
| nearly everything | check (21,408 lines), emit (12,938), parser (3,897), project (3,196) |
The syntax tree is the root. ast/mod.rs is 1,191 lines, and 60,000 lines
of Rust pattern-match on it. Moving it is not a module’s move: it is the
decision of what a Span is (a usize range today, where Nikaia has no
usize), whether Spanned[T] is a generic struct Nikaia writes, and how the
Rust that remains reads a tree the other language declares.
The .nika is written beside std’s, lowered by nikaia lower-std and
committed; the compiler’s module keeps its name and its callers, and shrinks to
the conversions a Nikaia module cannot write for this crate - an error into
anyhow, an i64 into a usize. The whole test suite is the proof: the
tests that held the Rust module hold the Nikaia one, unchanged or changed only
where a type’s spelling moved.
A module moves once everything it reads is Nikaia or crosses the boundary as
plain data (text, numbers, lists). The leaves go first; manifest is next.
Its TOML reader stays the toml crate, behind the adapter (§3, corrected at
0.0.253); what moves is every decision the manifest makes. The syntax tree
moves by a record of its own that answers the three questions in §1, when the
leaves are gone and the modules that read only the tree are the next ring.
That record is ADR-252 (0.0.261): the tree moves whole, and a
Span is two u32.
Never worked around in the .nika - the move exists to find it. A workaround
that is the language’s own answer (.clone() where ADR-216 D4 says a view
stays a view) is not a workaround; a line bent to avoid a lowering the emitter
gets wrong is, and is not written.
scripts/self_hosting.py counts the lines that say something in each language
scripts/badges.py draws it into the README
beside the version. A package that moves a module says the new share in its
changelog entry.Stage 1 is reached when main’s pipeline - read, check, lower - runs Nikaia
from end to end, with Rust left only as adapters to crates (winnow, clap,
the file system). Stage 2 is reached when the lowered compiler lowers its
own sources to byte-identical Rust, and a test compares the two.
emit::template is the fifth module in Nikaia (§5).toml) moves without it: the crate stays
behind the adapter, as D5 lets winnow and clap stay, and what moves is
the module’s own logic (corrected at 0.0.253, on the owner’s question).A second compiler written from scratch in Nikaia, the way many languages bootstrapped. It would be tested by nothing until it was finished, and finished means 44,000 lines; the module-at-a-time road is tested at every step by the suite that already exists.
The largest module first (check), for the share. It reads everything,
so it cannot move until everything it reads can; starting there is starting at
the end.
Built at 0.0.252.
scripts/self_hosting.py, and the self-hosted badge.emit::template in Nikaia: crates/nikaia-std/src/tools/template.nika,
the HTML scan that decides where a hole sits (ADR-017), with Position and
Segment as Nikaia enums the emitter matches on directly. The Rust module is
eighteen lines of adapter. The scan walks characters; the Rust it
replaced kept its last nine bytes and aborted on a character outside ASCII,
which tests/templates.rs now holds.self handed to a parameter that only borrows was
NK1131, as if it were given away;match over a lent value had no type, so a method
called on it resolved to nothing and the function could not be sync -
which made an Error’s message a future the generated Display could not
print;String moved whole into a struct literal or an enum variant was counted
as a view of itself, and the let went into a keep (ADR-209) and became a
reference;.clone() of a trimmed text was not recognised as text of its own, for the
same keep.std.contracts gains char::is_whitespace and char::to_ascii_lowercase.Built at 0.0.258: the ledger’s reader, its shape half.
crates/nikaia-std/src/tools/ledger.nika: which lines of a
nikaia.contracts say something, which open a table and what it is called,
where a key ends, what a quoted string holds and where a list’s entry ends.
Ledger::parse keeps what each table and key means - Sync, Ty,
Signature and the rest are the checker’s types, and move with it. The
share is 2.3 %.pub enum with a
variant of two payloads ("Pair(i64, i64)") or a pub field typed
collections::HashMap[String, i64] wrote a ledger its consumer could not
read. The Nikaia reader splits outside quotes, which is what fixed it;Ledger::candidates counted a free function as a method: text.lines()
could be io::lines(), which takes no self, so every text.lines() over
a lent parameter kept it. Only an entry whose signature takes self is a
candidate now;mut was kept for being changed (entries.push(…)),
as ref mut self was before 0.0.257. It is a &mut by the author’s word
(ADR-094 D3) and is kept no more.mut parameter handed away whole reaches rustc
(#161) - NK1131’s refusal, not yet asked for it.Ledger::render never wrote a comma inside a list entry for std, so
the split held for every test - until a program’s enum did.Built at 0.0.260: what nikaia.toml may say.
crates/nikaia-std/src/tools/manifest.nika: which [build] keys exist,
which have moved (with the note that says where) and which are withdrawn
(in their own words), which machines a [build.<target>] table may name and
what it may carry, which of the three shapes a dependency is, and the crate
name a program writes for a manifest key. Manifest::parse reads the file
with the toml crate and hands each question plain text; the toml::Values
Cargo is given stay in Rust (§3). The share is 2.5 %.type that was not a word
(type = 1) was read as no type at all, and the entry passed as a Nikaia
package with its marker silently ignored. It names no ecosystem, and is
refused like type = "c".ref Array[String]
(0.0.252), which is the &[String] clippy’s ptr_arg asks for; ref Vec
lowers to &Vec.