← board

A lowercase call name is hijacked by a case-matching class

Repro

class F:
    def __init__(self):
        self.n = 0

def f(a, b):
    return a + b

print(f(1, 2))

CPython prints 3. pxx (also at pinned, so pre-existing):

Expected: ), but got:  (Kind: 80, Line: 8)
pascal26:8: error: unexpected token

f(1, 2) is compiled as constructing F, whose constructor takes no arguments — hence the complaint about the second one. Rename the class to Zed and it compiles and prints 3.

Why this is the OLD landmine, on a path the fix did not reach

0f7ca1b7f made the NilPy value-position class lookup case-SENSITIVE (PyIsClassTypeExact) precisely so a local named like a class stops being typed as that class — the lesson recorded there was that the fix had to go one level down so every consumer moved together. The call path still reaches FindUClass, which is Pascal-style case-insensitive, so it kept the old behaviour. Same root, second site: the shape devdocs/dev/normalise-dont-special-case.md warns about.

Python is case-sensitive; f and F are unrelated names, and no NilPy program can want this. The lookup on this path should be exact-case too — check whether PyIsClassTypeExact can simply be used here rather than adding a third rule.

Also broken through the same lookup

class S:
    def __init__(self, *args): self.n = len(args)
s = S
o = s(1, 2)     # SEGFAULT — `s(1, 2)` constructs S with a mismatched signature

which is why minimal NilPy repros should keep identifier names non-colliding (Zed, q, k) — see project_nilpy_name_matching_a_class_is_typed_as_that_class.

Gate

make test-nilpy + self-host byte-identical, with a .npy case covering a def, a local and a parameter each named like a class in the other case, diffed against CPython. Watch the genuine type positions (isinstance(x, F), except F:, an annotation) keep the case-INSENSITIVE lookup they rely on.

Resolution (2026-08-11)

The site the ticket guessed at was not the one

The ticket assumed the CALL path still reached FindUClass. It does not — the call path was already exact via PyIsExactCtorName. A probe found the real one in two builds:

parser.inc's NilPy shadow rule"a user class named like a pylib builtin shadows the routine" — cleared procIdx on FindUClassInUnit(name, -1) >= 0, a case-insensitive lookup. So def f lost its own proc to class F, and the name then fell through to whatever handles a class. FindProc('f') was answering correctly the whole time; it had just been thrown away three hundred lines earlier.

That also explains why the symptom MOVED. Before classes became values the leftover name hit the class-CAST branch and failed to parse (Expected: ) on the second argument); after, it became a VT_CLASSREF variant and the program compiled clean and raised "takes 0 positional arguments" from the constructor of a class it never named. Same collision, worse failure — which is why this was worth fixing now rather than filing and moving on.

Fix — one rule, at every site that looks a class up by name

Python is case-sensitive: f and F are unrelated names and neither shadows the other. Three sites now say so, joining the two that already did:

site was now
the NilPy class-shadows-routine rule FindUClassInUnit (insensitive) … and PyIsClassTypeExact(name)
the class-CAST branch Name(x) IsClassType (insensitive) exact under PyExprMode — and Python has no cast syntax, so a genuine F(...) is already claimed by the construction intercept
the class-as-a-VALUE lookup FindUClass (insensitive) exact under PyExprMode

with PyIsClassTypeExact (typing) and PyIsExactCtorName (construction) unchanged. Five sites, one rule — the shape devdocs/dev/normalise-dont-special-case.md describes, and the reason its own advice is grep for the sibling: this family has now been fixed in three separate sessions because each site was found alone.

The FPC seed canary earned its place

gate.sh quick went RED on the first run — not on the self-host, which passed, but on the FPC seed: PyIsClassTypeExact is called from parser.inc, which is included before the pyparser.inc that defines it. pxx tolerates that; FPC needs a forward. One line, and exactly the bug-a-fpc-seed-drift-emitasmx64-forward shape the canary exists to catch.

Verified

test/test_nilpy_lowercase_name_vs_class.npy (.expected from CPython), wired into make test-nilpy: a def, a local, a parameter and two module globals each named like a class in the other case, plus the class itself still constructing, isinstance-ing, and working as a value, and the def still usable as a value. The three sibling tests that pin the earlier halves — instance_named_like_its_class, local_named_like_a_class, case_sensitive — all stay green. Gate: tools/gate.sh quick GREEN + make test-nilpy.

Log