haskell / haskell/core-libraries-committee
Stop `read` silent overflow on bounded integral types
- Dominant language
- Haskell
- Stars
- 109
- Forks
- 20
- PR merge metrics
- No merged PRs in 30d
Description
Hello, I'd like come back to a topic that I've [raised][5] several weeks ago. Sorry it took me so long.
Motivation
==========
> read "298" :: Word8
42
I fail to imagine a situation where this is right.
Current `read` may silently glance over a mistake, be it by the user who gave out-of-bounds data, or by the programmer who chose an insufficient datatype.
I've written a [workaround library][1], partially covering `ReadP`, `parsec` and `attoparsec`, but I find it a nuisance to use a library that fixes other libraries.
**Good performance:** I have conducted [benchmarking with criterion][7], the outcome suggests my implementation is slightly faster than the current implementation for **all except** the 64bit types (maybe my *criterion* is wrong, though).
I've startet implementing this in GHC, crudely, see output below, but I would need some help with the module structure and testing.
Previous discussion
-------------------
I've triggered some [discussion][2] about this on the `haskell-cafe` mailing list, followed by [some more][5] on `ghc-devs`.
Initially, I was targeting some parsing libraries, but a recurring argument in discussion seems to be that Haskell just does it this way. So I feel like I should tackle `base` first.
The in my opinion strongest arguments against changing `base` are:
* it would violate the standard, and
* performance concerns (which I think I can refute).
Proposal
========
The silent overflowing of `read` for **bounded integral types** is a bug that should be eradicated.
* I do *not* want to modify `read` for `Integer` or `Natural`.
* I do *not* want to modify explicit conversion with `fromInteger`.
* With *bounded integral types* I refer to `Int`, `Int8`, …, and
their unsigned variants `Word`, `Word8`, …, etc.
Putting current (left) and suggested (right) behaviour side by side, only the first and the last expression yield a different result:
GHCi, version 9.10.1 | GHCi, version 9.15.20250904
|
λ> import Data.Int | λ> import Data.Int
|
λ> read "298" :: Int8 | λ> read "298" :: Int8
42 | *** Exception: Prelude.read: no parse
|
λ> read "298" :: Int16 | λ> read "298" :: Int16
298 | 298
|
λ> 298 :: Int8 | λ> 298 :: Int8
:4:1: warning: | :4:1: warning:
[GHC-97441] [-Woverflowed-literals] | [GHC-97441] [-Woverflowed-literals]
Literal 298 is out of the Int8 | Literal 298 is out of the Int8
range -128..127 | range -128..127
42 | 42
|
λ> fromInteger 298 :: Int8 | λ> fromInteger 298 :: Int8
42 | 42
|
λ> read (repeat '1') :: Int64 | read (repeat '1') :: Int64
^CInterrupted. | *** Exception: Prelude.read: no parse
(output on the right is from a locally modified GHC)
Reasoning
---------
By using `fromInteger`, a programmer makes the desired conversion explicit, justifying the assumption that the potential overflow is anticipated, and expected or safeguarded against.
By using `read` at a *particular* type *other* than `Integer`, one clearly wants to read a value of that type. That value should match the canonical assumption of the provider of the input.
If overflow is desired, use `read @Integer` and `fromInteger`, making the desired behaviour explicit.
The workings of Haskell's overloaded numerals in Haskell source code remain unchanged.
In some sense, parsing is the outermost interface of some software entity. If error detection is neglected there, then there is no way to identify invalid input.
Open questions
--------------
1. Unsurprisingly, discussion so far ([haskell-cafe][2], [ghc-devs][5]) showed some scepticism, especially claims that “fixing” this would violate the Haskell specification. I'm open to the objection, but I've failed to verify this.
2. Would there be unfortunate consequences?
3. Reading from a producer of an infinite sequence of zeros currently gives `*** Exception: stack overflow`, I need to work on this. Should leading zeros be allowed at all? Should there be a “generous, but sensible limit on the number of leading zeros” ([Viktor][6])?
Implementation
--------------
I've managed to compile GHC (piece of cake, really) and I've dared to modify the `base` library (less cakeish). Some more samples:
λ> read "0b1111111" :: Int8
127
λ> read "0b10000000" :: Int8
*** Exception: Prelude.read: no parse
λ> read "-0b10000000" :: Int8
-128
λ> read "-0b10000001" :: Int8
*** Exception: Prelude.read: no parse
λ> read "-0x8000" :: Int16
-32768
λ> read "-0x8001" :: Int16
*** Exception: Prelude.read: no parse
λ> read "-0x8001" :: Int32
-32769
What Now?
---------
The code quality of my GHC modification certainly is on the extremely experimental side. I struggle with the module structure, and I would need help with testing. Also, I would certainly need help with an “impact assessment”.
Is there interest in this? If so, how should I proceed?
Cheers
Stefan
[1]: https://github.com/s5k6/robust-int
[2]: https://mail.haskell.org/pipermail/haskell-cafe/2025-July/137134.html
[5]: https://mail.haskell.org/pipermail/ghc-devs/2025-July/022046.html
[6]: https://mail.haskell.org/pipermail/ghc-devs/2025-July/022048.html
[7]: https://github.com/s5k6/robust-int/blob/master/unspecialized.html
Contributor guide
No contributing guide indexed for this repository
Assessment
This issue has not been assessed yet.