Say that a ring is refused, since the last note said the opposite

BUILT.md described a tolerated cycle as a property — "mutually dependent
packages simply work" — and NEXT.md still listed a package importing a package
as the real gap, which it stopped being some commits ago. Both now say what the
code does.

Written down with them: what a name imported through an intermediate package is
called, and why the inner alias is forced rather than chosen; that the diamond
is proven by the numbers pkg-diamond prints rather than by its compiling; and
that pkgs is topologically ordered while the declaration list deliberately is
not.

Package visibility stays on the list. The gap is that a package has no way to
mark a name private, which is surface syntax; the predicate and the refusal it
would hang off are already there.
This commit is contained in:
Joseph Ferano 2026-09-12 16:47:06 +07:00
parent c604911ecb
commit dc73b63446

View File

@ -346,10 +346,28 @@ all four. A file carries no `.c` and no `link` file; those belong to a directory
**A package may import a package.** The qualification flattens to the *inner* alias — raylib imported by a package that
is itself imported is still `rl/…`, never `sand/rl/…` — because a directory reached along two routes has to arrive under
one set of names or the checker sees every declaration twice. A directory is keyed by its real path and read once, which
is also what ends a cycle: a package that imports itself meets its own entry and contributes nothing the second time,
and the namespace being flat, mutually dependent packages simply work. The same directory under two *different* aliases
is refused.
one set of names or the checker sees every declaration twice. It is the one decision here that would be hardest to
change later, because a qualified name is an identity the `layout` op and the break loop both resolve by.
A directory is keyed by its real path and read once, so a **diamond** shares one copy of its bottom package.
`programs/pkg-diamond.flan` is the case that proves it at the level that matters: `area/` and `draw/` both import
`shape/`, a `shape/Box` is built inside `area/` and passed to a function declared inside `draw/`. Read `shape/` twice
and there are two structs both named `shape/Box` which do not unify, so what proves the dedupe is that the program
prints `3 6 20` and not that it compiles.
The same directory under two *different* aliases is refused, including when one of the two is a package's own import
and the other is pages away in the entry file — `programs/pkg-alias-clash.flan`.
**A ring is refused and named.** Earlier it was not: the read-once table swallowed the second arrival, so mutually
dependent packages appeared to work, and BUILT.md said so. What that cost is a definite package order, which is what
the macro expander needs — every `defmacro` has to be compiled before anything that calls it — so it is now a refusal
that names the ring, `a -> b -> c -> a`, rather than the single import that happened to close it. The two questions are
kept apart by two pieces of state: the chain currently being read, and the set already finished. Found in the first is
a cycle; found only in the second is the diamond's second route and still a no-op.
`Load.t.pkgs` comes back **dependencies first** — the topological order the acyclic rule buys. The declaration list is
deliberately unsorted: `check.ml` collects every top-level name in one pass before it checks any body, so top-level
names are order-independent by construction. The order exists for the expander, which cannot work that way.
**Visibility is one rule: `main` is not exported.** A package carrying one would collide with the importer's the moment
anything imported it, so a program could never be a package; and `main` is a reachability root, so an imported one would
@ -357,6 +375,9 @@ keep everything it calls alive. Writing `sand/main` is refused at the line that
checker it would be "unknown name", which is true and useless.
Still missing: a package-private marker for anything other than `main`, which is why `rl/get-color-raw` is callable.
The gap is surface syntax and not `load.ml``exported` is one predicate and the refusal machinery that points at the
line which tried already exists, so a second rule is a line. What does not exist is any way for a package to *mark* a
name private, and inventing one is a reader and parser change.
## The link follows the program