NilPy: a subclass overlaid the parent's fields and VMT slots
Found 2026-07-20 while testing [[bug-a-nilpy-method-call-on-variant-receiver]] with an inheritance ladder.
Repro
class A:
def __init__(self, n: int) -> None:
self.n = n
def get(self) -> int:
return self.n
class B(A):
def twice(self) -> int:
return self.n * 2
b = B(3)
print(b.get()) # printed 6 (twice()'s answer), CPython prints 3
print(b.twice()) # 6
Silent — no diagnostic, just the wrong method.
Cause
PyRegisterClassMembers started a subclass's field offsets at 8 and its virtual
slot counter at 0, ignoring the parent entirely. So B's fields overlaid A's, and
B's first method took the slot A's first method already owned — an inherited
call went through that slot and landed in the subclass method. The Pascal path
does both continuations (parser.inc: curFieldOff := UClsSize_[parentCi] /
UClsVirtCount[ci] := UClsVirtCount[parentCi]); the NilPy path never got them.
Fixing the numbering exposed the second half: a subclass VMT is emitted per class and starts zeroed, and only the class's OWN methods got fixups, so every inherited-but-not-overridden slot stayed NIL and the call segfaulted.
Fix
f933cdf4 — subclass fields start past the parent's instance size, slots past the parent's slot count, an override reuses the parent's slot (so a base-typed reference dispatches to the override), and each inherited slot gets a VMT fixup to the nearest ancestor's method.
Regression test
test/test_nilpy_inheritance.npy, wired into make test-nilpy; output diffed
against CPython running the same file.