ADR-047: A Package Is a Directory, and a Library Arrives by Path

Status: Superseded by ADR-286 — its D1 is D1 there, its D2 is D15 Related: ADR-046 D1 (what use does; this record decides what it names, and moves that boundary from the file to the package), ADR-002 D1 §5 and D4 (the three things the toolchain refuses to guess at, and the path std already arrives by), ADR-045 §3 (the portability rule this makes reachable), ADR-043 D6 (the overflow check, whose foreign/own split D2.5 assigns rather than inherits), ADR-020 (the ledger a consumer reads).

1. Context

There is no way for one Nikaia package to depend on another. Part III 13.2’s manifest shows a Nikaia package beside a Rust one and refuses the first with its reason: no record names a registry, a name space or a distribution format, so the compiler does not guess (ADR-002 D1 §5). type = "rust" resolves through Cargo; std arrives by path into a sysroot. Between them, nothing.

And that is not only a missing convenience. The reason a crossing verdict may not consult user_parallelism is that a library built at one setting has to stay usable at the other — the sentence that decided the shape of ADR-045 D1. It is a correct rule and it should stay. But it protects a situation nothing can construct: there are no Nikaia libraries, because there is no way to depend on one.

A second half arrived with the first. With file = module there is no re-export: no way for a unit to offer a name another unit declares. A library is many files, so a consumer must name the file a declaration happens to sit in — the library’s internal layout is its public surface, and moving a declaration between files breaks every consumer.

2. Decisions

D1 — A package is a directory, and its files share one namespace.

The files in it see one another with no use at all. What the package offers outward is whatever says pub, in whichever file it is declared. A consumer writes use http and names the package, never a file inside it.

This removes the re-export problem rather than patching it. There is nothing to re-offer, because the names inside a package already share a space — so no new form is needed at all. And a library’s internal file layout stops being its public surface by construction: moving a declaration from one file to another is housekeeping, and breaks no consumer.

The field points the same way. Java, Go and Rust all put the boundary of privacy and publication above the single file. Python is the one that puts it at the file, and __init__.py — a front door maintained by hand, forgotten names and all — is the consequence rather than an accident of it.

The alternative, considered and refused: keep file = module and add a re-export form. Not wrong, and it would leave Part I 9.2 untouched. It costs a new form in a language that has just withdrawn two (ADR-022, ADR-041), and it moves the upkeep onto every library author forever.

What it changes, and this is written down rather than discovered: Part I 9.2’s privacy boundary moves from the file to the package — a declaration is private to its package unless it says pub — and two files of one package may not declare the same name. Those sentences are rewritten rather than extended.

D2 — A package may be depended on by path, and by nothing else yet.

std already arrives that way (ADR-002 D4), so this generalises a mechanism rather than adding one.

No registry, no name space, no distribution format. The three things ADR-002 D1 §5 refuses to guess at stay open, deliberately. Go went years with the fetch location as the identity; Rust and JavaScript built their registries early and carry decisions made when there were three packages. A registry is built when there are libraries, not before.

Five rules come with it, or they get decided by accident:

  1. The manifest key is the name. use writes the key and the path says where it comes from. Two libraries that both want to be http are therefore the consumer’s to name apart, which is the authority ADR-046 D3 already gives them.
  2. Transitive dependencies are not visible. If A depends on B and B on C, A does not see C. Otherwise a library’s surface is everything it happens to use, and swapping an internal dependency breaks consumers.
  3. The same path is the same package; a different path is a different package, identical contents included. With no versions there is nothing to unify, and two paths to two copies would be two types of one name.
  4. A dependency’s [build] section is ignored, and the compiler says so. A package is built with the settings of the program that uses it. Anything else would put two answers to the parallelism question in one build (ADR-037 D2).
  5. A Nikaia dependency is part of the program, not a foreign package. ADR-043 D6 turns overflow checks on for the program and off for every foreign package. A Nikaia dependency is written in this language and carries this language’s promise, so an overflow in it is the same broken state. Without this stated it lands on the foreign side by accident — that is where a dependency mechanically appears — and then a library computes silently wrong numbers while the program calling it aborts.

3. Consequences

