The three conditions, each measured on its own
- The argument's class declares
__iter__. Not "a dunder":__len__,__repr__,__eq__and two extra plain methods are all clean. - The call omits a trailing defaulted argument. The same call with the default supplied is correct.
- The callee is defined in a NON-MAIN module. A def in the main module resolves by another path and never reaches the retry.
Conditions 1 and 2 were measured here; all three were confirmed
one-variable-at-a-time by the lekkerzeilen-c8 seat, which also supplied the
first four-line reproducer and established that ui.py, the for loop, the
imported tuple, the local name shadowing the method, and which method is called
are all irrelevant.
What said it, and it was the compiler's own decision point
Instrumented at the branch rather than inferred from the crash:
FIXITER procIdx=-1 i=0 nArgs=1 pRec=0 listCi=34 base+list=50
FIXITER retry name=x2 -> procIdx=-1
FIXITER procIdx=2097 i=0 nArgs=2 pRec=0 listCi=34 base+list=50
The one-argument call fails to match, is drained, fails to match again, and the drain is never taken back. The two-argument call matches at once and nothing is touched.
The fault is mov (%rdi),%rax ; call *0x28(%rax) -- a virtual dispatch through
the object's class pointer, where the object is a TPyList and slot 0x28 of its
table holds data, not code.
Why the crash looked like a threading or a default-value bug
The argument arrives at the call site perfectly formed -- the caller's variant
slot holds {VType=7, <the drained list>} -- and the DEFAULT itself binds
correctly, so a probe that prints title reports T and clears the obvious
suspect. The damage is only visible at the first USE of the earlier argument,
in the callee, in a class the source never mentions.
The other direction, which the fixture also pins
The speculative drain must still happen where it is meant to. sum(h) and
sorted(h) over the same object are rows in the fixture, because "never drain"
would otherwise pass it.