Skip to content

Commit 6992bbf

Browse files
authored
Match CPython remainder for subnormal inputs (#21)
Port CPython m_remainder rather than delegating to the native remainder function, which returns an incorrect result for subnormal inputs on Windows x64. Preserve ties-to-even, signed zeros, NaN propagation, and domain errors through math_2. Add signed subnormal regression cases and CPython-comparison property tests. Implementation, investigation, validation, and review assisted by Codex. Assisted-by: Codex:GPT-5
1 parent dd1e9a4 commit 6992bbf

4 files changed

Lines changed: 76 additions & 15 deletions

File tree

‎src/m.rs‎

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -192,11 +192,6 @@ pub fn nextafter(x: f64, y: f64) -> f64 {
192192
unsafe { m_sys::nextafter(x, y) }
193193
}
194194

195-
#[inline(always)]
196-
pub fn remainder(x: f64, y: f64) -> f64 {
197-
unsafe { m_sys::remainder(x, y) }
198-
}
199-
200195
#[inline(always)]
201196
pub fn trunc(n: f64) -> f64 {
202197
unsafe { m_sys::trunc(n) }

‎src/m_rust.rs‎

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -190,11 +190,6 @@ pub fn nextafter(x: f64, y: f64) -> f64 {
190190
libm::nextafter(x, y)
191191
}
192192

193-
#[inline(always)]
194-
pub fn remainder(x: f64, y: f64) -> f64 {
195-
libm::remainder(x, y)
196-
}
197-
198193
#[inline(always)]
199194
pub fn trunc(n: f64) -> f64 {
200195
libm::trunc(n)

‎src/m_sys.rs‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,6 @@ unsafe extern "C" {
5555
pub fn ldexp(x: f64, n: i32) -> f64;
5656
pub fn modf(n: f64, iptr: *mut f64) -> f64;
5757
pub fn nextafter(x: f64, y: f64) -> f64;
58-
pub fn remainder(x: f64, y: f64) -> f64;
5958
pub fn trunc(n: f64) -> f64;
6059

6160
// Special functions

‎src/math/misc.rs‎

Lines changed: 76 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -247,12 +247,46 @@ pub fn fmod(x: f64, y: f64) -> Result<f64> {
247247
///
248248
/// CPython implements this from scratch using fmod (m_remainder in
249249
/// mathmodule.c) rather than calling the C library's remainder().
250-
/// We delegate to libm's remainder() which is correct on all platforms
251-
/// where it conforms to IEEE 754. If you find a platform where the
252-
/// results differ from CPython, please file a bug.
253250
#[inline]
254251
pub fn remainder(x: f64, y: f64) -> Result<f64> {
255-
super::math_2(x, y, crate::m::remainder)
252+
super::math_2(x, y, m_remainder)
253+
}
254+
255+
fn m_remainder(x: f64, y: f64) -> f64 {
256+
if x.is_finite() && y.is_finite() {
257+
if y == 0.0 {
258+
return f64::NAN;
259+
}
260+
261+
let absx = x.abs();
262+
let absy = y.abs();
263+
let m = m::fmod(absx, absy);
264+
// Comparing with absy / 2 can round or underflow for subnormals.
265+
// CPython compares the remainder with its complement instead.
266+
let c = absy - m;
267+
let r = if m < c {
268+
m
269+
} else if m > c {
270+
-c
271+
} else {
272+
// At a tie, reducing half the preceding multiple modulo absy
273+
// distinguishes odd and even multiples without rounding.
274+
m - 2.0 * m::fmod(0.5 * (absx - m), absy)
275+
};
276+
return m::copysign(1.0, x) * r;
277+
}
278+
279+
if x.is_nan() {
280+
return x;
281+
}
282+
if y.is_nan() {
283+
return y;
284+
}
285+
if x.is_infinite() {
286+
return f64::NAN;
287+
}
288+
debug_assert!(y.is_infinite());
289+
x
256290
}
257291

258292
/// Return the value of the least significant bit of x.
@@ -474,6 +508,32 @@ mod tests {
474508
}
475509
}
476510

511+
#[test]
512+
fn regression_remainder_subnormal_cases() {
513+
let tiny = f64::from_bits(1);
514+
for (x, y, expected) in [
515+
(12.0, 23.0, -11.0),
516+
(11.0, 23.0, 11.0),
517+
(35.0, 23.0, -11.0),
518+
(3.0, 2.0, -1.0),
519+
(5.0, 2.0, 1.0),
520+
(4.0, 2.0, 0.0),
521+
] {
522+
for sx in [1.0, -1.0] {
523+
for sy in [1.0, -1.0] {
524+
let x = sx * x * tiny;
525+
let y = sy * y * tiny;
526+
assert_eq!(
527+
remainder(x, y).unwrap().to_bits(),
528+
(sx * expected * tiny).to_bits(),
529+
"remainder({x}, {y})",
530+
);
531+
test_remainder(x, y);
532+
}
533+
}
534+
}
535+
}
536+
477537
#[test]
478538
fn regression_remainder_halfway_even_cases() {
479539
// These cases exercise the half-way branch in CPython's custom
@@ -574,6 +634,18 @@ mod tests {
574634
test_remainder(x, y);
575635
}
576636

637+
#[test]
638+
fn proptest_remainder_subnormal(
639+
x in 1_u64..(1_u64 << 52),
640+
y in 1_u64..(1_u64 << 52),
641+
negative_x: bool,
642+
negative_y: bool,
643+
) {
644+
let x = f64::from_bits(x | ((negative_x as u64) << 63));
645+
let y = f64::from_bits(y | ((negative_y as u64) << 63));
646+
test_remainder(x, y);
647+
}
648+
577649
#[test]
578650
fn proptest_copysign(x: f64, y: f64) {
579651
test_copysign(x, y);

0 commit comments

Comments
 (0)