a .py module shipped in lib/** cannot be imported
- Type: feature (NilPy module resolution) — Track A file ownership
(
compiler/parser.inc, the shared unit-resolution path), Track N facing - Opened: 2026-08-10, sizing [[feature-b-tkhtmlview-in-nilpy]]. Found before the port started, which is the point — without this the port would land and then not be importable as a library.
Measured
$ cat lib/pcl/zzprobe.py
def hello(): return "from-lib-pcl"
$ cat /tmp/imp/main.py
from zzprobe import hello
print(hello())
$ pascal26 /tmp/imp/main.py /tmp/imp/main
pascal26:1: error: import: no unit named zzprobe and no shim mimic_zzprobe
The identical file as a sibling of main.py works and prints
from-lib-pcl. So the module is fine; only its location is unreachable.
Cause — read off the resolver, not guessed
compiler/parser.inc's NilPy arm tries .py then .npy in CurUnitDir
only — the importing file's own directory, which is Python's rule and is
deliberately so (its comment explains that lib/pcl/tkinter.pas's uses tk
must not be hijacked by a stray tk.npy beside the user's script).
When that misses, the import falls through to the ordinary unit search chain —
which resolves .pas. Nothing in the chain ever tries .py/.npy in a
library root, so import re finds lib/rtl/re.pas but an import X can never
find lib/**/X.py.
Why it matters now
It is the prerequisite for shipping any library written in NilPy. The immediate case is [[feature-b-tkhtmlview-in-nilpy]], which was scoped as "pure Track B" and is not, purely because of this. Beyond that it is what makes "NilPy as a library language" real rather than a property of single-directory programs.
Shape of the fix
Extend the NilPy arm so that after the sibling probe misses, the search roots
(-Fu / -I entries and the shipped lib/** roots) are each tried for
<name>.py then <name>.npy, before the .pas fall-through — or after
it; that ordering is the one real design question:
.pybefore.paslets a NilPy library shadow a same-named Pascal unit, which is how a port would replacetkhtmlview.paswithout touching callers..pasbefore.pykeeps every existing resolution bit-identical and makes a port an explicit swap (delete the.pas).
Prefer the second unless the first is needed: it cannot change any program that
compiles today, and bug-nilpy-stdlib-name-binds-pascal-unit is the record of
how much subtlety lives in this ordering. Keep the sibling-first rule
untouched either way — it is Python's own semantics and it is load-bearing
(see the tk.npy note in the resolver's comment).
Gate
The probe above importing from lib/pcl/; make test-nilpy green (the suite
has multi-module tests that rely on sibling-first — those must not move);
self-host byte-identical; import re still reaching lib/rtl/re.pas.
Log
- 2026-08-10 — resolved, commit 555f2d3b6.
Resolution (2026-08-10)
A NilPy import now falls back to <root>/<name>.py then .npy across the
user search roots (-Fu/-I) and the shipped lib/rtl / lib/pcl roots.
Placed LAST among the real lookups, after every .pas/.pp/.c/.h probe
and before only the mimic_ shim mapping — the conservative arm of the ticket's
one design question:
- after
.pas, so no program that compiles today can change resolution. A NilPy library cannot silently shadow a same-named Pascal unit, and replacing one stays an explicit swap (delete the.pas). - before the shim, because a module we actually ship should beat a
mimic_substitution for the same name.
The sibling-first probe is untouched — it is Python's own rule and load-bearing
(its own comment records the tk.npy incident).
Measured — all three at once, because they are one ordering decision
| before | after | |
|---|---|---|
.py in lib/pcl/, imported from elsewhere |
import: no unit named zzprobe |
from-lib-pcl |
| same name as a SIBLING of the importer | from-sibling |
from-sibling |
import re → lib/rtl/re.pas |
bbnbnb |
bbnbnb |
The sibling row is the one that proves Python's precedence survived; the re
row proves a Pascal unit still wins its own lookup.
Regression test: test/test_nilpy_import_py_from_library_path.npy with
test/nilpylib/zzlibmod.py, wired into make test-nilpy via -Futest/nilpylib.
It asserts the library import and import re in the SAME file deliberately —
they are one ordering decision, and separate tests would let one regress while
the other passes.
Gate: tools/gate.sh quick GREEN (self-host fixedpoint + testmgr quick +
FPC seed canary), make test-nilpy green, self-host byte-identical.
Unblocks [[feature-b-tkhtmlview-in-nilpy]], which was scoped as "pure Track B" and was not, purely because of this. It is now.