Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 what whatever turns 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 install fails 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.lock that 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.lock is a machine-managed, "do not edit by hand" artifact (like Cargo.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 import line 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.