← board

What was measured

2026-09-13 at c139938e5476. One class, one alias, seven rows, each wrapped so one failure does not mask the rest. CPython answers every row.

class Gl:
    @staticmethod
    def sm(a):     return "S" + str(a)
    @classmethod
    def cm(cls, a): return "C" + str(a)
    def inst(self, a): return "I" + str(a)

alias = Gl

row                                    CPython   pxx
alias.sm(1)                            S1        S1      <- the CALL path, fixed
(lambda g: g(1))(alias.sm)             S1        AttributeError: type object 'Gl' has no attribute 'sm'
str(alias.sm is not None)              True      AttributeError: (same)
getattr(alias, "sm")(1)                S1        AttributeError: (same)
str(hasattr(alias, "sm"))              True      **False**
(lambda g: g(1))(alias.cm)             C1        AttributeError: type object 'Gl' has no attribute 'cm'
(lambda g: g(1))(alias().inst)         I1        I1      <- an INSTANCE method is fine

The row that matters most is the hasattr one

Five of the six failures RAISE, which is loud. hasattr returns False, which is not: it is a legal answer, the program continues, and a feature-detection branch quietly takes the absent arm. That is the same class of silent wrong answer as bug-n-getattr-cannot-see-a-method-and-segfaults-through-a-dynamic-receiver, where getattr(o, "m", None) returned the DEFAULT and the caller's containment test answered False for every position.

Cause, and why the two halves disagree

Two separate mechanisms reach a member through a class held as a value:

So fixing the call did not fix the read, and it could not have.

The fix

An analogous PyEmitClsMethBinds(ci) publishing the PROC ADDRESS of each @staticmethod / @classmethod beside the attributes, and a tag-11 read that returns it as a callable. pyclsattr_bind already takes a kind, so the registry has somewhere to record that this entry is a method rather than a slot.

And whatever it hands back MUST be normalised to the function-object ABI. PyMethodUsedAsValue is the gate. A method published to a registry and called back unnormalised does not crash at the call: it answers '' for a string, SEGFAULTS on return for an int, and behaves perfectly for a method returning None, because None needs no hidden destination. That is measured -- it is what the literal-getattr fix had to grow PyModuleGetattrsLiteral for -- so a new publication route will pass every test whose methods return nothing.

hasattr and getattr must move together. They are one mechanism since 7fd25913c routed the literal name to the same runtime resolver as the computed one, and the whole reason that ticket existed was the two spellings disagreeing.

Not established

What a classmethod's cls should bind to through this route (the registry would have to carry the blob, which it already keys on); whether staticmethod read as a value should compare equal to itself across two reads; and no census of the read-as-a-value spelling in real code -- lekkerzeilen CALLS gl.get_string(...) and does not read it, which is why the call path landed alone.

See also

NO CORPUS DEMAND TODAY — measured, not assumed (2026-09-13)

frankh-30 grepped the running lekkerzeilen across lekkerzeilen/*.py and lekkerzeilen/platform/*.py: the demo never reads gl as a value. No hasattr(gl, ...), no getattr(gl, "<method>"), no f = gl.clear. It only ever CALLS through it. The one value-ish read in the tree is platform/_ctypes_backend.py:326, getattr(gl.lib, "_name", "libGL") — and that is the ctypes backend, which is not the one pxx builds, and it reads gl.lib, an attribute, not gl itself.

So the p45 stands on the DEFECT CLASS, not on a blocked program: hasattr answering FALSE rather than raising is a silent-wrong-branch bug, and that is worth keeping regardless of who is calling it today. What this measurement removes is the argument that the demo needs it — it does not. Recorded so the next reader does not re-ask a question that has been answered against the corpus.

Sibling context: [[bug-n-a-class-reached-through-a-unit-alias-is-not-a-value]] and [[bug-n-a-from-imported-class-loses-its-methods-unless-it-is-renamed]] both closed the same day on that corpus; this arm and the two-carriers p40 are what is left of the group.