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,
```haskell
with# :: a -> r -> r
```
which evaluates the `r`, ensuring that the `a` remains alive throughout
evaluation. This is equivalent to the common pattern,
```haskell
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
```haskell
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
Trac #14346.
However, with `with#` we can write this as
```haskell
test :: IO ()
test = do
arr <- newPinnedByteArray 42
with# arr (unIO (doSomething (byteArrayContents arr)))
```
This construction the conpiler 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.