[Git][ghc/ghc][wip/primop-traits] Clear up PrimOp effect semantics

Sebastian Graf gitlab at gitlab.haskell.org
Wed Apr 1 15:24:21 UTC 2020



Sebastian Graf pushed to branch wip/primop-traits at Glasgow Haskell Compiler / GHC


Commits:
bbd20a71 by Sebastian Graf at 2020-04-01T17:23:55+02:00
Clear up PrimOp effect semantics

Previously we classified PrimOps with two boolean flags, `can_fail` and
`has_side_effects`. Although there is quite a slew of documentation
surrounding them, see `Note [PrimOp can_fail and has_side_effects]`
and `Note [Transformations affected by can_fail and has_side_effects]`,
I found it quite hard to understand and also was confused of
conservative misclassifications for some read-only primops like
`readMutVar#` (which is marked as `has_side_effect`, although it
actually shouldn't per semantics of `has_side_effect`, see #3207), but
not for others (`indexIntArr#`, which is just `can_fail`).

This patch defines a total order of 5 different `PrimOpEffect`s:

- `NoEffect`: A pure primop
- `ReadEffect`: Ideally, `readMutVar#` should be this kind of effect,
  but due to #3207 it's a `WriteEffect` and this effect will probably be
  unused (TODO: Implement and comment on this).
- `ThrowsImprecise`: Possibly throws an imprecise exception (and may
  perform read effects)
- `WriteEffect`: May write to a mutable ref cell, array or the world (or
  read from them or throw an imprecise exception)
- `ThrowsPrecise`: May throw a precise exception, or do any of the
  aforementioned effects.

Each effect is strictly "stronger" than its predecessor in this list
wrt. to program transformation that are sound to apply to it.
For example, we may speculatively execute read effects (as long as their
data dependencies such as the state token are satisfied), but we may not
speculate division (for fear of imprecise division-by-zero errors).

Which `PrimOpEffect` inhibits which transformation, including examples,
is spelled out in the rewritten `Note [Transformations affected by
PrimOpEffect]`.

Fixes #17900.

- - - - -


1 changed file:

- compiler/prelude/PrimOp.hs


Changes:

=====================================
compiler/prelude/PrimOp.hs
=====================================
@@ -299,132 +299,180 @@ perform a heap check or they block.
 primOpOutOfLine :: PrimOp -> Bool
 #include "primop-out-of-line.hs-incl"
 
+
 {-
 ************************************************************************
 *                                                                      *
             Failure and side effects
 *                                                                      *
 ************************************************************************
+-}
 
-Note [Checking versus non-checking primops]
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-  In GHC primops break down into two classes:
-
-   a. Checking primops behave, for instance, like division. In this
-      case the primop may throw an exception (e.g. division-by-zero)
-      and is consequently is marked with the can_fail flag described below.
-      The ability to fail comes at the expense of precluding some optimizations.
-
-   b. Non-checking primops behavior, for instance, like addition. While
-      addition can overflow it does not produce an exception. So can_fail is
-      set to False, and we get more optimisation opportunities.  But we must
-      never throw an exception, so we cannot rewrite to a call to error.
-
-  It is important that a non-checking primop never be transformed in a way that
-  would cause it to bottom. Doing so would violate Core's let/app invariant
-  (see Note [Core let/app invariant] in GHC.Core) which is critical to
-  the simplifier's ability to float without fear of changing program meaning.
-
-
-Note [PrimOp can_fail and has_side_effects]
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-Both can_fail and has_side_effects mean that the primop has
-some effect that is not captured entirely by its result value.
-
-----------  has_side_effects ---------------------
-A primop "has_side_effects" if it has some *write* effect, visible
-elsewhere
-    - writing to the world (I/O)
-    - writing to a mutable data structure (writeIORef)
-    - throwing a synchronous Haskell exception
-
-Often such primops have a type like
-   State -> input -> (State, output)
-so the state token guarantees ordering.  In general we rely *only* on
-data dependencies of the state token to enforce write-effect ordering
-
- * NB1: if you inline unsafePerformIO, you may end up with
-   side-effecting ops whose 'state' output is discarded.
-   And programmers may do that by hand; see #9390.
-   That is why we (conservatively) do not discard write-effecting
-   primops even if both their state and result is discarded.
-
- * NB2: We consider primops, such as raiseIO#, that can raise a
-   (Haskell) synchronous exception to "have_side_effects" but not
-   "can_fail".  We must be careful about not discarding such things;
-   see the paper "A semantics for imprecise exceptions".
-
- * NB3: *Read* effects (like reading an IORef) don't count here,
-   because it doesn't matter if we don't do them, or do them more than
-   once.  *Sequencing* is maintained by the data dependency of the state
-   token.
-
-----------  can_fail ----------------------------
-A primop "can_fail" if it can fail with an *unchecked* exception on
-some elements of its input domain. Main examples:
-   division (fails on zero denominator)
-   array indexing (fails if the index is out of bounds)
-
-An "unchecked exception" is one that is an outright error, (not
-turned into a Haskell exception,) such as seg-fault or
-divide-by-zero error.  Such can_fail primops are ALWAYS surrounded
-with a test that checks for the bad cases, but we need to be
-very careful about code motion that might move it out of
-the scope of the test.
-
-Note [Transformations affected by can_fail and has_side_effects]
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-The can_fail and has_side_effects properties have the following effect
-on program transformations.  Summary table is followed by details.
-
-            can_fail     has_side_effects
-Discard        YES           NO
-Float in       YES           YES
-Float out      NO            NO
-Duplicate      YES           NO
+-- | A classification of primops by triggered side effects.
+-- See Note [Classification by PrimOpEffect].
+-- The `Ord` instance is significant. A "stronger" effect means less
+-- transformations are sound to apply to them.
+data PrimOpEffect
+  = NoEffect
+  | ReadEffect
+  | ThrowsImprecise
+  | WriteEffect
+  | ThrowsPrecise
+  deriving (Eq, Ord)
+
+-- | Can we discard a call to the primop, i.e. @case a `op` b of _ -> rhs@?
+-- This is a question that i.e. the Simplifier asks before dropping the @case at .
+-- See Note [Transformations affected by can_fail and has_side_effects].
+isDiscardablePrimOpEffect :: PrimOpEffect -> Bool
+isDiscardablePrimOpEffect eff = eff <= ThrowsImprecise
+
+-- | Can we duplicate a call to the primop?
+-- This is a question that i.e. the Simplifier asks when inlining definitions
+-- involving primops with multiple syntactic occurrences.
+-- See Note [Transformations affected by can_fail and has_side_effects].
+isDupablePrimOpEffect :: PrimOpEffect -> Bool
+-- isDupablePrimOpEffect eff = True -- #3207, see the Note
+isDupablePrimOpEffect eff = eff <= ThrowsImprecise
+
+-- | Can we perform other actions first before entering the primop?
+-- This is the question that i.e. @FloatIn@ asks.
+-- See Note [Transformations affected by can_fail and has_side_effects].
+isDeferrablePrimOpEffect :: PrimOpEffect -> Bool
+isDeferrablePrimOpEffect eff = eff <= WriteEffect
+
+-- | Can we speculatively execute this primop, before performing other actions
+-- that should come first according to evaluation strategy?
+-- This is the question that i.e. @FloatOut@ (of a @case@) asks.
+-- See Note [Transformations affected by can_fail and has_side_effects].
+isSpeculatablePrimOpEffect :: PrimOpEffect -> Bool
+isSpeculatablePrimOpEffect eff = eff <= ReadEffect
+
+{- Note [Classification by PrimOpEffect]
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Some primops have effects that are not captured entirely by their result value.
+We distinguish these cases:
+
+  * NoEffect: Pure primop, like `plusInt#`.
+  * ReadEffect: A read-only primop, like `readMutVar#`, if it wasn't for #3207.
+  * ThrowsImprecise: Possibly throws an *imprecise* exception, like
+    division-by-zero or a segfault arising from an out of bounds array access.
+    An imprecise exception is an outright error and transformations may play
+    fast and loose by turning one imprecise exception into another, or bottom.
+    See Note [Precise vs imprecise exceptions] in GHC.Types.Demand.
+  * WriteEffect: A write side-effect, either writing to the RealWorld (IO) or
+    to a mutable variable (`writeMutVar#`).
+  * ThrowsPrecise: Possibly throws a *precise* exception. `raiseIO#` is the
+    only primop that does that.
+    See Note [Precise vs imprecise exceptions] in GHC.Types.Demand.
+
+Why is this classification necessary? Because the kind of effect a primop
+performs influences the transformations we are allowed to apply to it.
+For example let binding a division-by-zero (which `ThrowsImprecise`) might
+violate Core's let/app invariant (see Note [Core let/app invariant] in
+GHC.Core) which is critical to the simplifier's ability to float without fear
+of changing program meaning.
+
+See Note [Transformations affected by PrimOpEffect].
+
+Note [Transformations affected by PrimOpEffect]
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+The PrimOpEffect of a primop affects applicability of program transformations
+in the following way.
+Summary table is followed by details.
+
+           | NoEff. | ReadEff. | ThrowsImp. | WriteEff. | ThrowsPrec. |
+Discard    | YES    | YES      | YES        | NO        | NO          |
+Dupe       | YES    | NO^[1]   | NO^[1]     | NO        | NO          |
+Defer      | YES    | YES      | YES        | YES       | NO          |
+Speculate  | YES    | YES      | NO         | NO        | NO          |
+
+[1] This is only due to #3207, and should only apply to read effects that can
+be interleaved with write effects (e.g. mutable arrays as opposed to immutable
+ones). More details below.
+
+Note how there is a total order on effects in terms of which program
+tranformations they inhibit. A "stronger" effect means less transformations
+are sound to apply to it. NoEffect means any tranformation is sound;
+ThrowsPrecise means none of the following is.
+Whether or not a primop is cheap to evaluate is an orthogonal concern.
 
 * Discarding.   case (a `op` b) of _ -> rhs  ===>   rhs
-  You should not discard a has_side_effects primop; e.g.
+  You should not discard a WriteEffect or ThrowsPrecise primop; e.g.
      case (writeIntArray# a i v s of (# _, _ #) -> True
-  Arguably you should be able to discard this, since the
-  returned stat token is not used, but that relies on NEVER
-  inlining unsafePerformIO, and programmers sometimes write
-  this kind of stuff by hand (#9390).  So we (conservatively)
-  never discard a has_side_effects primop.
-
-  However, it's fine to discard a can_fail primop.  For example
+  Arguably you should be able to discard this, since the returned state token
+  is not used, but that relies on NEVER inlining unsafePerformIO, and
+  programmers sometimes write this kind of stuff by hand (#9390).  So we
+  (conservatively) never discard such a primop.
+  The situation with ThrowsPrecise primops such as raiseIO# is even more
+  restrictive: We may never discard a side effect throwing a precise exception.
+
+  However, it's fine to discard a ThrowsImprecise primop.  For example
      case (indexIntArray# a i) of _ -> True
-  We can discard indexIntArray#; it has can_fail, but not
-  has_side_effects; see #5658 which was all about this.
-  Notice that indexIntArray# is (in a more general handling of
-  effects) read effect, but we don't care about that here, and
-  treat read effects as *not* has_side_effects.
-
-  Similarly (a `/#` b) can be discarded.  It can seg-fault or
-  cause a hardware exception, but not a synchronous Haskell
-  exception.
-
-
-
-  Synchronous Haskell exceptions, e.g. from raiseIO#, are treated
-  as has_side_effects and hence are not discarded.
+  We can discard indexIntArray#; it might throw an imprecise segmentation
+  fault, but no precise exception, so we are OK with not observing it.
+  See #5658 which was all about this.
+  Similarly (a `/#` b) can be discarded.  It can seg-fault or cause a hardware
+  exception, but not a precise Haskell exception.
+  It's obviously fine to discard a ReadEffect if its result aren't used.
+
+* Duplication.  Example: The Simplifier inlines a (multi occ) binding.
+  You cannot duplicate any effectful primop participating in state token
+  threading. Not even what is actually a read-only effect like `readMutVar#`,
+  see #3207.
+  You might wonder how that can be problematic, but just look at
+  Control.Monad.ST.Lazy.Imp.strictToLazy!  We get something like this
+        p = case readMutVar# s v of
+              (# s', r #) -> (S# s', r)
+        s' = case p of (s', r) -> s'
+        r  = case p of (s', r) -> r
 
-* Float in.  You can float a can_fail or has_side_effects primop
-  *inwards*, but not inside a lambda (see Duplication below).
+  (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.
 
-* Float out.  You must not float a can_fail primop *outwards* lest
-  you escape the dynamic scope of the test.  Example:
+  If it wasn't for working around state token threading
+  (see https://gitlab.haskell.org/ghc/ghc/issues/3207#note_257470 for other
+  approaches), then duplication wouldn't be an issue at all, soundness-wise.
+  But for the time being, we mark primops that participate in state token
+  threading such as `readMutVar#` (a ReadEffect at heart) and `readArray#`
+  (ThrowsImprecise) as WriteEffect and say that we may not duplicate
+  WriteEffect.
+
+* Deferring.  Example: FloatIn, here inside a single-alt case:
+     case (a `op` b) of (# s, x #) -> case e of p -> rhs
+     ==>
+     case e of p -> case (a `op` b) of (# s, x #) -> rhs
+  Note that e might diverge (or throw an imprecise exception) and thus the
+  side-effect we would observe by evaluating op might not happen if we defer it
+  after e.
+
+  That is a problem if op ThrowsPrecise: If e diverges, the user can catch
+  the precise exception /before/ FloatIn, but not afterwards. Hence we may not
+  float in a ThrowsPrecise primop like raiseIO#.
+
+  But since e can never throw an imprecise exception, there is no
+  non-imprecise-exceptional control flow in which it is possible to observe
+  that a WriteEffect (and anything "weaker") didn't happen. So it's OK to
+  defer (every weaker than or equal to) write effects. So you can float a
+  WriteEffect *inwards*, but not inside a lambda (see Duplication below [SG: It
+  isn't obvious to me how that explains why we shouldn't float inside a lambda
+  at all]).
+
+* Speculating.  Example: Float out.
+  You must not float a ThrowsImprecise primop *outwards* lest you escape the
+  dynamic scope of the 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
+  Here we must not float the division outwards to give
       case x/# d of r ->
       case d ># 0# of
         True  -> r +# 1
         False -> 0
+  Now the potential division by zero will be performed in both branches.
 
-  Nor can you float out a has_side_effects primop.  For example:
+  Similarly you can't float out a (stronger) WriteEffect 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
@@ -435,22 +483,9 @@ Duplicate      YES           NO
   the writeMutVar will be performed in both branches, which is
   utterly wrong.
 
-* Duplication.  You cannot duplicate a has_side_effect 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 #) -> (S# 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.
-
-  However, it's fine to duplicate a can_fail primop.  That is really
-  the only difference between can_fail and has_side_effects.
+  It's OK to speculate read effects such as readMutVar#, though, because
+  they can't move before a i.e. write effect purely by data dependency on the
+  state token.
 
 Note [Implementation: how can_fail/has_side_effects affect transformations]
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~



View it on GitLab: https://gitlab.haskell.org/ghc/ghc/-/commit/bbd20a71a1f27bc03cb8e59acf5ccb6516a2de37

-- 
View it on GitLab: https://gitlab.haskell.org/ghc/ghc/-/commit/bbd20a71a1f27bc03cb8e59acf5ccb6516a2de37
You're receiving this email because of your account on gitlab.haskell.org.


-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.haskell.org/pipermail/ghc-commits/attachments/20200401/71109960/attachment-0001.html>


More information about the ghc-commits mailing list