Measured 2026-09-13 (frankH), at ee5b6adc7 and under stable_pinned
Two defs, one file, nothing else in it:
def c_rsplit(label: str):
return label.rsplit(" ", 1) # ['C', 'minor'] correct
def c_split(label):
return label.split(",") # raw memory; CPython ['a', 'b', 'c']
PXXDBG=n.ret on that file, both compilers:
| def | n.ret | run |
|---|---|---|
c_rsplit(label: str) |
tk=22 then tk=6 rec=50 | correct |
c_split(label) |
tk=23 | garbage |
Same under stable_linux_amd64/default/stable_pinned, which predates this
week's work — so this is older than the fixes it was found beside.
Where it is, and why the obvious widening is refused
PyInferReturnType's method-call arm (compiler/pyparser.inc, the PyRetMethodType
block). Three sub-arms in an else if chain:
- the receiver resolves to a user class →
PyRetMethodTypeanswers; - the receiver is a local the scan cannot type → tyVariant (added 2026-09-13
for the ARGUMENT-decides family; see
test_nilpy_a_returned_method_call_takes_its_type_from_the_argument.npy); - a str method on a receiver bound in this def →
PyStrMethodInfo's row, but only when that row is a SCALAR.
split/rsplit/partition/splitlines are tabulated as tyClass, and arm 3
pairs cur := smRetTk with PyInferLastCi := -1 — right for a scalar, and for
a class it produces "a class result with no class", which is worse. The source
says so in its own words and names the regression it caused (7ddcb9650). So
the arm declines, cur keeps whatever PyInferExprType took from the argument
list, and "," makes it a string.
Do not just extend arm 3 to the class rows — that is the change that was reverted. Two candidate directions, unmeasured:
- give arm 3 the class IDENTITY for those four rows (
PyInferLastCi:= the TPyList ci), so the answer is tyClass WITH a class rather than without one; - or answer tyVariant for them, which is what the run-time dispatch produces anyway and which needs no class identity at all.
The annotated row must stay tk=6 rec=50 either way, and it reaches that by a
different route (PyInferExprType can type an annotated label), so it is the
control.
Also worth a look while in here
c_rsplit prints two n.ret lines with DIFFERENT kinds — tk=22 from one pass
and tk=6 rec=50 from the other. The routine's own comments call two passes
disagreeing "a silent ABI mismatch". It works today; nobody has established why.
How it was found, and the warning in it
Reduced from lekkerzeilen. It was first measured as WORKING in a seven-def probe and is broken when it is the only thing in the file — the contaminant was inside the probe, in the right population, and honest. "Would this row still pass if it were the ONLY thing in the run?" answers NO here, which is the question that caught it.
Log
- 2026-09-21 — resolved; this names the commit that carried the resolve, which is not always the one that carried the change — commit c63455470.