Status: Superseded by ADR-286 — its D3-D7 are D2, D3, D7, D8, D9 there; its D1-D2 are not carried, displaced by ADR-047 D1 (D1 there) Related: ADR-011 D2 (the lowering is name for name), ADR-020 (the ledger, which is keyed by name), ADR-024 (the checker that resolves through those names)
Part I 9.1 is one sentence — every file in Nikaia is implicitly a module — and Part I 9.2 and
9.3 spend a page on what pub means across one. Part III 13.1 lays out src/main.nika beside a
manifest, a lockfile and a ledger. None of it existed: use parsed, lowered to a comment, and a
program was one file.
The reason it stayed that way is ADR-011 D2. The lowering is name for name, and nothing in this compiler resolves a module — a deliberate limit that paid for itself in every other decision. Multi-file compilation looks like the one feature that has to break it.
It does not. What follows is the argument that it is the opposite: multi-file compilation is name resolution, Nikaia already does name resolution in exactly one place, and that place has been shaped for this since ADR-020.
std.contracts has looked like this since the day it existed:
[fn."fs::map"]
[fn."io::lines"]
[fn."text::digit_value"]
Module-qualified keys, in a file, read by name. And everything built on the ledger since then
resolves a call by asking it for a::b: the sync check (ADR-020), the type checker
(ADR-024), the provenance analysis (ADR-010), Kap 5.1’s options, ADR-025’s fallible
loops, ADR-027’s inferred sync, ADR-028’s method resolution.
So a program’s own ledger takes the same shape, and none of those learns a new trick. The file they ask simply has more in it.
use utils wants utils.nika in the entry’s directory. Part I 9.1 says a file is a module; this
says which file.
One segment. use std::fs names the library and never a file — that is the one path a use
may have. Anything else with a path is refused with a sentence, because a nested module is a
nested mod with a resolution rule of its own, and inventing one to avoid an error message is how
a language acquires two.
mod at the crate root, and the lowering stays name for name.pub use nikaia_std::prelude::*;
pub mod stock { use super::*; pub fn total(…) -> i64 { … } }
pub mod page { use super::*; pub fn render(…) -> String { … } }
fn main() { … } // the entry module's items, because `main` has to be here
stock::total(…) in Nikaia is stock::total(…) in Rust. Nothing resolved it — ADR-011 D2 is
untouched. What changed is that the compiler now knows which files take part, which is a question
about the file system and not about names.
use super::*; is one line doing two jobs: it brings in the preamble the crate root wrote, and it
brings in the sibling modules, because a mod b at the root is a name at the root.
pub becomes pub, so the language below enforces Part I 9.2.A private item is private to its mod, which is its file. This is ADR-017 D2’s move
again: put the rule where the language below can act on it rather than re-implementing it.
It caught a bug that one file could not show. The emitter wrote pub on every struct
field, whatever the source said. In a single-file program nothing can tell; across a boundary it
silently grants access Part I 9.2 denies. pub on a field also did not parse, though 9.2
specifies it — so the one thing that would have made the bug visible was missing too. Both are
fixed here, and the golden fixture that had frozen the old behaviour is regenerated.
NK1110: privacy in Nikaia’s words, before rustc is asked.D3 is enforcement; this is the message. Part III C.1 calls an untranslated backend error a bug in this compiler, and reaching a private item across files is now an ordinary thing to do wrong.
error[NK1110]: `secret` is private to `utils.nika`
4 | let n = utils::secret()
^
= an item is private to the file that declares it unless it says `pub` (Part I, 9.2)
help: write `pub fn secret` in `utils.nika`, or reach it through something that is public
How it knows a call crosses a boundary, without knowing where it is. Only a call written
module::item can be from another file — a call inside utils.nika writes secret(),
unqualified. The checker is told which names are modules, which is what separates utils::secret
from Stats::new; that is the whole of the extra knowledge it needs.
Part III 13.5 puts nikaia.contracts at the project root, and D5 is why that is not merely tidy:
sync is a fixpoint over the call graph (ADR-027 D1), and a call graph that stopped at a file
boundary would give a different answer depending on which file was looked at first. 13.5’s
determinism is a promise about the source tree.
Modules are recorded in name order rather than in the order their imports were discovered, for the same reason.
Part III 13.1 already asks for “the SHA256 of each .nika source that took part”. It cannot be
computed without knowing what took part, and that means parsing the entry. So ADR-021 D13’s on a
hit nothing is parsed becomes nothing is parsed twice: the parse is what tells the compiler
which files the program is, and a hit still saves the lowering, the checks and the inference —
the larger part, and the parse itself became 20 % cheaper with the whitespace hoist.
A single file is unchanged in every respect, including the key’s bytes: the joined source of one file is that file.
A rustc message about the third module is traded back for a place in the third module. Each
unit is emitted with offsets of its own and the driver stamps them as it joins the text; without
that step a message about one file would point into another.
examples/inventory/ is the first program here made of more than one file — the same program
examples/report/src/main.nika is, in three. Both are kept on purpose: a reader can see what a boundary
buys and what it costs on a program small enough to hold in the head. What it buys is that
stock.nika keeps weight to itself and that neither file has to be read to use the other.
The example harness reads a program rather than a file, so an example may be one file or several and the tests do not care which.
A bug that only a second file could reach: "{utils::double(21)}" was read as the expression
utils written with the format specifier :double(21). A hole in an interpolated string is
Nikaia source, and Nikaia source names things across modules. :: is a path now.
Parsed owns its interner, so merging means renumbering
every symbol — and the emitter produces text, which joins for free. One Parsed per file also
keeps each file’s spans its own, which D7 needs.mod at the crate root makes stock::total resolve in the language below, which
is where every other name already resolves.sync inference depend on which file was read first (D5),
and 13.5 specifies one file at the project root.pub. Two enforcers of one rule
disagree eventually, and the one in the language below cannot be forgotten. NK1110 is a
message, not a second enforcer: it reports what rustc would refuse, which is why it can be
conservative without being unsound.nikaia.toml as the list of modules. Cargo needs one because a crate is a unit of
distribution; a Nikaia module is a file and the use already names it. A manifest that repeated
the imports would be a second place to forget.use utils and use std::fs; one segment for a module, and a nested path refused with a
sentence.mod blocks in one crate, so the cycle needs
no breaking, only not walking twice.sync-checked and provenance-analysed against the program’s ledger.pub on an item and on a field, parsed and emitted.dsl Stock from … in another file finds no Stock. This is a limit of the
compiler rather than of the language, and it points the right way: the file about reading a
stock list is where reading one belongs. examples/inventory/src/stock.nika wraps its grammar in a
pub fn read, which is the shape a module boundary would have asked for anyway.NK1110. The ledger
records a field’s type and not yet its visibility, and the checker would additionally need to
know which file it is in — a field access is written e.category and says nothing about where
e came from. A private field reached across files is a rustc error mapped back to the right
.nika line, which is worse than NK1110 and better than nothing.sync = "inferred".src/ convention, and incremental compilation per module. The first is
D1’s deliberate refusal; the third is ADR-021 D6’s “keys are per translation unit”, which a
program of several files now stretches — the unit is the program.