← board

A keyword call through a statically unknown callee does not compile

Measured at HEAD (9477001a8, self-host converged)

def mk(k):
    g = lambda x, y=2: x * 100 + y + k
    return g
a = mk(1)
print(a(x=5))
pascal26:5: error: undefined variable (x)
  near:  print  a  x >>>

CPython prints 501. The keyword NAME is being parsed as an ordinary expression identifier, which is what "undefined variable (x)" is reporting.

The same call through a PARAMETER compiles and is correct

def use(g):
    return g(y=5, x=6)
f = lambda x, y=2: x * 10 + y
print(use(f))                  # 65, matching CPython

So the lowering to pyvar_callv_kw and the runtime name matching both work. The difference is only whether the frontend could name a candidate callee at the call site: a parameter cannot be resolved statically, so that site takes the dynamic path — while a, a local holding a call RESULT, apparently is looked up and, finding no def named a, falls back to treating x=5 as an expression.

Why it matters

It is a COMPILE error, so it cannot be worked around at runtime, and it hits the common closure-factory shape. It also inverts the usual expectation: the case the frontend knows LESS about is the one that works.

Suggested first look

Wherever the argument parser decides that name = is a keyword argument rather than an expression. That decision should depend on the SYNTAX (an identifier followed by = at argument level is always a keyword argument in Python), not on whether a candidate callee was resolved — resolution can then supply the better diagnostic, but should not gate the parse.

Resolution — 2026-08-27: ALREADY FIXED, now gated

Re-measured at HEAD and on pinned v386: both binaries print 503 for the ticket's own repro. (CPython prints 503, not the 501 written above — x=5, y=2, k=1 is 5*100 + 2 + 1. The arithmetic in the original write-up was off by two; the defect it reported was real.)

Fixed by the work it was filed alongside — [[refactor-a-one-signature-record-for-every-callable-carrier]] gave the runtime dispatcher the parameter names, and the parse-time gate this ticket names went with it. No change of its own was needed.

Gated rather than just closed. It was a COMPILE error, so no runtime check could ever have caught a regression, and none of the callee shapes was covered anywhere. test/test_nilpy_keyword_call_unknown_callee.npy + .expected now carries all of them, verified against CPython:

callee reached as result
a local holding a CALL RESULT (the ticket's own shape) 503 / 508 / 110
a dict VALUE 36 / 65
a list ELEMENT 67 / 25
a PARAMETER (the control — this always worked) 605
the result of a branching call 69
a local holding a bare def name, and the static spelling 67 / 67

The sweep found two live defects, both filed, one fixed:

This ticket's own "Suggested first look" is still the right frame for that one — an identifier followed by = at argument level is always a keyword argument in Python, and the decision should not be gated on resolving a callee.

Log