← board

Measured 2026-10-03 (frankuser)

def g(xs, k):
    for x in xs:
        yield x + k
def wrap(it):
    return g(list(it), 1)
def gs(s):
    s = s + "+"
    yield s
def wrap_str(a):
    return gs(a + "!")
print(list(wrap((7, 8))))       # pxx: []        CPython: [8, 9]
print(list(wrap_str("ab")))     # pxx: ['00']    CPython: ['ab!+']

Variant parameters were already safe: they ride a pycell_new cell assigned through the managed-variant path, and pycell_free_at releases it at done. Class and str parameters had no equivalent -- the done path walks skLocal symbols only, so a parameter slot was never released, and nothing retained it.

Fix

Census, -dPXX_ALLOC_CENSUS: a mix of for-in over g(fresh list), break, a named list, sum(), a wrapper returning the generator, next() on a stepped generator, a generator never started, and class-instance arguments -- 10/100/1000 rounds live 15/12/8. String arguments 10/100/1000 live 26/16/31. Main's compiler on the same program raises TypeError (it read freed memory).

Test: test/test_nilpy_a_generator_outlives_its_caller_and_its_arguments.npy.

A third seeding site, found by the HD sweep

PyEmitGeneratorIterCursor (the synthesized __pxx_gen_iter__ that makes a class with a generator __iter__ iterable at run time) stored self with a raw SlSet. Once done/close released class parameters, that store was released without having been retained: test_nilpy_a_generator_closed_early_or_exhausted_does_not_leak reported "RELEASE of a FREED object" twice, inside a def only. It now seeds through GenSeedArg like the other two sites; the test is clean and its census flat (live 83/85 at 100/1000 passes).