← board

NilPy: // and % are WRONG for negative operands, silently

Found 2026-07-20 in the operator sweep.

print(-7 // 2)     # CPython -4    pxx -3
print(-7 % 2)      # CPython  1    pxx -1
print(7 % -2)      # CPython -1    pxx  1
print(7 // 2)      # 3             correct
print(7 % 2)       # 1             correct

Positive operands are right, which is why nothing caught it.

Cause

Python's // floors — it rounds toward NEGATIVE INFINITY — and its % takes the sign of the DIVISOR, with the identity a == (a // b) * b + (a % b) holding for every sign combination. Pascal's div truncates toward zero and its mod takes the sign of the DIVIDEND. NilPy lowers straight onto the Pascal pair.

Why Track A, not N

Checked before filing the fix: // lexes to tkDiv and % to Pascal's mod in pylexer.inc (Track N), but the ARITHMETIC layer is the shared parser's ParseExpr — pyparser.inc owns only the bitwise and boolean layers above it. So the node is built, and its semantics chosen, outside Track N's files. The fix belongs in the shared lowering, gated on PyExprMode the way other NilPy-specific behaviour already is.

A source-level rewrite in the lexer (the trick f-strings use) is NOT a way around this: finding the two operands of a binary operator in raw text means parsing the expression, which is the duplication that keeps costing this frontend.

Shape

Both need a correction on the lowered result, or a pair of pylib helpers:

floordiv(a, b) = (a div b) - ord((a mod b <> 0) and ((a < 0) <> (b < 0)))
pymod(a, b)    = ((a mod b) + b) mod b        -- for b <> 0

Keep them together: the identity above must hold, and fixing one without the other breaks it.

Division by zero already raises; that behaviour should not change.

Why p75

Silent, and it is arithmetic — the class of bug that produces a plausible number and corrupts everything downstream. uforth is a Forth VM whose whole job is integer arithmetic; it has 6 % sites and 1 //, and Forth's own /MOD semantics are floored, so the conformance suite tests exactly this.

10 / 3 prints 3.333333333333334 where CPython prints 3.3333333333333335 — a float REPR difference (Python emits the shortest string that round-trips, 17 significant digits here; pxx emits 16). Cosmetic next to the above, but it will show up in any output diff, so it needs its own decision about whether NilPy matches CPython's repr exactly.

Gate

test-nilpy green with every sign combination of // and % diffed against CPython, plus the round-trip identity + --tier quick + self-host byte-identical + make fpc-check.

Log