← board

Multi-parameter lambdas are still interpreted

Measured

f = lambda a, b: a + b        # 7   — correct
g = lambda a, b, c: a * b + c # 10  — correct

Answers match CPython. But the bodies are still shipped as source text and run by the tree-walker:

strings <bin> | grep -c '^return '   ->  2      (one per lambda)

Zero for a 1-parameter lambda since the parent fix. Expect the same ~6.9 µs vs 1.0 µs per-call gap measured there.

Why it is gated

PyParseLambdaStub's lift is guarded by nParams <= 1. The reason is the bridge: pyboundfn_callv(objptr, a0, res) passes exactly one argument, and a lifted proc reserves parameter 0 for the lambda's own argument with every capture bound after it. A second own parameter has nowhere to ride.

pyboundfn_callvn(objptr, a0, a1, a2, ...) already exists — the multi-argument bridge is there. What is missing is the lowering choosing it and the capture layout reserving N own slots instead of 1.

Why the priority is only 45

Every key= / map / filter / sorted callback is 1-parameter, and that is the overwhelming majority of lambdas in real Python — which is why the parent fix took that case first. Multi-parameter lambdas show up in reduce, custom comparators, and callback APIs that pass several values, so this is real but far less hot.

Fix direction

Reserve nParams own slots at the head of the lifted parameter list rather than exactly one, bind captures after them, and route the call through pyboundfn_callvn. The existing zero-parameter case (a dummy $lamarg0 absorbs the bridge's single argument) shows the slot bookkeeping is already parameterised in shape, just not in count.

Watch PY_MAX_CAPS: own parameters and captures share the bound-slot budget, so N own parameters reduce the captures a lambda can hold.

Gate

The two lambdas above compiled (strings ... | grep -c '^return ' = 0), answers still matching CPython, the NilPy suite green, and self-host fixedpoint.


STILL LIVE — an output diff cannot see this one (verified 2026-08-11, claude-an-1)

f = lambda a, b: a + b and g = lambda a, b, c: a * b + c both return the RIGHT VALUES on pinned and HEAD, so a CPython output diff shows no difference and this ticket looks stale in an automated sweep. It is not: the claim is about HOW they run, not what they answer.

The tell (per [[project_nilpy_every_lambda_is_an_interpreted_source_closure]]) is the lambda's SOURCE TEXT embedded in the binary:

$ strings ./prog | grep -c 'a + b\|a \* b + c'
2

Still 2 at HEAD — both lambdas are still interpreted pyeval source closures. Ticket stands.

Resolution (2026-08-11)

The runtime was already parameterised in own-argument count and nobody had told it: pyboundfn_callvn carries a0/a1/a2 and places NOwn of them ahead of the bound slots, and pyboundfn_setown exists precisely to set NOwn. Only this lowering hard-coded one own slot. So the fix is the ticket's own direction, with no runtime work at all:

The bug the first attempt introduced is the part worth remembering: the capture-bind loop ran for j := 1 to lamN - 1, so with two own parameters it bound the lambda's own b as bound slot 0 — over lamSyms[1] = -1, because an own parameter binds no enclosing symbol. That built an AN_IDENT on symbol -1 and the IR came out with a garbage type kind: invalid type kind in IR node, pointing at the end of the file. The loop now starts at lamOwn. Same hardcoded 1 in the defaults' varMask scan, fixed with it.

Verified: strings <bin> | grep -c on the lambda bodies is 0 where it was 2, answers matching CPython for 2- and 3-parameter lambdas with and without a capture, a lambda passed as a value and called through a parameter, key=, and the 0- and 1-parameter cases that already lifted. make test-nilpy EXIT=0, gate.sh quick GREEN.

Two adjacent findings, both PRE-EXISTING on pinned and neither caused here:

Log