← board

str(3 % 2.5) prints the double's BIT PATTERN

print(3 % 2.5)        # CPython: 0.5    pxx: 0.5      <- correct
print(str(3 % 2.5))   # CPython: 0.5    pxx: 4602678819172646912
x = 3 % 2.5
print(str(x))         # CPython: 0.5    pxx: 4602678819172646912

4602678819172646912 is 0.5 reinterpreted as an Int64. str(3 / 2) and str(3.0 * 2) are both fine, so it is specific to % (and //) with MIXED int/float operands.

Cause, already located

The parser types a % b with TypeDivideResult(<left operand>), which ignores a float RIGHT operand — so 3 % 2.5 is an integer-typed node at parse time. ir.inc later notices the float operand, routes the node to pyfloormod_f and retypes it ASTTk[node] := Ord(tyDouble) — but that is too late for str(), which picked its overload at PARSE time from the integer type and therefore binds the Int64 one.

print(3 % 2.5) is correct only because print reads the node type after lowering.

Fix direction

Type the node from BOTH operands at parse time, under PyExprMode only: tkDiv/tkMod with either operand float yields float. Pascal's own div/mod are integer-only, so guarding on PyExprMode keeps the Pascal dialect and the self-host binary untouched. Then the ir.inc retype becomes a no-op agreement rather than a correction.

Found while verifying the division-by-zero fix against the operator sweep — the sweep wraps every case in str(...), which is exactly what exposed it, and the same reason it survived the plain print tests.

Gate

make test-nilpy + self-host byte-identical, plus str()/print()/f-string of % and // over every int/float operand pairing.

RESOLVED — type // and % from BOTH operands under PyExprMode

One arm in parser.inc's binop typing, ahead of the two TypeDivideResult(left) arms: under PyExprMode, tkDiv/tkMod with either operand float yields FloatBinopResultTk(left, right). Pascal's own div/mod are integer spellings, so the Pascal dialect and the self-host binary are untouched, and ir.inc's retype to tyDouble becomes an agreement rather than a correction.

Verified against CPython: str() of // and % over every int/float pairing, f-string interpolation, %-formatting, and the same through variables — all match. str(3 % 2.5) is 0.5 instead of 4602678819172646912.

The operator sweep's % divergence count fell from 166 lines to 148; what remains there is the mixed-type operand family ([[decide-nilpy-mixed-type-operand-policy]]) plus the partial-output artifact of [[bug-nilpy-print-emits-arguments-before-evaluating-later-ones]].

Gate

tools/gate.sh full.

Log