Note [Transformations affected by primop effects]

GHC/Builtin/PrimOps.hs:451 compiler 3 tickets

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

Referenced by 5