Measured
2026-09-14, at the fix for bug-n-a-promotable-int-field-is-boxed-as-an-object.
Three files, one line apart:
| file | lowering | hasattr(h, "K") |
CPython |
|---|---|---|---|
K = 7 alone |
copy-at-construction (a field) | True | True |
K = 7 plus H2 = H |
shared slot | False | True |
K = 2 << 20 plus H2 = H |
shared slot | False | True |
H2 = H is what makes PyClassKinUsedAsValueEx force caClassW, in
compiler/pyparser.inc (both class-attribute branches call
PyClsAttrEnsureGlobal and then skip AddUField). PXXDBG=a.reclayout
prints no rec=H line at all for those two.
Why it is not the promotable-int ticket
That one was a missing KIND in two boxers, and every route reached the field.
Here there is no field to reach on any route, for any type, and the two
lowerings are a deliberate design decision recorded in
decide-nilpy-class-attribute-instance-read-model. The fix is a route from
pydynattr_get/pydynattr_hasattr to the shared slot, keyed by the
RECEIVER's class -- which is the question FindClassVar already answers at
compile time and PyClsAttrRefGet already answers at run time for a class
reference.
Not blocking lekkerzeilen
The demo's wall was the promotable-int one. This was found beside it and is filed rather than fixed because the route it needs is a design choice about where the runtime registry lives, not a missing arm.
The shape to start from
compiler/builtin/pylib.pas: pydynattr_get (the field/property/class/
getattr ladder), PyClsAttrRefGet (reads a shared slot given a class
reference), PyBoxByKind (shared boxing, now including kinds 27/28).
A positive control must include a LITERAL-valued attribute: the bug is about
the lowering, and a fixture that only tries expression values would pass for
the wrong reason once the promotable-int arms are in.