Skip to content

Introduce with# as preferred alternative to touch#

Ben Gamari requested to merge wip/with into master

This introduces with# as a safer alternative to touch#, which is quite susceptible to the simplifier optimizing it away. with# is a new primop of this form,

with# :: a -> (State# s -> (# State s, r #)) -> State# s -> (# State# s, r #)

which evaluates the r, ensuring that the a remains alive throughout evaluation. This is equivalent to the common pattern,

with' :: a                                -- ^ value to keep alive
      -> (State# s -> (# State# s, r #))  -- ^ value to evaluate
      -> State# s -> (# State# s, r #)
with' x r s0 =
    case r s0 of { (# s1, r' #) ->
    case touch# x s1 of { s2 -> (# s2, r' #) } }

but much harder for the simplifier to mess up.

Consider, for instance the case

test :: IO ()
test = do
    arr <- newPinnedByteArray 42
    doSomething (byteArrayContents arr)
    touch arr

If the simplifier believes that doSomething will fail to return (e.g. because it is of the form forever ...), then it will be tempted to drop the continuation containing touch#. This would result in the garbage collector inappropriately freeing arr, resulting in catastrophe. This was the cause of #14346 (closed).

However, with with# we can write this as

test :: IO ()
test = do
    arr <- newPinnedByteArray 42
    with# arr (unIO (doSomething (byteArrayContents arr)))

This construction the compiler can't mangle as there is no continuation to drop. Instead, with# keeps arr alive by pushing a stack frame. Consequently, arr will be kept alive as long as we are in unIO (doSomething ...). If and when doSomething returns, the frame will be dropped and arr allowed to die.

Edited by Ben Gamari

Merge request reports