ADR-020: The Ledger Records What the Compiler Knows, and a Library Brings Its Own

Status: Accepted Related: ADR-005 D3 (the Borrow Contract Ledger this extends), ADR-283 (views and what tethers them), ADR-010 (provenance decides the hasher), ADR-288 (what earns the sync D2 records)


1. Context

Part III 13.5 specifies nikaia.contracts in detail — its format, its determinism guarantee, --locked, and that a published package ships its ledger so a consumer builds against contracts rather than guesses.

Without it, two things the specification promises cannot be checked. ADR-019 D2 says a sync function cannot call io::read_to_string, and without a ledger the compiler has no way to know what io::read_to_string is: std is a Rust crate to it. ADR-010 says a map’s hasher follows the provenance of its keys, and provenance has to come from somewhere — a call whose result is a buffer.

Both need the same thing: a place where what a caller must know about a body it cannot see is written down, for this program and for every library it uses.


2. Decisions

D1 — The ledger records what the compiler knows, and says which inference produced it.

Changed by ADR-288 D5: the header of the derivation lives in nikaia.derived beside the contract.

An inference that reads signatures and one that reads bodies both produce a ledger, and a reader must be able to tell them apart — so the header carries it:

version = 1
inference = "stage0-signatures+sync-bodies"

A later compiler writes a different name there, and --locked fails on the difference rather than quietly accepting the weaker answer as the stronger one. The alternative — leaving it out and hoping the version implies it — is how a file that is supposed to be a memory starts lying.

D2 — sync and throws go in it, not only borrows.

The file is called the Borrow Contract Ledger because ADR-005 was about borrows. But the question it answers is “what must a caller know about a body it cannot see”, and may it pause and may it fail are two more answers to exactly that question, needed by exactly the same callers.

Recording only borrows would mean a second file for the second question, with the same determinism rules, the same --locked, and the same distribution story. One file.

D3 — An absent sync means not sync; an absent entry means nothing is known.

This is the distinction the whole thing rests on. A caller that finds an entry without sync knows the callee may pause. A caller that finds no entry knows nothing and may not assume.

It is also what makes the file worth shipping rather than deriving: a consumer cannot infer io::read_to_string’s contract, so the library has to state it.

D4 — Only what is true is written.

sync = false on every entry would treble the file and say nothing. A diff of this file should show a promise being made or withdrawn, and a column of falses buries that in noise.

D5 — A library ships one ledger, and the parts it could not infer are written in it and reviewed.

std is half Rust. A compiler that does not read Rust cannot infer fs::map’s contract, and building a second frontend to do so would be a large amount of work to derive twelve lines.

So crates/nikaia-std/std.contracts has both, in one file, marked: the entries for modules written in Nikaia are inferred from their sources, and crates/nikaia/tests/contracts.rs regenerates them and fails if the file has drifted; the entries for the modules still written in Rust are written by hand and reviewed like code. The hand-written share is a measure of how much of std is still Rust, and shrinks as ADR-014 proceeds.

A hand-maintained copy of a derived truth goes stale, which is why only the part that cannot be derived is hand-maintained, and the part that can is tested.

D6 — Names are what a caller writes.

fs::map, io::read_to_string, Stats::add, fs::Mapped::deref. The anonymous constructor of Kap 4.2 is Type::new, because that is what the lowering names it and therefore what a caller reaches.

D7 — The toolchain recorded is Nikaia’s version, not rustc’s.

sync, throws and the borrow contract are decided by this compiler and never by the one it emits Rust for. A ledger diff blamed on “the toolchain” must name the toolchain that actually inferred it, or the narration of 13.5 points at the wrong thing.


3. Consequences

The sync check has what it needs. Part II 12.1’s rule — a sync function may only call sync functions — is a lookup: with std’s ledger a call to io::read_to_string from a sync function is a fact the compiler finds, and NK2202 is what it says about it. A method call resolves through the type checker (ADR-288 D7).

Provenance has a place to be recorded. ADR-010’s sources are contracts of exactly this kind — fs::map yields Trusted, an HTTP body yields Untrusted — and the ledger is where they are written.

The type checker has its signature store. Parameter and result types are two more keys on an entry, signature and fields (ADR-024), not a second file.


4. What was rejected

This site is open source. Improve this page.