From b042949fe92dba24ba82e64ea2fcf5d173ef1267 Mon Sep 17 00:00:00 2001 From: Thomas Honeyman Date: Mon, 9 Nov 2026 01:41:25 +0000 Subject: [PATCH 1/2] Fix rem zero divisors and signed zshr FFI results --- CHANGELOG.md | 3 +++ src/Data/Int.js | 2 +- src/Data/Int.purs | 5 +++++ src/Data/Int/Bits.js | 2 +- src/Data/Int/Bits.purs | 2 ++ test/Test/Data/Int.js | 7 +++++++ test/Test/Data/Int.purs | 42 +++++++++++++++++++++++++++++++++++++++++ 7 files changed, 61 insertions(+), 2 deletions(-) create mode 100644 test/Test/Data/Int.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d322f0..44205a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,9 @@ New features: Bugfixes: +- Return zero instead of `NaN` from `Data.Int.rem` when the divisor is zero (#58). +- Keep the FFI result of `Data.Int.Bits.zshr` within the signed 32-bit `Int` range (#59). This changes longstanding behavior: shifting a negative input by zero or a multiple of 32 now returns that input instead of converting it to an unsigned number. Nonzero effective shifts still zero-fill. A companion compiler fix is required for direct calls, which existing compilers inline without the signed coercion; this library fix alone does not fully resolve #59. + Other improvements: ## [v6.0.1](https://github.com/purescript/purescript-integers/releases/tag/v6.0.1) - 2026-10-17 diff --git a/src/Data/Int.js b/src/Data/Int.js index 98bedbb..2764ca3 100644 --- a/src/Data/Int.js +++ b/src/Data/Int.js @@ -49,7 +49,7 @@ export const quot = function (x) { export const rem = function (x) { return function (y) { - return x % y; + return (x % y) | 0; }; }; diff --git a/src/Data/Int.purs b/src/Data/Int.purs index fe2a77a..13e8ac0 100644 --- a/src/Data/Int.purs +++ b/src/Data/Int.purs @@ -228,6 +228,8 @@ fromStringAs = fromStringAsImpl Just Nothing -- | div 2 (-3) == 0 -- | quot 2 (-3) == 0 -- | ``` +-- | +-- | Returns zero when the divisor is zero. foreign import quot :: Int -> Int -> Int -- | The `rem` function provides the remainder after _truncating_ integer @@ -246,6 +248,9 @@ foreign import quot :: Int -> Int -> Int -- | mod 2 (-3) == 2 -- | rem 2 (-3) == 2 -- | ``` +-- | +-- | Returns zero when the divisor is zero, consistent with `quot` and `mod`. +-- | The quotient/remainder law only applies to nonzero divisors. foreign import rem :: Int -> Int -> Int -- | Raise an Int to the power of another Int. diff --git a/src/Data/Int/Bits.js b/src/Data/Int/Bits.js index ddb6225..21299d2 100644 --- a/src/Data/Int/Bits.js +++ b/src/Data/Int/Bits.js @@ -32,7 +32,7 @@ export const shr = function (n1) { export const zshr = function (n1) { return function (n2) { - return n1 >>> n2; + return (n1 >>> n2) | 0; }; }; diff --git a/src/Data/Int/Bits.purs b/src/Data/Int/Bits.purs index 98bc5bc..307c754 100644 --- a/src/Data/Int/Bits.purs +++ b/src/Data/Int/Bits.purs @@ -34,6 +34,8 @@ foreign import shl :: Int -> Int -> Int foreign import shr :: Int -> Int -> Int -- | Bitwise zero-fill shift right. +-- | Only the low five bits of the shift count are used. The result is a signed +-- | 32-bit `Int`, so shifting by zero (or a multiple of 32) returns the input. foreign import zshr :: Int -> Int -> Int -- | Bitwise NOT. diff --git a/test/Test/Data/Int.js b/test/Test/Data/Int.js new file mode 100644 index 0000000..4073aaa --- /dev/null +++ b/test/Test/Data/Int.js @@ -0,0 +1,7 @@ +export const applyIntBinary = function (f) { + return function (x) { + return function (y) { + return f(x)(y); + }; + }; +}; diff --git a/test/Test/Data/Int.purs b/test/Test/Data/Int.purs index c01cd7d..6bc8f5c 100644 --- a/test/Test/Data/Int.purs +++ b/test/Test/Data/Int.purs @@ -3,6 +3,7 @@ module Test.Data.Int (testInt) where import Prelude import Data.Int (binary, ceil, even, floor, fromNumber, fromString, fromStringAs, hexadecimal, octal, odd, parity, pow, quot, radix, rem, round, toNumber, toStringAs) +import Data.Int.Bits (zshr) import Data.Maybe (Maybe(..), fromJust) import Data.Number (infinity, nan) import Effect (Effect) @@ -10,6 +11,10 @@ import Effect.Console (log) import Partial.Unsafe (unsafePartial) import Test.Assert (assert) +-- Keep calls opaque to the compiler's bitwise-operation inliner so these +-- assertions exercise the library's FFI implementation. +foreign import applyIntBinary :: (Int -> Int -> Int) -> Int -> Int -> Int + testInt :: Effect Unit testInt = do @@ -170,6 +175,43 @@ testInt = do go 2 (-3) go (-2) (-3) + log "quot and rem return zero for a zero divisor" + let + testZeroDivisor x = do + assert $ quot x 0 == 0 + assert $ rem x 0 == 0 + assert $ applyIntBinary rem x 0 == 0 + testZeroDivisor 0 + testZeroDivisor 5 + testZeroDivisor (-5) + testZeroDivisor bottom + testZeroDivisor top + + log "rem preserves the dividend's sign" + assert $ applyIntBinary rem (-8) 3 == (-2) + assert $ applyIntBinary rem 8 (-3) == 2 + assert $ applyIntBinary rem (-8) (-3) == (-2) + + log "zshr FFI returns signed Ints and masks shift counts to five bits" + let + testShiftCount count = do + assert $ applyIntBinary zshr (-1) count == (-1) + assert $ applyIntBinary zshr (-8) count == (-8) + assert $ applyIntBinary zshr bottom count == bottom + assert $ applyIntBinary zshr top count == top + testShiftCount 0 + testShiftCount 32 + testShiftCount 64 + testShiftCount (-32) + + log "zshr zero-fills rather than sign-extends negative inputs" + assert $ zshr (-8) 1 == 2147483644 + assert $ zshr bottom 31 == 1 + assert $ zshr (-8) 33 == 2147483644 + assert $ applyIntBinary zshr (-8) 1 == 2147483644 + assert $ applyIntBinary zshr bottom 31 == 1 + assert $ applyIntBinary zshr (-8) 33 == 2147483644 + log "pow" assert $ pow 2 2 == 4 assert $ pow 5 3 == 125 From 8671880275a9b14d72edf94ecd0bcb49d6c47ba4 Mon Sep 17 00:00:00 2001 From: Thomas Honeyman Date: Sun, 8 Nov 2026 20:47:46 -0500 Subject: [PATCH 2/2] Revise bugfixes in CHANGELOG.md Update bugfixes section with new issue references and clarify behavior changes. --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 44205a8..1805de0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,8 +10,8 @@ New features: Bugfixes: -- Return zero instead of `NaN` from `Data.Int.rem` when the divisor is zero (#58). -- Keep the FFI result of `Data.Int.Bits.zshr` within the signed 32-bit `Int` range (#59). This changes longstanding behavior: shifting a negative input by zero or a multiple of 32 now returns that input instead of converting it to an unsigned number. Nonzero effective shifts still zero-fill. A companion compiler fix is required for direct calls, which existing compilers inline without the signed coercion; this library fix alone does not fully resolve #59. +- Return zero instead of `NaN` from `Data.Int.rem` when the divisor is zero (#60 by @thomashoneyman). +- Keep the FFI result of `Data.Int.Bits.zshr` within the signed 32-bit `Int` range (#60 by @thomashoneyman). Shifting a negative input by zero or a multiple of 32 now returns that input instead of converting it to an unsigned number. Other improvements: