Skip to content

Match monty's floating point modulo behavior to cpython#498

Open
marcbrooker wants to merge 3 commits into
pydantic:mainfrom
marcbrooker:minor-bugs
Open

Match monty's floating point modulo behavior to cpython#498
marcbrooker wants to merge 3 commits into
pydantic:mainfrom
marcbrooker:minor-bugs

Conversation

@marcbrooker

@marcbrooker marcbrooker commented Jun 9, 2026

Copy link
Copy Markdown

Summary by cubic

Match float modulo to CPython: use floored semantics, normalize zero sign, and align py_mod_eq fast path to fix wrong results with negative operands and -0.0/+0.0 anomalies.

  • Bug Fixes
    • Implemented Python-style modulo via py_float_mod for float%float, float%int, and int%float (remainder takes the divisor’s sign).
    • Fixed py_mod_eq fast path to use py_float_mod for float cases to keep compare-mod-equals consistent.
    • Normalized exact-zero results with f64::copysign so 0.0 matches the divisor’s sign; strengthened zero-sign tests with math.copysign.

Written for commit 75d71ef. Summary will update on new commits.

Review in cubic

…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.
@codspeed-hq

codspeed-hq Bot commented Jun 9, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 17 untouched benchmarks
⏩ 15 skipped benchmarks1


Comparing marcbrooker:minor-bugs (75d71ef) with main (592d757)

Open in CodSpeed

Footnotes

  1. 15 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@cubic-dev-ai cubic-dev-ai Bot 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.

2 issues found across 2 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread crates/monty/src/value.rs
Comment thread crates/monty/test_cases/arith__float_mod.py Outdated
@codecov

codecov Bot commented Jun 9, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 75.00000% with 3 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
crates/monty/src/value.rs 75.00% 3 Missing ⚠️

📢 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.

@cubic-dev-ai cubic-dev-ai Bot 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.

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

Comment thread crates/monty/src/value.rs
(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),

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.

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>

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.

1 participant