Skip to content

divmod() returns wrong float result when the remainder is zero #7722

@jseop-lim

Description

@jseop-lim

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.0, -0.0)

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

print(divmod(0, -1.0))

Environment

  • RustPython d248a04 (Python 3.14.0)
  • OS: Debian 12

Metadata

Metadata

Assignees

No one assigned

    Labels

    C-bugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions