← board

A user class named like a pylib builtin loses to the builtin

Repro

class Counter:
    k = 0
    def __init__(self):
        Counter.k += 1
Counter()
print(Counter.k)
pascal26:4: error: "k": no such member on this record/class
  near:     Counter  >>> k

Rename the class to anything else and it prints 1.

Cause

compiler/builtin/pylib.pas declares collections.Counter as a set of overloaded FUNCTIONS (function Counter: TPyDict; and friends, pylib.pas:1024-1027). The user's class Counter does not displace them, so Counter.k resolves against the pylib entity and dies in RequireRecMember — an error message about a missing MEMBER, which sends the reader looking at the class body rather than at the name.

Why it matters beyond the one name

Counter is only the instance that was tripped over. Every pylib-provided Python builtin spelled as a Pascal routine is the same hazard, and Python programs shadow builtins routinely — a user class or function named list, set, bytes, type, filter, map is legal Python and means the user's one from that point on. The general rule is that a NilPy module-level definition must win over anything pylib provides.

Suggested shape

A user class/def at module scope should mask the pylib name for the rest of the module. Worth checking whether the same hole exists for a user def with a builtin's name, and whether the fix belongs at declaration time (mark the pylib symbol shadowed) or at lookup time (prefer a user class when one exists) — the declaration-time form is the one that cannot drift between lookup sites.

Gate

A .npy diffed against CPython: a user class named Counter with class attributes and methods, used through its own name and through an instance; the same for a user def named after a builtin; plus a control that the pylib Counter/collections.Counter still works in a module that does NOT shadow it.

Resolved 2026-08-03

One comparison at the identifier-resolution site in ParseFactor: under NilPyUserCode, an unqualified name that binds to no symbol but DOES name a class declared in the main program (FindUClassInUnit(name, -1) >= 0) drops its proc binding. The class branches further down then take the name, exactly as they do for a class pylib never heard of.

UClsUnitIdx = -1 is the whole guard — "the user wrote this class". A pylib class still loses to a pylib routine as before, and Pascal is untouched.

test/test_nilpy_user_class_shadows_builtin.npy (+ .expected, wired into make test-nilpy), byte-identical to CPython: the Counter repro with class attributes, an __init__ that writes through the class name and a method that reads it; plus classes named list, dict and type, read through the class name and through an instance.

gate.sh quick GREEN, self-host fixedpoint byte-identical.

Two things found beside it, both filed rather than folded in

Log