← board

a .py module shipped in lib/** cannot be imported

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:

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

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:

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 relib/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.