An imported module's ** is computed by the fallback, not by the RTL's Power
Found while running lekkerzeilen, which is how the severity got measured correctly: the symptom was a crash, the crash was somebody else's bug, and what is left here is a last ulp.
Defect 1 — the scan runs before the imports
compiler/pyparser.inc:41693:
pyWantsPow := False;
for i := 0 to TokCount - 2 do
if (Tokens[i].Kind = tkStar) and (Tokens[i + 1].Kind = tkStar) then
PyParseImportRun is at 41769 and the install at 41796. So the scan answers
about the main module's tokens only.
A contained fix is possible at 41796, because by then the imports have been
parsed and the tokens exist — set pyWantsPow there too rather than moving the
scan. What makes it not purely mechanical is the ParseUsesUnit('math') pull
that sits INSIDE the scan at 41708: the comment above 41713 records that math
must be pulled BEFORE ParseUsesUnitAmbient('pylib') because the last unit
named wins a name and math's integer Abs otherwise hides pylib's float one
(measured: abs(-1.5) stopped resolving). So the scan cannot simply move down,
and the install site cannot simply pull math.
Defect 2 — unit initialisation runs before the main body
The assignment is emitted as the program's FIRST statement (41796 onward,
mainNode := PySeqAppend(mainNode, powAsgn)). An imported .py is a unit, and
Pascal runs unit initialisation sections before the main body. So a module-level
** in an imported module executes with PyPowHook = nil regardless of
defect 1.
Control, and it is what makes this a second defect rather than a restatement
of the first — predicted before running that a ** in the main module would
fix the imported one; it did not:
where the ** runs |
printed |
|---|---|
| import time (module level, in the imported unit) | 1.9952623149688793 |
| after the program starts (same function, called from main) | 1.9952623149688795 |
CPython gives ...95. Both rows are from one program with ** in main, so the
hook WAS installed; only the ordering separates them.
What the cost is, stated honestly
One ulp on a ** in an imported module. The hookless path is
PyMathExp(e * PyMathLn(b)). It is not a crash and not an F-lane ticket by the
"rank the mechanism, not the datatype" test — the mechanism is a hook that does
not get installed — but the OBSERVABLE is a last digit, so do not rank it as
though it were the ValueError that led here.
The regression fixture already exists and pins the FALLBACK, not this
test/test_nilpy_pow_zero_base_in_an_imported_module.npy (+ its aux module
test/nilpy_pow0_mod.py) deliberately contains no ** of its own, because a
** in main would retire its four post-start rows. It therefore passes both
before and after this ticket is fixed — it measures pypow_cx's arithmetic,
which is correct now. Fixing this ticket makes four of its rows stop reaching
that code at all; the import-time rows will still reach it until defect 2 is
also fixed. Anyone fixing either half should add a row asserting the
...688795 value, which is the only thing that can observe the hook arriving.