pypow_v's integer path silently wraps past 64 bits instead of promoting
print(2 ** 70)
CPython: 1180591620717411303424
pxx: 0
Found 2026-07-31 while gating [[feature-pyeval-power-operator]] (which only
needed exec()'s missing ** grammar wired to the existing pypow_v, and did
that). This is a separate, pre-existing gap in pypow_v itself
(compiler/builtin/pylib.pas), present in the ordinary COMPILED NilPy **
too (parser.inc's ParseFactor lowers to the same pypow_v) — not
introduced by, or specific to, the exec() path.
Cause
pypow_v's integer branch does repeated squaring in plain Int64:
n := pyvar_to_int(b);
ibase := pyvar_to_int(a);
ir := 1;
while n > 0 do
begin
if (n and 1) = 1 then ir := ir * ibase;
ibase := ibase * ibase;
n := n shr 1;
end;
No overflow check anywhere in the loop — unlike pymul_v/pyeval's own
PyIMul, which detect an overflowing * (dividing back through and
comparing) and promote to the bignum runtime (PromoOp/PXXPromoMul) when
it does. pypow_v never had that check, so a big-enough integer result just
wraps mod 2^64 with no error and no promotion.
Shape of a fix
Mirror PyIMul's overflow probe (r := ia * ib; if (ia <> 0) and (r div ia <> ib) then <promote>) inside pypow_v's squaring loop, upgrading to the
PXXPromo* primitives (PXXPromoInit/FromVariant/Mul/ToVariant/Clear
— see pyeval.pas's PromoOp for the exact calling shape) once either
operand overflows. Requires pylib.pas to uses promoint (it currently
does not depend on that unit at all — check for a circularity before adding
it; pyeval.pas already uses both without issue, which suggests none, but
verify).
Gate
make test-nilpy + self-host byte-identical, plus 2 ** 70, 2 ** 100, and
a negative-base odd/even exponent case ((-2) ** 65, (-2) ** 66) diffed
against CPython, through BOTH the compiled ** and exec("... ** ...").
2026-08-04 — the premise about pymul_v is WRONG, and the real fork is a size trade-off
This ticket says pypow_v has no overflow check "unlike pymul_v/pyeval's own
PyIMul, which detect an overflowing * … and promote". Half of that is false,
and it is the half that would have made the fix cheap.
pymul_v does NOT promote. Its numeric arm (pylib.pas) is a plain Int64
multiply with no probe at all:
r^.VType := 2;
r^.Payload := pyvar_to_int(a) * pyvar_to_int(b);
Only pyeval's PromoOp/PXXPromoMul promote, and pyeval is the unit that
uses promoint. Measured, not read: routing pypow_v's squaring loop through
pymul_v over variants was implemented and changed nothing — 2 ** 70
still printed 0. Reverted rather than left in, since an inert change is how a
wrong premise gets a foothold.
So the fix costs something, and the choice is a real one
pylib uses promointand the probe goes inpypow_vas this ticket describes. Correct and complete — it also fixespow(a, b), which routes to the same place. Butpromointis what the DCE note measures at 35KB → 92KB ([[project_promotable_int_stages123]]), and pylib is linked into EVERY NilPy program, so every binary pays it whether or not it ever raises anything to a large power. On a ~1.27MB NilPy binary that is roughly +4%.- Put the promoting power in pyeval (which already uses
promoint) and point the**lowering at it —PyMakePowinpyparser.incis now the ONE place that chooses the callee, so this is a one-line redirect. Costs nothing for programs that do not link pyeval. Butpow(a, b)— pylib's function — still wraps unless it is redirected too, and it cannot reach pyeval, so the two spellings of the same operation would diverge. That asymmetry is worse than the bug for anyone who hits it. - Raise OverflowError instead of promoting. Cheap, honest, no new
dependency, and strictly better than silently printing 0 — but it is a
deliberate divergence from Python, where
2 ** 70simply works.
Route 2's asymmetry is the thing that makes this a judgement call rather than a
task: option 1 is the only one where 2 ** 70 and pow(2, 70) agree, and it is
the one that costs every program 57KB.
Not implemented. Recorded here rather than guessed at; the ticket keeps its
priority and its gate. Note pow(base, exp, mod) (the three-argument modular
form, added today) is unaffected either way — it never builds the full power.
Log
- 2026-08-04 — resolved, commit 814f2abfe.