Measured 2026-09-13 (frankH), at the tree carrying the module-qualified fix
fec1abd13's sibling: the qualified value read now normalises the def
(test_nilpy_a_module_qualified_def_is_a_value*.npy), but only where the scan
can SEE the read. Two reductions, four files each, both in one import order and
correct in the other.
The return side. retkinds.npy holds def r_three(): return ([1], [2], {"m": 3});
retkreader.npy does Holder(details=retkinds.r_three) and calls it back through
the field, exactly as lekkerzeilen's vessel.py:604 does:
import retkinds # the program imports the DEFS' module first
import retkreader # ...and the reader second
print(retkreader.three().fittings())
CPython ([1], [2])
pxx TypeError: expected a str, list, dict or bytes, got int
Reverse the program's two imports — or drop the first, so the reader pulls the
defs in itself — and it is correct. That is what
test_nilpy_a_module_qualified_def_is_a_value_across_modules.npy does, and it is
written that way ON PURPOSE with this ticket named beside it: a fixture that
passes in one order and fails in the other must not be spelled in the order that
passes (CLAUDE.md, the ordered-list rule).
The parameter side, which is worse. Same order, annotated parameters:
# ga.py
def g(x: int): return x + 1
def gs(s: str): return s + "!"
def gl2(xs: list): return len(xs)
# gb.py
import ga
def run():
f = ga.g; print(f(4)) # 5 -- correct
h = ga.gs; print(h("a")) # a! -- correct
k = ga.gl2; print(k([1,2,3])) # SIGSEGV
# d6.py
import ga
import gb
gb.run()
So this is not only a return-convention gap: an unnormalised def also reads its ARGUMENTS through the wrong convention, and a class-typed parameter crashes.
Why a wider scan is the wrong fix
The scan cannot be widened to cover tokens that do not exist yet. A module is
lexed when its import is REACHED, so the reads that matter may be lexed after the
def is compiled, and no amount of scanning fixes an ordering. Nor may the
normalisation be made unconditional: that is
bug-nilpy-import-name-forces-function-object-abi, which is recorded as fatal for
a Callable[...] parameter.
The shape of the answer
Put the RETURN KIND on the signature record and marshal in the bridge, the way
PyHostCall already does for a method (rk := mi^.RetKind, then a thunk chosen
by the RESULT with every argument one register wide, and PXXObjRetain on a
class result). TPySigRec is {Code, ReqN, TotN, Star, Dflts, Names} and
TPyBoundRec carries only IsFunc: Boolean, so today the pair bridge has no way
to ask. PYSIG_SIZE is 48 with fields at 0/8/16/24/32/40 — a RetKind at +48
(size 56) and a ParamKinds pointer beside it is the mechanical part; the
dispatch is PyHostCall's ptrFamily loop, which is exactly what
bug-n-a-star-unpack-through-a-callable-value-stops-at-four-arguments already
names as its own non-ladder answer. One trampoline closes both tickets and
this one, which is the argument for building it rather than widening anything
again.