Skip to content

Commit dd1cbac

Browse files
Match CPython's _float_div_mod, fixing divmod and % zero-handling (RustPython#7745)
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
1 parent c98d26e commit dd1cbac

2 files changed

Lines changed: 97 additions & 20 deletions

File tree

crates/common/src/float_ops.rs

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -68,38 +68,42 @@ pub const fn div(v1: f64, v2: f64) -> Option<f64> {
6868
}
6969

7070
pub fn mod_(v1: f64, v2: f64) -> Option<f64> {
71-
if v2 != 0.0 {
72-
let val = v1 % v2;
73-
match (v1.signum() as i32, v2.signum() as i32) {
74-
(1, 1) | (-1, -1) => Some(val),
75-
_ if (v1 == 0.0) || (v1.abs() == v2.abs()) => Some(val.copysign(v2)),
76-
_ => Some((val + v2).copysign(v2)),
77-
}
78-
} else {
79-
None
80-
}
71+
divmod(v1, v2).map(|(_, m)| m)
8172
}
8273

8374
pub fn floordiv(v1: f64, v2: f64) -> Option<f64> {
84-
if v2 != 0.0 {
85-
Some((v1 / v2).floor())
86-
} else {
87-
None
88-
}
75+
divmod(v1, v2).map(|(d, _)| d)
8976
}
9077

78+
// Canonical (floordiv, mod) for floats matching CPython's _float_div_mod
79+
// (Objects/floatobject.c). `mod_` and `floordiv` delegate here so that
80+
// `divmod(a, b) == (a // b, a % b)` holds by construction.
9181
pub fn divmod(v1: f64, v2: f64) -> Option<(f64, f64)> {
92-
if v2 != 0.0 {
93-
let mut m = v1 % v2;
94-
let mut d = (v1 - m) / v2;
82+
if v2 == 0.0 {
83+
return None;
84+
}
85+
let mut m = v1 % v2;
86+
let mut d = (v1 - m) / v2;
87+
if m != 0.0 {
88+
// Non-zero remainder must have the sign of the divisor.
9589
if v2.is_sign_negative() != m.is_sign_negative() {
9690
m += v2;
9791
d -= 1.0;
9892
}
99-
Some((d, m))
10093
} else {
101-
None
94+
// Zero remainder: sign matches divisor (IEEE 754 / CPython contract).
95+
m = (0.0_f64).copysign(v2);
10296
}
97+
let d = if d != 0.0 {
98+
let f = d.floor();
99+
// Snap up if (v1 - m) / v2 undershot the true integer quotient by
100+
// more than half an ULP (mirrors CPython's `if (div - *floordiv > 0.5)`).
101+
if d - f > 0.5 { f + 1.0 } else { f }
102+
} else {
103+
// Zero quotient: take the sign of the true quotient v1 / v2.
104+
(0.0_f64).copysign(v1 / v2)
105+
};
106+
Some((d, m))
103107
}
104108

105109
// nextafter algorithm based off of https://gitlab.com/bronsonbdevost/next_afterf

extra_tests/snippets/builtin_divmod.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import math
2+
13
from testutils import assert_raises
24

35
assert divmod(11, 3) == (3, 2)
@@ -7,3 +9,74 @@
79

810
assert_raises(ZeroDivisionError, divmod, 5, 0, _msg="divmod by zero")
911
assert_raises(ZeroDivisionError, divmod, 5.0, 0.0, _msg="divmod by zero")
12+
13+
14+
def signbit(x):
15+
return math.copysign(1.0, x) < 0
16+
17+
18+
# Zero remainder with opposite-sign divisor — quotient and remainder must
19+
# both be zero with the divisor's sign, not propagate through the
20+
# sign-correction branch.
21+
q, r = divmod(0.0, -1.0)
22+
assert q == 0.0 and signbit(q)
23+
assert r == 0.0 and signbit(r)
24+
25+
q, r = divmod(6.0, -3.0)
26+
assert q == -2.0
27+
assert r == 0.0 and signbit(r)
28+
29+
q, r = divmod(-100.0, 10.0)
30+
assert q == -10.0
31+
assert r == 0.0 and not signbit(r)
32+
33+
# Zero quotient — sign matches the true quotient v1 / v2, not the sign that
34+
# leaks from the (v1 - m) / v2 intermediate calculation.
35+
q, r = divmod(-1.0, -2.0)
36+
assert q == 0.0 and not signbit(q)
37+
assert r == -1.0
38+
39+
q, r = divmod(-0.0, 1.0)
40+
assert q == 0.0 and signbit(q)
41+
assert r == 0.0 and not signbit(r)
42+
43+
# Spec invariant: divmod(a, b) == (a // b, a % b), including signed zero.
44+
for a, b in [
45+
(0.0, -1.0),
46+
(6.0, -3.0),
47+
(-6.0, 3.0),
48+
(100.0, -10.0),
49+
(-100.0, 10.0),
50+
(-1.0, -2.0),
51+
(-0.0, 1.0),
52+
(-0.0, -1.0),
53+
(7.0, 3.0),
54+
(-7.0, 3.0),
55+
(7.0, -3.0),
56+
(-7.0, -3.0),
57+
(3.7, 1.5),
58+
(-3.7, 1.5),
59+
]:
60+
dm = divmod(a, b)
61+
assert dm[0] == a // b
62+
assert dm[1] == a % b
63+
assert signbit(dm[0]) == signbit(a // b)
64+
assert signbit(dm[1]) == signbit(a % b)
65+
66+
# Spec invariants for float divmod:
67+
# q * b + r == a, r == 0 or sign(r) == sign(b), 0 <= abs(r) < abs(b).
68+
for a, b in [
69+
(7.0, 3.0),
70+
(7.0, -3.0),
71+
(-7.0, 3.0),
72+
(-7.0, -3.0),
73+
(6.0, -3.0),
74+
(100.0, -10.0),
75+
(3.7, 1.5),
76+
(5.5, 2.0),
77+
(-5.5, 2.0),
78+
]:
79+
q, r = divmod(a, b)
80+
assert q * b + r == a
81+
assert r == 0.0 or (r < 0.0) == (b < 0.0)
82+
assert abs(r) < abs(b)

0 commit comments

Comments
 (0)