← board

A star-unpack through a callable value stops at four arguments

The fact, and what it cost

from .geometry import _quad          # a DOTTED from-import binds a VALUE
_quad(out, *(quad if side > 0.0 else tuple(reversed(quad))))

lekkerzeilen/lines.py:782, and 813, 965, 1222. One written argument plus a four-element star, into def _quad(out, a, b, c, d). At run time:

Unhandled exception: TypeError: forwarded call got 5 arguments, expected 0 to 4

It named no callee, which is its own finding — see below.

Reduced to three files, no lekkerzeilen: a package whose lines.py does from .geometry import _quad and calls it with a star. The discriminator is the DOTTED module path, measured:

shape before
from geo import _quad (single-segment module) works
from . import geometry then geometry._quad(*xs) works
from .geometry import _quad (relative, name) refused
from pkg.geometry import _quad (absolute dotted, name) refused

So a from <dotted> import <name> binds the name as a callable VALUE where a single-segment one resolves it to a proc. That is not itself the bug — binding a name to a value is what CPython does, and pxx resolving it statically is an optimisation. The bug is that the value road refused an arity CPython accepts.

The carrier turned out to be the {code, recv} PAIR (tag 8, pycallback_is), which is what a plain def bound to a name becomes — not the raw code address the first guess assumed. Found by widening the raw-address rung first and watching the refusal move to a bound method reached as a value takes at most 4 arguments.

What was done

Eight where there was four, on every road a wide call can take:

Why eight, and why the ladder is not the answer

The ladder is structural, not a preference. A callable value is called through its code address, and this compiler has no variadic indirect call: an indirect call needs a static arity, so the arms must be enumerated somewhere. The METHOD road escaped this (pydyn_methl, 95e7eb26e) because it dispatches through RTTI and PyHostCall's binder already takes a TPyList. The value road has no such binder — it has a bare code pointer.

Eight because the widest star-unpack in the lekkerzeilen corpus is seven (Grid(*row[1:])), with one to spare.

THE CEILING IS A CAP AND THIS SENTENCE IS WHY IT WILL NOT GO SILENT: every overflow raises, and the refusal now names the callee and the ceiling. The failure mode to avoid is the one frankuser hit from the other side this week — PY_MAX_FIELD_CANDIDATES = 16, where candidates past the cap were dropped without being counted, and which survived because nobody wrote down that the cap was a cap.

The non-ladder answer, so the next seat does not widen eight to sixteen. A trampoline: one entry point taking a TPyList, which marshals the arguments into the platform's own argument registers/stack and jumps to the code address — PyHostCall's pointer-family arm already does exactly this shape for up to five (pa: array[0..4] of Int64, pvHold for the by-address Variants), so the mechanism exists and only its own ceiling and the per-ABI half are missing. That is the work: generalise that marshalling loop to N and give the value road the same door the method road got. It is a Track A / ABI job rather than a frontend one, which is why it was not taken alongside a demo wall, and it retires this ticket, the four TPyCb* families, and MAX_STAR_DYN together.

The diagnostic was half the cost

forwarded call got 5 arguments, expected 0 to 4 named no callee, out of a binary with 11711 procedures. Locating it meant an AST census over 34 modules for every def whose parameters are all defaulted: eight candidates and no answer. pystar_check_arity and pystar_check_arity_kw now take the callee's name, and PyStarGuardName appends it at all five emit sites so a sixth added later cannot go back to the nameless form. The name goes in FRONT of the existing sentence, so the wording two Makefile assertions, four fixtures and several tickets already quote is untouched.

For a callee reached as a value there is no Procs[] entry to ask, so PyDynCalleeName takes the source spelling — the variable for fn(*xs), the attribute for h.cb(*xs), and the identifier token before the ( otherwise. It must be read BEFORE the argument list is parsed, because that last arm is token-positional.

Test

test_nilpy_wide_call_through_a_callable_value.npy, .expected from CPython: all four carrier families at 5 and 8, defaults filled at a wide arity, written arguments as well as a star, a written argument BESIDE a star (the arrangement a star-only fixture never reaches), a lambda (the closure road, which never had a cap), and an attribute holding a callable.