IO hack in demand analysis should apply in more situations
Consider
{-# LANGUAGE ScopedTypeVariables #-}
import Data.IORef
import Control.Exception
import Control.Monad
data Exc = Exc deriving Show
instance Exception Exc
-- Recursive instead of NOINLINE because of #17673
f :: Int -> Int -> IO ()
f 0 x = do
let true = sum [0..4] == 10
when true $ throwIO Exc
x `seq` return ()
f n x = f (n-1) (x+1)
main = f 1 (error "expensive computation") `catch` \(_ :: Exc) -> return ()
GHC will change its semantics from exiting normally to crashing when compiled with optimisations:
$ ghc -O0 io.hs
<compilation output>
$ ./io
$ ghc -O2 io.hs
<compilation output>
$ ./io
io: expensive computation
CallStack (from HasCallStack):
error, called at io.hs:18:13 in main:Main
This is because we are turning a precise exception (throwing Exc) into an imprecise one (forcing the bottom in x), the very thing we aim to avoid by applying the IO hack.
So, clearly not (isPrimOpId f) is unsound. Checking for exprOkForSideEffects would be sound, but is too imprecise (#17653 (closed)) because we also use it to make sure state threads are used linearly by read-only operations (#3207 (closed)).
Edit: While implementing !2525 (closed) I realised that it's not really a problem that not (isPrimOpId f) is unsound, because raiseIO# will never occur in a case scrutinee after simplification. There's !2956 (closed) which fixes this ticket.