← board

max(*[xs]) / min(*[xs]) bind the wrong overload

Repro

print(max(*[[4, 9, 2]]))     # CPython 9      pxx [4, 9, 2]
print(min(*[[4, 9, 2]]))     # CPython 2      pxx [4, 9, 2]

The direct spelling is correct on both pinned and HEAD:

xs = [4, 9, 2]
print(max(xs))               # 9    agrees
print(max([4, 9, 2]))        # 9    agrees

Two spellings of one operation answering differently is the tell.

Where it comes from

PyStarForwardCall(procIdx, listNode, dictNode) takes the callee's procIdx already resolved — chosen before the forwarded argument's runtime type is known. max/min are overloaded (a 2-argument scalar form and a container-taking form), and the resolution that happens on the star path picks the scalar one, so a forwarded single list argument binds where the direct call would have selected the container overload.

Note this is the by-name-resolution family (project_nilpy_byname_findproc_lowerings_are_the_unchecked_population): a lowering that names a callee and skips overload resolution. Worth grepping the other star entry points for callees with more than one overload before closing — sum and sorted have a single signature and are unaffected, which is exactly why the sweep only caught max/min.

Deliberately NOT covered by the star-forwarder test

test/test_nilpy_star_forward.npy was extended for the container-parameter fix and stops short of max/min on purpose: a test that covers someone else's open bug goes red for a change that did not cause it.

Gate

.npy diffed against CPython: max(*[xs]), min(*[xs]), both against their direct spellings, plus a two-scalar forward (max(*[1, 2])) which must keep selecting the scalar overload. Per-fix loop.

RESOLVED — the ticket's cause was wrong; it is not overload resolution at all

Filed as "the star path resolves the callee to one procIdx before it knows the argument is a container". Measured, and that is not what happens. max/min never reach the forwarder: parser.inc has an arm just for them (PyStarIsIterableForm) that rewrites max(*xs) to the single-argument iterable form max(xs) — deliberately, from [[bug-nilpy-star-unpack-into-a-fixed-arity-builtin]], because the run-time arity dispatch used to look for a three-argument max.

That rewrite is a real equivalence, but only above one element. CPython's max(*xs) is max(xs[0], xs[1], …):

So the boundary is the starred COUNT, not the argument's type, and it is a run-time fact — which is why no compile-time overload choice could have fixed it, and why max(v) on a variant holding a list was right all along (checked: via a dict value, a list element, and a def parameter).

Fix. pystar_iterable(l: TPyList): TPyList in compiler/builtin/pylib.pas returns l unchanged for a count other than 1, and pystar_as_list(l.at(0)) for a count of 1. For two or more this is byte-for-byte the previous lowering. pystar_as_list, not a cast, because the single element may be any iterable — max(*["abc"]) is CPython's max("abc") = 'c'. The frontend wraps the star operand in it at the one PyStarIsIterableForm arm (PyStarIterableNode); zip(*rows) shares PyZipStarOperand but not this arm and is unchanged.

Verified byte-identical to CPython across 12 rows: the one-element list, a float list, a one-element str, two-and-more scalars, a named xs, string comparison, zip(*rows), and the direct max(xs) control. Regression rows added to test/test_nilpy_zip_star_and_n_way.npy — which already carried the max(*xs)/min(*xs) coverage from the earlier ticket, so this is the same file the equivalence was first recorded in — with its .expected updated. tools/gate.sh quick GREEN.

No repin needed despite the compiler/builtin change: pinned compiles against its own frozen compiler/builtin/*.pas, and only a HEAD-built frontend emits the new call, so nothing that builds against pinned can reach it.

Log