Unsound optimization breaks pattern matching on sum type
Summary
Enabling -O2
makes a case expression pattern match pick the 7th constructor of a sum type also when called with any constructor from the 2nd to the 6th.
The following factors seem to concur in triggering this behaviour (see the Expected behaviour section below for more details):
-O2
- usage of the
streamly-core
library (I could not reproduce without it) - a strictness annotation on the type used to hold the constructor to pass to the buggy function
- splitting the code into separate modules
On the other hand, using Debug.Trace.trace
to examine the constructor before feeding it into the case expression fixes the issue, making it a Heisenbug.
Steps to reproduce
$ git clone https://github.com/demaledetti/test-bug
$ cd test-bug
$ cabal clean && cabal build --ghc-options -v > cabal.log.ko 2>&1 && for i in {A..J}; do cabal run testbug -- $i; done
parser: A
testbug: src/TestBug/Parser.hs:(15,18)-(25,34): Non-exhaustive patterns in case
parser: B
testbug: Prelude.undefined
CallStack (from HasCallStack):
undefined, called at src/TestBug/Parser.hs:23:10 in test-bug-0.1.0.0-inplace:TestBug.Parser
parser: C
testbug: Prelude.undefined
CallStack (from HasCallStack):
undefined, called at src/TestBug/Parser.hs:23:10 in test-bug-0.1.0.0-inplace:TestBug.Parser
parser: D
testbug: Prelude.undefined
CallStack (from HasCallStack):
undefined, called at src/TestBug/Parser.hs:23:10 in test-bug-0.1.0.0-inplace:TestBug.Parser
parser: E
testbug: Prelude.undefined
CallStack (from HasCallStack):
undefined, called at src/TestBug/Parser.hs:23:10 in test-bug-0.1.0.0-inplace:TestBug.Parser
parser: F
testbug: Prelude.undefined
CallStack (from HasCallStack):
undefined, called at src/TestBug/Parser.hs:23:10 in test-bug-0.1.0.0-inplace:TestBug.Parser
parser: G
testbug: Prelude.undefined
CallStack (from HasCallStack):
undefined, called at src/TestBug/Parser.hs:23:10 in test-bug-0.1.0.0-inplace:TestBug.Parser
parser: H
OK
parser: I
OK
parser: J
testbug: src/TestBug/Parser.hs:(15,18)-(25,34): Non-exhaustive patterns in case
Expected behaviour
With -O1
:
$ cabal clean && cabal build -O1 --ghc-options -v > cabal.log.ok 2>&1 && for i in {A..J}; do cabal run -O1 testbug -- $i; done
parser: A
testbug: src/TestBug/Parser.hs:(15,18)-(25,34): Non-exhaustive patterns in case
parser: B
OK
parser: C
OK
parser: D
OK
parser: E
OK
parser: F
OK
parser: G
testbug: Prelude.undefined
CallStack (from HasCallStack):
undefined, called at src/TestBug/Parser.hs:23:10 in test-bug-0.1.0.0-inplace:TestBug.Parser
parser: H
OK
parser: I
OK
parser: J
testbug: src/TestBug/Parser.hs:(15,18)-(25,34): Non-exhaustive patterns in case
For other ways to get the correct behaviour without lowering the optimization level, including the ones mentioned in the Summary section above, see these comments in the source code:
$ grep -rnI "the bug" src/ testbug/
src/TestBug/Data.hs:12: -- simplifying it away makes the bug disappear
src/TestBug/Parser.hs:5:-- makes the bug go away (making it a Heisenbug)
src/TestBug/Parser.hs:16: -- uncommenting the next line makes the bug disappear
src/TestBug/Parser.hs:26: -- uncommenting the next line makes the bug disappear
testbug/Main.hs:13:-- makes the bug go away
Environment
- GHC version used: 9.8.2
Optional:
- Operating System:
- System Architecture:
$ uname -a
Linux eppere 6.7.5-gentoo #1 SMP Mon Feb 19 15:51:31 CET 2024 x86_64 Intel(R) Core(TM) i7-1065G7 CPU @ 1.30GHz GenuineIntel GNU/Linux
Logs mentioned above:
Log of build with -O2
but removing the strictness annotation in Main.hs
(commenting line 14, uncommenting line 15):
(easier to diff vs cabal.log.ko)