32. Bare-name imports merge installed packages with built-ins
- Status: Accepted
- Date: 2026-09-05
Context
ADR 0018 split every import specifier two ways: a
./- or ../-prefixed specifier is a file, anything else is a built-in
module, looked up by exact name with no filesystem probe.
ADR 0031 adds a third kind of thing a bare
name can mean — an installed package. This ADR decides how import "name"
tells a built-in from a package, without reopening import's syntax
(a decision already locked in with the user) and without weakening ADR
0018/0019's "a specifier's meaning is knowable with no filesystem probe"
guarantee.
ADR 0019 already faced a similarly-shaped problem — telling a built-in from a
file — and rejected adding a prefix (std:math) for it: "Explicit, but
ugly for the common case and a new bit of syntax." This ADR is not free to
wave that reasoning away without saying why the two cases differ.
Decision
One merged namespace, no new syntax
import "name" (no ./) checks the built-in module table and the installed-
package table together. There is no prefix distinguishing them — import "math" and import "requests" are written identically, whichever kind each
turns out to be.
Built-ins win, checked once, before it can ever matter
A package's local import name (the key it is given in [dependencies],
ADR 0031) is checked against
korrin::stdlib::module_names() at korrin pkg add/install time, not at
import time. A colliding name is refused outright: "math collides with a
built-in module of the same name; choose a different local name, e.g. korrin pkg add ... --name my_math."
Because of that gate, the two tables a bare specifier could resolve against are disjoint by construction by the time any Korrin program runs. Runtime dispatch needs no new mechanism to enforce precedence — it already tries the small, fixed built-in table first; a package lookup is a new fallback reached only on a miss, and in practice a miss on the built-in table always means "not a built-in," never "shadowed by a package."
Why this doesn't reopen ADR 0019's rejected prefix
ADR 0019's problem was: a specifier's meaning must be knowable without
consulting what files exist on disk. ./ vs. bare-name already solves that
completely, unconditionally, and this ADR does not touch it. The problem
here is different in kind: bare names now have two tables instead of
one, and the question is not "how do we know which table without probing
the filesystem" (dispatch order already answers that, for free) but "how do
we guarantee the two tables never actually collide." A prefix would answer
that question too, but at the cost ADR 0019 already named and rejected once
— a new bit of syntax for the common case — for no benefit a one-time,
off-the-hot-path install check doesn't already provide more cheaply.
Consequences
import "whatever"never needs a human (or a docs page) to explain which of two syntaxes to use depending on whatwhateverturns out to be. There is one syntax.- A package cannot shadow a built-in, ever — not silently, not by accident.
If the stdlib later grows a module whose name an existing installed
package already uses, that specific package's next
korrin pkg installfails loudly with the same collision message, rather than the program's behavior silently changing underneath it. This is a real, if narrow, forward-compatibility cost the stdlib's authors should keep in mind when naming a new built-in module. - A hand-edited
korrin.lockthat smuggles a builtin-colliding name past the install-time gate still resolves to the built-in, never the package — the safe failure direction, and one that needs no extra runtime code, since it's exactly what the existing "built-in table first" dispatch order already does.korrin.lockis a machine-managed, "do not edit by hand" artifact (likeCargo.lock); a runtime re-validation pass guarding against someone deliberately corrupting a generated file was judged not worth adding. - No new grammar, no new error code, no new diagnostic path for the ordinary
case — the entire cost of this decision is one new check inside
korrin pkg add/install.
Alternatives considered
- A prefix for packages (
import "pkg:requests") or for built-ins (import "std:math", reopening ADR 0019). Explicit and unambiguous by construction, at the cost of a new bit of syntax ADR 0019 already argued against for a materially identical-looking case, for a problem an install-time check already solves without it. - Packages checked first, built-ins second. Same no-new-syntax shape, opposite precedence. Rejected: it means a newly installed package could silently shadow an existing program's use of a built-in the moment it's added, which is a worse failure mode than the chosen direction (a collision is caught at the point something changes, not silently absorbed by it).
- A filesystem/table probe with no install-time gate at all — just try
packages, fall back to built-ins (or vice versa), at import time, forever.
Rejected on the same grounds ADR 0019 already used against probing: the
meaning of an
importline would depend on state that can silently change later (a package added, a stdlib module added), rather than being fixed the moment the dependency was added.