Summary
divmod(0, -1.0) returns (-1.0, -1.0) — the quotient and remainder are both off by one. The float divmod implementation enters its sign-correction branch even when the remainder is exactly zero, so it adds the divisor to a zero remainder and decrements the quotient, producing values that are arithmetically wrong (0 == -1.0 * -1.0 + -1.0 is false).
Expected
0 / -1.0 is exactly -0.0 with no remainder, so divmod should return a quotient and remainder that satisfy the identity a == b * q + r with r == 0.
Actual
>>> divmod(0, -1.0)
(-1.0, -1.0)
The returned values do not satisfy the divmod identity: -1.0 * -1.0 + -1.0 == 0.0, which only holds because the quotient and remainder are both wrong by the same amount. The true quotient of 0 / -1.0 is zero, and the true remainder is zero.
Root Cause
crates/common/src/float_ops.rs#L91-L103 enters its sign-correction branch whenever v2.is_sign_negative() != m.is_sign_negative(), without checking whether the remainder is zero. For divmod(0.0, -1.0), Rust's % returns +0.0 while v2 is negative, so the branch fires on a zero remainder: it adds the divisor (m += v2, giving -1.0) and decrements the quotient (d -= 1.0, giving -1.0) — yielding (-1.0, -1.0) instead of (0.0, 0.0).
The function is also missing a quotient-snap step. When the floor of the quotient is zero, the sign of the resulting signed zero should match the sign of the true quotient v1 / v2, but the current code just propagates whatever signed zero (v1 - m) / v2 happens to produce.
Fix
Guard the sign-correction branch with m != 0.0; on the zero-remainder path, set m = copysign(0.0, v2). After the remainder is finalized, snap the quotient to floor(d) when non-zero and to copysign(0.0, v1 / v2) when zero.
This same function is also responsible for the related signed-zero defect on divmod(-1.0, -2.0) returning (-0.0, -1.0) instead of (0.0, -1.0) — only the quotient-snap branch is missing in that case, while the report above also exercises the zero-remainder branch.
Python Documentation
divmod reference: "the quotient and remainder when using integer division ... For floating-point numbers the result is (q, a % b), where q is usually math.floor(a / b) ... In any case q * b + r is very close to a, if r is non-zero it has the same sign as b, and 0 <= abs(r) < abs(b)."
CPython's _float_div_mod (Objects/floatobject.c#L620-L656) demonstrates the correct structure for reference:
// When the remainder is non-zero, force its sign to match the divisor.
// When it is zero, fall back to copysign — this preserves IEEE-754 signed-zero
// semantics consistently across platforms (fmod's zero sign is platform-defined).
if (*mod) {
if ((wx < 0) != (*mod < 0)) {
*mod += wx;
div -= 1.0;
}
}
else {
*mod = copysign(0.0, wx);
}
// Snap the quotient: round toward floor when non-zero;
// when zero, take the sign of the true quotient vx / wx.
if (div) {
*floordiv = floor(div);
if (div - *floordiv > 0.5) {
*floordiv += 1.0;
}
}
else {
*floordiv = copysign(0.0, vx / wx);
}
Reproduction
Environment
- RustPython d248a04 (Python 3.14.0)
- OS: Debian 12
Summary
divmod(0, -1.0)returns(-1.0, -1.0)— the quotient and remainder are both off by one. The floatdivmodimplementation enters its sign-correction branch even when the remainder is exactly zero, so it adds the divisor to a zero remainder and decrements the quotient, producing values that are arithmetically wrong (0 == -1.0 * -1.0 + -1.0is false).Expected
0 / -1.0is exactly-0.0with no remainder, sodivmodshould return a quotient and remainder that satisfy the identitya == b * q + rwithr == 0.Actual
The returned values do not satisfy the divmod identity:
-1.0 * -1.0 + -1.0 == 0.0, which only holds because the quotient and remainder are both wrong by the same amount. The true quotient of0 / -1.0is zero, and the true remainder is zero.Root Cause
crates/common/src/float_ops.rs#L91-L103enters its sign-correction branch wheneverv2.is_sign_negative() != m.is_sign_negative(), without checking whether the remainder is zero. Fordivmod(0.0, -1.0), Rust's%returns+0.0whilev2is negative, so the branch fires on a zero remainder: it adds the divisor (m += v2, giving-1.0) and decrements the quotient (d -= 1.0, giving-1.0) — yielding(-1.0, -1.0)instead of(0.0, 0.0).The function is also missing a quotient-snap step. When the floor of the quotient is zero, the sign of the resulting signed zero should match the sign of the true quotient
v1 / v2, but the current code just propagates whatever signed zero(v1 - m) / v2happens to produce.Fix
Guard the sign-correction branch with
m != 0.0; on the zero-remainder path, setm = copysign(0.0, v2). After the remainder is finalized, snap the quotient tofloor(d)when non-zero and tocopysign(0.0, v1 / v2)when zero.This same function is also responsible for the related signed-zero defect on
divmod(-1.0, -2.0)returning(-0.0, -1.0)instead of(0.0, -1.0)— only the quotient-snap branch is missing in that case, while the report above also exercises the zero-remainder branch.Python Documentation
divmodreference: "the quotient and remainder when using integer division ... For floating-point numbers the result is(q, a % b), whereqis usuallymath.floor(a / b)... In any caseq * b + ris very close toa, ifris non-zero it has the same sign asb, and0 <= abs(r) < abs(b)."CPython's
_float_div_mod(Objects/floatobject.c#L620-L656) demonstrates the correct structure for reference:Reproduction
Environment