Status: Superseded by ADR-286 — its D1-D4 are D19-D22 there
Related: ADR-047 D2 (the path dependency this completes, and the
five rules it carries), ADR-046 D4 (the use check that does not
have to widen under this answer), ADR-002 D1 (the same move, made
once already: the manifest is translated and Cargo resolves) and D4 (std by path
into the sysroot, which is this shape working today),
ADR-043 (the overflow checks, and the one thing that stops being
free)
ADR-047 D2 gave a package a path dependency, and it is built one level deep: a package that declares Nikaia dependencies of its own is refused rather than resolved. So a library that uses a library does not exist, and the refusal’s own advice — “depend on them directly for now” — asks the consumer to know and restate the library’s internals, which is what a library is for avoiding.
What stops the next level is not the visibility rule. D2 rule 2 already says a
transitive dependency is invisible, and it is enforced twice. What stops it is
that everything is flattened into one Rust crate: cargo_project says of a
Nikaia dependency that it “declares nothing to Cargo: its files are read into the
same crate”, and collect_with reads each dependency’s src/ into the same unit
list.
The flattening is deliberate, and it buys exactly one thing: D2 rule 5. A Nikaia dependency gets ADR-043’s overflow checks because it is literally the program, rather than landing on the foreign side where a dependency mechanically appears.
It also creates a question Cargo does not have. Every package becomes a mod at
one root, named by the manifest key of whoever depends on it, so:
b and B names C as c, the root carries mod b and
mod c, and A can write c::thing() having depended on nothing — rule 2 broken
silently;c, two packages want one
mod c: refusing tells A about B’s internals, allowing it gives two types one
name.There are three ways to answer the question above, and the two not taken are recorded here because each fails for a reason that outlives this decision.
(a) Name the module by package identity. The root module’s name comes from
something stable about the package rather than from the consumer’s word, and the
manifest key becomes a local alias onto it. The clash disappears; the leak does
not — the module is still at the root, so ADR-046 D4’s check has to
widen from use lines to qualified names. And “package identity” is the
registry-shaped question ADR-002 D1 §5 declines to answer, arriving
early and for a reason that has nothing to do with distribution.
(b) Nest the modules. B’s dependencies live inside B’s module: mod b, with
mod c inside it. A has no path to C, so rule 2 is structural rather than
checked — the same property D1 gets. But the same package reached through two
parents becomes two modules, which contradicts ADR-047 D2 rule 3:
same path, same package. It is the cheapest of the three to emit and it breaks a
rule decided one record earlier.
What each costs. (a) pulls the registry question forward and widens a check
that was written for use lines. (b) is cheap and breaks rule 3. Neither removes
the question — they answer it with machinery this project would then own, where
D1 answers it with machinery it already leans on: a name resolves per crate, so
two packages under one key are in two different crates and never meet, and a
package reached twice is one crate because Cargo already works that way.
A build emits a Cargo workspace: the entry package as the binary crate, and every
package it depends on — transitively — as a library crate beside it, each with its
own Cargo.toml naming its own dependencies under its own manifest keys.
std already arrives this way (ADR-002 D4), so this generalises the
shape the generated project already has rather than introducing one.
A dependency is emitted as Cargo’s renaming form — the package is named by its identity, the key is the name the depending crate uses:
[dependencies]
c = { package = "…", path = "…" }
So two packages that both want the key c are in two different crates and never
meet, and the same package reached through two parents is one crate, which is
ADR-047 D2 rule 3 holding because Cargo already works that way.
A transitive package is not a dependency of the crate that would name it, so
naming it is a resolution failure in Rust with no Nikaia rule behind it. The two
places D2 rule 2 is enforced today — the empty reachable set
collect_with hands a dependency, and the refusal in packages_of — are no longer
what makes it true.
And ADR-046 D4 does not have to widen. The alternative answers to
this question both left the transitive module reachable from the root and needed
the use-line check extended to qualified names; under D1 there is nothing at the
root to reach.
This is what the flattening was buying, and it is the price of the answer: with separate crates, each generated Nikaia crate has to be named on the program’s side of the profile, beside the exception that turns the checks off for every foreign package.
[profile.release]
overflow-checks = false
[profile.release.package.<each Nikaia crate>]
overflow-checks = true
Without it a library computes silently wrong numbers while its caller aborts, which is exactly the accident ADR-047 D2 rule 5 was written to prevent.
Four crates, written out as the generator would emit them, and the four things the answer turns on, each run rather than argued:
| asked | result |
|---|---|
| does the workspace build | yes |
can app name the transitive c |
no — E0433: failed to resolve: use of unresolved module or unlinked crate |
| does the overflow check reach a Nikaia library crate | yes — panic_const_add_overflow from inside it |
| does a package not on the list still wrap | yes — i32::MAX + 1 came back -2147483648 |
| does the diamond unify | yes — app and libb both take libc, and a c::Id built in app is accepted by a function in libb. In Cargo when this was written by hand, and in the checker in front of it since: see §5 |
The third and fourth together are D4: the split is what ADR-043 requires, and it works in the generated manifest’s own terms.
packages_of already computes.All four decisions, and the generator produces the shape §3 was built by hand to check.
project::members_of walks the graph from the entry package breadth-first, so a
package reached twice is one member and the order is the same on every machine;
project::cargo_workspace writes a virtual root and one member per package, with
D2’s renaming form for each Nikaia dependency and D4’s profile rows for each
crate. A build emits a workspace always, even for a program that depends on
no package at all: one shape that is always taken is worth more than a second one
taken rarely, and it is the shape the overflow rows need. The refusal in
project::packages_of is gone, and rule 2 is enforced nowhere — naming a
transitive package now fails to resolve in Rust, reported against the .nika line
that wrote it.
modules::collect_with still reads a dependency’s files, and this is the one
place the answer is not simply “its own crate”. A package’s public surface is what
its files declare, so the ledger and the type check need them; the crate that
carries them is generated beside this one, so Program::emit writes none of them.
Their own use lines are checked against their own manifest keys rather than
against nothing, which is D3 from the other side: the level below a dependency is
that dependency’s business.
The diamond is built too, and it was the last thing D2 decided that the
compiler in front of Cargo did not do. Cargo unifies a package reached through two
parents and §3 shows it accepting the value; the ledger named a type by the key it
was reached through, so a program that depends on libc as deep and on
libb, which depends on libc as c, was told deep::Id is not c::Id — one
type refused for having two spellings, with a help line asking for what was
already written.
A type’s identity is now the package rather than the word a consumer happened
to use for it. project::renames_in reads each dependency’s own manifest keys and
resolves each to its directory, giving the one word this build uses for it: the
program’s own key where the program names it too, and otherwise the package’s
[package] name, which members_of has already refused to let two packages
share. Either way it is a function of the directory and not of who asked.
Ledger::absorb_renaming applies the table after qualifying, so a name the
package declares itself is never touched — qualify has already put that
package’s word in front of it.
One level deep, because one level is what is read: what a dependency’s own
dependencies are named is that crate’s business (D3), and nothing below the level
modules::collect_with reads is in this program’s ledger to be renamed.