ADR-045’s portability rule becomes testable. It has protected a situation nothing could construct; a path dependency is the smallest change that makes the first Nikaia library the first real test of something so far only argued.

ADR-046’s boundary moves and its rules stand. use still names a unit and brings nothing in; the unit is a package. D4’s “a prefix must be introduced” and D5’s “one name per file” are unchanged in what they say and changed in what they range over.

#229’s cross-file type defect half disappears. Naming a type across a file boundary stops being a thing that happens; what remains is the cross-package case, which is what the entry was always really about.

And #143 does not go away with the boundary. A Shared field’s count is decided per file, and the files of a package are still several runs of that analysis rather than one — so the fail-closed floor that keeps two of them agreeing is exactly as necessary inside a package as it was between two modules. What D1 changes is only which name the field is written under. Making the analysis run over a package at once is the follow-up, and it would let the floor be lifted where the whole package is visible; across a package boundary it can never be.

4. What this does not decide

A registry, a name space, a distribution format and versions are ADR-103’s: a version resolves through Cargo under the crate name nikaia_<name>, and D2’s path arm stands beside it.

5. What is built

D1 and D2, and what is left of D2 is depth rather than shape.

The package (D1)

A package is the entry’s directory: every .nika beside it takes part, whether or not anything names it, and they are one namespace. The emitted Rust is one crate root with every file’s items in it — no mod, no use super::*, nothing to resolve, because one namespace here is one namespace there. The entry goes first, being the file that may declare main, and the rest in file-name order, so the output is a function of the tree and not of a directory listing (Part III 13.5).

Two files declaring the same name is refused here, naming both files, rather than left to rustc — which would report a duplicate definition against a file nobody wrote (Part III, C.1).

Outside a project the entry is compiled on its own (modules::collect_one). A package is a directory of a project, which is what a nikaia.toml declares; a directory of loose examples is a directory of programs, and compiling one must not pull in the other ten.

The dependency (D2)

http = { path = "../http" } resolves, relative to the manifest’s own directory. The package’s src/ is read as a namespace of its own, its items become a pub mod http at the crate root — so http::serve() in Nikaia is http::serve() in Rust and nothing resolved it (ADR-011 D2) — and Ledger::absorb qualifies both the key and the types inside each entry with the package’s name, a package at a time rather than a file at a time. That last part is what makes a signature written -> Request inside the package the same type as the http::Request a caller writes.

A library needs no main.nika. What a package offers is its public surface; an entry point is what a program has.

All five rules are built, and each is a message rather than a silence:

  1. The manifest key is the name. Nothing inside a package names it.
  2. Transitive dependencies are not visible, kept twice: a dependency’s use lines are checked against nothing, and a dependency that declares Nikaia dependencies of its own is refused rather than flattened. That is the depth limit — one level — and it is the one thing here that is thin rather than partial: what is missing is the resolution, not the visibility rule.
  3. The same path is the same package, so two names for one directory are refused: they would be two mods of one package and two types of one name.
  4. A dependency’s [build] is ignored, and the compiler says so — once per build, not once per pass, because a note a reader meets three times reads as three problems.
  5. A Nikaia dependency is part of the program. It declares nothing to Cargo, because its files are in the same crate — so ADR-043 D6’s overflow check reaches it by construction, and a test asserts that a library’s arithmetic aborts.

What this made observable

NK1110 fires for the first time. Privacy is per package, so until a second package existed the check had nothing that could reach it. pub now means something a program can see, and “secret is private to http” is a real diagnostic rather than a catalogued one.

And pub on a field means something too. The ledger records it — fields = ["pub id: i64", "method: i64"] — because a consumer of another package needs to know which fields it may touch and nothing inside the package does. The language below cannot help here the way it does for an item: the emitted struct is in the same crate, so pub on a field buys nothing there. Reading such a field and giving one a value in a struct literal are both NK1110, and pub is optional on the way in — a ledger written before the word reads as a type with no public fields, which is the fail-closed direction (ADR-010 D1).

What is not built

A dependency’s own dependencies, per rule 2 above.

A version, which ADR-103 resolves through Cargo and which is not built; the manifest says so when one is written.

This site is open source. Improve this page.