The float ** operator evaluates its exponent twice
Found 2026-08-16 while reading PyMakePow for
[[bug-nilpy-float-overflow-answers-inf-where-cpython-raises]] — not from a
report. Nothing failed; the VALUE was always right.
Repro
n = 0
def f():
global n
n = n + 1
return 2.0
print(1.5 ** f())
print("calls:", n) # CPython 1 NilPy 2
Cause
PyMakePow's static float route sends the base through pypow_dom (the guard
that refuses a negative base with a fractional exponent by name) and then calls
the RTL's Power. The exponent reaches both: once inside the guard's
argument list, once as Power's second argument. The AST is a tree, so a node
referenced twice is EMITTED twice, and CloneAST does not help — a clone is a
second subtree, hence a second call.
math.pow's lowering, in the same file, already parks its exponent in a temp
via PyEvalOnce and its comment says exactly why. This arm was written from
that one and did not copy the line. Same shape as every other entry in
[[project_nilpy_lvalue_vs_selector_path_must_both_know]]: one concept, N
independent sites, and the one that stays broken is the one nobody diffed.
Fix
ppExp := PyEvalOnce(ppExp); before the guard's argument is built, in
PyMakePow (compiler/pyparser.inc).
Why it can matter
Only side effects are observable — every value was already correct — so the
failure mode is a counter that double-counts, a generator advanced twice, a log
line printed twice. That is the same class as
bug-c-a-struct-assignment-used-as-a-value-runs-its-rhs-twice, and it is the
class the human-written test corpus does not catch, because tests assert values.
Gate
test/test_nilpy_pow_evaluates_operands_once.npy — six spellings (static float
both ways round, base-only, augmented **=, math.pow which was already right,
and a variant receiver through pypow_v), each printing the call counter,
diffed against CPython. Plus the per-fix loop.