Skip to content

unsafePerformIO/NOINLINE tricks no longer work

Summary

Starting with GHC 9.0.1, the following program:

{-# OPTIONS_GHC -O1 #-}

import System.IO.Unsafe
import Data.IORef

{-# NOINLINE resourceId #-}
resourceId :: IO Int
resourceId = unsafePerformIO counter

counter :: IO (IO Int)
counter = do
    ref <- newIORef 0
    pure $ atomicModifyIORef' ref $ \i -> let j = i + 1 in (j, j)

main = do
    print =<< resourceId
    print =<< resourceId

Has gone from printing 1 then 2, to printing 1 then 1.

Whether it's a bug or GHC just getting better at optimisation is debatable. It definitely breaks code that works with 8.10, but I appreciate that with unsafePerformIO that isn't evidence of anything. I'm raising a bug after looking at the core, which seems like it might be broken.

Steps to reproduce

Using GHC 9.0:

$ ghc --make Test.hs
$ ./Test
1
1

Expected behaviour

1
2

Environment

GHC 9.0.1. Reproduced on Windows, but happens on Linux/Mac CI machines too.

Core output

The reason I raised this as a bug is after looking at the output of -ddump-simpl, available at https://gist.github.com/ndmitchell/a45d1db2eea8f8b2ee207e5d29475fce#file-core-hs-L67. Approximately, I think it has done the equivalent of:

resourceId = runRW $ \rw1 rw2 -> <all the work>

Namely, it pushed the creation of the IORef inside the inner IO, which I don't believe is allowed.

To upload designs, you'll need to enable LFS and have an admin enable hashed storage. More information