← board

Repro (17 lines, CPython prints tag)

class Tag:
    def tag(self) -> str:
        return "tag"


class P:
    pass


class D(P, Tag):
    pass


class E(D):
    pass


e = E()
print(e.tag())

Measured 2026-09-19 with pxx at HEAD after 578c9c39e, and with pinned v411:

Where to look

Measure both for E before theorising. In particular, find out why an inherited REAL-chain slot is nil too, which the ordering suspect does not explain.

Not on That Space Program's path: its only two-base class, EphemerisMissing, has no subclass.

2026-09-19 — FIXED (frankH)

The suspect above was wrong, and the observation it could not explain was the tell. Measured with gdb on the repro: the call is call *0x20(%rax) (slot 4) through E's VMT, and E's whole VMT is zero. Varying the shape settled it: E().who() (inherited from P through D's REAL parent) printed "own", E's own first method. So E's slot count started at the root's, not at D's.

Cause: PyClassHeaderSweep hoists a class's full registration when its parent is resolved, and defers a class with several bases to PyParseClass. E(D) has one base, D resolves, so E was hoisted, and laid out before D's statement had laid D out (size 0, UClsVirtCount still the root count). The VMT fill was correct about the numbers it was given.

Fix (pyparser.inc): PyLayoutDeferred[ci] records a class the pre-pass declined; a subclass of one declines too. It is transitive because a Python base precedes its subclass in the source.

Test: test_nilpy_a_subclass_of_a_multiply_inheriting_class_is_laid_out_after_it, .expected is CPython 3.14.4's output. It covers a flattened method, a method inherited through the real chain two levels up, a field from the parent's __init__, the subclass's own method, an override one level further down, dispatch through a base-typed name, and isinstance. Pinned v411 (bc884808fda5) SIGSEGVs on it.

Log