From dcc9624ee61e34f725b08be784f6e49d4618739d Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Sun, 29 Mar 2026 17:01:23 +0200 Subject: [PATCH] [Python] Add Math.DivRem support for int, int64, and bigint Implement div_rem in int32.py, long.py, and big_int.py, supporting both the 2-arg (returns tuple) and 3-arg (outref) overloads. Route Math.DivRem and bigint.DivRem through these library functions in Python Replacements. For bigint, uses truncated division (adjusting Python's floored //) to match .NET semantics. int32/int64 Rust wrappers already truncate. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/Fable.Cli/CHANGELOG.md | 1 + src/Fable.Compiler/CHANGELOG.md | 1 + src/Fable.Transforms/Python/Replacements.fs | 12 +++- src/fable-library-py/fable_library/big_int.py | 22 ++++++- src/fable-library-py/fable_library/int32.py | 23 ++++++- src/fable-library-py/fable_library/long.py | 21 ++++++- tests/Python/TestArithmetic.fs | 62 +++++++++---------- 7 files changed, 107 insertions(+), 35 deletions(-) diff --git a/src/Fable.Cli/CHANGELOG.md b/src/Fable.Cli/CHANGELOG.md index 767d117a30..667f436e7c 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] Add `Math.DivRem` support for int, int64, and bigint (by @dbrattli) * [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) diff --git a/src/Fable.Compiler/CHANGELOG.md b/src/Fable.Compiler/CHANGELOG.md index 3dd5c4bfc8..c479f7d6a0 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] Add `Math.DivRem` support for int, int64, and bigint (by @dbrattli) * [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) diff --git a/src/Fable.Transforms/Python/Replacements.fs b/src/Fable.Transforms/Python/Replacements.fs index b022712352..ce8f64fb6d 100644 --- a/src/Fable.Transforms/Python/Replacements.fs +++ b/src/Fable.Transforms/Python/Replacements.fs @@ -1267,6 +1267,16 @@ let operators (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr o | _ -> Helper.ImportedCall("math", "trunc", t, args, i.SignatureArgTypes, ?loc = r) |> Some + | "DivRem", _ -> + let modName = + match i.SignatureArgTypes with + | Number(BigInt, _) :: _ -> "big_int" + | Number(Int64, _) :: _ + | Number(UInt64, _) :: _ -> "long" + | _ -> "int32" + + Helper.LibCall(com, modName, "div_rem", t, args, i.SignatureArgTypes, ?loc = r) + |> Some | "Sign", _ -> match args with | ExprType(Number(Decimal, _)) :: _ -> @@ -2412,7 +2422,7 @@ let bigints (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg: Helper.LibCall(com, "BigInt", methodName, t, args, i.SignatureArgTypes, ?loc = r) |> Some | None, "DivRem" -> - Helper.LibCall(com, "big_int", "divRem", t, args, i.SignatureArgTypes, ?loc = r) + Helper.LibCall(com, "big_int", "div_rem", t, args, i.SignatureArgTypes, ?loc = r) |> Some | None, meth when meth.StartsWith("get_", StringComparison.Ordinal) -> Helper.LibValue(com, "big_int", meth, t) |> Some diff --git a/src/fable-library-py/fable_library/big_int.py b/src/fable-library-py/fable_library/big_int.py index d71cdd790e..c82895cd57 100644 --- a/src/fable-library-py/fable_library/big_int.py +++ b/src/fable-library-py/fable_library/big_int.py @@ -1,5 +1,5 @@ from decimal import Decimal -from typing import Any, SupportsInt +from typing import Any, SupportsInt, overload from .core import FSharpRef @@ -89,6 +89,25 @@ def op_modulus(a: int, b: int) -> int: return r +@overload +def div_rem(x: int, y: int) -> tuple[int, int]: ... + + +@overload +def div_rem(x: int, y: int, out: FSharpRef[int]) -> int: ... + + +def div_rem(x: int, y: int, out: FSharpRef[int] | None = None) -> int | tuple[int, int]: + # .NET uses truncated division; Python // floors toward -inf. + # Adjust when signs differ to get truncation toward zero. + q = -((-x) // y) if (x < 0) != (y < 0) else x // y + r = x - q * y + if out is None: + return (q, r) + out.contents = r + return q + + def op_right_shift(a: int, num_bits: int) -> int: return a >> num_bits @@ -272,6 +291,7 @@ def try_parse(string: str, def_value: FSharpRef[int]) -> bool: "abs", "add", "compare", + "div_rem", "divide", "equals", "from_int32", diff --git a/src/fable-library-py/fable_library/int32.py b/src/fable-library-py/fable_library/int32.py index 7b95be094d..becf748e5b 100644 --- a/src/fable-library-py/fable_library/int32.py +++ b/src/fable-library-py/fable_library/int32.py @@ -1,4 +1,6 @@ -from .core import int8, int16, int32 +from typing import SupportsInt, overload + +from .core import FSharpRef, int8, int16, int32 from .core._core import get_range from .core._core import parse_int32 as parse from .core._core import try_parse_int32 as try_parse @@ -12,6 +14,24 @@ def sign(x: int32) -> int32: return int32.NEG_ONE if x < int32.ZERO else int32.ONE if x > int32.ZERO else int32.ZERO +@overload +def div_rem[T: SupportsInt](x: T, y: T) -> tuple[T, T]: ... + + +@overload +def div_rem[T: SupportsInt](x: T, y: T, out: FSharpRef[T]) -> T: ... + + +def div_rem[T: SupportsInt](x: T, y: T, out: FSharpRef[T] | None = None) -> T | tuple[T, T]: + # Rust wrapper types already use truncated division and remainder + q = x // y # type: ignore[operator] + r = x % y # type: ignore[operator] + if out is None: + return (q, r) + out.contents = r + return q + + def op_unary_negation_int8(x: int8) -> int8: return -x @@ -26,6 +46,7 @@ def op_unary_negation_int32(x: int32) -> int32: __all__ = [ "AllowHexSpecifier", + "div_rem", "get_range", "op_unary_negation_int8", "op_unary_negation_int16", diff --git a/src/fable-library-py/fable_library/long.py b/src/fable-library-py/fable_library/long.py index 50d7ed84ca..6f12763c8a 100644 --- a/src/fable-library-py/fable_library/long.py +++ b/src/fable-library-py/fable_library/long.py @@ -1,6 +1,6 @@ from typing import Any, Literal, SupportsFloat, SupportsInt, overload -from .core import float64, int32, int64, uint64 +from .core import FSharpRef, float64, int32, int64, uint64 from .core._core import get_range_64 as get_range from .core._core import parse_int64 as parse from .core._core import try_parse_int64 as try_parse @@ -127,6 +127,24 @@ def op_modulus(a: int64 | uint64, b: int64 | uint64) -> int64 | uint64: return a % b +@overload +def div_rem(x: int64, y: int64) -> tuple[int64, int64]: ... + + +@overload +def div_rem(x: int64, y: int64, out: FSharpRef[int64]) -> int64: ... + + +def div_rem(x: int64, y: int64, out: FSharpRef[int64] | None = None) -> int64 | tuple[int64, int64]: + # Rust wrapper types already use truncated division and remainder + q = x // y + r = x % y + if out is None: + return (q, r) + out.contents = r + return q + + @overload def op_right_shift(a: int64, b: int32) -> int64: ... @@ -278,6 +296,7 @@ def to_string(x: int64 | uint64) -> str: __all__ = [ "compare", + "div_rem", "from_bits", "from_int", "from_integer", diff --git a/tests/Python/TestArithmetic.fs b/tests/Python/TestArithmetic.fs index c1e4658cb1..374d839a5d 100644 --- a/tests/Python/TestArithmetic.fs +++ b/tests/Python/TestArithmetic.fs @@ -66,36 +66,36 @@ let ``test Infix modulo with negative numbers`` () = 5 % -3 |> equal 2 -5 % -3 |> equal -2 -// [] -// let ``test Math.DivRem works with bytes`` () = -// Math.DivRem(5y, 2y) |> equal struct (2y, 1y) -// Math.DivRem(4y, 2y) |> equal struct (2y, 0y) +[] +let ``test Math.DivRem works with bytes`` () = + Math.DivRem(5y, 2y) |> equal struct (2y, 1y) + Math.DivRem(4y, 2y) |> equal struct (2y, 0y) -// [] -// let ``test Math.DivRem works with ints`` () = -// Math.DivRem(5, 2) |> equal struct (2, 1) -// Math.DivRem(4, 2) |> equal struct (2, 0) +[] +let ``test Math.DivRem works with ints`` () = + Math.DivRem(5, 2) |> equal struct (2, 1) + Math.DivRem(4, 2) |> equal struct (2, 0) -// [] -// let ``test Math.DivRem works with longs`` () = -// Math.DivRem(5L, 2L) |> equal struct (2L, 1L) -// Math.DivRem(4L, 2L) |> equal struct (2L, 0L) +[] +let ``test Math.DivRem works with longs`` () = + Math.DivRem(5L, 2L) |> equal struct (2L, 1L) + Math.DivRem(4L, 2L) |> equal struct (2L, 0L) -// [] -// let ``test Math.DivRem works with ints and outref`` () = -// let mutable rem = -1 -// Math.DivRem(5, 2, &rem) |> equal 2 -// rem |> equal 1 -// Math.DivRem(4, 2, &rem) |> equal 2 -// rem |> equal 0 +[] +let ``test Math.DivRem works with ints and outref`` () = + let mutable rem = -1 + Math.DivRem(5, 2, &rem) |> equal 2 + rem |> equal 1 + Math.DivRem(4, 2, &rem) |> equal 2 + rem |> equal 0 -// [] -// let ``test Math.DivRem works with longs and outref`` () = -// let mutable rem = -1L -// Math.DivRem(5L, 2L, &rem) |> equal 2L -// rem |> equal 1L -// Math.DivRem(4L, 2L, &rem) |> equal 2L -// rem |> equal 0L +[] +let ``test Math.DivRem works with longs and outref`` () = + let mutable rem = -1L + Math.DivRem(5L, 2L, &rem) |> equal 2L + rem |> equal 1L + Math.DivRem(4L, 2L, &rem) |> equal 2L + rem |> equal 0L [] let ``test Evaluation order is preserved by generated code`` () = @@ -464,11 +464,11 @@ let ``test BigInt Infix modulo with negative numbers`` () = 5I % -3I |> equal 2I -5I % -3I |> equal -2I -// [] -// let ``test BigInt.DivRem works`` () = // See #1744 -// let quotient,remainder = bigint.DivRem(5I,2I) -// 2I |> equal quotient -// 1I |> equal remainder +[] +let ``test BigInt.DivRem works`` () = // See #1744 + let quotient,remainder = bigint.DivRem(5I,2I) + 2I |> equal quotient + 1I |> equal remainder [] let ``test BigInt Evaluation order is preserved by generated code`` () =