← board

NilPy: a call returning a CLASS lost its class identity (silent, then SIGSEGV)

Found 2026-07-20 while wiring slices ([[feature-nilpy-bytes-and-slices]]) — the slice hook would not fire on self.memory[a:b] because the field had no class identity. That turned out to be the symptom, not the cause.

Symptom

class VM:
    def __init__(self) -> None:
        self.memory = bytearray(16)
        self.memory[0] = 65
    def show(self) -> None:
        print(self.memory[0])     # 65 — correct
        print(len(self.memory))   # SIGSEGV

self.memory[0] read back correctly, so the field looked fine. len() on it segfaulted: with no class identity the overload set picked len(const s: AnsiString) over len(b: TPyBytes) and read a class pointer as a string handle. Exactly the failure the str-method branches of PyInferExprType already carry comments about — a third instance of the same family.

Root cause

PyInferExprType's plain-function-call branch (pyparser.inc) typed the call from Procs[procIdx].RetType but never recorded WHICH class when that type was tyClass:

procIdx := FindProc(name);
if procIdx >= 0 then tk := Procs[procIdx].RetType;   { class identity dropped }

So the class field pre-pass got tk = tyClass with PyInferLastCi = -1 and stored the field with fldRec = REC_NONE. Every consumer that resolves a record from the field — overload resolution, the default indexed property, the new slice hook — then saw REC_NONE.

Affects any NilPy field or local inferred from a pylib function returning a class: bytearray(), bytes(), and the same shape for lists/dicts.

Sibling bug fixed in the same sweep

The method-call branch DID record a class, but assigned a rec id into PyInferLastCi, which is a class index:

if tk = tyClass then PyInferLastCi := ProcRetRecId[UMthProc_[si]];

Consumers then add REC_UCLASS_BASE a second time. Both branches now convert explicitly (- REC_UCLASS_BASE).

Fix

pyparser.inc, both branches of PyInferExprType: record the returned class as a class INDEX. Landed with the slice work.

Gate

make test-nilpy green (incl. the new test_nilpy_slices.npy), --tier quick GREEN, self-host byte-identical. FPC-clean relative to HEAD (the 6 open pyparser.inc FPC errors are the pre-existing fpc-bootstrap regression, verified identical on committed HEAD).