Note [Transformations affected by primop effects]
The PrimOpEffect properties have the following effect on program transformations. The summary table is followed by details. See also Note [Classifying primop effects] for exactly what each column means. NoEffect CanFail ThrowsException ReadWriteEffect Discard YES YES NO NO Defer (float in) YES YES SAFE SAFE Speculate (float out) YES NO NO NO Duplicate YES YES YES NO (SAFE means we could perform the transformation but do not.) * Discarding: case (a `op` b) of _ -> rhs ===> rhs You should not discard a ReadWriteEffect primop; e.g. case (writeIntArray# a i v s of (# _, _ #) -> True One could argue in favor of discarding this, since the returned State# token is not used. But in practice unsafePerformIO can easily produce similar code, and programmers sometimes write this kind of stuff by hand (#9390). So we (conservatively) never discard a ReadWriteEffect primop. Digression: We could try to track read-only effects separately from write effects to allow the former to be discarded. But in fact we want a more general rewrite for read-only operations: case readOp# state# of (# newState#, _unused_result #) -> body ==> case state# of newState# -> body Such a rewrite is not yet implemented, but would have to be done in a different place anyway. Discarding a ThrowsException primop would also discard any exception it might have thrown. For `raise#` or `raiseIO#` this would defeat the whole point of the primop, while for `dataToTagLarge#` or `seq#` this would make programs unexpectly lazier. However, it's fine to discard a CanFail primop. For example case (indexIntArray# a i) of _ -> True We can discard indexIntArray# here; this came up in #5658. Notice that CanFail primops like indexIntArray# can only trigger an exception when used incorrectly, i.e. a call that might not succeed is undefined behavior anyway. * Deferring (float-in): See Note [Floating primops] in GHC.Core.Opt.FloatIn. In the absence of data dependencies (including state token threading), we reserve the right to re-order the following things arbitrarily: * Side effects * Imprecise exceptions * Divergent computations (infinite loops) This lets us safely float almost any primop *inwards*, but not inside a (multi-shot) lambda. (See "Duplication" below.) However, the main reason to float-in a primop application would be to discard it (by floating it into some but not all branches of a case), so we actually only float-in NoEffect and CanFail operations. See also Note [Floating primops] in GHC.Core.Opt.FloatIn. (This automatically side-steps the question of precise exceptions, which mustn't be re-ordered arbitrarily but need at least ThrowsException.) * Speculation (strict float-out): You must not float a CanFail primop *outwards* lest it escape the dynamic scope of a run-time validity test. Example: case d ># 0# of True -> case x /# d of r -> r +# 1 False -> 0 Here we must not float the case outwards to give case x/# d of r -> case d ># 0# of True -> r +# 1 False -> 0 Otherwise, if this block is reached when d is zero, it will crash. Exactly the same reasoning applies to ThrowsException primops. Nor can you float out a ReadWriteEffect primop. For example: if blah then case writeMutVar# v True s0 of (# s1 #) -> s1 else s0 Notice that s0 is mentioned in both branches of the 'if', but only one of these two will actually be consumed. But if we float out to case writeMutVar# v True s0 of (# s1 #) -> if blah then s1 else s0 the writeMutVar will be performed in both branches, which is utterly wrong. What about a read-only operation that cannot fail, like readMutVar#? In principle we could safely float these out. But there are not very many such operations and it's not clear if there are real-world programs that would benefit from this. * Duplication: You cannot duplicate a ReadWriteEffect primop. You might wonder how this can occur given the state token threading, but just look at Control.Monad.ST.Lazy.Imp.strictToLazy! We get something like this p = case readMutVar# s v of (# s', r #) -> (State# s', r) s' = case p of (s', r) -> s' r = case p of (s', r) -> r (All these bindings are boxed.) If we inline p at its two call sites, we get a catastrophe: because the read is performed once when s' is demanded, and once when 'r' is demanded, which may be much later. Utterly wrong. #3207 is real example of this happening. Floating p into a multi-shot lambda would be wrong for the same reason. However, it's fine to duplicate a CanFail or ThrowsException primop.
References 2
- Classifying primop effects GHC.Builtin.PrimOps
- Floating primops GHC.Core.Opt.FloatIn
Referenced by 5
- Classifying primop effects GHC.Builtin.PrimOps ×2
- Floating primops GHC.Core.Opt.FloatIn
- GHC.Core.Utils call site
- GHC.CoreToStg.Prep call site