Match monty's floating point modulo behavior to cpython#498
Match monty's floating point modulo behavior to cpython#498marcbrooker wants to merge 3 commits into
Conversation
…ncated fmod Rust's `%` on f64 is C-style fmod: the remainder takes the sign of the dividend. Python's `%` uses floored division: the remainder takes the sign of the divisor. For example, `-7.0 % 3.0` should be `2.0` (Python) not `-1.0` (C fmod). The difference is always exactly one multiple of the divisor, which is why the fuzzer saw offsets like 9.0, 10.0, 5.0, etc. Fix: add py_float_mod(a, b) that adjusts the C remainder when its sign disagrees with the divisor, applied to all three float-mod arms (float%float, float%int, int%float). Integer modulo already had this correction.
When the floored modulo result is exactly zero, IEEE 754 can leave a stale sign bit from the dividend (e.g. `-1.0 % 1.0` → `-0.0`). CPython normalizes this: the zero result always carries the divisor's sign. Fix: use f64::copysign(r, b) in the zero branch of py_float_mod so the sign bit matches the divisor, consistent with CPython's float_rem.
Merging this PR will not alter performance
Comparing Footnotes
|
There was a problem hiding this comment.
2 issues found across 2 files
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
…n tests py_mod_eq (the COMPARE_MOD_EQ optimization) still used raw Rust `%` for float cases, so the fast path disagreed with py_mod for negative floats. Switch to py_float_mod for consistency. Also strengthen the zero-result test assertions to verify the sign bit via math.copysign, since `== 0.0` cannot distinguish +0.0 from -0.0.
There was a problem hiding this comment.
1 issue found across 2 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="crates/monty/src/value.rs">
<violation number="1" location="crates/monty/src/value.rs:555">
P1: `py_mod_eq` float branches call `py_float_mod` without zero-division guards, diverging from runtime `%` semantics</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| (Self::Float(v1), Self::Float(v2)) => Some(v1 % v2 == right_value as f64), | ||
| (Self::Float(v1), Self::Int(v2)) => Some(v1 % (*v2 as f64) == right_value as f64), | ||
| (Self::Int(v1), Self::Float(v2)) => Some((*v1 as f64) % v2 == right_value as f64), | ||
| (Self::Float(v1), Self::Float(v2)) => Some(py_float_mod(*v1, *v2) == right_value as f64), |
There was a problem hiding this comment.
P1: py_mod_eq float branches call py_float_mod without zero-division guards, diverging from runtime % semantics
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At crates/monty/src/value.rs, line 555:
<comment>`py_mod_eq` float branches call `py_float_mod` without zero-division guards, diverging from runtime `%` semantics</comment>
<file context>
@@ -552,9 +552,9 @@ impl PyTrait<'_> for Value {
- (Self::Float(v1), Self::Float(v2)) => Some(v1 % v2 == right_value as f64),
- (Self::Float(v1), Self::Int(v2)) => Some(v1 % (*v2 as f64) == right_value as f64),
- (Self::Int(v1), Self::Float(v2)) => Some((*v1 as f64) % v2 == right_value as f64),
+ (Self::Float(v1), Self::Float(v2)) => Some(py_float_mod(*v1, *v2) == right_value as f64),
+ (Self::Float(v1), Self::Int(v2)) => Some(py_float_mod(*v1, *v2 as f64) == right_value as f64),
+ (Self::Int(v1), Self::Float(v2)) => Some(py_float_mod(*v1 as f64, *v2) == right_value as f64),
</file context>
Summary by cubic
Match float modulo to CPython: use floored semantics, normalize zero sign, and align
py_mod_eqfast path to fix wrong results with negative operands and -0.0/+0.0 anomalies.py_float_modfor float%float, float%int, and int%float (remainder takes the divisor’s sign).py_mod_eqfast path to usepy_float_modfor float cases to keep compare-mod-equals consistent.f64::copysignso 0.0 matches the divisor’s sign; strengthened zero-sign tests withmath.copysign.Written for commit 75d71ef. Summary will update on new commits.