← board

NilPy: a lifted lambda always discarded its own return value

Found while researching [[bug-nilpy-callable-value-abi-sorted-key-and-builtins]] — not previously documented as its own ticket, and distinct from anything sorted() does.

Repro

def outer():
    h = lambda s: len(s)
    return h("abcde")
print(outer())          # CPython: 5    pxx (before): None

A lambda whose body is a single call gets LIFTED to a real compiled proc (feature-nilpy-lambda-compiled-closure's PyCompileLambdaBody), not interpreted. The lifter was first built for a side-effect-only handler shape (lambda: on_click()) and its body-compile treated the parsed expression as a discard-only statement — "the expression IS the statement" — so $pyresult was never written and stayed at its zero-initialised None. This has nothing to do with sorted(), map, or any caller: calling a lifted lambda directly and reading its value answered None unconditionally, for every value-shaped lambda that happened to qualify for lifting.

Fix

Wrap the parsed expression in a real AN_EXIT node — exactly what a written return <expr> produces — so it stores into $pyresult through CompileAST's ordinary return-coercion path, the same one every other return in the compiler already uses.

Known remaining gap

A tyClass/tyRecord result is excluded: lambda s: log.append(s) (append returns Self) boxed into $pyresult and released by a caller that discards it drove the SAME captured log to refcount 0 and freed it out from under the enclosing scope that still owned it — traced with -dPXX_OBJTRACE (4 retains / 5 releases where the scalar/string case balances at 2/2). This is an aliased-captured-object ownership gap around the bound-fn call bridge — the same territory [[bug-nilpy-bound-fn-closure-objects-are-never-freed]] already tracks, not something to special-case away here. The side-effect-only shape (returning Self, discarded by the caller) keeps its old discard-only behaviour, unchanged and still correct.

Gate

make test-nilpy + self-host byte-identical, plus a .npy covering a value-returning lifted lambda (int, string, arithmetic, .upper()) AND the side-effect-only shape, diffed against CPython.

Resolved

Fixed and gated green — commit bd6d7ba9085d7f0528c7cd6339e0442f19b283c3. Regression: test/test_nilpy_lifted_lambda_return_value.npy.

Log