From 6914f84f4cb81245508cf9597dc04e89d8b9d760 Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Sun, 29 Mar 2026 16:27:27 +0200 Subject: [PATCH] [Python] Fix modulo with negative numbers for bigint (fixes #4462) Python's `%` operator uses floored remainder semantics while .NET uses truncated remainder. For bigint, `op_modulus` was using Python's `%` directly, so `-5I % 3I` returned `1` instead of `-2`. Fix by adjusting the result when dividend and divisor have different signs. int32 and int64 were already correct thanks to Rust wrapper types that implement truncated remainder via Rust's native `%` operator. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/Fable.Cli/CHANGELOG.md | 1 + src/Fable.Compiler/CHANGELOG.md | 1 + src/fable-library-py/fable_library/big_int.py | 7 ++++++- tests/Python/TestArithmetic.fs | 18 ++++++++++++++++++ 4 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/Fable.Cli/CHANGELOG.md b/src/Fable.Cli/CHANGELOG.md index 5e99e09c67..767d117a30 100644 --- a/src/Fable.Cli/CHANGELOG.md +++ b/src/Fable.Cli/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +* [Python] Fix modulo with negative numbers using Python floored semantics instead of .NET truncated semantics for bigint (fixes #4462) (by @dbrattli) * [Beam] Fix `System.String.Concat` with 4+ arguments not being supported (by @dbrattli) * [TS/Python] Fix invalid `this` argument type in structs (#4453) (by @ncave) * [JS/TS] Fix `N` format specifier (`ToString("N0")`, `String.Format("{0:N0}", ...)`) producing a trailing dot when precision is 0 (fix #2582) (by @MangelMaxime) diff --git a/src/Fable.Compiler/CHANGELOG.md b/src/Fable.Compiler/CHANGELOG.md index e992779832..3dd5c4bfc8 100644 --- a/src/Fable.Compiler/CHANGELOG.md +++ b/src/Fable.Compiler/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +* [Python] Fix modulo with negative numbers using Python floored semantics instead of .NET truncated semantics for bigint (fixes #4462) (by @dbrattli) * [Beam] Fix `System.String.Concat` with 4+ arguments not being supported (by @dbrattli) * [TS/Python] Fix invalid `this` argument type in structs (#4453) (by @ncave) * [JS/TS] Fix `N` format specifier (`ToString("N0")`, `String.Format("{0:N0}", ...)`) producing a trailing dot when precision is 0 (fix #2582) (by @MangelMaxime) diff --git a/src/fable-library-py/fable_library/big_int.py b/src/fable-library-py/fable_library/big_int.py index de6b4d3ed2..d71cdd790e 100644 --- a/src/fable-library-py/fable_library/big_int.py +++ b/src/fable-library-py/fable_library/big_int.py @@ -81,7 +81,12 @@ def op_division(a: int, b: int) -> int: def op_modulus(a: int, b: int) -> int: - return a % b + # .NET uses truncated remainder, Python uses floored remainder. + # They differ when the dividend and divisor have different signs. + r = a % b + if r != 0 and ((a < 0) != (b < 0)): + r -= b + return r def op_right_shift(a: int, num_bits: int) -> int: diff --git a/tests/Python/TestArithmetic.fs b/tests/Python/TestArithmetic.fs index a60c99963a..c1e4658cb1 100644 --- a/tests/Python/TestArithmetic.fs +++ b/tests/Python/TestArithmetic.fs @@ -60,6 +60,12 @@ let ``test Integer division doesn't produce floats`` () = let ``test Infix modulo can be generated`` () = 4 % 3 |> equal 1 +[] +let ``test Infix modulo with negative numbers`` () = + -5 % 3 |> equal -2 + 5 % -3 |> equal 2 + -5 % -3 |> equal -2 + // [] // let ``test Math.DivRem works with bytes`` () = // Math.DivRem(5y, 2y) |> equal struct (2y, 1y) @@ -384,6 +390,12 @@ let ``test Int64 Integer division doesn't produce floats`` () = let ``test Int64 Infix modulo can be generated`` () = 4L % 3L |> equal 1L +[] +let ``test Int64 Infix modulo with negative numbers`` () = + -5L % 3L |> equal -2L + 5L % -3L |> equal 2L + -5L % -3L |> equal -2L + [] let ``test Int64 Evaluation order is preserved by generated code`` () = (4L - 2L) * 2L + 1L |> equal 5L @@ -446,6 +458,12 @@ let ``test BigInt Integer division doesn't produce floats`` () = let ``test BigInt Infix modulo can be generated`` () = 4I % 3I |> equal 1I +[] +let ``test BigInt Infix modulo with negative numbers`` () = + -5I % 3I |> equal -2I + 5I % -3I |> equal 2I + -5I % -3I |> equal -2I + // [] // let ``test BigInt.DivRem works`` () = // See #1744 // let quotient,remainder = bigint.DivRem(5I,2I)