ADR-154: The Prelude Is a Written List, Small and Closed

Status: Superseded by ADR-313 — its D1-D3 are D1-D3 there, its D4-D5 are D7-D8 Date: September 19, 2026 Target Version: Nikaia 0.0.31 Related: ADR-140 D5 (use brings nothing in, for std as for a package — which left this open), ADR-286 (a package’s prefix at every use), ADR-286 D1 (use std::… names the library), ADR-288 D11 (an entry exists because a program asked), Part I 2.2, Part I 9.1

1. Context

ADR-140 D5 decided how use works — it brings no name in, for std as for a package — and said in passing that Vec, String and HashMap need no use at all, which is the prelude and is unchanged.

Unchanged from what, is the problem. There is no written prelude. The line is drawn by what the compiler happens to know: crates/nikaia-std/src/lib.rs has a prelude module, it was grown one pub use at a time as things were needed, and it carries fs, io, cli, html, task and HashMap — which is to say a program can read a file without saying so.

A prelude is a promise that is very hard to take back: every name in it is a name some program writes without importing, and removing one breaks that program. So the size of the list matters more than its contents.

2. Decisions

D1 — The prelude is written on Part I’s first page, and it is this.

And nothing else.

D2 — Nothing in it does I/O but printing, and nothing in it pauses.

That is the rule the list is built from rather than a property it happens to have. fs and io leave: reading a file is a thing a program should say it does, and use std::fs is that sentence. It is also what makes the prelude safe for a profile with no filesystem (ADR-119) — the list does not have to be cut down for one.

D3 — HashMap is the first thing outside it.

It is the name that makes this a decision rather than a tidy-up: a map is common enough to argue for, and use std::collections is one line, and a prelude that grows by common enough has no floor. collections::HashMap at the use, which is ADR-286’s shape one library over.

D4 — A name joins the list by a record, and never by being needed once.

Additive is the easy direction and it is the one that ends with everything in the prelude. A name in it has to be argued for in a record of its own, with the D2 rule as the bar.

D5 — The emitter’s own prelude is not this list.

The generated Rust imports what the lowering needs — rt so the fn main the emitter writes can start the runtime, TrustedMap so a map has a hasher. Those are not names a .nika file may write, and the two lists are allowed to differ because they answer different questions: what the program may name, and what the generated file needs to compile.

3. Consequences

4. What this does not decide

5. What is built

All of it but three names on D1’s own list, which std does not have.

  1. D1’s list is on Part I’s first page (1.3), so the promise is written where a reader looks rather than in a file of the compiler’s.
  2. The two lists are separated (D5). The Nikaia-level one is not a list at all in the end, and that is the finding below; the emitter’s grew the modules the generated Rust now names — collections, text, foreign — because the lowering writes the prefix the source wrote.
  3. Three refusals, each with the line to add. A function that lives in a module written without it (NK1117: std has text::digit_value — write use std::text, and text::digit_value(…) here); a module used before it is introduced (NK1117: fs is used here and introduced nowhere), which is Part I 9.1’s D4 for std rather than for a package; and a type written without its module (NK1135: HashMap is written without its module), which is D3.
  4. The corpus and the pages, which cost far less than this record feared: the .nika corpus already wrote use std::fs and its kind almost everywhere, so what it came to was two files, fifty-four inline sources in the test suite and six blocks of the specification.

What made it small: the list is not a list

The enforceable form of D1 turned out to be a fact about the ledger rather than a written list in the compiler: std’s entries that live in a module are keyed module::name, and the ones that need no use — print, println, eprint, eprintln — are keyed bare. So the prelude is the bare keys, and adding a name to it is what D4 says it is: a record, and a line in std.contracts keyed without a module.

Ledger::lookup had matched on the last segment, which is what made the question unanswerable: digit_value and text::digit_value were one name. It matches exactly now, and that also closed a defect of its own — a program with its own fn read found io::read, in a unit that does not carry the ledger of the package beside it, and the throws column then spoke for a callee nobody had resolved.

sleep moved into std::time by D2’s own rule: the rule the list is built from is that nothing in it pauses, and that is the one entry here that does. So Part II 12.4’s line is time::sleep(5.seconds()).

What the type half cost, and what it found

HashMap, HashSet, BTreeMap and BTreeSet became collections::… entries; CStr, Duration, Sender and Receiver went into the modules they live in; and a type’s methods went with it, because a method belongs to its type — collections::HashMap::len, the shape fs::Mapped::deref already had.

A type’s name then carried its module everywhere, and that is what the work was: the rules that name a type by hand had to ask for the type rather than for where it is reached from. The copy list, the crossing analysis’s containers, the indexing rule, the hash a map gets and the C boundary’s CStr all read the last segment now, through one function that says why.

Two defects fell out of it, both in the ledger’s own parser and both silent:

What is not built

#190 carries them.

This site is open source. Improve this page.