From 4bc7b1e5abaee101198c336ea2ff9c1d0c12149d Mon Sep 17 00:00:00 2001
From: Fraser Tweedale <frase@frase.id.au>
Date: Fri, 21 Jul 2023 23:47:50 +1000
Subject: [PATCH] =?UTF-8?q?numberToRangedRational:=20fix=20edge=20cases=20?=
 =?UTF-8?q?for=20exp=20=E2=89=88=20(maxBound=20::=20Int)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Currently a negative exponent less than `minBound :: Int` results in
Infinity, which is very surprising and obviously wrong.

```
λ> read "1e-9223372036854775808" :: Double
0.0
λ> read "1e-9223372036854775809" :: Double
Infinity
```

There is a further edge case where the exponent can overflow when
increased by the number of tens places in the integer part, or
underflow when decreased by the number of leading zeros in the
fractional part if the integer part is zero:

```
λ> read "10e9223372036854775807" :: Double
0.0
λ> read "0.01e-9223372036854775808" :: Double
Infinity
```

To resolve both of these issues, perform all arithmetic and
comparisons involving the exponent in type `Integer`.  This approach
also eliminates the need to explicitly check the exponent against
`maxBound :: Int` and `minBound :: Int`, because the allowed range
of the exponent (i.e. the result of `floatRange` for the target
floating point type) is certainly within those bounds.

This change implements CLC proposal 192:
https://github.com/haskell/core-libraries-committee/issues/192
---
 libraries/base/Text/Read/Lex.hs               | 18 ++++++++---------
 libraries/base/changelog.md                   |  1 +
 libraries/base/tests/all.T                    |  1 +
 libraries/base/tests/read-float-double.hs     | 20 +++++++++++++++++++
 libraries/base/tests/read-float-double.stdout |  8 ++++++++
 5 files changed, 38 insertions(+), 10 deletions(-)
 create mode 100644 libraries/base/tests/read-float-double.hs
 create mode 100644 libraries/base/tests/read-float-double.stdout

diff --git a/libraries/base/Text/Read/Lex.hs b/libraries/base/Text/Read/Lex.hs
index 03d22228bb98..ba0c563c513a 100644
--- a/libraries/base/Text/Read/Lex.hs
+++ b/libraries/base/Text/Read/Lex.hs
@@ -47,7 +47,7 @@ import GHC.Unicode
 import GHC.Real( Rational, (%), fromIntegral, Integral,
                  toInteger, (^), quot, even )
 import GHC.List
-import GHC.Enum( minBound, maxBound )
+import GHC.Enum( maxBound )
 import Data.Maybe
 
 -- local copy to break import-cycle
@@ -121,12 +121,9 @@ numberToFixed _ _ = Nothing
 numberToRangedRational :: (Int, Int) -> Number
                        -> Maybe Rational -- Nothing = Inf
 numberToRangedRational (neg, pos) n@(MkDecimal iPart mFPart (Just exp))
-    -- if exp is out of integer bounds,
-    -- then the number is definitely out of range
-    | exp > fromIntegral (maxBound :: Int) ||
-      exp < fromIntegral (minBound :: Int)
-    = Nothing
-    | otherwise
+    -- Calculate amount to increase/decrease the exponent, based on (non
+    -- leading zero) places in the iPart, or leading zeros in the fPart.
+    -- If iPart and fPart are all zeros, return Nothing.
     = let mFirstDigit = case dropWhile (0 ==) iPart of
                         iPart'@(_ : _) -> Just (length iPart')
                         [] -> case mFPart of
@@ -139,10 +136,11 @@ numberToRangedRational (neg, pos) n@(MkDecimal iPart mFPart (Just exp))
       in case mFirstDigit of
          Nothing -> Just 0
          Just firstDigit ->
-             let firstDigit' = firstDigit + fromInteger exp
-             in if firstDigit' > (pos + 3)
+             -- compare exp to bounds as Integer to avoid over/underflow
+             let firstDigit' = toInteger firstDigit + exp
+             in if firstDigit' > toInteger (pos + 3)
                 then Nothing
-                else if firstDigit' < (neg - 3)
+                else if firstDigit' < toInteger (neg - 3)
                 then Just 0
                 else Just (numberToRational n)
 numberToRangedRational _ n = Just (numberToRational n)
diff --git a/libraries/base/changelog.md b/libraries/base/changelog.md
index ae6647eb6849..d63d1fc01612 100644
--- a/libraries/base/changelog.md
+++ b/libraries/base/changelog.md
@@ -38,6 +38,7 @@
   * Add nominal role annotations to SNat/SSymbol/SChar ([CLC proposal #170](https://github.com/haskell/core-libraries-committee/issues/170))
   * Make `Semigroup`'s `stimes` specializable. ([CLC proposal #8](https://github.com/haskell/core-libraries-committee/issues/8))
   * Deprecate `Data.List.NonEmpty.unzip` ([CLC proposal #86](https://github.com/haskell/core-libraries-committee/issues/86))
+  * Fixed exponent overflow/underflow bugs in the `Read` instances for `Float` and `Double` ([CLC proposal #192](https://github.com/haskell/core-libraries-committee/issues/192))
 
 ## 4.18.0.0 *March 2023*
   * Shipped with GHC 9.6.1
diff --git a/libraries/base/tests/all.T b/libraries/base/tests/all.T
index 95da992e817d..dfeb95a8f0ed 100644
--- a/libraries/base/tests/all.T
+++ b/libraries/base/tests/all.T
@@ -14,6 +14,7 @@ def normalise_quotes (str):
 #--------------------------------------
 test('SystemInfoTest', normal, compile_and_run, [''])
 test('readFloat', exit_code(1), compile_and_run, [''])
+test('read-float-double', normal, compile_and_run, [''])
 test('enumDouble', normal, compile_and_run, [''])
 test('enumRatio', normal, compile_and_run, [''])
 test('enumNumeric', normal, compile_and_run, [''])
diff --git a/libraries/base/tests/read-float-double.hs b/libraries/base/tests/read-float-double.hs
new file mode 100644
index 000000000000..628434671e0d
--- /dev/null
+++ b/libraries/base/tests/read-float-double.hs
@@ -0,0 +1,20 @@
+-- Test edge cases fixed in
+-- https://gitlab.haskell.org/ghc/ghc/-/merge_requests/10933
+
+main :: IO ()
+main = do
+  let
+    lo = toInteger (minBound :: Int)
+    hi = toInteger (maxBound :: Int)
+    readDouble = read :: String -> Double
+    readFloat = read :: String -> Float
+
+  print $ readDouble ("1e" <> show lo)
+  print $ readDouble ("1e" <> show (lo - 1))
+  print $ readDouble ("0.01e" <> show lo)
+  print $ readDouble ("10e" <> show hi)
+
+  print $ readFloat ("1e" <> show lo)
+  print $ readFloat ("1e" <> show (lo - 1))
+  print $ readFloat ("0.01e" <> show lo)
+  print $ readFloat ("10e" <> show hi)
diff --git a/libraries/base/tests/read-float-double.stdout b/libraries/base/tests/read-float-double.stdout
new file mode 100644
index 000000000000..59cc9b8d199e
--- /dev/null
+++ b/libraries/base/tests/read-float-double.stdout
@@ -0,0 +1,8 @@
+0.0
+0.0
+0.0
+Infinity
+0.0
+0.0
+0.0
+Infinity
-- 
GitLab