← board

A module member named like its module hides the module's other members

Repro

mimic_zz.py:

def zz_left(a):
    return 1

zz = zz_left          # a member whose name is the module's name
import zz
print(zz.zz_left([1]))
# error: Nil Python: no class declares a method or callable field .zz_left()

Delete the zz = zz_left line and the same qualified call compiles and runs.

The boundary

shape result
module defines a same-named member; mod.other_member(...) compile error
module defines no same-named member; mod.other_member(...) OK
from mod import other_member (module defines a same-named member) OK
from mod import mod_named_member OK
module whose ONLY member is the same-named one, called qualified OK

Reading that fits every row: at a qualified call the name binds to the imported member in preference to the module, so zz.zz_left looks for zz_left on a function. It is a shadowing question, not a missing-feature one. (Hypothesis — nothing here read the resolver.)

Severity, and why it is p40 rather than higher

It is a compile error, not a wrong answer, and the from-import spelling — which is what the corpus actually uses (from bisect import bisect_left) — works. So nothing is silently wrong and nothing is blocked; a caller who writes the qualified form gets told, if confusingly.

Filed because the shape is common in the stdlib (a module exporting a legacy alias of its own name), and because the report names a "class" for what is a module and a function, which sends the reader somewhere unhelpful.

Track B site

lib/rtl/mimic_bisect.py keeps the platonic bisect = bisect_right alias, so test/lib_mimic_bisect.npy exercises the module through from-imports only and says why at the site.


RESOLVED — 2026-08-27, agent-A

Fixed. The ticket's hypothesis — "at a qualified call the name binds to the imported member in preference to the module" — was right in outcome and one step off in mechanism, and the step matters: nothing binds the name at all. The qualifier is simply never CONSUMED.

Measured

One print inside ConsumeUnitQualifier, at the FindSym early exit:

ZZCQ name=zzm sym=468 unit=636 cur=-1 symunit=636 symtxt=zzm

unit=636 — the qualifier WOULD resolve. symunit=636 — but a symbol exists, and it is the module's OWN global, living in the very unit the qualifier names, while cur=-1 says we are compiling the main program. The function's rule

{ A local/param/global SYMBOL shadows a same-named unit (FPC scoping) }
if FindSym(name) >= 0 then Exit;

fired, nothing was consumed, and zzm.zz_left became a member access on a function value — hence the report naming a "class" for what is a module and a function.

That rule is right for Pascal and for the case it was written for (function NetToHost(Net: in_addr) keeping Net.s_addr a field access). It is FindSym, though, which is not scope-local: it reaches a global in ANOTHER unit, and that is a scope Python does not have. In an importing module bisect is bound to the MODULE; the module's internal rebinding of its own name is not visible there at all.

The fix

compiler/pasparser_name.inc — the early exit keeps the symbol index and lets exactly one shape through:

if not (isNilPy and (SymUnitIdx[u] >= 0) and
        (SymUnitIdx[u] <> CurrentUnitIdx) and
        (PyFindUnitDotted(name) = SymUnitIdx[u])) then Exit;

As narrow as the collision: the symbol must live in exactly the unit this qualifier names, and we must not be compiling that unit — because inside it the module's own binding really does win, which is Python's rule too and is a row in the witness. A local, a parameter, a same-unit global and a symbol from some unrelated unit all still shadow, unchanged.

Verified

shape before after
module rebinds its own name; mod.other(...) compile error correct
the same-named member itself, mod.mod(...) correct correct
inside the module, its own name correct correct
from mod import other / from mod import mod as alias correct correct
module with no same-named member correct correct

And the case that filed it, against the real shim: import bisect then bisect.bisect_left, bisect.bisect_right, bisect.bisect and bisect.insort all match CPython, on lib/rtl/mimic_bisect.py exactly as it is written, alias line included.

Track B follow-up (not done here, deliberately)

test/lib_mimic_bisect.npy says at its site that it exercises the module through from-imports only because of this bug. That note is now stale, but the test builds with $(PXX_STABLE) — the PINNED binary — so the qualified spelling only becomes available to it after a pin. Left for Track B to adopt then; this ticket does not touch B's files.

Gate

make compiler/pascal26self-host fixedpoint: verified — 1 round(s). tools/gate.sh quick → GREEN. Witness test/test_nilpy_module_member_named_like_its_module.npy + helper module registered in test-core, .expected generated by CPython, a compile error at pinned v381 and green now.

Log