← board

A def stored in a NAME and then called SEGFAULTS

def f(ch: str) -> str:
    return "hi " + ch

def g(cb) -> str:
    return cb("x")

print(g(f))     # ok — "hi x"
x = f
print(g(x))     # SIGSEGV

CPython prints hi x twice. The difference is only whether the function value passes through a NAME. It reproduces with x as a module global or as a local, and the same shape crashes when the callee is a METHOD:

class D:
    def m(self, cb) -> str:
        return cb("x")
D().m(f)        # SIGSEGV

Cause

x = f goes through PyMakeFuncValue (pyparser.inc ~5397), which boxes the function as pybound_new(@f, nil) — a VT_BOUNDMETHOD (tag 8) variant whose payload is a pointer to a {Code, Recv} pair OBJECT, not a code address.

cb("x") goes through PyMakeDynCall (pyparser.inc ~5184), whose callee address is pyvar_callee_addr(v) = PPyVarRec(@v)^.Payload — for tag 8 that is the pair object, which is then jumped to. pyvar_callee_addr's nil check does not fire (the payload is a valid pointer), so there is no diagnostic.

g(f) works because the argument position boxes the raw proc address rather than a bound pair, so the payload really is the code.

Fix sketch

Two levels:

Why it matters

songformatter's analyze_key(chords, chord_to_notes=notes_of, ...) forwards a module-level def through a parameter into every detector's analyze, so the whole key-analysis entry point segfaults. Found while verifying [[bug-nilpy-slice-of-variant-local-returned-is-unusable]]; sibling of [[bug-nilpy-callable-in-local-var-call-does-nothing]] (that one is the LAMBDA half and fails silently; this one is the def half and crashes).

Repro / gate

The snippet at the top, plus the method form. Then, in a cp -r of ~/songformatter:

from key_analysis import analyze_key
NOTES = {"C": ["C", "E", "G"], "F": ["F", "A", "C"], "G": ["G", "B", "D"],
         "Am": ["A", "C", "E"], "Dm": ["D", "F", "A"], "Em": ["E", "G", "B"]}
def notes_of(ch: str) -> list[str]:
    return NOTES.get(ch, [])
res = analyze_key(["C", "F", "G", "C", "Am", "F", "G", "C"], chord_to_notes=notes_of)
print(res.final.winner.label)   # CPython: C

Log