Skip to content

Match CPython's _float_div_mod, fixing divmod and % zero-handling#7745

Merged
youknowone merged 1 commit into
RustPython:mainfrom
changjoon-park:fix-float-divmod-mod
May 1, 2026
Merged

Match CPython's _float_div_mod, fixing divmod and % zero-handling#7745
youknowone merged 1 commit into
RustPython:mainfrom
changjoon-park:fix-float-divmod-mod

Conversation

@changjoon-park

@changjoon-park changjoon-park commented Apr 30, 2026

Copy link
Copy Markdown
Contributor

Background

crates/common/src/float_ops.rs had three independent implementations:

  • mod_(v1, v2) — Python % operator
  • floordiv(v1, v2) — Python // operator
  • divmod(v1, v2) — Python divmod() builtin

Each independently converted Rust's dividend-sign % to CPython's divisor-sign convention. Two of them (mod_ and divmod) had the same gap — the sign-correction branch fired on zero remainders, producing values that violate Python's spec invariants.

Bugs

1. Zero-remainder + opposite-sign divisor (divmod and %)

divmod(0.0, -1.0)
# CPython:    (-0.0, -0.0)
# Pre-fix:    (-1.0, -1.0)   ← q and r both off by one
6.0 % -3.0
# CPython:    -0.0
# Pre-fix:    -3.0           ← |r| == |b|, violates 0 <= |r| < |b|
divmod(6.0, -3.0)
# CPython:    (-2.0, -0.0)
# Pre-fix:    (-3.0, -3.0)

The dividend-is-non-zero-exact-multiple cases fell into the default sign-correction branch, which adds the divisor to a zero remainder and decrements the quotient — yielding values that violate the magnitude invariant 0 <= |r| < |b|.

2. Signed-zero quotient leak (divmod)

divmod(-1.0, -2.0)
# CPython:    (+0.0, -1.0)
# Pre-fix:    (-0.0, -1.0)   ← quotient sign leaked from (v1 - m) / v2

When the true quotient is exactly zero, the (v1 - m) / v2 intermediate produces a signed zero whose sign reflects the intermediate division, not the true quotient v1 / v2. CPython's _float_div_mod snaps zero quotients via copysign(0.0, vx / wx).

3. Internal consistency violation (Python language spec)

