← board

min(a, b, c) / max(a, b, c) — only the 2-argument and iterable forms exist

Found by proactive CPython-diff sweeping.

print(min(3, 1, 2))

CPython: 1. pxx: compile error —

no overload of min matches these arguments
  argument types: (Integer, Integer, Integer)
  candidates:
    min(Variant, Variant)
    min(Int64, Int64)
    min(Double, Double)
    min(class)
    min(AnsiString)

min/max (compiler/builtin/pylib.pas) are ordinary overloaded Pascal functions resolved through the normal call/overload machinery — there is no special parser dispatch for them at all (unlike enumerate/zip, which get a PyExprMode-gated ParseFactor branch). Only the 2-argument scalar overloads and the min(iterable)/max(iterable) single-container form exist, so any call with 3+ positional arguments has no matching overload.

Fix direction (not attempted here)

Adding fixed-arity overloads (3-arg, 4-arg, ...) doesn't generalize — Python's min/max accept any number of positional arguments. The clean fix is a PyExprMode-gated special case in ParseFactor (mirroring the enumerate(xs)/zip(a, b) value-form branches already there) that: on seeing min(/max( with 3+ comma-separated top-level arguments ahead (via the existing CountCallArgsAhead helper) and no user shadow (FindSym('min') < 0), parses each argument and folds them through the EXISTING, already-correct 2-argument overload left-to-right — min(a, b, c)min(min(a, b), c) — rather than reimplementing overload resolution. The 2-arg case's own argument-type dispatch (Int64/Double/Variant, matching OverloadArgRank) already picks the right concrete function per pair, so the fold only needs to chain calls, not re-derive typing.

Not attempted this pass — time-boxed during a broader CPython-diff sweep; building the AST-node-chaining call construction safely (reusing whatever the ordinary 2-arg call-parsing path already builds) needs its own careful look rather than a rushed patch to shared ParseFactor call-dispatch code.

Gate

A .npy case with min/max at 2, 3, 4+ positional args, mixed int/float/string arguments, diffed against CPython, gated in test-nilpy + --tier quick + self-host byte-identical.

Done 2026-08-03

The ticket was partly stale: all-Variant 3- and 4-argument overloads had since been hand-written into pylib.pas, so min(3, 1, 2) and min(3, 1, 2, 0) already worked and only 5+ still had no candidate — which is exactly the "adding fixed arities one at a time never terminates" the ticket predicted, caught mid-sequence.

Implemented as the ticket's fix direction, with one change of substrate: PyParseVariadicMinMax (pyparser.inc), dispatched from ParseFactor under PyExprMode, folds left-to-right through the EXISTING 2-argument overload — min(a,b,c) = min(min(a,b),c). Each pair resolves through MatchProcCall(nm, 2, argTypes, False) on the two operands' actual types, NOT FindProc(nm): FindProc returns one proc and never consults overloads (project_findproc_by_name_ignores_overloads), which would have silently picked a single arm for every operand shape. Folding keeps the int fold an int rather than boxing through the Variant arity overloads.

The guard is the part that needed measuring

The branch is claimed only when CountCallArgsAhead >= 3 and PyAnyProcWithArity(name, n) is false. Without the second test the fold would steal calls an ordinary overload can already serve — pylib's own 3/4-arg overloads, and a NilPy def min(a, b, c, d, e) over the builtin. Verified: a 5-argument user shadow still returns its own answer (100), and the fold takes over only where nothing of that arity exists.

Measuring that guard turned up a separate, pre-existing bug, filed as [[bug-nilpy-user-def-loses-to-pylibs-variant-overload-at-the-same-arity]]: a user def min(x, y, z) compiles and is then silently not called, because pylib's all-Variant 3-arg overload outranks it. The fold provably cannot reach that call (pylib has arity 3, so the guard rejects the branch), and the same shadow at 2 and at 5 arguments works — it is an overload-ranking gap, not this change.

Verified

test/test_nilpy_min_max_variadic.npy (new, registered in both test-nilpy Makefile sites): min/max at 3, 4, 5 and 7 arguments over ints, floats and strings; expression arguments; variables; the 1- and 2-argument and iterable forms unchanged; and the 5-arg user shadow. All 17 lines byte-identical to CPython. tools/gate.sh quick GREEN (self-host fixedpoint + --tier quick + FPC seed canary).

Log