Match CPython's _float_div_mod, fixing divmod and % zero-handling#7745
Conversation
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
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughThe Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Review rate limit: 7/8 reviews remaining, refill in 7 minutes and 30 seconds.Comment |
thanks for the review ! |
thanks ! |
Background
crates/common/src/float_ops.rshad three independent implementations:mod_(v1, v2)— Python%operatorfloordiv(v1, v2)— Python//operatordivmod(v1, v2)— Pythondivmod()builtinEach independently converted Rust's dividend-sign
%to CPython's divisor-sign convention. Two of them (mod_anddivmod) 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 (
divmodand%)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)When the true quotient is exactly zero, the
(v1 - m) / v2intermediate produces a signed zero whose sign reflects the intermediate division, not the true quotientv1 / v2. CPython's_float_div_modsnaps zero quotients viacopysign(0.0, vx / wx).3. Internal consistency violation (Python language spec)
Python guarantees
divmod(a, b) == (a // b, a % b). RustPython didn't:Three values, three different paths, three results that all disagreed.
Fix
Mirror CPython's
_float_div_mod(Objects/floatobject.c) by makingdivmodthe canonical implementation.mod_andfloordivbecome thin wrappers that delegate and project, sodivmod(a, b) == (a // b, a % b)holds by construction — no separate paths to keep in sync.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) / v2undershoots 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)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_decimal— 1,471 tests, 0 regressions.A regression test was added to
extra_tests/snippets/builtin_divmod.pycovering the bug class plus the divmod identity and magnitude/sign invariants.Notes
divmodcarries the canonical implementation (rather than introducing a private_float_div_modhelper) because all three public functions in this module already share the sameOption<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) / v2can lose precision.Closes #7722
Summary by CodeRabbit
Bug Fixes
Tests