Python guarantees divmod(a, b) == (a // b, a % b). RustPython didn't:

a, b = 6.0, -3.0
divmod(a, b)               # (-3.0, -3.0)   ← divmod's own bug
(a // b, a % b)            # (-2.0, -3.0)   ← floordiv correct, mod_ buggy

Three values, three different paths, three results that all disagreed.

Fix

Mirror CPython's _float_div_mod (Objects/floatobject.c) by making divmod the canonical implementation. mod_ and floordiv become thin wrappers that delegate and project, so divmod(a, b) == (a // b, a % b) holds by construction — no separate paths to keep in sync.

pub fn mod_(v1: f64, v2: f64) -> Option<f64> {
    divmod(v1, v2).map(|(_, m)| m)
}

pub fn floordiv(v1: f64, v2: f64) -> Option<f64> {
    divmod(v1, v2).map(|(d, _)| d)
}

pub fn divmod(v1: f64, v2: f64) -> Option<(f64, f64)> {
    if v2 == 0.0 {
        return None;
    }
    let mut m = v1 % v2;
    let mut d = (v1 - m) / v2;
    if m != 0.0 {
        if v2.is_sign_negative() != m.is_sign_negative() {
            m += v2;
            d -= 1.0;
        }
    } else {
        m = (0.0_f64).copysign(v2);     // zero remainder: sign of divisor
    }
    let d = if d != 0.0 {
        let f = d.floor();
        if d - f > 0.5 { f + 1.0 } else { f }   // CPython's half-up snap
    } else {
        (0.0_f64).copysign(v1 / v2)     // zero quotient: sign of true quotient
    };
    Some((d, m))
}

The half-up snap on non-zero quotient (if d - f > 0.5 { f + 1.0 } else { f }) mirrors CPython's safety net for cases where (v1 - m) / v2 undershoots the true integer quotient by more than half an ULP.

Verification

CPython 3.14.4 byte-identical for 44 cases spanning sign permutations, zero edges, exact divisions, IEEE 754 signed zero, and subnormals:

  • divmod: 29 cases (all pass; pre-fix 8 failed)
  • % operator (mod_): 8 cases (all pass; pre-fix 5 failed)
  • // operator (floordiv): 7 cases (all pass on both)
  • Internal consistency divmod == (//, %): 6 cases (all pass; pre-fix all 6 failed)

Regression sweep: test_float, test_int, test_complex, test_math, test_time, test_datetime, test_fractions, test_decimal1,471 tests, 0 regressions.

A regression test was added to extra_tests/snippets/builtin_divmod.py covering the bug class plus the divmod identity and magnitude/sign invariants.

Notes

divmod carries the canonical implementation (rather than introducing a private _float_div_mod helper) because all three public functions in this module already share the same Option<f64>-based API; a thin-wrapper pattern is the smallest delta that achieves single-source-of-truth without changing the public surface.

The half-up snap was added to match CPython exactly even though no probed case currently exercises it. It is correctness-relevant for extreme magnitudes where the intermediate (v1 - m) / v2 can lose precision.

Closes #7722

Summary by CodeRabbit

  • Bug Fixes

    • Improved floating-point division operations to match Python's standard library behavior more closely, with better handling of edge cases involving signed zeros and operands with opposite signs.
  • Tests

    • Added comprehensive test coverage for floating-point division edge cases and invariants.

float_ops::divmod, mod_, and floordiv each carried their own conversion
from Rust's dividend-sign `%` to CPython's divisor-sign convention. Both
divmod and mod_ mishandled the zero-remainder case where the dividend
is a non-zero exact multiple of the divisor (e.g. divmod(6.0, -3.0),
6.0 % -3.0): the sign-correction branch fired on a zero remainder and
produced (-3.0, -3.0) and -3.0 respectively, violating the magnitude
invariant 0 <= abs(r) < abs(b). divmod also leaked the wrong signed-
zero quotient when the true quotient was zero (divmod(-1.0, -2.0)
returned (-0.0, -1.0) instead of (+0.0, -1.0)).

These are independent bugs in two functions, but both come from the
same root cause: zero-remainder needs a separate path from the sign-
correction branch.

Mirror CPython's `_float_div_mod` (Objects/floatobject.c) by making
divmod the canonical implementation and turning mod_ and floordiv into
thin wrappers. divmod(a, b) == (a // b, a % b) now holds by
construction.

Closes RustPython#7722

@ShaharNaveh ShaharNaveh left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks great!
tysm:)

@coderabbitai

coderabbitai Bot commented Apr 30, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 5d8697ca-4a71-4cec-b146-16700547d42a

📥 Commits

Reviewing files that changed from the base of the PR and between e79df4a and adeeaca.

📒 Files selected for processing (2)
  • crates/common/src/float_ops.rs
  • extra_tests/snippets/builtin_divmod.py

📝 Walkthrough

Walkthrough

The divmod function in float_ops.rs is refactored to correctly handle floating-point division, remainder, and quotient sign semantics. The mod_ and floordiv functions now delegate to this canonical implementation. Comprehensive tests validate signed-zero behavior and verify the divmod identity across various operand combinations.

Changes

Cohort / File(s) Summary
Float Operations Logic
crates/common/src/float_ops.rs
Refactored divmod to match CPython's _float_div_mod behavior: guards sign-correction logic with m != 0.0 check, uses copysign for zero remainders to preserve IEEE-754 semantics, snaps quotient to floor(d) when non-zero and copysign(0.0, v1 / v2) when zero. Functions mod_ and floordiv now delegate to divmod instead of independent calculations. Fixes divmod(0, -1.0) returning (-1.0, -1.0) instead of (-0.0, -0.0).
Float Divmod Tests
extra_tests/snippets/builtin_divmod.py
Added comprehensive test suite with signbit helper to validate signed-zero handling. Tests cover edge cases with opposite-sign operands, zero remainders, and quotient sign correction. Includes parameterized loops verifying divmod(a,b) == (a//b, a%b) equivalence and validating divmod invariants: remainder reconstruction, sign matching divisor, and magnitude bounds.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested reviewers

  • youknowone
  • ShaharNaveh

Poem

🐰 A remainder that's signed with zero's grace,
Now matches CPython's proper place,
With quotient snapping to floor so true,
IEEE-754 semantics shine through,
Divmod's bugs have been patched today! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: fixing divmod and % zero-handling to match CPython's behavior, which is the core objective of this PR.
Linked Issues check ✅ Passed The PR addresses all requirements from issue #7722: guards sign-correction with m != 0.0, sets zero remainders with copysign(0.0, v2), and snaps quotient sign/value to match CPython's _float_div_mod behavior.
Out of Scope Changes check ✅ Passed All changes are directly scoped to the stated objectives: float_ops.rs refactors divmod as canonical implementation with sign/zero fixes, and builtin_divmod.py adds targeted regression tests with signed-zero and sign-matching verification.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 7/8 reviews remaining, refill in 7 minutes and 30 seconds.

Comment @coderabbitai help to get the list of available commands and usage tips.

@changjoon-park

Copy link
Copy Markdown
Contributor Author

looks great! tysm:)

thanks for the review !

@changjoon-park

Copy link
Copy Markdown
Contributor Author

looks great! tysm:)

thanks !

@youknowone youknowone merged commit dd1cbac into RustPython:main May 1, 2026
20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

3 participants