← board

Inferred return type of / is int — the float is truncated, or reinterpreted

Repro

def a(x):
    return x / 2
def b(x: int):
    return x / 2
def c(x: float):
    return x / 2
def d(x: int) -> float:
    return x / 2
def e(x):
    r = x / 2
    return r
print(a(5)); print(b(5)); print(c(5.0)); print(d(5)); print(e(5))
pxx (v363) CPython 3
a(5) — no annotations at all 2 2.5
b(5) — param annotated, return not 4612811918334230528 2.5
c(5.0) — param annotated float, return not 4612811918334230528 2.5
d(5) — return annotated -> float 2.5 2.5
e(5) — via a local, return not annotated 2.5 2.5

4612811918334230528 is 0x4004000000000000 — the IEEE-754 bit pattern of 2.5 read as an Int64. So the value is computed correctly as a double and then handed back through an integer-typed return slot without conversion.

What this says about the shape

Upward compatibility

Clear-cut under the N rule: this is code CPython accepts and runs, producing a different value under NilPy. Not a divergence to record in devdocs/dev/nilpy-semantics-divergences.md — a defect.

Gate

make test-nilpy is superseded by the per-fix loop; add a .npy regression test covering all five arms above and wire it into the Makefile's enumerated list (devdocs/progress history: a test file that is not enumerated is not a test).

Log

Log

Resolved 2026-08-26

Where the result type was decided: PyInferExprType — the TOKEN scan in compiler/pyparser.inc that types an expression for the pre-passes (a def's registered return type, a field's declared type). It has no binop dispatch at all: it walks the tokens and PyWidens the operand types it recognises, and PyWiden(Int64, Int64) is Int64. So x / 2 was an integer.

How many sites shared the rule: five, four of which already had it. FloatBinopResultTk (the / binop, pasparser_expr.inc), the two augmented- assignment sites here (/=, both calling FloatBinopResultTk and noting the local as a float), and the codegen's force-jump to the double path (ir_codegen.inc:3694). This token scan was the one owner with no copy — which is exactly why print(5 / 2) and r = x / 2; return r were right while return x / 2 was not. Fixed by calling the same FloatBinopResultTk (not a hardcoded tyDouble: on ESP, int/int under / is native Single).

A sixth owner turned up in the same function's caller: the bare-ident return chase (x /= 2 then return x) matched only =, so it never saw an augmented true division. Given the /= arm in the same ordered walk, so a later plain x = 5 still wins.

What the boundary shapes revealed. Twenty-odd shapes, diffed against CPython:

Not a bisect. Endpoint measurement (pinned v363 vs HEAD) showed the shapes identical at both ends — latent since the scan was written, not newly introduced.

Nothing needs a Track A ticket. / and // already lex to distinct tokens (tkSlash / tkDiv) and lower to the right shared IR ops; the whole defect was in N's own file. FloatBinopResultTk lives in pasparser_expr.inc but is only being CALLED, not changed.

Four sibling defects found and filed rather than folded in — each proven NOT to be about division by a control shape with no / in it:

Regression test: test/test_nilpy_true_division_return_type.npy + .expected generated from CPython 3, wired into test-core (which runs in the quick tier — test-nilpy does not). Forty assertions: all five arms of this ticket, every operand shape above, the // / % / negative / mixed-type controls that must stay integer, division under a comparison and under in, and the five redundant-paren shapes. The pinned binary fails 15 of the 40 lines.