packages feed

io-sim 1.2.0.0 → 1.3.0.0

raw patch · 14 files changed

+1301/−701 lines, 14 filesdep −paralleldep ~basedep ~io-classesdep ~si-timersPVP ok

version bump matches the API change (PVP)

Dependencies removed: parallel

Dependency ranges changed: base, io-classes, si-timers, strict-stm

API changes (from Hackage documentation)

+ Control.Monad.IOSim: EventEffect :: VectorClock -> Effect -> SimEventType
+ Control.Monad.IOSim: EventRaces :: Races -> SimEventType
+ Control.Monad.IOSim: FailureInternal :: String -> Failure
+ Control.Monad.IOSim: InternalError :: String -> SimResult a
+ Control.Monad.IOSim: RacyThreadId :: [Int] -> IOSimThreadId
+ Control.Monad.IOSim: ThreadId :: [Int] -> IOSimThreadId
+ Control.Monad.IOSim: [explorationDebugLevel] :: ExplorationOptions -> Int
+ Control.Monad.IOSim: controlSimTraceST :: Maybe Int -> ScheduleControl -> IOSim s a -> ST s (SimTrace a)
+ Control.Monad.IOSim: data IOSimThreadId
+ Control.Monad.IOSim: data Timeout s
+ Control.Monad.IOSim: exploreSimTraceST :: forall s a test. Testable test => (ExplorationOptions -> ExplorationOptions) -> (forall s. IOSim s a) -> (Maybe (SimTrace a) -> SimTrace a -> ST s test) -> ST s Property
+ Control.Monad.IOSim: pattern TraceInternalError :: String -> SimTrace a
- Control.Monad.IOSim: Deadlock :: !Time -> ![Labelled ThreadId] -> SimResult a
+ Control.Monad.IOSim: Deadlock :: !Time -> ![Labelled IOSimThreadId] -> SimResult a
- Control.Monad.IOSim: EventThreadForked :: ThreadId -> SimEventType
+ Control.Monad.IOSim: EventThreadForked :: IOSimThreadId -> SimEventType
- Control.Monad.IOSim: EventThreadStatus :: ThreadId -> ThreadId -> SimEventType
+ Control.Monad.IOSim: EventThreadStatus :: IOSimThreadId -> IOSimThreadId -> SimEventType
- Control.Monad.IOSim: EventThrowTo :: SomeException -> ThreadId -> SimEventType
+ Control.Monad.IOSim: EventThrowTo :: SomeException -> IOSimThreadId -> SimEventType
- Control.Monad.IOSim: EventThrowToUnmasked :: Labelled ThreadId -> SimEventType
+ Control.Monad.IOSim: EventThrowToUnmasked :: Labelled IOSimThreadId -> SimEventType
- Control.Monad.IOSim: EventTimeoutCreated :: TimeoutId -> ThreadId -> Time -> SimEventType
+ Control.Monad.IOSim: EventTimeoutCreated :: TimeoutId -> IOSimThreadId -> Time -> SimEventType
- Control.Monad.IOSim: EventUnblocked :: [ThreadId] -> SimEventType
+ Control.Monad.IOSim: EventUnblocked :: [IOSimThreadId] -> SimEventType
- Control.Monad.IOSim: ExplorationOptions :: Int -> Int -> Maybe Int -> Maybe ScheduleControl -> ExplorationOptions
+ Control.Monad.IOSim: ExplorationOptions :: Int -> Int -> Maybe Int -> Maybe ScheduleControl -> Int -> ExplorationOptions
- Control.Monad.IOSim: FailureDeadlock :: ![Labelled ThreadId] -> Failure
+ Control.Monad.IOSim: FailureDeadlock :: ![Labelled IOSimThreadId] -> Failure
- Control.Monad.IOSim: FailureSloppyShutdown :: [Labelled ThreadId] -> Failure
+ Control.Monad.IOSim: FailureSloppyShutdown :: [Labelled IOSimThreadId] -> Failure
- Control.Monad.IOSim: MainException :: !Time -> SomeException -> ![Labelled ThreadId] -> SimResult a
+ Control.Monad.IOSim: MainException :: !Time -> !Labelled IOSimThreadId -> SomeException -> ![Labelled IOSimThreadId] -> SimResult a
- Control.Monad.IOSim: MainReturn :: !Time -> a -> ![Labelled ThreadId] -> SimResult a
+ Control.Monad.IOSim: MainReturn :: !Time -> !Labelled IOSimThreadId -> a -> ![Labelled IOSimThreadId] -> SimResult a
- Control.Monad.IOSim: SimEvent :: !Time -> !ThreadId -> !Maybe ThreadLabel -> !SimEventType -> SimEvent
+ Control.Monad.IOSim: SimEvent :: !Time -> !IOSimThreadId -> !Maybe ThreadLabel -> !SimEventType -> SimEvent
- Control.Monad.IOSim: SimPOREvent :: !Time -> !ThreadId -> !Int -> !Maybe ThreadLabel -> !SimEventType -> SimEvent
+ Control.Monad.IOSim: SimPOREvent :: !Time -> !IOSimThreadId -> !Int -> !Maybe ThreadLabel -> !SimEventType -> SimEvent
- Control.Monad.IOSim: [seThreadId] :: SimEvent -> !ThreadId
+ Control.Monad.IOSim: [seThreadId] :: SimEvent -> !IOSimThreadId
- Control.Monad.IOSim: pattern SimPORTrace :: Time -> ThreadId -> Int -> Maybe ThreadLabel -> SimEventType -> SimTrace a -> SimTrace a
+ Control.Monad.IOSim: pattern SimPORTrace :: Time -> IOSimThreadId -> Int -> Maybe ThreadLabel -> SimEventType -> SimTrace a -> SimTrace a
- Control.Monad.IOSim: pattern SimTrace :: Time -> ThreadId -> Maybe ThreadLabel -> SimEventType -> SimTrace a -> SimTrace a
+ Control.Monad.IOSim: pattern SimTrace :: Time -> IOSimThreadId -> Maybe ThreadLabel -> SimEventType -> SimTrace a -> SimTrace a
- Control.Monad.IOSim: pattern TraceDeadlock :: Time -> [Labelled ThreadId] -> SimTrace a
+ Control.Monad.IOSim: pattern TraceDeadlock :: Time -> [Labelled IOSimThreadId] -> SimTrace a
- Control.Monad.IOSim: pattern TraceMainException :: Time -> SomeException -> [Labelled ThreadId] -> SimTrace a
+ Control.Monad.IOSim: pattern TraceMainException :: Time -> Labelled IOSimThreadId -> SomeException -> [Labelled IOSimThreadId] -> SimTrace a
- Control.Monad.IOSim: pattern TraceMainReturn :: Time -> a -> [Labelled ThreadId] -> SimTrace a
+ Control.Monad.IOSim: pattern TraceMainReturn :: Time -> Labelled IOSimThreadId -> a -> [Labelled IOSimThreadId] -> SimTrace a
- Control.Monad.IOSim: ppEvents :: [(Time, ThreadId, Maybe ThreadLabel, SimEventType)] -> String
+ Control.Monad.IOSim: ppEvents :: [(Time, IOSimThreadId, Maybe ThreadLabel, SimEventType)] -> String
- Control.Monad.IOSim: traceEvents :: SimTrace a -> [(Time, ThreadId, Maybe ThreadLabel, SimEventType)]
+ Control.Monad.IOSim: traceEvents :: SimTrace a -> [(Time, IOSimThreadId, Maybe ThreadLabel, SimEventType)]

Files

CHANGELOG.md view
@@ -1,5 +1,47 @@ # Revsion history of io-sim +## next release++### Breaking changes++### Non-breaking changes++## 1.3.0.0++### Breaking changes++* `MainReturn`, `MainException` and the pattern synonyms `TraceMainReturn`,+  `TraceMainException` changed their signature.  They will now also show the main thread id.+* Renamed `ThreadId` to `IOSimThreadId` to avoid a clash with `ThreadId`+  associated type family of `MonadFork`.  It makes it much simpler to paste+  failing `ScheduleControl` in `ghci` or tests.+* `BlockedReason` was modified: `BlockedOnOther` was removed, in favour of `BlockedOnDelay` and `BlockOnThrowTo`.+* The `Failure` type (for example returned by `runSim`) now also contains+  a constructor for internal failures.  This improved error reporting when+  there's a bug in `IOSimPOR`.  Currently it's only used by some of the+  assertions in `IOSimPOR`.++#### Non-breaking changes++* Refactored the internal API to avoid `unsafePerformIO`.+* Fixed bugs which lead to discovery of schedules which are impossible to run.+* Added haddocks, refactored the code base to improve readability.+* Fixed reported `step` in `EventTxWakup`+* Added debugging information schedule, (`explorationDebugLevel` option).+  Mostly useful for debugging `IOSimPOR` itself.  This information will+  contains `Effect`, discovered races and schedules.+* Addded or improved pretty printers for `SimTrace`.  Among other changes,+  a racy `StepId`: `(RacyThreadId [1,2], 2)`, is now pretty printed as `Thread+  {1,2}.2`, a non racy step is printed as `Thread [1,2].2`.+* Fixed trace of calls to the `deschedule` function.+* Exposed `Timeout` type as part of the `newTimeout` API.+* When `explorationDebugLevel` is set, avoid printing the same trace twice.+* Reimplemented `labelTVarIO` and `traceTVarIO` in `ST` monad, which simplifies+  trace of these calls.+* Fixed `traceTVar` for `TVar`'s created with `registerDelay`.+* Added pretty printer for `SimResult`, and other pretty printer improvements.+* Support `ghc-9.8`.+ ## 1.2.0.0  ### Breaking changes@@ -11,20 +53,20 @@   - `selectTraceEventsSayWithTime`   - `selectTraceEventsSayWithTime'` -### Non breaking changes+### Non-breaking changes  * Provide `MonadInspectMVar` instance for `IOSim`. - Added NFData & NoThunks instances for `ThreadId`  ## 1.1.0.0 -### Non breaking changes+### Non-breaking changes  * `io-classes-1.1.0.0`  ## 1.0.0.1 -### Non breaking changes+### Non-breaking changes  * Support `ghc-9.6`. @@ -41,7 +83,7 @@  * Added `TimeoutId` to `EventThreadDelay` and `EventThreadFired` events. -### Non breaking changes+### Non-breaking changes  * Fixed `threadDelay` in presence of asynchronous exceptions (in `IOSim` and `IOSimPOR`) (#80). * Fixed bug in `IOSim` & `IOSimPOR` which resulted in reusing existing
README.md view
@@ -15,6 +15,7 @@   * inspection of `STM` mutable data structures;   * deadlock detection;   * `MonadFix` instances for both `IOSim` and its corresponding `STM` monad.+  * partial order reduction (see [IOSimPOR]).  `io-sim` together with [`io-classes`] is a drop-in replacement for the `IO` monad (with some ramifications).  It was designed to write easily testable@@ -42,3 +43,4 @@  [`io-classes`]: https://hackage.haskell.org/package/io-classes [`si-timers`]: https://hackage.haskell.org/package/si-timers+[IOSimPOR]: https://github.com/input-output-hk/io-sim/tree/main/io-sim/how-to-use-IOSimPOR.md
io-sim.cabal view
@@ -1,6 +1,6 @@ cabal-version:       3.0 name:                io-sim-version:             1.2.0.0+version:             1.3.0.0 synopsis:            A pure simulator for monadic concurrency with STM. description:   A pure simulator monad with support of concurency (base, async), stm,@@ -15,7 +15,7 @@ build-type:          Simple extra-doc-files:     CHANGELOG.md README.md bug-reports:         https://github.com/input-output-hk/io-sim/issues-tested-with:         GHC == { 8.10, 9.2, 9.4, 9.6 }+tested-with:         GHC == { 8.10, 9.2, 9.4, 9.6, 9.8 }  flag asserts   description: Enable assertions@@ -74,16 +74,15 @@                        RankNTypes,                        ScopedTypeVariables,                        TypeFamilies-  build-depends:       base              >=4.9 && <4.19,-                       io-classes       ^>=1.2,+  build-depends:       base              >=4.9 && <4.20,+                       io-classes       ^>=1.3,                        exceptions        >=0.10,                        containers,                        deepseq,                        nothunks,-                       parallel,                        psqueues          >=0.2 && <0.3,-                       strict-stm       ^>=1.2,-                       si-timers        ^>=1.2,+                       strict-stm       ^>=1.3,+                       si-timers        ^>=1.3,                        time              >=1.9.1 && <1.13,                        quiet,                        QuickCheck,@@ -108,7 +107,6 @@                        containers,                        io-classes,                        io-sim,-                       parallel,                        QuickCheck,                        si-timers,                        strict-stm,@@ -117,6 +115,9 @@                        tasty-hunit,                        time   ghc-options:         -fno-ignore-asserts+                       -rtsopts+  if impl(ghc >= 9.8)+    ghc-options:       -Wno-x-partial  benchmark bench   import:              warnings
src/Control/Monad/IOSim.hs view
@@ -1,3 +1,5 @@+{-# LANGUAGE BangPatterns        #-}+{-# LANGUAGE LambdaCase          #-} {-# LANGUAGE ExplicitNamespaces  #-} {-# LANGUAGE NamedFieldPuns      #-} {-# LANGUAGE RankNTypes          #-}@@ -5,6 +7,8 @@ {-# LANGUAGE TupleSections       #-}  {-# OPTIONS_GHC -Wno-name-shadowing #-}+{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-}+ module Control.Monad.IOSim   ( -- * Simulation monad     IOSim@@ -19,7 +23,9 @@     -- ** Explore races using /IOSimPOR/     -- $iosimpor   , exploreSimTrace+  , exploreSimTraceST   , controlSimTrace+  , controlSimTraceST   , ScheduleMod (..)   , ScheduleControl (..)     -- *** Exploration options@@ -37,11 +43,21 @@   , unshareClock     -- * Simulation trace   , type SimTrace-  , Trace (Cons, Nil, SimTrace, SimPORTrace, TraceDeadlock, TraceLoop, TraceMainReturn, TraceMainException, TraceRacesFound)+  , Trace (Cons,+           Nil,+           SimTrace,+           SimPORTrace,+           TraceDeadlock,+           TraceLoop,+           TraceMainReturn,+           TraceMainException,+           TraceRacesFound,+           TraceInternalError)   , SimResult (..)   , SimEvent (..)   , SimEventType (..)   , ThreadLabel+  , IOSimThreadId (..)   , Labelled (..)     -- ** Dynamic Tracing   , traceM@@ -77,6 +93,7 @@   , EventlogEvent (..)   , EventlogMarker (..)     -- * Low-level API+  , Timeout   , newTimeout   , readTimeout   , cancelTimeout@@ -86,30 +103,34 @@ import           Prelude  import           Data.Bifoldable+import           Data.Bifunctor (first) import           Data.Dynamic (fromDynamic)+import           Data.Functor (void) import           Data.List (intercalate)+import           Data.Maybe (catMaybes) import           Data.Set (Set) import qualified Data.Set as Set import           Data.Typeable (Typeable)  import           Data.List.Trace (Trace (..))+import qualified Data.List.Trace as Trace  import           Control.Exception (throw)  import           Control.Monad.ST.Lazy+import           Data.STRef.Lazy  import           Control.Monad.Class.MonadThrow as MonadThrow  import           Control.Monad.IOSim.Internal (runSimTraceST) import           Control.Monad.IOSim.Types-import           Control.Monad.IOSimPOR.Internal (controlSimTraceST)+import qualified Control.Monad.IOSimPOR.Internal as IOSimPOR (controlSimTraceST) import           Control.Monad.IOSimPOR.QuickCheckUtils  import           Test.QuickCheck --import           Data.IORef import           System.IO.Unsafe+import qualified Debug.Trace as Debug   selectTraceEvents@@ -119,10 +140,11 @@ selectTraceEvents fn =       bifoldr ( \ v _                -> case v of-                    MainException _ e _       -> throw (FailureException e)+                    MainException _ _ e _     -> throw (FailureException e)                     Deadlock      _   threads -> throw (FailureDeadlock threads)-                    MainReturn    _ _ _       -> []+                    MainReturn    _ _ _ _     -> []                     Loop                      -> error "Impossible: selectTraceEvents _ TraceLoop{}"+                    InternalError msg         -> throw (FailureInternal msg)               )               ( \ b acc -> b : acc )               []@@ -161,19 +183,39 @@ -- unsafe, of course, since that function may return different results -- at different times. -detachTraceRaces :: SimTrace a -> (() -> [ScheduleControl], SimTrace a)-detachTraceRaces trace = unsafePerformIO $ do-  races <- newIORef []-  let readRaces ()  = concat . reverse . unsafePerformIO $ readIORef races-      saveRaces r t = unsafePerformIO $ do-                        modifyIORef races (r:)-                        return t-  let go (SimTrace a b c d trace)      = SimTrace a b c d $ go trace-      go (SimPORTrace a b c d e trace) = SimPORTrace a b c d e $ go trace-      go (TraceRacesFound r trace)     = saveRaces r $ go trace-      go t                             = t-  return (readRaces, go trace)+-- | Detach discovered races.  This is written in `ST` monad to support+-- simulations which do not terminate, in which case we will only detach races+-- up to the point we evaluated the simulation.+--+detachTraceRacesST :: forall a s. SimTrace a -> ST s (ST s [ScheduleControl], SimTrace a)+detachTraceRacesST trace0 = do+  races <- newSTRef []+  let readRaces :: ST s [ScheduleControl]+      readRaces = concat . reverse <$> readSTRef races +      saveRaces :: [ScheduleControl] -> ST s ()+      saveRaces rs = modifySTRef races (rs:)++      go :: SimTrace a -> ST s (SimTrace a)+      go (SimTrace a b c d trace)      = SimTrace a b c d <$> go trace+      go (SimPORTrace _ _ _ _ EventRaces {} trace)+                                       = go trace+      go (SimPORTrace a b c d e trace) = SimPORTrace a b c d e <$> go trace+      go (TraceRacesFound rs trace)    = saveRaces rs >> go trace+      go t                             = return t++  trace <- go trace0+  return (readRaces, trace)+++-- | Like `detachTraceRacesST`, but it doesn't expose discovered races.+--+detachTraceRaces :: forall a. SimTrace a -> SimTrace a+detachTraceRaces = Trace.filter (\a -> case a of+                                         SimPOREvent { seType = EventRaces {} } -> False+                                         SimRacesFound {} -> False+                                         _                -> True)+ -- | Select all the traced values matching the expected type. This relies on -- the sim's dynamic trace facility. --@@ -309,17 +351,23 @@        FailureException SomeException         -- | The threads all deadlocked.-     | FailureDeadlock ![Labelled ThreadId]+     | FailureDeadlock ![Labelled IOSimThreadId]         -- | The main thread terminated normally but other threads were still        -- alive, and strict shutdown checking was requested.        -- See 'runSimStrictShutdown'.-     | FailureSloppyShutdown [Labelled ThreadId]+     | FailureSloppyShutdown [Labelled IOSimThreadId]         -- | An exception was thrown while evaluation the trace.        -- This could be an internal assertion failure of `io-sim` or an        -- unhandled exception in the simulation.      | FailureEvaluation SomeException++       -- | An internal failure of the simulator.+       --+       -- Please open an issue at+       -- <https://github.com/input-output-hk/io-sim/issues>.+     | FailureInternal String   deriving Show  instance Exception Failure where@@ -334,7 +382,18 @@              , intercalate ", " (show `map` threads)              , ">>"              ]-    displayException (FailureEvaluation err) = "evaluation error:" ++ displayException  err+    displayException (FailureEvaluation err) =+      concat [ "<<evaluation error: "+             , displayException  err+             , ">>"+             ]+    displayException (FailureInternal msg) =+      concat [ "<<internal failure: "+             , msg+             , ">>\n"+             , "please report the issue at\n"+             , "https://github.com/input-output-hk/io-sim/issues"+             ]       -- | 'IOSim' is a pure monad.@@ -353,7 +412,7 @@  -- | Like 'runSim' but fail when the main thread terminates if there are other -- threads still running or blocked. If one is trying to follow a strict thread--- cleanup policy then this helps testing for that.+-- clean-up policy then this helps testing for that. -- runSimStrictShutdown :: forall a. (forall s. IOSim s a) -> Either Failure a runSimStrictShutdown mainAction = traceResult True (runSimTrace mainAction)@@ -380,16 +439,17 @@     go (SimTrace _ _ _ _ t)             = eval t     go (SimPORTrace _ _ _ _ _ t)        = eval t     go (TraceRacesFound _ t)            = eval t-    go (TraceMainReturn _ _ tids@(_:_))+    go (TraceMainReturn _ _ _ tids@(_:_))                                | strict = pure $ Left (FailureSloppyShutdown tids)-    go (TraceMainReturn _ x _)          = pure $ Right x-    go (TraceMainException _ e _)       = pure $ Left (FailureException e)+    go (TraceMainReturn _ _ x _)        = pure $ Right x+    go (TraceMainException _ _ e _)     = pure $ Left (FailureException e)     go (TraceDeadlock   _   threads)    = pure $ Left (FailureDeadlock threads)     go TraceLoop{}                      = error "Impossible: traceResult TraceLoop{}"+    go (TraceInternalError msg)         = pure $ Left (FailureInternal msg)  -- | Turn 'SimTrace' into a list of timestamped events. ---traceEvents :: SimTrace a -> [(Time, ThreadId, Maybe ThreadLabel, SimEventType)]+traceEvents :: SimTrace a -> [(Time, IOSimThreadId, Maybe ThreadLabel, SimEventType)] traceEvents (SimTrace time tid tlbl event t)      = (time, tid, tlbl, event)                                                   : traceEvents t traceEvents (SimPORTrace time tid _ tlbl event t) = (time, tid, tlbl, event)@@ -399,7 +459,7 @@  -- | Pretty print a timestamped event. ---ppEvents :: [(Time, ThreadId, Maybe ThreadLabel, SimEventType)]+ppEvents :: [(Time, IOSimThreadId, Maybe ThreadLabel, SimEventType)]          -> String ppEvents events =     intercalate "\n"@@ -442,8 +502,8 @@ -- slot.  In /IOSim/ and /IOSimPOR/ time only moves explicitly through timer -- events, e.g. things like `Control.Monad.Class.MonadTimer.SI.threadDelay`, -- `Control.Monad.Class.MonadTimer.SI.registerDelay` or the--- `Control.Monad.Class.MonadTimer.NonStandard.MonadTimeout` api.  The usual--- quickcheck techniques can help explore different schedules of+-- `Control.Monad.Class.MonadTimer.NonStandard.MonadTimeout` API.  The usual+-- QuickCheck techniques can help explore different schedules of -- threads too.  -- | Execute a simulation, discover & revert races.  Note that this will execute@@ -454,6 +514,8 @@ -- On property failure it will show the failing schedule (`ScheduleControl`) -- which can be plugged to `controlSimTrace`. --+-- Note: `exploreSimTrace` evaluates each schedule in parallel (using `par`).+-- exploreSimTrace   :: forall a test. Testable test   => (ExplorationOptions -> ExplorationOptions)@@ -464,128 +526,133 @@   -- ^ a callback which receives the previous trace (e.g. before reverting   -- a race condition) and current trace   -> Property-exploreSimTrace optsf mainAction k =-  case explorationReplay opts of-    Nothing ->-      explore (explorationScheduleBound opts) (explorationBranching opts) ControlDefault Nothing .&&.-      let size = cacheSize() in size `seq`-      tabulate "Modified schedules explored" [bucket size] True-    Just control ->-      replaySimTrace opts mainAction control (k Nothing)+exploreSimTrace optsf main k =+    runST (exploreSimTraceST optsf main (\a b -> pure (k a b)))+++-- | An 'ST' version of 'exploreSimTrace'. The callback also receives+-- 'ScheduleControl'.  This is mostly useful for testing /IOSimPOR/ itself.+--+-- Note: `exploreSimTraceST` evaluates each schedule sequentially.+--+exploreSimTraceST+  :: forall s a test. Testable test+  => (ExplorationOptions -> ExplorationOptions)+  -> (forall s. IOSim s a)+  -> (Maybe (SimTrace a) -> SimTrace a -> ST s test)+  -> ST s Property+exploreSimTraceST optsf main k =+    case explorationReplay opts of+      Just control -> do+        trace <- controlSimTraceST (explorationStepTimelimit opts) control main+        counterexample ("Schedule control: " ++ show control)+          <$> k Nothing trace+      Nothing -> do+        cacheRef <- createCacheST+        prop <- go cacheRef (explorationScheduleBound opts)+                            (explorationBranching opts)+                            ControlDefault Nothing+        !size <- cacheSizeST cacheRef+        return $ tabulate "Modified schedules explored" [bucket size] prop   where     opts = optsf stdExplorationOptions -    explore :: Int -> Int -> ScheduleControl -> Maybe (SimTrace a) -> Property-    explore n m control passingTrace =--      -- ALERT!!! Impure code: readRaces must be called *after* we have-      -- finished with trace.-      let (readRaces, trace0) = detachTraceRaces $-                                controlSimTrace-                                  (explorationStepTimelimit opts) control mainAction-          (sleeper,trace) = compareTraces passingTrace trace0-      in ( counterexample ("Schedule control: " ++ show control)-         $ counterexample-            (case sleeper of-              Nothing -> "No thread delayed"-              Just ((t,tid,lab),racing) ->-                showThread (tid,lab) ++-                " delayed at time "++-                show t ++-                "\n  until after:\n" ++-                unlines (map (("    "++).showThread) $ Set.toList racing)-             )-         $ k passingTrace trace-         )-      .&&| let limit     = (n+m-1) `div` m-               -- To ensure the set of schedules explored is deterministic, we-               -- filter out cached ones *after* selecting the children of this-               -- node.-               races     = filter (not . cached) . take limit $ readRaces ()-               branching = length races-           in -- tabulate "Races explored" (map show races) $-              tabulate "Branching factor" [bucket branching] $-              tabulate "Race reversals per schedule" [bucket (raceReversals control)] $-              conjoinPar-                [ --Debug.trace "New schedule:" $-                  --Debug.trace ("  "++show r) $-                  --counterexample ("Schedule control: " ++ show r) $-                  explore n' ((m-1) `max` 1) r (Just trace0)-                | (r,n') <- zip races (divide (n-branching) branching) ]+    go :: STRef s (Set ScheduleControl)+       -> Int -- schedule bound+       -> Int -- branching factor+       -> ScheduleControl+       -> Maybe (SimTrace a)+       -> ST s Property+    go cacheRef n m control passingTrace = do+      traceWithRaces <- IOSimPOR.controlSimTraceST (explorationStepTimelimit opts) control main+      (readRaces, trace0) <- detachTraceRacesST traceWithRaces+      (readSleeperST, trace) <- compareTracesST passingTrace trace0+      () <- traceDebugLog (explorationDebugLevel opts) traceWithRaces+      conjoinNoCatchST+        [ do sleeper <- readSleeperST+             prop <- k passingTrace trace+             return $ counterexample ("Schedule control: " ++ show control)+                    $ counterexample+                       (case sleeper of+                         Nothing -> "No thread delayed"+                         Just ((t,tid,lab),racing) ->+                           showThread (tid,lab) +++                           " delayed at time "+++                           show t +++                           "\n  until after:\n" +++                           unlines (map (("    "++).showThread) $ Set.toList racing)+                        )+                      prop+        , do let limit = (n+m-1) `div` m+              -- To ensure the set of schedules explored is deterministic, we+              -- filter out cached ones *after* selecting the children of this+              -- node.+             races <- catMaybes+                  <$> (readRaces >>= traverse (cachedST cacheRef) . take limit)+             let branching = length races+             -- tabulate "Races explored" (map show races) $+             tabulate "Branching factor" [bucket branching]+                .  tabulate "Race reversals per schedule" [bucket (raceReversals control)]+                .  conjoin+               <$> sequence+                     [ go cacheRef n' ((m-1) `max` 1) r (Just trace0)+                     | (r,n') <- zip races (divide (n-branching) branching) ]+        ]      bucket :: Int -> String-    bucket n | n<10  = show n-             | n>=10 = buck n 1-             | otherwise = error "Ord Int is not a total order!"  -- GHC made me do it!+    bucket n | n<10      = show n+             | otherwise = buck n 1     buck n t | n<10      = show (n*t) ++ "-" ++ show ((n+1)*t-1)-             | n>=10     = buck (n `div` 10) (t*10)-             | otherwise = error "Ord Int is not a total order!"  -- GHC made me do it!+             | otherwise = buck (n `div` 10) (t*10) +    -- divide n into k factors which sums up to n     divide :: Int -> Int -> [Int]     divide n k =-      [ n `div` k + if i<n `mod` k then 1 else 0+      [ n `div` k + if i < n `mod` k then 1 else 0       | i <- [0..k-1] ] -    showThread :: (ThreadId,Maybe ThreadLabel) -> String+    showThread :: (IOSimThreadId,Maybe ThreadLabel) -> String     showThread (tid,lab) =-      show tid ++ (case lab of Nothing -> ""-                               Just l  -> " ("++l++")")--    -- cache of explored schedules-    cache :: IORef (Set ScheduleControl)-    cache = unsafePerformIO cacheIO+      ppIOSimThreadId tid ++ (case lab of Nothing -> ""+                                          Just l  -> " ("++l++")")      -- insert a schedule into the cache-    cached :: ScheduleControl -> Bool-    cached = unsafePerformIO . cachedIO--    -- compute cache size; it's a function to make sure that `GHC` does not-    -- inline it (and share the same thunk).-    cacheSize :: () -> Int-    cacheSize = unsafePerformIO . cacheSizeIO+    cachedST :: STRef s (Set ScheduleControl) -> ScheduleControl -> ST s (Maybe ScheduleControl)+    cachedST cacheRef a = do+      set <- readSTRef cacheRef+      writeSTRef cacheRef (Set.insert a set)+      return $ if Set.member a set+               then Nothing+               else Just a      ---    -- Caching in IO monad+    -- Caching in ST monad     --      -- It is possible for the same control to be generated several times.     -- To avoid exploring them twice, we keep a cache of explored schedules.-    cacheIO :: IO (IORef (Set ScheduleControl))-    cacheIO = newIORef $+    createCacheST :: ST s (STRef s (Set ScheduleControl))+    createCacheST = newSTRef $               -- we use opts here just to be sure the reference cannot be               -- lifted out of exploreSimTrace               if explorationScheduleBound opts >=0                 then Set.empty                 else error "exploreSimTrace: negative schedule bound" -    cachedIO :: ScheduleControl -> IO Bool-    cachedIO m = atomicModifyIORef' cache $ \set ->-      (Set.insert m set, Set.member m set)---    cacheSizeIO :: () -> IO Int-    cacheSizeIO () = Set.size <$> readIORef cache+    cacheSizeST :: STRef s (Set ScheduleControl) -> ST s Int+    cacheSizeST = fmap Set.size . readSTRef  --- | A specialised version of `controlSimTrace'.------ An internal function.+-- |  Trace `SimTrace` to `stderr`. ---replaySimTrace :: forall a test. (Testable test)-               => ExplorationOptions-               -- ^ race exploration options-               -> (forall s. IOSim s a)-               -> ScheduleControl-               -- ^ a schedule control to reproduce-               -> (SimTrace a -> test)-               -- ^ a callback which receives the simulation trace. The trace-               -- will not contain any race events-               -> Property-replaySimTrace opts mainAction control k =-  let (_,trace) = detachTraceRaces $-                  controlSimTrace (explorationStepTimelimit opts) control mainAction-  in property (k trace)+traceDebugLog :: Int -> SimTrace a -> ST s ()+traceDebugLog logLevel _trace | logLevel <= 0 = pure ()+traceDebugLog 1 trace = Debug.traceM $ "Simulation trace with discovered schedules:\n"+                                    ++ Trace.ppTrace (ppSimResult 0 0 0) (ppSimEvent 0 0 0) (ignoreRaces $ void `first` trace)+traceDebugLog _ trace = Debug.traceM $ "Simulation trace with discovered schedules:\n"+                                    ++ Trace.ppTrace (ppSimResult 0 0 0) (ppSimEvent 0 0 0) (void `first` trace) + -- | Run a simulation using a given schedule.  This is useful to reproduce -- failing cases without exploring the races. --@@ -600,9 +667,23 @@                 -> (forall s. IOSim s a)                 -- ^ a simulation to run                 -> SimTrace a-controlSimTrace limit control mainAction =-    runST (controlSimTraceST limit control mainAction)+controlSimTrace limit control main =+    runST (controlSimTraceST limit control main) +controlSimTraceST :: Maybe Int -> ScheduleControl -> IOSim s a -> ST s (SimTrace a)+controlSimTraceST limit control main =+  detachTraceRaces <$> IOSimPOR.controlSimTraceST limit control main+++--+-- Utils+--++ignoreRaces :: SimTrace a -> SimTrace a+ignoreRaces = Trace.filter (\a -> case a of+                                    SimPOREvent { seType = EventRaces {} } -> False+                                    _ -> True)+ raceReversals :: ScheduleControl -> Int raceReversals ControlDefault      = 0 raceReversals (ControlAwait mods) = length mods@@ -618,38 +699,59 @@ -- this far, then we collect its identity only if it is reached using -- unsafePerformIO. -compareTraces :: Maybe (SimTrace a1)-              -> SimTrace a2-              -> (Maybe ((Time, ThreadId, Maybe ThreadLabel),-                         Set.Set (ThreadId, Maybe ThreadLabel)),-                  SimTrace a2)-compareTraces Nothing trace = (Nothing, trace)-compareTraces (Just passing) trace = unsafePerformIO $ do-  sleeper <- newIORef Nothing-  return (unsafePerformIO $ readIORef sleeper,-          go sleeper passing trace)-  where go sleeper (SimPORTrace tpass tidpass _ _ _ pass')+-- TODO: return StepId+compareTracesST :: forall a b s.+                   Maybe (SimTrace a) -- ^ passing+                -> SimTrace b         -- ^ failing+                -> ST s ( ST s (Maybe ( (Time, IOSimThreadId, Maybe ThreadLabel)+                                      , Set.Set (IOSimThreadId, Maybe ThreadLabel)+                                      ))+                        , SimTrace b+                        )+compareTracesST Nothing trace = return (return Nothing, trace)+compareTracesST (Just passing) trace = do+  sleeper <- newSTRef Nothing+  trace' <- go sleeper passing trace+  return ( readSTRef sleeper+         , trace'+         )+  where+        go :: STRef s (Maybe ( (Time, IOSimThreadId, Maybe ThreadLabel)+                             , Set.Set (IOSimThreadId, Maybe ThreadLabel)+                             ))+           -> SimTrace a -- ^ passing+           -> SimTrace b -- ^ failing+           -> ST s (SimTrace b)+        go sleeper (SimPORTrace tpass tidpass _ _ _ pass')                    (SimPORTrace tfail tidfail tstepfail tlfail evfail fail')           | (tpass,tidpass) == (tfail,tidfail) =               SimPORTrace tfail tidfail tstepfail tlfail evfail-                $ go sleeper pass' fail'-        go sleeper (SimPORTrace tpass tidpass tsteppass tlpass _ _) fail =-          unsafePerformIO $ do-            writeIORef sleeper $ Just ((tpass, tidpass, tlpass),Set.empty)-            return $ SimPORTrace tpass tidpass tsteppass tlpass EventThreadSleep-                   $ wakeup sleeper tidpass fail-        go _ SimTrace {} _ = error "compareTraces: invariant violation"-        go _ _ SimTrace {} = error "compareTraces: invariant violation"-        go _ _ fail = fail+                <$> go sleeper pass' fail'+        go sleeper (SimPORTrace tpass tidpass tsteppass tlpass _ _) fail = do+          writeSTRef sleeper $ Just ((tpass, tidpass, tlpass),Set.empty)+          SimPORTrace tpass tidpass tsteppass tlpass EventThreadSleep+            <$> wakeup sleeper tidpass fail+        go _ SimTrace {} _ = error "compareTracesST: invariant violation"+        go _ _ SimTrace {} = error "compareTracesST: invariant violation"+        go _ _ fail = return fail +        wakeup :: STRef s (Maybe ( (Time, IOSimThreadId, Maybe ThreadLabel)+                                 , Set.Set (IOSimThreadId, Maybe ThreadLabel)+                                 ))+               -> IOSimThreadId+               -> SimTrace b+               -> ST s (SimTrace b)         wakeup sleeper tidpass                fail@(SimPORTrace tfail tidfail tstepfail tlfail evfail fail')           | tidpass == tidfail =-              SimPORTrace tfail tidfail tstepfail tlfail EventThreadWake fail-          | otherwise = unsafePerformIO $ do-              Just (slp,racing) <- readIORef sleeper-              writeIORef sleeper $ Just (slp,Set.insert (tidfail,tlfail) racing)-              return $ SimPORTrace tfail tidfail tstepfail tlfail evfail-                     $ wakeup sleeper tidpass fail'-        wakeup _ _ SimTrace {} = error "compareTraces: invariant violation"-        wakeup _ _ fail = fail+              return $ SimPORTrace tfail tidfail tstepfail tlfail EventThreadWake fail+          | otherwise = do+              ms <- readSTRef sleeper+              case ms of+                Just (slp, racing) -> do+                  writeSTRef sleeper $ Just (slp,Set.insert (tidfail,tlfail) racing)+                  SimPORTrace tfail tidfail tstepfail tlfail evfail+                    <$> wakeup sleeper tidpass fail'+                Nothing -> error "compareTraceST: invariant violation"+        wakeup _ _ SimTrace {} = error "compareTracesST: invariant violation"+        wakeup _ _ fail = return fail
src/Control/Monad/IOSim/CommonTypes.hs view
@@ -1,13 +1,36 @@ {-# LANGUAGE DeriveAnyClass             #-} {-# LANGUAGE DeriveGeneric              #-} {-# LANGUAGE DerivingStrategies         #-}+{-# LANGUAGE DerivingVia                #-} {-# LANGUAGE GADTs                      #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE ScopedTypeVariables        #-}  -- | Common types shared between `IOSim` and `IOSimPOR`. ---module Control.Monad.IOSim.CommonTypes where+module Control.Monad.IOSim.CommonTypes+  ( IOSimThreadId (..)+  , ppIOSimThreadId+  , StepId+  , ppStepId+  , childThreadId+  , setRacyThread+  , TVarId (..)+  , TimeoutId (..)+  , ClockId (..)+  , VectorClock (..)+  , ppVectorClock+  , unTimeoutId+  , ThreadLabel+  , TVarLabel+  , TVar (..)+  , SomeTVar (..)+  , Deschedule (..)+  , ThreadStatus (..)+  , BlockedReason (..)+    -- * Utils+  , ppList+  ) where  import           Control.DeepSeq (NFData (..)) import           Control.Monad.Class.MonadSTM (TraceValue)@@ -15,10 +38,13 @@  import           NoThunks.Class +import           Data.List (intercalate, intersperse) import           Data.Map (Map)+import qualified Data.Map as Map import           Data.STRef.Lazy import           Data.Set (Set) import           GHC.Generics+import           Quiet   -- | A thread id.@@ -28,28 +54,50 @@ -- `Control.Monad.Class.MonadTest.exploreRaces` was -- executed in it or it's a thread forked by a racy thread. ---data ThreadId = RacyThreadId [Int]-              | ThreadId     [Int]    -- non racy threads have higher priority+data IOSimThreadId =+    -- | A racy thread (`IOSimPOR` only), shown in the trace with curly braces,+    -- e.g. `Thread {2,3}`.+    RacyThreadId [Int]+    -- | A non racy thread.  They have higher priority than racy threads in+    -- `IOSimPOR` scheduler.+  | ThreadId     [Int]   deriving stock    (Eq, Ord, Show, Generic)   deriving anyclass NFData   deriving anyclass NoThunks +ppIOSimThreadId :: IOSimThreadId -> String+ppIOSimThreadId (RacyThreadId as) = "Thread {"++ intercalate "," (map show as) ++"}"+ppIOSimThreadId     (ThreadId as) = "Thread " ++ show as -childThreadId :: ThreadId -> Int -> ThreadId+childThreadId :: IOSimThreadId -> Int -> IOSimThreadId childThreadId (RacyThreadId is) i = RacyThreadId (is ++ [i]) childThreadId (ThreadId     is) i = ThreadId     (is ++ [i]) -setRacyThread :: ThreadId -> ThreadId+setRacyThread :: IOSimThreadId -> IOSimThreadId setRacyThread (ThreadId is)      = RacyThreadId is setRacyThread tid@RacyThreadId{} = tid +-- | Execution step in `IOSimPOR` is identified by the thread id and+-- a monotonically increasing number (thread specific).+--+type StepId = (IOSimThreadId, Int) +ppStepId :: (IOSimThreadId, Int) -> String+ppStepId (tid, step) | step < 0+                     = concat [ppIOSimThreadId tid, ".-"]+ppStepId (tid, step) = concat [ppIOSimThreadId tid, ".", show step]++ newtype TVarId      = TVarId    Int   deriving (Eq, Ord, Enum, Show) newtype TimeoutId   = TimeoutId Int   deriving (Eq, Ord, Enum, Show) newtype ClockId     = ClockId   [Int] deriving (Eq, Ord, Show)-newtype VectorClock = VectorClock { getVectorClock :: Map ThreadId Int }-  deriving Show+newtype VectorClock = VectorClock { getVectorClock :: Map IOSimThreadId Int }+  deriving Generic+  deriving Show via Quiet VectorClock +ppVectorClock :: VectorClock -> String+ppVectorClock (VectorClock m) = "VectorClock " ++ "[" ++ concat (intersperse ", " (ppStepId <$> Map.toList m)) ++ "]"+ unTimeoutId :: TimeoutId -> Int unTimeoutId (TimeoutId a) = a @@ -80,7 +128,7 @@        -- To avoid duplicates efficiently, the operations rely on a copy of the        -- thread Ids represented as a set.        ---       tvarBlocked :: !(STRef s ([ThreadId], Set ThreadId)),+       tvarBlocked :: !(STRef s ([IOSimThreadId], Set IOSimThreadId)),         -- | The vector clock of the current value.        --@@ -110,5 +158,13 @@   deriving (Eq, Show)  data BlockedReason = BlockedOnSTM-                   | BlockedOnOther+                   | BlockedOnDelay+                   | BlockedOnThrowTo   deriving (Eq, Show)++--+-- Utils+--++ppList :: (a -> String) -> [a] -> String+ppList pp as = "[" ++ concat (intersperse ", " (map pp as)) ++ "]"
src/Control/Monad/IOSim/Internal.hs view
@@ -1,4 +1,5 @@ {-# LANGUAGE BangPatterns              #-}+{-# LANGUAGE CPP                       #-} {-# LANGUAGE DerivingVia               #-} {-# LANGUAGE ExistentialQuantification #-} {-# LANGUAGE FlexibleInstances         #-}@@ -12,6 +13,10 @@ -- incomplete uni patterns in 'schedule' (when interpreting 'StmTxCommitted') -- and 'reschedule'. {-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-}+#if __GLASGOW_HASKELL__ >= 908+-- We use partial functions from `Data.List`.+{-# OPTIONS_GHC -Wno-x-partial #-}+#endif  module Control.Monad.IOSim.Internal   ( IOSim (..)@@ -26,7 +31,7 @@   , TimeoutException (..)   , EventlogEvent (..)   , EventlogMarker (..)-  , ThreadId+  , IOSimThreadId   , ThreadLabel   , Labelled (..)   , SimTrace@@ -83,12 +88,12 @@ --  data Thread s a = Thread {-    threadId      :: !ThreadId,+    threadId      :: !IOSimThreadId,     threadControl :: !(ThreadControl s a),     threadStatus  :: !ThreadStatus,     threadMasking :: !MaskingState,     -- other threads blocked in a ThrowTo to us because we are or were masked-    threadThrowTo :: ![(SomeException, Labelled ThreadId)],+    threadThrowTo :: ![(SomeException, Labelled IOSimThreadId)],     threadClockId :: !ClockId,     threadLabel   ::  Maybe ThreadLabel,     threadNextTId :: !Int@@ -102,7 +107,7 @@ labelledTVarId :: TVar s a -> ST s (Labelled TVarId) labelledTVarId TVar { tvarId, tvarLabel } = (Labelled tvarId) <$> readSTRef tvarLabel -labelledThreads :: Map ThreadId (Thread s a) -> [Labelled ThreadId]+labelledThreads :: Map IOSimThreadId (Thread s a) -> [Labelled IOSimThreadId] labelledThreads threadMap =     -- @Map.foldr'@ (and alikes) are not strict enough, to not ratain the     -- original thread map we need to evaluate the spine of the list.@@ -121,11 +126,11 @@      -- ^ `newTimeout` timer.      | TimerRegisterDelay !(TVar s Bool)      -- ^ `registerDelay` timer.-     | TimerThreadDelay !ThreadId !TimeoutId-     -- ^ `threadDelay` timer run by `ThreadId` which was assigned the given+     | TimerThreadDelay !IOSimThreadId !TimeoutId+     -- ^ `threadDelay` timer run by `IOSimThreadId` which was assigned the given      -- `TimeoutId` (only used to report in a trace).-     | TimerTimeout !ThreadId !TimeoutId !(TMVar (IOSim s) ThreadId)-     -- ^ `timeout` timer run by `ThreadId` which was assigned the given+     | TimerTimeout !IOSimThreadId !TimeoutId !(TMVar (IOSim s) IOSimThreadId)+     -- ^ `timeout` timer run by `IOSimThreadId` which was assigned the given      -- `TimeoutId` (only used to report in a trace).  @@ -134,10 +139,10 @@ -- | Internal state. -- data SimState s a = SimState {-       runqueue :: !(Deque ThreadId),+       runqueue :: !(Deque IOSimThreadId),        -- | All threads other than the currently running thread: both running        -- and blocked threads.-       threads  :: !(Map ThreadId (Thread s a)),+       threads  :: !(Map IOSimThreadId (Thread s a)),        -- | current time        curTime  :: !Time,        -- | ordered list of timers and timeouts@@ -211,7 +216,7 @@         -- the main thread is done, so we're done         -- even if other threads are still running         return $ SimTrace time tid tlbl EventThreadFinished-               $ TraceMainReturn time x (labelledThreads threads)+               $ TraceMainReturn time (Labelled tid tlbl) x (labelledThreads threads)        ForkFrame -> do         -- this thread is done@@ -278,7 +283,7 @@           -- An unhandled exception in the main thread terminates the program           return (SimTrace time tid tlbl (EventThrow e) $                   SimTrace time tid tlbl (EventThreadUnhandled e) $-                  TraceMainException time e (labelledThreads threads))+                  TraceMainException time (Labelled tid tlbl) e (labelledThreads threads))          | otherwise -> do           -- An unhandled exception in any other thread terminates the thread@@ -424,7 +429,7 @@       let !expiry  = d `addTime` time           !timers' = PSQ.insert nextTmid expiry (TimerThreadDelay tid nextTmid) timers           !thread' = thread { threadControl = ThreadControl (Return ()) (DelayFrame nextTmid k ctl) }-      !trace <- deschedule (Blocked BlockedOnOther) thread' simstate { timers   = timers'+      !trace <- deschedule (Blocked BlockedOnDelay) thread' simstate { timers   = timers'                                                                      , nextTmid = succ nextTmid }       return (SimTrace time tid tlbl (EventThreadDelay nextTmid expiry) trace) @@ -460,6 +465,9 @@       let !timers' = PSQ.delete tmid timers           !thread' = thread { threadControl = ThreadControl k ctl }       !written <- execAtomically' (runSTM $ writeTVar tvar TimeoutCancelled)+      -- note: we are not running traceTVar on 'tvar', since its not exposed to+      -- the user, and thus it cannot have an attached callback.+      !_ <- traverse_ (\(SomeTVar tvar') -> commitTVar tvar') written       (wakeup, wokeby) <- threadsUnblockedByWrites written       mapM_ (\(SomeTVar var) -> unblockAllThreadsFromTVar var) written       let (unblocked,@@ -609,10 +617,10 @@           -- exception and the source thread id to the pending async exceptions.           let adjustTarget t = t { threadThrowTo = (e, Labelled tid tlbl) : threadThrowTo t }               threads'       = Map.adjust adjustTarget tid' threads-          !trace <- deschedule (Blocked BlockedOnOther) thread' simstate { threads = threads' }+          !trace <- deschedule (Blocked BlockedOnThrowTo) thread' simstate { threads = threads' }           return $ SimTrace time tid tlbl (EventThrowTo e tid')                  $ SimTrace time tid tlbl EventThrowToBlocked-                 $ SimTrace time tid tlbl (EventDeschedule (Blocked BlockedOnOther))+                 $ SimTrace time tid tlbl (EventDeschedule (Blocked BlockedOnThrowTo))                  $ trace         else do           -- The target thread has async exceptions unmasked, or is masked but@@ -769,6 +777,10 @@         -- Reuse the STM functionality here to write all the timer TVars.         -- Simplify to a special case that only reads and writes TVars.         !written <- execAtomically' (runSTM $ mapM_ timeoutSTMAction fired)+        !ds  <- traverse (\(SomeTVar tvar) -> do+                            tr <- traceTVarST tvar False+                            !_ <- commitTVar tvar+                            return tr) written         (wakeupSTM, wokeby) <- threadsUnblockedByWrites written         !_ <- mapM_ (\(SomeTVar tvar) -> unblockAllThreadsFromTVar tvar) written @@ -794,6 +806,10 @@                   ++ [ ( time', ThreadId [-1], Just "register delay timer"                        , EventRegisterDelayFired tmid)                      | (tmid, TimerRegisterDelay _) <- zip tmids fired ]+                  ++ [ (time', ThreadId [-1], Just "register delay timer", EventLog (toDyn a))+                     | TraceValue { traceDynamic = Just a } <- ds ]+                  ++ [ (time', ThreadId [-1], Just "register delay timer", EventSay a)+                     | TraceValue { traceString = Just a } <- ds ]                   ++ [ (time', tid', tlbl', EventTxWakeup vids)                      | tid' <- wakeupSTM                      , let tlbl' = lookupThreadLabel tid' threads@@ -809,6 +825,7 @@                      | (tid, _, _) <- timeoutExpired ])                     trace   where+    timeoutSTMAction :: TimerCompletionInfo s -> STM s ()     timeoutSTMAction (Timer var) = do       x <- readTVar var       case x of@@ -821,7 +838,7 @@     timeoutSTMAction TimerThreadDelay{}       = return ()     timeoutSTMAction TimerTimeout{}           = return () -unblockThreads :: Bool -> [ThreadId] -> SimState s a -> ([ThreadId], SimState s a)+unblockThreads :: Bool -> [IOSimThreadId] -> SimState s a -> ([IOSimThreadId], SimState s a) unblockThreads !onlySTM !wakeup !simstate@SimState {runqueue, threads} =     -- To preserve our invariants (that threadBlocked is correct)     -- we update the runqueue and threads together here@@ -834,10 +851,10 @@     !unblocked = [ tid                  | tid <- wakeup                  , case Map.lookup tid threads of-                    Just Thread { threadStatus = ThreadBlocked BlockedOnOther }-                      -> not onlySTM                     Just Thread { threadStatus = ThreadBlocked BlockedOnSTM }                       -> True+                    Just Thread { threadStatus = ThreadBlocked _ }+                      -> not onlySTM                     _ -> False                  ]     -- and in which case we mark them as now running@@ -863,7 +880,7 @@ -- receive a 'ThreadKilled' exception. -- forkTimeoutInterruptThreads :: forall s a.-                               [(ThreadId, TimeoutId, TMVar (IOSim s) ThreadId)]+                               [(IOSimThreadId, TimeoutId, TMVar (IOSim s) IOSimThreadId)]                             -> SimState s a                             -> ST s (SimState s a) forkTimeoutInterruptThreads timeoutExpired simState =@@ -883,13 +900,13 @@   where     -- we launch a thread responsible for throwing an AsyncCancelled exception     -- to the thread which timeout expired-    throwToThread :: [(Thread s a, TMVar (IOSim s) ThreadId)] +    throwToThread :: [(Thread s a, TMVar (IOSim s) IOSimThreadId)]       (simState', throwToThread) = List.mapAccumR fn simState timeoutExpired        where         fn :: SimState s a-           -> (ThreadId, TimeoutId, TMVar (IOSim s) ThreadId)-           -> (SimState s a, (Thread s a, TMVar (IOSim s) ThreadId))+           -> (IOSimThreadId, TimeoutId, TMVar (IOSim s) IOSimThreadId)+           -> (SimState s a, (Thread s a, TMVar (IOSim s) IOSimThreadId))         fn state@SimState { threads } (tid, tmid, lock) =           let t = case tid `Map.lookup` threads of                     Just t' -> t'@@ -1003,13 +1020,13 @@           | p == p' -> collectAll (k:ks) p (x:xs) psq'         _           -> (reverse ks, p, reverse xs, psq) -traceMany :: [(Time, ThreadId, Maybe ThreadLabel, SimEventType)]+traceMany :: [(Time, IOSimThreadId, Maybe ThreadLabel, SimEventType)]           -> SimTrace a -> SimTrace a traceMany []                      trace = trace traceMany ((time, tid, tlbl, event):ts) trace =     SimTrace time tid tlbl event (traceMany ts trace) -lookupThreadLabel :: ThreadId -> Map ThreadId (Thread s a) -> Maybe ThreadLabel+lookupThreadLabel :: IOSimThreadId -> Map IOSimThreadId (Thread s a) -> Maybe ThreadLabel lookupThreadLabel tid threads = join (threadLabel <$> Map.lookup tid threads)  @@ -1041,7 +1058,7 @@  execAtomically :: forall s a c.                   Time-               -> ThreadId+               -> IOSimThreadId                -> Maybe ThreadLabel                -> TVarId                -> StmA s a@@ -1244,7 +1261,6 @@        -> ST s [SomeTVar s]     go !written action = case action of       ReturnStm () -> do-        !_ <- traverse_ (\(SomeTVar tvar) -> commitTVar tvar) written         return (Map.elems written)       ReadTVar v k  -> do         x <- execReadTVar v@@ -1322,7 +1338,7 @@ traceTVarST :: TVar s a             -> Bool -- true if it's a new 'TVar'             -> ST s TraceValue-traceTVarST TVar{tvarCurrent, tvarUndo, tvarTrace} new = do+traceTVarST TVar{tvarId, tvarCurrent, tvarUndo, tvarTrace} new = do     mf <- readSTRef tvarTrace     case mf of       Nothing -> return TraceValue { traceDynamic = (Nothing :: Maybe ())@@ -1333,7 +1349,7 @@         case (new, vs) of           (True, _) -> f Nothing v           (_, _:_)  -> f (Just $ last vs) v-          _         -> error "traceTVarST: unexpected tvar state"+          _         -> error ("traceTVarST: unexpected tvar state " ++ show tvarId)   @@ -1341,10 +1357,10 @@ -- Blocking and unblocking on TVars -- -readTVarBlockedThreads :: TVar s a -> ST s [ThreadId]+readTVarBlockedThreads :: TVar s a -> ST s [IOSimThreadId] readTVarBlockedThreads TVar{tvarBlocked} = fst <$> readSTRef tvarBlocked -blockThreadOnTVar :: ThreadId -> TVar s a -> ST s ()+blockThreadOnTVar :: IOSimThreadId -> TVar s a -> ST s () blockThreadOnTVar tid TVar{tvarBlocked} = do     (tids, tidsSet) <- readSTRef tvarBlocked     when (tid `Set.notMember` tidsSet) $ do@@ -1365,7 +1381,7 @@ -- the var writes that woke them. -- threadsUnblockedByWrites :: [SomeTVar s]-                         -> ST s ([ThreadId], Map ThreadId (Set (Labelled TVarId)))+                         -> ST s ([IOSimThreadId], Map IOSimThreadId (Set (Labelled TVarId))) threadsUnblockedByWrites written = do   !tidss <- sequence              [ (,) <$> labelledTVarId tvar <*> readTVarBlockedThreads tvar
src/Control/Monad/IOSim/InternalTypes.hs view
@@ -15,7 +15,7 @@ import           Control.Concurrent.Class.MonadSTM import           Control.Monad.Class.MonadThrow (MaskingState (..)) -import           Control.Monad.IOSim.Types (IOSim (..), SimA (..), ThreadId, TimeoutId)+import           Control.Monad.IOSim.Types (IOSim (..), SimA (..), IOSimThreadId, TimeoutId)  import           GHC.Exts (oneShot) @@ -43,7 +43,7 @@              -> !(ControlStack s c a)              -> ControlStack s b a   TimeoutFrame :: TimeoutId-               -> TMVar (IOSim s) ThreadId+               -> TMVar (IOSim s) IOSimThreadId                -> (Maybe b -> SimA s c)                -> !(ControlStack s c a)                -> ControlStack s b a@@ -74,7 +74,7 @@   | DelayFrame' TimeoutId ControlStackDash   deriving Show -data IsLocked = NotLocked | Locked !ThreadId+data IsLocked = NotLocked | Locked !IOSimThreadId   deriving (Eq, Show)  -- | Unsafe method which removes a timeout.
src/Control/Monad/IOSim/Types.hs view
@@ -5,6 +5,7 @@ {-# LANGUAGE ExistentialQuantification  #-} {-# LANGUAGE FlexibleInstances          #-} {-# LANGUAGE GADTSyntax                 #-}+{-# LANGUAGE LambdaCase                 #-} {-# LANGUAGE MultiParamTypeClasses      #-} {-# LANGUAGE NamedFieldPuns             #-} {-# LANGUAGE NumericUnderscores         #-}@@ -22,7 +23,6 @@   , traceSTM   , liftST   , SimA (..)-  , StepId   , STMSim   , STM (..)   , runSTM@@ -34,6 +34,7 @@   , setCurrentTime   , unshareClock   , ScheduleControl (..)+  , isDefaultSchedule   , ScheduleMod (..)   , ExplorationOptions (..)   , ExplorationSpec@@ -45,10 +46,12 @@   , EventlogEvent (..)   , EventlogMarker (..)   , SimEventType (..)+  , ppSimEventType   , SimEvent (..)   , SimResult (..)+  , ppSimResult   , SimTrace-  , Trace.Trace (SimTrace, SimPORTrace, TraceMainReturn, TraceMainException, TraceDeadlock, TraceRacesFound, TraceLoop)+  , Trace.Trace (SimTrace, SimPORTrace, TraceMainReturn, TraceMainException, TraceDeadlock, TraceRacesFound, TraceLoop, TraceInternalError)   , ppTrace   , ppTrace_   , ppSimEvent@@ -81,8 +84,7 @@ import           Control.Monad.Class.MonadAsync hiding (Async) import qualified Control.Monad.Class.MonadAsync as MonadAsync import           Control.Monad.Class.MonadEventlog-import           Control.Monad.Class.MonadFork hiding (ThreadId)-import qualified Control.Monad.Class.MonadFork as MonadFork+import           Control.Monad.Class.MonadFork import           Control.Monad.Class.MonadST import           Control.Monad.Class.MonadSTM.Internal (MonadInspectSTM (..),                      MonadLabelledSTM (..), MonadSTM, MonadTraceSTM (..),@@ -132,6 +134,7 @@   import qualified System.IO.Error as IO.Error (userError)+import Data.List (intercalate)  {-# ANN module "HLint: ignore Use readTVarIO" #-} newtype IOSim s a = IOSim { unIOSim :: forall r. (a -> SimA s r) -> SimA s r }@@ -180,13 +183,13 @@                   SimA s a -> (e -> SimA s a) -> (a -> SimA s b) -> SimA s b   Evaluate     :: a -> (a -> SimA s b) -> SimA s b -  Fork         :: IOSim s () -> (ThreadId -> SimA s b) -> SimA s b-  GetThreadId  :: (ThreadId -> SimA s b) -> SimA s b-  LabelThread  :: ThreadId -> String -> SimA s b -> SimA s b+  Fork         :: IOSim s () -> (IOSimThreadId -> SimA s b) -> SimA s b+  GetThreadId  :: (IOSimThreadId -> SimA s b) -> SimA s b+  LabelThread  :: IOSimThreadId -> String -> SimA s b -> SimA s b    Atomically   :: STM  s a -> (a -> SimA s b) -> SimA s b -  ThrowTo      :: SomeException -> ThreadId -> SimA s a -> SimA s a+  ThrowTo      :: SomeException -> IOSimThreadId -> SimA s a -> SimA s a   SetMaskState :: MaskingState  -> IOSim s a -> (a -> SimA s b) -> SimA s b   GetMaskState :: (MaskingState -> SimA s b) -> SimA s b @@ -447,7 +450,7 @@ blockUninterruptible a = IOSim (SetMaskState MaskedUninterruptible a)  instance MonadThread (IOSim s) where-  type ThreadId (IOSim s) = ThreadId+  type ThreadId (IOSim s) = IOSimThreadId   myThreadId       = IOSim $ oneShot $ \k -> GetThreadId k   labelThread t l  = IOSim $ oneShot $ \k -> LabelThread t l (k ()) @@ -467,6 +470,10 @@  instance MonadLabelledSTM (IOSim s) where   labelTVar tvar label = STM $ \k -> LabelTVar label tvar (k ())+  labelTVarIO tvar label = IOSim $ oneShot $ \k ->+                                   LiftST ( lazyToStrictST $+                                            writeSTRef (tvarLabel tvar) $! (Just label)+                                          ) k   labelTQueue  = labelTQueueDefault   labelTBQueue = labelTBQueueDefault @@ -551,6 +558,10 @@ -- instance MonadTraceSTM (IOSim s) where   traceTVar _ tvar f = STM $ \k -> TraceTVar tvar f (k ())+  traceTVarIO tvar f = IOSim $ oneShot $ \k ->+                               LiftST ( lazyToStrictST $+                                        writeSTRef (tvarTrace tvar) $! Just f+                                      ) k   traceTQueue  = traceTQueueDefault   traceTBQueue = traceTBQueueDefault @@ -575,7 +586,7 @@         MVarEmpty _ _ -> pure Nothing         MVarFull x _  -> pure (Just x) -data Async s a = Async !ThreadId (STM s (Either SomeException a))+data Async s a = Async !IOSimThreadId (STM s (Either SomeException a))  instance Eq (Async s a) where     Async tid _ == Async tid' _ = tid == tid'@@ -742,14 +753,14 @@     -- | Used when using `IOSim`.   = SimEvent {       seTime        :: !Time,-      seThreadId    :: !ThreadId,+      seThreadId    :: !IOSimThreadId,       seThreadLabel :: !(Maybe ThreadLabel),       seType        :: !SimEventType     }     -- | Only used for /IOSimPOR/   | SimPOREvent {       seTime        :: !Time,-      seThreadId    :: !ThreadId,+      seThreadId    :: !IOSimThreadId,       seStep        :: !Int,       seThreadLabel :: !(Maybe ThreadLabel),       seType        :: !SimEventType@@ -767,46 +778,92 @@            -> Int -- ^ width of thread label            -> SimEvent            -> String-ppSimEvent timeWidth tidWidth tLabelWidth SimEvent {seTime, seThreadId, seThreadLabel, seType} =++ppSimEvent timeWidth tidWidth tLabelWidth SimEvent {seTime = Time time, seThreadId, seThreadLabel, seType} =     printf "%-*s - %-*s %-*s - %s"            timeWidth-           (show seTime)+           (show time)            tidWidth-           (show seThreadId)+           (ppIOSimThreadId seThreadId)            tLabelWidth            threadLabel-           (show seType)+           (ppSimEventType seType)   where     threadLabel = fromMaybe "" seThreadLabel-ppSimEvent timeWidth tidWidth tLableWidth SimPOREvent {seTime, seThreadId, seStep, seThreadLabel, seType} =++ppSimEvent timeWidth tidWidth tLableWidth SimPOREvent {seTime = Time time, seThreadId, seStep, seThreadLabel, seType} =     printf "%-*s - %-*s %-*s - %s"            timeWidth-           (show seTime)+           (show time)            tidWidth-           (show (seThreadId, seStep))+           (ppStepId (seThreadId, seStep))            tLableWidth            threadLabel-           (show seType)+           (ppSimEventType seType)   where     threadLabel = fromMaybe "" seThreadLabel+ ppSimEvent _ _ _ (SimRacesFound controls) =     "RacesFound "++show controls + -- | A result type of a simulation. data SimResult a-    = MainReturn    !Time a             ![Labelled ThreadId]+    = MainReturn    !Time !(Labelled IOSimThreadId) a ![Labelled IOSimThreadId]     -- ^ Return value of the main thread.-    | MainException !Time SomeException ![Labelled ThreadId]+    | MainException !Time !(Labelled IOSimThreadId) SomeException ![Labelled IOSimThreadId]     -- ^ Exception thrown by the main thread.-    | Deadlock      !Time               ![Labelled ThreadId]+    | Deadlock      !Time               ![Labelled IOSimThreadId]     -- ^ Deadlock discovered in the simulation.  Deadlocks are discovered if     -- simply the simulation cannot do any progress in a given time slot and     -- there's no event which would advance the time.     | Loop     -- ^ Only returned by /IOSimPOR/ when a step execution took longer than     -- 'explorationStepTimelimit` was exceeded.+    | InternalError String     deriving (Show, Functor) +ppSimResult :: Show a+            => Int+            -> Int+            -> Int+            -> SimResult a+            -> String+ppSimResult timeWidth tidWidth thLabelWidth r = case r of+    MainReturn (Time time) tid a tids ->+      printf "%-*s - %-*s %-*s - %s %s"+             timeWidth+             (show time)+             tidWidth+             (ppIOSimThreadId (l_labelled tid))+             thLabelWidth+             (fromMaybe "" $ l_label tid)+             ("MainReturn " ++ show a)+             ("[" ++ intercalate "," (ppLabelled ppIOSimThreadId `map` tids) ++ "]")+    MainException (Time time) tid e tids ->+      printf "%-*s - %-*s %-*s - %s %s"+             timeWidth+             (show time)+             tidWidth+             (ppIOSimThreadId (l_labelled tid))+             thLabelWidth+             (fromMaybe "" $ l_label tid)+             ("MainException " ++ show e)+             ("[" ++ intercalate "," (ppLabelled ppIOSimThreadId `map` tids) ++ "]")+    Deadlock (Time time) tids ->+      printf "%-*s - %-*s %-*s - %s %s"+             timeWidth+             (show time)+             tidWidth+             ""+             thLabelWidth+             "" +             "Deadlock"+             ("[" ++ intercalate "," (ppLabelled ppIOSimThreadId `map` tids) ++ "]")+    Loop -> "<<io-sim-por: step execution exceded explorationStepTimelimit>>"+    InternalError e -> "<<io-sim internal error: " ++ show e ++ ">>"++ -- | A type alias for 'IOSim' simulation trace.  It comes with useful pattern -- synonyms. --@@ -816,21 +873,21 @@ -- ppTrace :: Show a => SimTrace a -> String ppTrace tr = Trace.ppTrace-               show-               (ppSimEvent timeWidth tidWith labelWidth)+               (ppSimResult timeWidth tidWidth labelWidth)+               (ppSimEvent timeWidth tidWidth labelWidth)                tr   where-    (Max timeWidth, Max tidWith, Max labelWidth) =+    (Max timeWidth, Max tidWidth, Max labelWidth) =         bimaximum       . bimap (const (Max 0, Max 0, Max 0))               (\a -> case a of-                SimEvent {seTime, seThreadId, seThreadLabel} ->-                  ( Max (length (show seTime))+                SimEvent {seTime = Time time, seThreadId, seThreadLabel} ->+                  ( Max (length (show time))                   , Max (length (show (seThreadId)))                   , Max (length seThreadLabel)                   )-                SimPOREvent {seTime, seThreadId, seThreadLabel} ->-                  ( Max (length (show seTime))+                SimPOREvent {seTime = Time time, seThreadId, seThreadLabel} ->+                  ( Max (length (show time))                   , Max (length (show (seThreadId)))                   , Max (length seThreadLabel)                   )@@ -845,10 +902,10 @@ ppTrace_ :: SimTrace a -> String ppTrace_ tr = Trace.ppTrace                 (const "")-                (ppSimEvent timeWidth tidWith labelWidth)+                (ppSimEvent timeWidth tidWidth labelWidth)                 tr   where-    (Max timeWidth, Max tidWith, Max labelWidth) =+    (Max timeWidth, Max tidWidth, Max labelWidth) =         bimaximum       . bimap (const (Max 0, Max 0, Max 0))               (\a -> case a of@@ -867,6 +924,8 @@               )       $ tr ++ -- | Trace each event using 'Debug.trace'; this is useful when a trace ends with -- a pure error, e.g. an assertion. --@@ -876,13 +935,13 @@         . Trace.toList  -pattern SimTrace :: Time -> ThreadId -> Maybe ThreadLabel -> SimEventType -> SimTrace a+pattern SimTrace :: Time -> IOSimThreadId -> Maybe ThreadLabel -> SimEventType -> SimTrace a                  -> SimTrace a pattern SimTrace time threadId threadLabel traceEvent trace =     Trace.Cons (SimEvent time threadId threadLabel traceEvent)                trace -pattern SimPORTrace :: Time -> ThreadId -> Int -> Maybe ThreadLabel -> SimEventType -> SimTrace a+pattern SimPORTrace :: Time -> IOSimThreadId -> Int -> Maybe ThreadLabel -> SimEventType -> SimTrace a                     -> SimTrace a pattern SimPORTrace time threadId step threadLabel traceEvent trace =     Trace.Cons (SimPOREvent time threadId step threadLabel traceEvent)@@ -894,24 +953,27 @@     Trace.Cons (SimRacesFound controls)                trace -pattern TraceMainReturn :: Time -> a -> [Labelled ThreadId]+pattern TraceMainReturn :: Time -> Labelled IOSimThreadId -> a -> [Labelled IOSimThreadId]                         -> SimTrace a-pattern TraceMainReturn time a threads = Trace.Nil (MainReturn time a threads)+pattern TraceMainReturn time tid a threads = Trace.Nil (MainReturn time tid a threads) -pattern TraceMainException :: Time -> SomeException -> [Labelled ThreadId]+pattern TraceMainException :: Time -> Labelled IOSimThreadId -> SomeException -> [Labelled IOSimThreadId]                            -> SimTrace a-pattern TraceMainException time err threads = Trace.Nil (MainException time err threads)+pattern TraceMainException time tid err threads = Trace.Nil (MainException time tid err threads) -pattern TraceDeadlock :: Time -> [Labelled ThreadId]+pattern TraceDeadlock :: Time -> [Labelled IOSimThreadId]                       -> SimTrace a pattern TraceDeadlock time threads = Trace.Nil (Deadlock time threads)  pattern TraceLoop :: SimTrace a pattern TraceLoop = Trace.Nil Loop -{-# COMPLETE SimTrace, SimPORTrace, TraceMainReturn, TraceMainException, TraceDeadlock, TraceLoop #-}+pattern TraceInternalError :: String -> SimTrace a+pattern TraceInternalError msg = Trace.Nil (InternalError msg) +{-# COMPLETE SimTrace, SimPORTrace, TraceMainReturn, TraceMainException, TraceDeadlock, TraceLoop, TraceInternalError #-} + -- | Events recorded by the simulation. -- data SimEventType@@ -924,17 +986,17 @@    | EventThrow          SomeException   -- ^ throw exception-  | EventThrowTo        SomeException ThreadId+  | EventThrowTo        SomeException IOSimThreadId   -- ^ throw asynchronous exception (`throwTo`)   | EventThrowToBlocked   -- ^ the thread which executed `throwTo` is blocked   | EventThrowToWakeup   -- ^ the thread which executed `throwTo` is woken up-  | EventThrowToUnmasked (Labelled ThreadId)+  | EventThrowToUnmasked (Labelled IOSimThreadId)   -- ^ a target thread of `throwTo` unmasked its exceptions, this is paired   -- with `EventThrowToWakeup` for threads which were blocked on `throwTo` -  | EventThreadForked    ThreadId+  | EventThreadForked    IOSimThreadId   -- ^ forked a thread   | EventThreadFinished   -- ^ thread terminated normally@@ -958,7 +1020,7 @@                        (Maybe Effect)    -- ^ effect performed (only for `IOSimPOR`)   | EventTxWakeup      [Labelled TVarId] -- ^ changed vars causing retry -  | EventUnblocked     [ThreadId]+  | EventUnblocked     [IOSimThreadId]   -- ^ unblocked threads by a committed STM transaction    --@@ -970,7 +1032,7 @@   | EventThreadDelayFired   TimeoutId   -- ^ thread woken up after a delay -  | EventTimeoutCreated        TimeoutId ThreadId Time+  | EventTimeoutCreated        TimeoutId IOSimThreadId Time   -- ^ new timeout created (via `timeout`)   | EventTimeoutFired          TimeoutId   -- ^ timeout fired@@ -994,8 +1056,8 @@   --    -- | event traced when `threadStatus` is executed-  | EventThreadStatus  ThreadId -- ^ current thread-                       ThreadId -- ^ queried thread+  | EventThreadStatus  IOSimThreadId -- ^ current thread+                       IOSimThreadId -- ^ queried thread    --   -- /IOSimPOR/ events@@ -1020,12 +1082,104 @@   | EventPerformAction StepId   -- ^ /IOSimPOR/ event: perform action of the given step   | EventReschedule           ScheduleControl++  | EventEffect VectorClock Effect+  -- ^ /IOSimPOR/ event: executed effect; Useful for debugging IOSimPOR or+  -- showing compact information about thread execution.+  | EventRaces Races+  -- ^ /IOSimPOR/ event: races.  Races are updated while we execute+  -- a simulation.  Useful for debugging IOSimPOR.   deriving Show +ppSimEventType :: SimEventType -> String+ppSimEventType = \case+  EventSay a -> "Say " ++ a+  EventLog a -> "Dynamic " ++ show a+  EventMask a -> "Mask " ++ show a+  EventThrow a -> "Throw " ++ show a+  EventThrowTo err tid ->+    concat [ "ThrowTo (",+              show err, ") ",+              ppIOSimThreadId tid ]+  EventThrowToBlocked -> "ThrowToBlocked"+  EventThrowToWakeup -> "ThrowToWakeup"+  EventThrowToUnmasked a ->+    "ThrowToUnmasked " ++ ppLabelled ppIOSimThreadId a+  EventThreadForked a ->+    "ThreadForked " ++ ppIOSimThreadId a+  EventThreadFinished -> "ThreadFinished"+  EventThreadUnhandled a ->+    "ThreadUnhandled " ++ show a+  EventTxCommitted written created mbEff ->+    concat [ "TxCommitted ",+             ppList (ppLabelled show) written, " ",+             ppList (ppLabelled show) created,+             maybe "" ((' ' :) . ppEffect) mbEff ] +  EventTxAborted mbEff ->+    concat [ "TxAborted",+             maybe "" ((' ' :) . ppEffect) mbEff ]+  EventTxBlocked blocked mbEff ->+   concat [ "TxBlocked ",+             ppList (ppLabelled show) blocked,+             maybe "" ((' ' :) . ppEffect) mbEff ]+  EventTxWakeup changed ->+    "TxWakeup " ++ ppList (ppLabelled show) changed+  EventUnblocked unblocked ->+    "Unblocked " ++ ppList ppIOSimThreadId unblocked+  EventThreadDelay tid t ->+    concat [ "ThreadDelay ",+             show tid, " ",+             show t ]+  EventThreadDelayFired  tid -> "ThreadDelayFired " ++ show tid+  EventTimeoutCreated timer tid t ->+    concat [ "TimeoutCreated ",+             show timer, " ",+             ppIOSimThreadId tid, " ",+             show t ]+  EventTimeoutFired timer ->+    "TimeoutFired " ++ show timer+  EventRegisterDelayCreated timer tvarId t ->+    concat [ "RegisterDelayCreated ",+             show timer, " ",+             show tvarId, " ",+             show t ]+  EventRegisterDelayFired timer -> "RegisterDelayFired " ++ show timer+  EventTimerCreated timer tvarId t ->+    concat [ "TimerCreated ",+              show timer, " ",+              show tvarId, " ",+              show t ]+  EventTimerUpdated timer t ->+    concat [ "TimerUpdated ",+             show timer, " ",+             show t ]+  EventTimerCancelled timer -> "TimerCancelled " ++ show timer+  EventTimerFired timer -> "TimerFired " ++ show timer+  EventThreadStatus  tid tid' ->+    concat [ "ThreadStatus ",+             ppIOSimThreadId tid, " ",+             ppIOSimThreadId tid' ]+  EventSimStart a -> "SimStart " ++ show a+  EventThreadSleep -> "ThreadSleep"+  EventThreadWake -> "ThreadWake"+  EventDeschedule a -> "Deschedule " ++ show a+  EventFollowControl a -> "FollowControl " ++ show a+  EventAwaitControl s a ->+    concat [ "AwaitControl ",+             ppStepId s, " ",+             show a ]+  EventPerformAction a -> "PerformAction " ++ ppStepId a+  EventReschedule a -> "Reschedule " ++ show a+  EventEffect clock eff ->+    concat [ "Effect ",+             ppVectorClock clock, " ",+             ppEffect eff ]+  EventRaces a -> show a+ -- | A labelled value. ----- For example 'labelThread' or `labelTVar' will insert a label to `ThreadId`+-- For example 'labelThread' or `labelTVar' will insert a label to `IOSimThreadId` -- (or `TVarId`). data Labelled a = Labelled {     l_labelled :: !a,@@ -1034,6 +1188,10 @@   deriving (Eq, Ord, Generic)   deriving Show via Quiet (Labelled a) +ppLabelled :: (a -> String) -> Labelled a -> String+ppLabelled pp Labelled { l_labelled = a, l_label = Nothing  } = pp a+ppLabelled pp Labelled { l_labelled = a, l_label = Just lbl } = concat ["Labelled ", pp a, " ", lbl]+ -- -- Executing STM Transactions --@@ -1102,58 +1260,7 @@                    -> StmStack s b c                    -> StmStack s a c - ------- Schedules-------- | Modified execution schedule.----data ScheduleControl = ControlDefault-                     -- ^ default scheduling mode-                     | ControlAwait [ScheduleMod]-                     -- ^ if the current control is 'ControlAwait', the normal-                     -- scheduling will proceed, until the thread found in the-                     -- first 'ScheduleMod' reaches the given step.  At this-                     -- point the thread is put to sleep, until after all the-                     -- steps are followed.-                     | ControlFollow [StepId] [ScheduleMod]-                     -- ^ follow the steps then continue with schedule-                     -- modifications.  This control is set by 'followControl'-                     -- when 'controlTargets' returns true.-  deriving (Eq, Ord, Show)---- | A schedule modification inserted at given execution step.----data ScheduleMod = ScheduleMod{-    -- | Step at which the 'ScheduleMod' is activated.-    scheduleModTarget    :: StepId,-    -- | 'ScheduleControl' at the activation step.  It is needed by-    -- 'extendScheduleControl' when combining the discovered schedule with the-    -- initial one.-    scheduleModControl   :: ScheduleControl,-    -- | Series of steps which are executed at the target step.  This *includes*-    -- the target step, not necessarily as the last step.-    scheduleModInsertion :: [StepId]-  }-  deriving (Eq, Ord)---- | Execution step is identified by the thread id and a monotonically--- increasing number (thread specific).----type StepId = (ThreadId, Int)--instance Show ScheduleMod where-  showsPrec d (ScheduleMod tgt ctrl insertion) =-    showParen (d>10) $-      showString "ScheduleMod " .-      showsPrec 11 tgt .-      showString " " .-      showsPrec 11 ctrl .-      showString " " .-      showsPrec 11 insertion----- --- Exploration options --- @@ -1189,10 +1296,21 @@     -- catching infinite loops etc.     --     -- The default value is `Nothing`.-    explorationReplay        :: Maybe ScheduleControl+    explorationReplay        :: Maybe ScheduleControl,     -- ^ A schedule to replay.     --     -- The default value is `Nothing`.+    explorationDebugLevel    :: Int+    -- ^ Log detailed trace to stderr containing information on discovered+    -- races.  The trace does not contain the result of the simulation, unless+    -- one will do that explicitly inside the simulation.+    --+    -- level 0: don't show any output,+    -- level 1: show simulation trace with discovered schedules+    -- level 2: show simulation trace with discovered schedules and races+    --+    -- NOTE: discovered schedules & races are not exposed to the user in the+    -- callback of `exploreSimTrace` or in the output of `controlSimTrace`.   }   deriving Show @@ -1201,7 +1319,8 @@     explorationScheduleBound = 100,     explorationBranching     = 3,     explorationStepTimelimit = Nothing,-    explorationReplay        = Nothing+    explorationReplay        = Nothing,+    explorationDebugLevel    = 0     }  type ExplorationSpec = ExplorationOptions -> ExplorationOptions
src/Control/Monad/IOSimPOR/Internal.hs view
@@ -1,4 +1,5 @@ {-# LANGUAGE BangPatterns              #-}+{-# LANGUAGE CPP                       #-} {-# LANGUAGE DerivingVia               #-} {-# LANGUAGE ExistentialQuantification #-} {-# LANGUAGE FlexibleContexts          #-}@@ -7,12 +8,18 @@ {-# LANGUAGE MultiParamTypeClasses     #-} {-# LANGUAGE NamedFieldPuns            #-} {-# LANGUAGE RankNTypes                #-}+-- only used to construct records if its clear to do so+{-# LANGUAGE RecordWildCards           #-} {-# LANGUAGE ScopedTypeVariables       #-} {-# LANGUAGE TypeFamilies              #-}  -- incomplete uni patterns in 'schedule' (when interpreting 'StmTxCommitted') -- and 'reschedule'. {-# OPTIONS_GHC -Wno-incomplete-uni-patterns -Wno-unused-matches #-}+#if __GLASGOW_HASKELL__ >= 908+-- We use partial functions from `Data.List`.+{-# OPTIONS_GHC -Wno-x-partial #-}+#endif  module Control.Monad.IOSimPOR.Internal   ( IOSim (..)@@ -27,7 +34,7 @@   , TimeoutException (..)   , EventlogEvent (..)   , EventlogMarker (..)-  , ThreadId+  , IOSimThreadId   , ThreadLabel   , Labelled (..)   , SimTrace@@ -86,12 +93,12 @@ --  data Thread s a = Thread {-    threadId      :: !ThreadId,+    threadId      :: !IOSimThreadId,     threadControl :: !(ThreadControl s a),     threadStatus  :: !ThreadStatus,     threadMasking :: !MaskingState,     -- other threads blocked in a ThrowTo to us because we are or were masked-    threadThrowTo :: ![(SomeException, Labelled ThreadId, VectorClock)],+    threadThrowTo :: ![(SomeException, Labelled IOSimThreadId, VectorClock)],     threadClockId :: !ClockId,     threadLabel   :: Maybe ThreadLabel,     threadNextTId :: !Int,@@ -112,21 +119,21 @@     ThreadDone -> True     _          -> False -threadStepId :: Thread s a -> (ThreadId, Int)+threadStepId :: Thread s a -> (IOSimThreadId, Int) threadStepId Thread{ threadId, threadStep } = (threadId, threadStep) -isRacyThreadId :: ThreadId -> Bool+isRacyThreadId :: IOSimThreadId -> Bool isRacyThreadId (RacyThreadId _) = True isRacyThreadId _                = True -isNotRacyThreadId :: ThreadId -> Bool+isNotRacyThreadId :: IOSimThreadId -> Bool isNotRacyThreadId (ThreadId _) = True isNotRacyThreadId _            = False  bottomVClock :: VectorClock bottomVClock = VectorClock Map.empty -insertVClock :: ThreadId -> Int -> VectorClock -> VectorClock+insertVClock :: IOSimThreadId -> Int -> VectorClock -> VectorClock insertVClock tid !step (VectorClock m) = VectorClock (Map.insert tid step m)  leastUpperBoundVClock :: VectorClock -> VectorClock -> VectorClock@@ -147,7 +154,7 @@ labelledTVarId :: TVar s a -> ST s (Labelled TVarId) labelledTVarId TVar { tvarId, tvarLabel } = Labelled tvarId <$> readSTRef tvarLabel -labelledThreads :: Map ThreadId (Thread s a) -> [Labelled ThreadId]+labelledThreads :: Map IOSimThreadId (Thread s a) -> [Labelled IOSimThreadId] labelledThreads threadMap =     -- @Map.foldr'@ (and alikes) are not strict enough, to not retain the     -- original thread map we need to evaluate the spine of the list.@@ -166,14 +173,14 @@      -- ^ `newTimeout` timer.      | TimerRegisterDelay !(TVar s Bool)      -- ^ `registerDelay` timer.-     | TimerThreadDelay !ThreadId !TimeoutId-     -- ^ `threadDelay` timer run by `ThreadId` which was assigned the given+     | TimerThreadDelay !IOSimThreadId !TimeoutId+     -- ^ `threadDelay` timer run by `IOSimThreadId` which was assigned the given      -- `TimeoutId` (only used to report in a trace).-     | TimerTimeout !ThreadId !TimeoutId !(TMVar (IOSim s) ThreadId)-     -- ^ `timeout` timer run by `ThreadId` which was assigned the given+     | TimerTimeout !IOSimThreadId !TimeoutId !(TMVar (IOSim s) IOSimThreadId)+     -- ^ `timeout` timer run by `IOSimThreadId` which was assigned the given      -- `TimeoutId` (only used to report in a trace). -type RunQueue = OrdPSQ (Down ThreadId) (Down ThreadId) ()+type RunQueue   = OrdPSQ (Down IOSimThreadId) (Down IOSimThreadId) () type Timeouts s = OrdPSQ TimeoutId Time (TimerCompletionInfo s)  -- | Internal state.@@ -182,7 +189,7 @@        runqueue         :: !RunQueue,        -- | All threads other than the currently running thread: both running        -- and blocked threads.-       threads          :: !(Map ThreadId (Thread s a)),+       threads          :: !(Map IOSimThreadId (Thread s a)),        -- | current time        curTime          :: !Time,        -- | ordered list of timers and timeouts@@ -312,10 +319,11 @@         -- even if other threads are still running         return $ SimPORTrace time tid tstep tlbl EventThreadFinished                $ traceFinalRacesFound simstate-               $ TraceMainReturn time x ( labelledThreads-                                        . Map.filter (not . isThreadDone)-                                        $ threads-                                        )+               $ TraceMainReturn time (Labelled tid tlbl) x+                                      ( labelledThreads+                                      . Map.filter (not . isThreadDone)+                                      $ threads+                                      )        ForkFrame -> do         -- this thread is done@@ -374,14 +382,17 @@       (Right thread0@Thread { threadMasking = maskst' }, timers'') -> do         -- We found a suitable exception handler, continue with that         -- We record a step, in case there is no exception handler on replay.-        let thread'  = stepThread thread0-            control' = advanceControl (threadStepId thread0) control-            races'   = updateRacesInSimState thread0 simstate+        let (thread', eff)  = stepThread thread0+            control'        = advanceControl (threadStepId thread0) control+            races'          = updateRaces thread0 simstate         trace <- schedule thread' simstate{ races = races',                                             control = control',                                             timers = timers'' }         return (SimPORTrace time tid tstep tlbl (EventThrow e) $-                SimPORTrace time tid tstep tlbl (EventMask maskst') trace)+                SimPORTrace time tid tstep tlbl (EventMask maskst') $+                SimPORTrace time tid tstep tlbl (EventEffect vClock eff) $+                SimPORTrace time tid tstep tlbl (EventRaces races')+                trace)        (Left isMain, timers'')         -- We unwound and did not find any suitable exception handler, so we@@ -392,7 +403,7 @@           return (SimPORTrace time tid tstep tlbl (EventThrow e) $                   SimPORTrace time tid tstep tlbl (EventThreadUnhandled e) $                   traceFinalRacesFound simstate { threads = Map.insert tid thread' threads } $-                  TraceMainException time e (labelledThreads threads))+                  TraceMainException time (Labelled tid tlbl) e (labelledThreads threads))          | otherwise -> do           -- An unhandled exception in any other thread terminates the thread@@ -525,7 +536,7 @@       let expiry  = d `addTime` time           timers' = PSQ.insert nextTmid expiry (TimerThreadDelay tid nextTmid) timers           thread' = thread { threadControl = ThreadControl (Return ()) (DelayFrame nextTmid k ctl) }-      trace <- deschedule (Blocked BlockedOnOther) thread'+      trace <- deschedule (Blocked BlockedOnDelay) thread'                           simstate { timers   = timers',                                      nextTmid = succ nextTmid }       return (SimPORTrace time tid tstep tlbl (EventThreadDelay nextTmid expiry) trace)@@ -599,7 +610,7 @@           thread'' = Thread { threadId      = tid'                             , threadControl = ThreadControl (runIOSim a)                                                             ForkFrame-                            , threadStatus  = ThreadRunning +                            , threadStatus  = ThreadRunning                             , threadMasking = threadMasking thread                             , threadThrowTo = []                             , threadClockId = threadClockId thread@@ -646,7 +657,7 @@           return $             SimPORTrace time tid tstep tlbl (EventTxCommitted written' created' (Just effect')) $             traceMany-              [ (time, tid', tstep, tlbl', EventTxWakeup vids')+              [ (time, tid', (-1), tlbl', EventTxWakeup vids')               | tid' <- unblocked               , let tlbl' = lookupThreadLabel tid' threads               , let Just vids' = Set.toList <$> Map.lookup tid' wokeby ] $@@ -759,10 +770,10 @@           let adjustTarget t =                 t { threadThrowTo = (e, Labelled tid tlbl, vClock) : threadThrowTo t }               threads'       = Map.adjust adjustTarget tid' threads-          trace <- deschedule (Blocked BlockedOnOther) thread' simstate { threads = threads' }+          trace <- deschedule (Blocked BlockedOnThrowTo) thread' simstate { threads = threads' }           return $ SimPORTrace time tid tstep tlbl (EventThrowTo e tid')                  $ SimPORTrace time tid tstep tlbl EventThrowToBlocked-                 $ SimPORTrace time tid tstep tlbl (EventDeschedule (Blocked BlockedOnOther))+                 $ SimPORTrace time tid tstep tlbl (EventDeschedule (Blocked BlockedOnThrowTo))                  $ trace         else do           -- The target thread has async exceptions unmasked, or is masked but@@ -807,19 +818,29 @@  deschedule :: Deschedule -> Thread s a -> SimState s a -> ST s (SimTrace a) -deschedule Yield thread@Thread { threadId = tid }-                 simstate@SimState{runqueue, threads, control} =+deschedule Yield thread@Thread { threadId     = tid,+                                 threadStep   = tstep,+                                 threadLabel  = tlbl,+                                 threadVClock = vClock }+                 simstate@SimState{runqueue,+                                   threads,+                                   curTime  = time,+                                   control } =      -- We don't interrupt runnable threads anywhere else.     -- We do it here by inserting the current thread into the runqueue in priority order. -    let thread'   = stepThread thread-        runqueue' = insertThread thread' runqueue-        threads'  = Map.insert tid thread' threads-        control'  = advanceControl (threadStepId thread) control in+    let (thread', eff) = stepThread thread+        runqueue'      = insertThread thread' runqueue+        threads'       = Map.insert tid thread' threads+        control'       = advanceControl (threadStepId thread) control+        races'         = updateRaces thread simstate in++    SimPORTrace time tid tstep tlbl (EventEffect vClock eff) .+    SimPORTrace time tid tstep tlbl (EventRaces races') <$>     reschedule simstate { runqueue = runqueue',                           threads  = threads',-                          races    = updateRacesInSimState thread simstate,+                          races    = races',                           control  = control' }  deschedule Interruptable thread@Thread {@@ -834,79 +855,109 @@                          }                         simstate@SimState{ curTime = time, threads } = do -    -- We're unmasking, but there are pending blocked async exceptions.-    -- So immediately raise the exception and unblock the blocked thread-    -- if possible.-    let thread' = thread { threadControl = ThreadControl (Throw e) ctl+    let effect' = effect <> wakeupEffects unblocked+        -- We're unmasking, but there are pending blocked async exceptions.+        -- So immediately raise the exception and unblock the blocked thread+        -- if possible.+        thread' = thread { threadControl = ThreadControl (Throw e) ctl                          , threadMasking = MaskedInterruptible                          , threadThrowTo = etids                          , threadVClock  = vClock `leastUpperBoundVClock` vClock'+                         , threadEffect  = effect'                          }         (unblocked,          simstate') = unblockThreads False vClock [l_labelled tid'] simstate     -- the thread is stepped when we Yield     !trace <- deschedule Yield thread' simstate'-    return $ SimPORTrace time tid tstep tlbl (EventDeschedule Yield)-           $ SimPORTrace time tid tstep tlbl (EventThrowToUnmasked tid')+    return $ SimPORTrace time tid tstep tlbl (EventThrowToUnmasked tid')+           $ SimPORTrace time tid tstep tlbl (EventEffect vClock effect')            -- TODO: step            $ traceMany [ (time, tid'', (-1), tlbl'', EventThrowToWakeup)                        | tid'' <- unblocked                        , let tlbl'' = lookupThreadLabel tid'' threads ]+           $ SimPORTrace time tid tstep tlbl (EventDeschedule Yield)              trace -deschedule Interruptable thread@Thread{threadId = tid } simstate@SimState{ control } =+deschedule Interruptable thread@Thread{threadId     = tid,+                                       threadStep   = tstep,+                                       threadLabel  = tlbl,+                                       threadVClock = vClock}+                         simstate@SimState{ control,+                                            curTime = time } =     -- Either masked or unmasked but no pending async exceptions.     -- Either way, just carry on.     -- Record a step, though, in case on replay there is an async exception.-    let thread' = stepThread thread in+    let (thread', eff) = stepThread thread+        races' = updateRaces thread simstate in++    SimPORTrace time tid tstep tlbl (EventEffect vClock eff) .+    SimPORTrace time tid tstep tlbl (EventRaces races') <$>     schedule thread'-             simstate{ races   = updateRacesInSimState thread simstate,+             simstate{ races   = races',                        control = advanceControl (threadStepId thread) control }  deschedule (Blocked _blockedReason) thread@Thread { threadId      = tid+                                                  , threadStep    = tstep+                                                  , threadLabel   = tlbl                                                   , threadThrowTo = _ : _                                                   , threadMasking = maskst-                                                  , threadEffect  = effect } simstate+                                                  , threadEffect  = effect }+                                    simstate@SimState{ curTime = time }     | maskst /= MaskedUninterruptible =     -- We're doing a blocking operation, which is an interrupt point even if     -- we have async exceptions masked, and there are pending blocked async     -- exceptions. So immediately raise the exception and unblock the blocked     -- thread if possible.-    deschedule Interruptable thread { threadMasking = Unmasked } simstate+    SimPORTrace time tid tstep tlbl (EventDeschedule Interruptable) <$>+      deschedule Interruptable thread { threadMasking = Unmasked } simstate -deschedule (Blocked blockedReason) thread@Thread{ threadId = tid, threadEffect = effect } simstate@SimState{threads, control} =-    let thread1 = thread { threadStatus = ThreadBlocked blockedReason }-        thread'  = stepThread thread1-        threads' = Map.insert (threadId thread') thread' threads in+deschedule (Blocked blockedReason) thread@Thread{ threadId     = tid,+                                                  threadStep   = tstep,+                                                  threadLabel  = tlbl,+                                                  threadVClock = vClock}+                                   simstate@SimState{ threads,+                                                      curTime = time,+                                                      control } =+    let thread1        = thread { threadStatus = ThreadBlocked blockedReason }+        (thread', eff) = stepThread thread1+        threads'       = Map.insert (threadId thread') thread' threads+        races'         = updateRaces thread1 simstate in++    SimPORTrace time tid tstep tlbl (EventEffect vClock eff) .+    SimPORTrace time tid tstep tlbl (EventRaces races') <$>     reschedule simstate { threads = threads',-                          races   = updateRacesInSimState thread1 simstate,+                          races   = races',                           control = advanceControl (threadStepId thread1) control } -deschedule Terminated thread@Thread { threadId = tid, threadVClock = vClock, threadEffect = effect }+deschedule Terminated thread@Thread { threadId = tid, threadStep = tstep, threadLabel = tlbl,+                                      threadVClock = vClock, threadEffect = effect }                                simstate@SimState{ curTime = time, control } = do     -- This thread is done. If there are other threads blocked in a     -- ThrowTo targeted at this thread then we can wake them up now.-    let thread1     = thread-        thread'     = stepThread $ thread { threadStatus = ThreadDone }-        wakeup      = map (\(_,tid',_) -> l_labelled tid') (reverse (threadThrowTo thread))+    let wakeup         = map (\(_,tid',_) -> l_labelled tid') (reverse (threadThrowTo thread))         (unblocked,          simstate'@SimState{threads}) =                       unblockThreads False vClock wakeup simstate-        threads'    = Map.insert tid thread' threads+        effect'        = effect <> wakeupEffects unblocked+        (thread', eff) = stepThread $ thread { threadStatus = ThreadDone,+                                               threadEffect = effect' }+        threads'       = Map.insert tid thread' threads+        races'         = threadTerminatesRaces tid $ updateRaces thread { threadEffect = effect' } simstate     -- We must keep terminated threads in the state to preserve their vector clocks,     -- which matters when other threads throwTo them.-    !trace <- reschedule simstate' { races = threadTerminatesRaces tid $-                                              updateRacesInSimState thread1 simstate,-                                    control = advanceControl (threadStepId thread) control,-                                    threads = threads' }+    !trace <- reschedule simstate' { races   = races',+                                     control = advanceControl (threadStepId thread) control,+                                     threads = threads' }     return $ traceMany                -- TODO: step                [ (time, tid', (-1), tlbl', EventThrowToWakeup)                | tid' <- unblocked                , let tlbl' = lookupThreadLabel tid' threads ]-               trace+          $ SimPORTrace time tid tstep tlbl (EventEffect vClock eff)+          $ SimPORTrace time tid tstep tlbl (EventRaces races')+            trace -deschedule Sleep thread@Thread { threadId = tid , threadEffect = effect }+deschedule Sleep thread@Thread { threadId = tid , threadEffect = effect' }                  simstate@SimState{runqueue, threads} =      -- Schedule control says we should run a different thread. Put@@ -921,13 +972,18 @@ reschedule :: SimState s a -> ST s (SimTrace a)  -- If we are following a controlled schedule, just do that.-reschedule simstate@SimState{ runqueue, threads,-                              control=control@(ControlFollow ((tid,tstep):_) _),-                              curTime=time-                              } =+reschedule simstate@SimState { runqueue, control = control@(ControlFollow ((tid,_):_) _) }+                             | not (Down tid `PSQ.member` runqueue) =+    return (Trace.Nil (InternalError ("assertion failure: " ++ ppIOSimThreadId tid ++ " not runnable")))++reschedule simstate@SimState { threads, control = control@(ControlFollow ((tid,_):_) _) }+                             | not (tid `Map.member` threads) =+    return (Trace.Nil (InternalError ("assertion failure: " ++ ppIOSimThreadId tid ++ " not in threads")))++reschedule simstate@SimState { runqueue, threads,+                               control = control@(ControlFollow ((tid,tstep):_) _),+                               curTime = time } =     fmap (SimPORTrace time tid tstep Nothing (EventReschedule control)) $-    assert (Down tid `PSQ.member` runqueue) $-    assert (tid `Map.member` threads) $     invariant Nothing simstate $     let thread = threads Map.! tid in     assert (threadId thread == tid) $@@ -971,6 +1027,10 @@         -- Reuse the STM functionality here to write all the timer TVars.         -- Simplify to a special case that only reads and writes TVars.         written <- execAtomically' (runSTM $ mapM_ timeoutAction fired)+        !ds  <- traverse (\(SomeTVar tvar) -> do+                            tr <- traceTVarST tvar False+                            !_ <- commitTVar tvar+                            return tr) written         (wakeupSTM, wokeby) <- threadsUnblockedByWrites written         mapM_ (\(SomeTVar tvar) -> unblockAllThreadsFromTVar tvar) written @@ -996,6 +1056,10 @@                   ++ [ ( time', ThreadId [-1], -1, Just "register delay timer"                        , EventRegisterDelayFired tmid)                      | (tmid, TimerRegisterDelay _) <- zip tmids fired ]+                  ++ [ (time', ThreadId [-1], -1, Just "register delay timer", EventLog (toDyn a))+                     | TraceValue { traceDynamic = Just a } <- ds ]+                  ++ [ (time', ThreadId [-1], -1, Just "register delay timer", EventSay a)+                     | TraceValue { traceString = Just a } <- ds ]                   ++ [ (time', tid', -1, tlbl', EventTxWakeup vids)                      | tid' <- wakeupSTM                      , let tlbl' = lookupThreadLabel tid' threads@@ -1021,15 +1085,15 @@         TimeoutFired     -> error "MonadTimer(Sim): invariant violation"         TimeoutCancelled -> return ()     timeoutAction (TimerRegisterDelay var) = writeTVar var True-    timeoutAction (TimerThreadDelay _ _)    = return ()+    timeoutAction (TimerThreadDelay _ _)   = return ()     timeoutAction (TimerTimeout _ _ _)     = return ()  unblockThreads :: forall s a.                   Bool -- ^ `True` if we are blocked on STM                -> VectorClock-               -> [ThreadId]+               -> [IOSimThreadId]                -> SimState s a-               -> ([ThreadId], SimState s a)+               -> ([IOSimThreadId], SimState s a) unblockThreads !onlySTM vClock wakeup simstate@SimState {runqueue, threads} =     -- To preserve our invariants (that threadBlocked is correct)     -- we update the runqueue and threads together here@@ -1046,17 +1110,18 @@                      case Map.lookup tid threads of                        Just   Thread { threadStatus = ThreadRunning }                          -> [ ]-                       Just t@Thread { threadStatus = ThreadBlocked BlockedOnOther }+                       Just t@Thread { threadStatus = ThreadBlocked BlockedOnSTM }+                         -> [t]+                       Just t@Thread { threadStatus = ThreadBlocked _ }                          | onlySTM                          -> [ ]                          | otherwise                          -> [t]-                       Just t@Thread { threadStatus = ThreadBlocked BlockedOnSTM }-                         -> [t]-                       _ -> [ ]+                       Just   Thread { threadStatus = ThreadDone } -> [ ]+                       Nothing -> [ ]                  ] -    unblockedIds :: [ThreadId]+    unblockedIds :: [IOSimThreadId]     !unblockedIds = map threadId unblocked      -- and in which case we mark them as now running@@ -1083,7 +1148,7 @@ -- receive a 'ThreadKilled' exception. -- forkTimeoutInterruptThreads :: forall s a.-                               [(ThreadId, TimeoutId, TMVar (IOSim s) ThreadId)]+                               [(IOSimThreadId, TimeoutId, TMVar (IOSim s) IOSimThreadId)]                             -> SimState s a                             -> ST s (SimState s a) forkTimeoutInterruptThreads timeoutExpired simState =@@ -1103,13 +1168,13 @@   where     -- we launch a thread responsible for throwing an AsyncCancelled exception     -- to the thread which timeout expired-    throwToThread :: [(Thread s a, TMVar (IOSim s) ThreadId)] +    throwToThread :: [(Thread s a, TMVar (IOSim s) IOSimThreadId)]      (simState', throwToThread) = List.mapAccumR fn simState timeoutExpired       where         fn :: SimState s a-           -> (ThreadId, TimeoutId, TMVar (IOSim s) ThreadId)-           -> (SimState s a, (Thread s a, TMVar (IOSim s) ThreadId))+           -> (IOSimThreadId, TimeoutId, TMVar (IOSim s) IOSimThreadId)+           -> (SimState s a, (Thread s a, TMVar (IOSim s) IOSimThreadId))         fn state@SimState { threads } (tid, tmid, lock) =           let t = case tid `Map.lookup` threads of                     Just t' -> t'@@ -1141,8 +1206,8 @@                 , lock                 )               )-        + -- | Iterate through the control stack to find an enclosing exception handler -- of the right type, or unwind all the way to the top level for the thread. --@@ -1226,13 +1291,13 @@           | p == p' -> collectAll (k:ks) p (x:xs) psq'         _           -> (reverse ks, p, reverse xs, psq) -traceMany :: [(Time, ThreadId, Int, Maybe ThreadLabel, SimEventType)]+traceMany :: [(Time, IOSimThreadId, Int, Maybe ThreadLabel, SimEventType)]           -> SimTrace a -> SimTrace a traceMany []                                   trace = trace traceMany ((time, tid, tstep, tlbl, event):ts) trace =     SimPORTrace time tid tstep tlbl event (traceMany ts trace) -lookupThreadLabel :: ThreadId -> Map ThreadId (Thread s a) -> Maybe ThreadLabel+lookupThreadLabel :: IOSimThreadId -> Map IOSimThreadId (Thread s a) -> Maybe ThreadLabel lookupThreadLabel tid threads = join (threadLabel <$> Map.lookup tid threads)  @@ -1247,10 +1312,10 @@ controlSimTraceST :: Maybe Int -> ScheduleControl -> IOSim s a -> ST s (SimTrace a) controlSimTraceST limit control mainAction =   SimPORTrace (curTime initialState)-           (threadId mainThread)-           0-           (threadLabel mainThread)-           (EventSimStart control)+              (threadId mainThread)+              0+              (threadLabel mainThread)+              (EventSimStart control)   <$> schedule mainThread initialState { control  = control,                                          control0 = control,                                          perStepTimeLimit = limit@@ -1279,7 +1344,7 @@  execAtomically :: forall s a c.                   Time-               -> ThreadId+               -> IOSimThreadId                -> Maybe ThreadLabel                -> TVarId                -> StmA s a@@ -1583,10 +1648,10 @@ -- Blocking and unblocking on TVars -- -readTVarBlockedThreads :: TVar s a -> ST s [ThreadId]+readTVarBlockedThreads :: TVar s a -> ST s [IOSimThreadId] readTVarBlockedThreads TVar{tvarBlocked} = fst <$> readSTRef tvarBlocked -blockThreadOnTVar :: ThreadId -> TVar s a -> ST s ()+blockThreadOnTVar :: IOSimThreadId -> TVar s a -> ST s () blockThreadOnTVar tid TVar{tvarBlocked} = do     (tids, tidsSet) <- readSTRef tvarBlocked     when (tid `Set.notMember` tidsSet) $ do@@ -1605,7 +1670,7 @@ -- the var writes that woke them. -- threadsUnblockedByWrites :: [SomeTVar s]-                         -> ST s ([ThreadId], Map ThreadId (Set (Labelled TVarId)))+                         -> ST s ([IOSimThreadId], Map IOSimThreadId (Set (Labelled TVarId))) threadsUnblockedByWrites written = do   tidss <- sequence              [ (,) <$> labelledTVarId tvar <*> readTVarBlockedThreads tvar@@ -1633,91 +1698,47 @@ -- Steps -- -data Step = Step {-    stepThreadId :: !ThreadId,-    stepStep     :: !Int,-    stepEffect   :: !Effect,-    stepVClock   :: !VectorClock-  }-  deriving Show---- steps race if they can be reordered with a possibly different outcome+-- | Check if two steps can be reordered with a possibly different outcome.+-- racingSteps :: Step -- ^ an earlier step             -> Step -- ^ a later step             -> Bool racingSteps s s' =-     stepThreadId s /= stepThreadId s'+     -- is s executed by a racy thread+     isRacyThreadId (stepThreadId s)+     -- steps which belong to the same thread cannot race+  && stepThreadId s /= stepThreadId s'+     -- if s wakes up s' then s and s' cannot race   && not (stepThreadId s' `elem` effectWakeup (stepEffect s))-  && (stepEffect s `racingEffects` stepEffect s'-   || throwsTo s s'-   || throwsTo s' s)+     -- s effects races with s' effects or either one throws to the other+  && (  stepEffect s `racingEffects` stepEffect s'+     || throwsTo s s'+     || throwsTo s' s+     )   where throwsTo s1 s2 =              stepThreadId s2 `elem` effectThrows (stepEffect s1)+          -- `throwTo` races with any other effect           && stepEffect s2 /= mempty  currentStep :: Thread s a -> Step-currentStep Thread { threadId     = tid,-                     threadStep   = tstep,-                     threadEffect = teffect,-                     threadVClock = vClock-                   } =-  Step { stepThreadId = tid,-         stepStep     = tstep,-         stepEffect   = teffect,-         stepVClock   = vClock-       }+currentStep Thread { threadId     = stepThreadId,+                     threadStep   = stepStep,+                     threadEffect = stepEffect,+                     threadVClock = stepVClock+                   } = Step {..} -stepThread :: Thread s a -> Thread s a+-- | Step a thread and return the previous `StepId` and its `Effect`.+--+stepThread :: Thread s a -> (Thread s a, Effect) stepThread thread@Thread { threadId     = tid,                            threadStep   = tstep,                            threadVClock = vClock } =-  thread { threadStep   = tstep+1,-           threadEffect = mempty,-           threadVClock = insertVClock tid (tstep+1) vClock-         }---- As we run a simulation, we collect info about each previous step-data StepInfo = StepInfo {-    stepInfoStep       :: Step,-    -- Control information when we reached this step-    stepInfoControl    :: ScheduleControl,-    -- threads that are still concurrent with this step-    stepInfoConcurrent :: Set ThreadId,-    -- steps following this one that did not happen after it-    -- (in reverse order)-    stepInfoNonDep     :: [Step],-    -- later steps that race with this one-    stepInfoRaces      :: [Step]-  }-  deriving Show------- Races-----data Races = Races { -- These steps may still race with future steps-                     activeRaces   :: ![StepInfo],-                     -- These steps cannot be concurrent with future steps-                     completeRaces :: ![StepInfo]-                   }-  deriving Show--noRaces :: Races-noRaces = Races [] []--updateRacesInSimState :: Thread s a -> SimState s a -> Races-updateRacesInSimState thread SimState{ control, threads, races } =-    traceRaces $-    updateRaces step-                (isThreadBlocked thread)-                control-                (Map.keysSet (Map.filter (\t -> not (isThreadDone t)-                                             && threadId t `Set.notMember`-                                                effectForks (stepEffect step)-                                         ) threads))-                races-  where-    step = currentStep thread+  ( thread { threadStep   = tstep+1,+             threadEffect = mempty,+             threadVClock = insertVClock tid (tstep+1) vClock+           },+    threadEffect thread+  )  -- | 'updateRaces' turns a current 'Step' into 'StepInfo', and updates all -- 'activeRaces'.@@ -1726,89 +1747,124 @@ -- concurrent set. When this becomes empty, a step can be retired into -- the "complete" category, but only if there are some steps racing -- with it.-updateRaces :: Step -> Bool -> ScheduleControl -> Set ThreadId -> Races -> Races-updateRaces newStep@Step{ stepThreadId = tid, stepEffect = newEffect }-            blocking-            control-            newConcurrent0-            races@Races{ activeRaces } =+updateRaces :: Thread s a -> SimState s a -> Races+updateRaces thread@Thread { threadId = tid }+            SimState{ control, threads, races = races@Races { activeRaces } } =+    let +        newStep@Step{ stepEffect = newEffect } = currentStep thread -  let justBlocking :: Bool-      justBlocking = blocking && onlyReadEffect newEffect+        concurrent0 = +          Map.keysSet (Map.filter (\t -> not (isThreadDone t)+                                      && threadId t `Set.notMember`+                                         effectForks newEffect+                                  ) threads) -      -- a new step cannot race with any threads that it just woke up-      new :: [StepInfo]-      !new | isNotRacyThreadId tid  = []  -- non-racy threads do not race-           | Set.null newConcurrent = []  -- cannot race with anything-           | justBlocking           = []  -- no need to defer a blocking transaction-           | otherwise              =-               [StepInfo { stepInfoStep       = newStep,-                           stepInfoControl    = control,-                           stepInfoConcurrent = newConcurrent,-                           stepInfoNonDep     = [],-                           stepInfoRaces      = []-                         }]-        where-          newConcurrent :: Set ThreadId-          newConcurrent = foldr Set.delete newConcurrent0 (effectWakeup newEffect)+        -- A new step to add to the `activeRaces` list.+        newStepInfo :: Maybe StepInfo+        !newStepInfo | isNotRacyThreadId tid = Nothing+                       -- non-racy threads do not race -      activeRaces' :: [StepInfo]-      !activeRaces' =-        [ -- if this step depends on the previous step, or is not concurrent,+                     | Set.null concurrent   = Nothing+                       -- cannot race with nothing++                     | isBlocking            = Nothing+                     -- no need to defer a blocking transaction++                     | otherwise =+            Just $! StepInfo { stepInfoStep       = newStep,+                               stepInfoControl    = control,+                               stepInfoConcurrent = concurrent,+                               stepInfoNonDep     = [],+                               stepInfoRaces      = []+                             }+          where+            concurrent :: Set IOSimThreadId+            concurrent = concurrent0 Set.\\ effectWakeup newEffect++            isBlocking :: Bool+            isBlocking = isThreadBlocked thread && onlyReadEffect newEffect++        -- Used to update each `StepInfo` in `activeRaces`.+        updateStepInfo :: StepInfo -> StepInfo+        updateStepInfo stepInfo@StepInfo { stepInfoStep       = step,+                                           stepInfoConcurrent = concurrent,+                                           stepInfoNonDep,+                                           stepInfoRaces  } =+          -- if this step depends on the previous step, or is not concurrent,           -- then any threads that it wakes up become non-concurrent also.-          let !lessConcurrent = foldr Set.delete concurrent (effectWakeup newEffect) in-          if tid `elem` concurrent then-            let theseStepsRace = isRacyThreadId tid && racingSteps step newStep-                happensBefore  = step `happensBeforeStep` newStep-                !nondep' | happensBefore = nondep-                         | otherwise     = newStep : nondep-                -- We will only record the first race with each thread---reversing-                -- the first race makes the next race detectable. Thus we remove a-                -- thread from the concurrent set after the first race.-                concurrent' | happensBefore  = Set.delete tid lessConcurrent-                            | theseStepsRace = Set.delete tid concurrent-                            | otherwise      = concurrent-                -- Here we record discovered races.-                -- We only record a new race if we are following the default schedule,-                -- to avoid finding the same race in different parts of the search space.-                !stepRaces' | (control == ControlDefault ||-                               control == ControlFollow [] []) &&-                              theseStepsRace  = newStep : stepRaces-                            | otherwise       = stepRaces+          let !lessConcurrent = concurrent Set.\\ effectWakeup newEffect in +          if tid `notElem` concurrent+            then stepInfo { stepInfoConcurrent = lessConcurrent }++            -- The core of IOSimPOR.  Detect if `newStep` is racing with any+            -- previous steps and update each `StepInfo`.+            else let theseStepsRace = step `racingSteps` newStep+                     -- `step` happened before `newStep` (`newStep` happened after+                     -- `step`)+                     happensBefore   = step `happensBeforeStep` newStep+                     -- `newStep` happens after any of the racing steps+                     afterRacingStep = any (`happensBeforeStep` newStep) stepInfoRaces++                     -- We will only record the first race with each thread.+                     -- Reversing the first race makes the next race detectable.+                     -- Thus we remove a thread from the concurrent set after the+                     -- first race.+                     !concurrent'+                       | happensBefore   = Set.delete tid lessConcurrent+                       | theseStepsRace  = Set.delete tid concurrent+                       | afterRacingStep = Set.delete tid concurrent+                       | otherwise       = concurrent++                     !stepInfoNonDep'+                       -- `newStep` happened after `step`+                       | happensBefore =           stepInfoNonDep+                       -- `newStep` did not happen after `step`+                       | otherwise     = newStep : stepInfoNonDep++                     -- Here we record discovered races.  We only record a new+                     -- race if we are following the default schedule, to avoid+                     -- finding the same race in different parts of the search+                     -- space.+                     !stepInfoRaces'+                       | theseStepsRace && isDefaultSchedule control+                                   = newStep : stepInfoRaces+                       | otherwise =           stepInfoRaces+             in stepInfo { stepInfoConcurrent = effectForks newEffect-                                             `Set.union` concurrent',-                          stepInfoNonDep     = nondep',-                          stepInfoRaces      = stepRaces'+                                               `Set.union` concurrent',+                          stepInfoNonDep     = stepInfoNonDep',+                          stepInfoRaces      = stepInfoRaces'                         } -          else stepInfo { stepInfoConcurrent = lessConcurrent }+        activeRaces' :: [StepInfo]+        !activeRaces' =+          case newStepInfo of+            Nothing ->       updateStepInfo <$> activeRaces+            Just si -> si : (updateStepInfo <$> activeRaces) -        | !stepInfo@StepInfo { stepInfoStep       = step,-                               stepInfoConcurrent = concurrent,-                               stepInfoNonDep     = nondep,-                               stepInfoRaces      = stepRaces-                            }-            <- activeRaces ]-  in normalizeRaces $ races { activeRaces = new ++ activeRaces' }+    in normalizeRaces races { activeRaces = activeRaces' } ++normalizeRaces :: Races -> Races+normalizeRaces Races{ activeRaces, completeRaces } =+  let !activeRaces'   =   filter (not . null . stepInfoConcurrent) activeRaces+      !completeRaces' = ( filter (not . null . stepInfoRaces)+                        . filter (null . stepInfoConcurrent)+                        $ activeRaces+                        )+                     ++ completeRaces+  in Races{ activeRaces = activeRaces', completeRaces = completeRaces' }++ -- When a thread terminates, we remove it from the concurrent thread -- sets of active races.--threadTerminatesRaces :: ThreadId -> Races -> Races+threadTerminatesRaces :: IOSimThreadId -> Races -> Races threadTerminatesRaces tid races@Races{ activeRaces } =   let activeRaces' = [ s{stepInfoConcurrent = Set.delete tid stepInfoConcurrent}                      | s@StepInfo{ stepInfoConcurrent } <- activeRaces ]   in normalizeRaces $ races{ activeRaces = activeRaces' } -normalizeRaces :: Races -> Races-normalizeRaces Races{ activeRaces, completeRaces } =-  let !activeRaces'   = filter (not . null. stepInfoConcurrent) activeRaces-      !completeRaces' = filter (not . null. stepInfoRaces)-                          (filter (null . stepInfoConcurrent) activeRaces)-                     ++ completeRaces-  in Races{ activeRaces = activeRaces', completeRaces = completeRaces' }- -- We assume that steps do not race with later steps after a quiescent -- period. Quiescent periods end when simulated time advances, thus we -- are assuming here that all work is completed before a timer@@ -1822,12 +1878,7 @@                          , not (null (stepInfoRaces s))                          ] ++ completeRaces } -traceRaces :: Races -> Races-traceRaces r = r--- traceRaces r@Races{activeRaces,completeRaces} =---   Debug.trace ("Tracking "++show (length (concatMap stepInfoRaces activeRaces)) ++" races") r - -- -- Schedule control --@@ -1878,7 +1929,7 @@ -- Schedule modifications -- -stepStepId :: Step -> (ThreadId, Int)+stepStepId :: Step -> (IOSimThreadId, Int) stepStepId Step{ stepThreadId = tid, stepStep = n } = (tid,n)  stepInfoToScheduleMods :: StepInfo -> [ScheduleMod]@@ -1895,7 +1946,7 @@       { scheduleModTarget    = stepStepId step       , scheduleModControl   = control       , scheduleModInsertion = takeWhile (/=stepStepId step')-                                         (map stepStepId (reverse nondep))+                                         (stepStepId `map` reverse nondep)                             ++ [stepStepId step']                             -- It should be unnecessary to include the delayed                             -- step in the insertion, since the default@@ -1954,4 +2005,4 @@                         "Extending "++show control,                         "     with "++show m,                         "   yields "++show control']) -}-              control'+  control'
src/Control/Monad/IOSimPOR/QuickCheckUtils.hs view
@@ -1,32 +1,22 @@+{-# LANGUAGE BangPatterns #-}+ {-# OPTIONS_GHC -Wno-name-shadowing #-}  module Control.Monad.IOSimPOR.QuickCheckUtils where -import           Control.Parallel+import           Control.Monad.ST.Lazy import           Test.QuickCheck.Gen import           Test.QuickCheck.Property --- Take the conjunction of several properties, in parallel This is a--- modification of code from Test.QuickCheck.Property, to run non-IO--- properties in parallel. It also takes care NOT to label its result--- as an IO property (using IORose), unless one of its arguments is--- itself an IO property. This is needed to permit parallel testing.-conjoinPar :: TestableNoCatch prop => [prop] -> Property-conjoinPar = conjoinSpeculate speculate-  where-  -- speculation tries to evaluate each Rose tree in parallel, to WHNF-  -- This will not perform any IO, but should evaluate non-IO properties-  -- completely.-  speculate [] = []-  speculate (rose:roses) = roses' `par` rose' `pseq` (rose':roses')-    where rose' = case rose of-                    MkRose result _ -> let ans = maybe True id $ ok result in ans `pseq` rose-                    IORose _        -> rose-          roses' = speculate roses- -- We also need a version of conjoin that is sequential, but does not -- label its result as an IO property unless one of its arguments -- is. Consequently it does not catch exceptions in its arguments.++conjoinNoCatchST :: TestableNoCatch prop => [ST s prop] -> ST s Property+conjoinNoCatchST sts = do+    ps <- sequence sts+    return $ conjoinNoCatch ps+ conjoinNoCatch :: TestableNoCatch prop => [prop] -> Property conjoinNoCatch = conjoinSpeculate id @@ -65,17 +55,7 @@         classes = classes result ++ classes r,         tables = tables result ++ tables r } --- |&&| is a replacement for .&&. that evaluates its arguments in--- parallel. |&&| does NOT label its result as an IO property, unless--- one of its arguments is--which .&&. does. This means that using--- .&&. inside an argument to conjoinPar limits parallelism, while--- |&&| does not. -infixr 1 |&&|--(|&&|) :: TestableNoCatch prop => prop -> prop -> Property-p |&&| q = conjoinPar [p, q]- -- .&&| is a sequential, but parallelism-friendly version of .&&., that -- tests its arguments in sequence, but does not label its result as -- an IO property unless one of its arguments is.@@ -104,7 +84,7 @@   propertyNoCatch = MkProperty . return . MkProp . return  instance TestableNoCatch Prop where-  propertyNoCatch p = MkProperty . return $ p+  propertyNoCatch = MkProperty . return  instance TestableNoCatch prop => TestableNoCatch (Gen prop) where   propertyNoCatch mp = MkProperty $ do p <- mp; unProperty (againNoCatch $ propertyNoCatch p)
src/Control/Monad/IOSimPOR/Types.hs view
@@ -1,4 +1,28 @@-module Control.Monad.IOSimPOR.Types where+{-# LANGUAGE NamedFieldPuns #-}+module Control.Monad.IOSimPOR.Types+  ( -- * Effects+    Effect (..)+  , readEffects+  , writeEffects+  , forkEffect+  , throwToEffect+  , wakeupEffects+  , onlyReadEffect+  , racingEffects+  , ppEffect+    -- * Schedules+  , ScheduleControl (..)+  , isDefaultSchedule+  , ScheduleMod (..)+    -- * Steps+  , StepId+  , ppStepId+  , Step (..)+  , StepInfo (..)+    -- * Races+  , Races (..)+  , noRaces+  ) where  import qualified Data.List as List import           Data.Set (Set)@@ -10,25 +34,41 @@ -- Effects -- --- | An `Effect` aggregates effects performed by a thread.  Only used by--- *IOSimPOR*.+-- | An `Effect` aggregates effects performed by a thread in a given+-- execution step.  Only used by *IOSimPOR*. -- data Effect = Effect {     effectReads  :: !(Set TVarId),     effectWrites :: !(Set TVarId),-    effectForks  :: !(Set ThreadId),-    effectThrows :: ![ThreadId],-    effectWakeup :: ![ThreadId]+    effectForks  :: !(Set IOSimThreadId),+    effectThrows :: ![IOSimThreadId],+    effectWakeup :: !(Set IOSimThreadId)   }-  deriving (Eq, Show)+  deriving (Show, Eq) +ppEffect :: Effect -> String+ppEffect Effect { effectReads, effectWrites, effectForks, effectThrows, effectWakeup } =+  "Effect { " +++    concat (List.intersperse ", " $+           [ "reads = "  ++ show effectReads  | not (null effectReads)  ]+        ++ [ "writes = " ++ show effectWrites | not (null effectWrites) ]+        ++ [ "forks = "  ++ ppList ppIOSimThreadId (Set.toList effectForks)  | not (null effectForks)  ]+        ++ [ "throws = " ++ ppList ppIOSimThreadId             effectThrows  | not (null effectThrows) ]+        ++ [ "wakeup = " ++ ppList ppIOSimThreadId (Set.toList effectWakeup) | not (null effectWakeup) ])+        ++ " }"++ instance Semigroup Effect where   Effect r w s ts wu <> Effect r' w' s' ts' wu' =-    Effect (r <> r') (w <> w') (s <> s') (ts ++ ts') (wu++wu')+    Effect (r <> r') (w <> w') (s <> s') (ts ++ ts') (wu <> wu')  instance Monoid Effect where-  mempty = Effect Set.empty Set.empty Set.empty [] []+  mempty = Effect Set.empty Set.empty Set.empty [] Set.empty +--+-- Effect smart constructors+--+ -- readEffect :: SomeTVar s -> Effect -- readEffect r = mempty{effectReads = Set.singleton $ someTvarId r } @@ -41,30 +81,154 @@ writeEffects :: [SomeTVar s] -> Effect writeEffects rs = mempty{effectWrites = Set.fromList (map someTvarId rs)} -forkEffect :: ThreadId -> Effect-forkEffect tid = mempty{effectForks = Set.singleton $ tid}+forkEffect :: IOSimThreadId -> Effect+forkEffect tid = mempty{effectForks = Set.singleton tid} -throwToEffect :: ThreadId -> Effect+throwToEffect :: IOSimThreadId -> Effect throwToEffect tid = mempty{ effectThrows = [tid] } -wakeupEffects :: [ThreadId] -> Effect-wakeupEffects tids = mempty{effectWakeup = tids}+wakeupEffects :: [IOSimThreadId] -> Effect+wakeupEffects tids = mempty{effectWakeup = Set.fromList tids} +--+-- Utils+--+ someTvarId :: SomeTVar s -> TVarId someTvarId (SomeTVar r) = tvarId r  onlyReadEffect :: Effect -> Bool onlyReadEffect e = e { effectReads = effectReads mempty } == mempty +-- | Check if two effects race.  The two effects are assumed to come from+-- different threads, from steps which do not wake one another, see+-- `racingSteps`.+-- racingEffects :: Effect -> Effect -> Bool racingEffects e e' =-      effectThrows e       `intersectsL` effectThrows e'-   || effectReads  e       `intersects`  effectWrites e'-   || effectWrites e       `intersects`  effectReads  e'-   || effectWrites e       `intersects`  effectWrites e'++       -- both effects throw to the same threads+       effectThrows e `intersectsL` effectThrows e'++       -- concurrent reads & writes of the same TVars+    || effectReads  e `intersects`  effectWrites e'+    || effectWrites e `intersects`  effectReads  e'++       -- concurrent writes to the same TVars+    || effectWrites e `intersects`  effectWrites e'+   where     intersects :: Ord a => Set a -> Set a -> Bool     intersects a b = not $ a `Set.disjoint` b      intersectsL :: Eq a => [a] -> [a] -> Bool     intersectsL a b = not $ null $ a `List.intersect` b+++---+--- Schedules+---++-- | Modified execution schedule.+--+data ScheduleControl = ControlDefault+                     -- ^ default scheduling mode+                     | ControlAwait [ScheduleMod]+                     -- ^ if the current control is 'ControlAwait', the normal+                     -- scheduling will proceed, until the thread found in the+                     -- first 'ScheduleMod' reaches the given step.  At this+                     -- point the thread is put to sleep, until after all the+                     -- steps are followed.+                     | ControlFollow [StepId] [ScheduleMod]+                     -- ^ follow the steps then continue with schedule+                     -- modifications.  This control is set by 'followControl'+                     -- when 'controlTargets' returns true.+  deriving (Eq, Ord, Show)+++isDefaultSchedule :: ScheduleControl -> Bool+isDefaultSchedule ControlDefault        = True+isDefaultSchedule (ControlFollow [] []) = True+isDefaultSchedule _                     = False++-- | A schedule modification inserted at given execution step.+--+data ScheduleMod = ScheduleMod{+    -- | Step at which the 'ScheduleMod' is activated.+    scheduleModTarget    :: StepId,+    -- | 'ScheduleControl' at the activation step.  It is needed by+    -- 'extendScheduleControl' when combining the discovered schedule with the+    -- initial one.+    scheduleModControl   :: ScheduleControl,+    -- | Series of steps which are executed at the target step.  This *includes*+    -- the target step, not necessarily as the last step.+    scheduleModInsertion :: [StepId]+  }+  deriving (Eq, Ord)+++instance Show ScheduleMod where+  showsPrec d (ScheduleMod tgt ctrl insertion) =+    showParen (d>10) $+      showString "ScheduleMod " .+      showsPrec 11 tgt .+      showString " " .+      showsPrec 11 ctrl .+      showString " " .+      showsPrec 11 insertion++--+-- Steps+--++data Step = Step {+    stepThreadId :: !IOSimThreadId,+    stepStep     :: !Int,+    stepEffect   :: !Effect,+    stepVClock   :: !VectorClock+  }+  deriving Show+++--+-- StepInfo+--++-- | As we execute a simulation, we collect information about each step.  This+-- information is updated as the simulation evolves by+-- `Control.Monad.IOSimPOR.Types.updateRaces`.+--+data StepInfo = StepInfo {+    -- | Step that we want to reschedule to run after a step in `stepInfoRaces`+    -- (there will be one schedule modification per step in+    -- `stepInfoRaces`).+    stepInfoStep       :: !Step,++    -- | Control information when we reached this step.+    stepInfoControl    :: !ScheduleControl,++    -- | Threads that are still concurrent with this step.+    stepInfoConcurrent :: !(Set IOSimThreadId),++    -- | Steps following this one that did not happen after it+    -- (in reverse order).+    stepInfoNonDep     :: ![Step],++    -- | Later steps that race with `stepInfoStep`.+    stepInfoRaces      :: ![Step]+  }+  deriving Show++--+-- Races+--++data Races = Races { -- These steps may still race with future steps+                     activeRaces   :: ![StepInfo],+                     -- These steps cannot be concurrent with future steps+                     completeRaces :: ![StepInfo]+                   }+  deriving Show++noRaces :: Races+noRaces = Races [] []
test/Main.hs view
@@ -11,7 +11,7 @@  tests :: TestTree tests =-  testGroup "IO Sim"+  testGroup "io-sim-tests"   [ Test.Control.Concurrent.Class.MonadMVar.tests   , Test.Control.Monad.IOSim.tests   , Test.Control.Monad.IOSimPOR.tests
test/Test/Control/Monad/IOSim.hs view
@@ -52,7 +52,7 @@  tests :: TestTree tests =-  testGroup "IO simulator"+  testGroup "IOSim"   [ testProperty "read/write graph (IO)"    prop_stm_graph_io   , testProperty "read/write graph (IOSim)" (withMaxSuccess 1000 prop_stm_graph_sim)   , testGroup "timeouts"
test/Test/Control/Monad/IOSimPOR.hs view
@@ -1,8 +1,9 @@-{-# LANGUAGE DeriveGeneric       #-}-{-# LANGUAGE FlexibleContexts    #-}-{-# LANGUAGE LambdaCase          #-}-{-# LANGUAGE RankNTypes          #-}-{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE DeriveGeneric         #-}+{-# LANGUAGE FlexibleContexts      #-}+{-# LANGUAGE LambdaCase            #-}+{-# LANGUAGE RankNTypes            #-}+{-# LANGUAGE PartialTypeSignatures #-}+{-# LANGUAGE ScopedTypeVariables   #-}  {-# OPTIONS_GHC -Wno-unused-top-binds #-} {-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-}@@ -10,24 +11,24 @@ module Test.Control.Monad.IOSimPOR (tests) where  import           Data.Fixed (Micro)-import           Data.Foldable (foldl')+import           Data.Foldable (foldl', traverse_) import           Data.Functor (($>))-import           Data.IORef import qualified Data.List as List import           Data.Map (Map) import qualified Data.Map as Map+import           Data.STRef.Lazy  import           System.Exit import           System.IO.Error (ioeGetErrorString, isUserError)-import           System.IO.Unsafe  import           Control.Exception (ArithException (..), AsyncException) import           Control.Monad import           Control.Monad.Fix-import           Control.Parallel+import           Control.Monad.ST.Lazy (ST, runST) -import           Control.Monad.Class.MonadFork import           Control.Concurrent.Class.MonadSTM+import           Control.Monad.Class.MonadAsync+import           Control.Monad.Class.MonadFork import           Control.Monad.Class.MonadSay import           Control.Monad.Class.MonadTest import           Control.Monad.Class.MonadThrow@@ -47,13 +48,21 @@ import           Test.QuickCheck import           Test.Tasty (TestTree, testGroup) import           Test.Tasty.QuickCheck+import qualified Data.List.Trace as Trace  tests :: TestTree tests =-  testGroup "IO simulator POR"-  [ testProperty "propSimulates"    propSimulates-  , testProperty "propExploration"  propExploration-  -- , testProperty "propPermutations" propPermutations+  testGroup "IOSimPOR"+  [ testGroup "schedule exploration"+    [ testProperty "propSimulates"    propSimulates+    , testProperty "propExploration"  propExploration+    , testGroup "issue113"+      [ testProperty "wakeup"         unit_issue113_wakeup+      , testProperty "racy"           unit_issue113_racy+      , testProperty "nonDep"         unit_issue113_nonDep+      ]+    -- , testProperty "propPermutations" propPermutations+    ]   , testGroup "IO simulator properties"     [ testProperty "read/write graph (IOSim)" (withMaxSuccess 1000 prop_stm_graph_sim)     , testGroup "timeouts"@@ -256,39 +265,64 @@ sortTasks (x:xs)         = (x:) <$> sortTasks xs sortTasks []             = [] -interpret :: forall s. TVar (IOSim s) Int -> TVar (IOSim s) [ThreadId (IOSim s)] -> Task -> IOSim s (ThreadId (IOSim s))-interpret r t (Task steps) = forkIO $ do-    context <- atomically $ do+data Compare = AreEqual | AreNotEqual+  deriving Show++instance Arbitrary Compare where+  arbitrary = frequency [(8, pure AreEqual),+                         (2, pure AreNotEqual)]++  shrink AreEqual    = []+  shrink AreNotEqual = [AreEqual]+++interpret :: forall s.+             Compare+          -> TVar (IOSim s) Int+          -> TVar (IOSim s) [ThreadId (IOSim s)]+          -> (Int, Task)+          -> IOSim s (Async (IOSim s) ())+interpret cmp r t (tlbl, Task steps) = async $ do+    labelThisThread (show tlbl)+    (ts, timer) <- atomically $ do       ts <- readTVar t-      when (null ts) retry+      check (not (null ts))       timer <- newTVar Nothing       return (ts,timer)-    mapM_ (interpretStep context) steps-  where interpretStep _ (WhenSet m n) = atomically $ do-          a <- readTVar r-          when (a/=m) retry-          writeTVar r n-        interpretStep (ts,_) (ThrowTo i) = throwTo (ts !! i) (ExitFailure 0)-        interpretStep _      (Delay i)   = threadDelay (fromIntegral i)-        interpretStep (_,timer) (Timeout tstep) = do-          timerVal <- atomically $ readTVar timer-          case (timerVal,tstep) of-            (_,NewTimeout n)            -> do tout <- newTimeout (fromIntegral n)-                                              atomically $ writeTVar timer (Just tout)-            (Just tout,CancelTimeout)   -> cancelTimeout tout-            (Just tout,AwaitTimeout)    -> atomically $ awaitTimeout tout >> return ()-            (Nothing,_)                 -> return ()+    mapM_ (interpretStep ts timer) steps+  where+    compareFn = case cmp of+      AreEqual    -> (==)+      AreNotEqual -> (/=)+    interpretStep :: [ThreadId (IOSim s)]+                  -> TVar (IOSim s) (Maybe (Timeout s))+                  -> Step+                  -> IOSim s ()+    interpretStep _ _ (WhenSet m n) = atomically $ do+      readTVar r >>= check . compareFn m+      writeTVar r n+    interpretStep ts _     (ThrowTo i) = throwTo (ts !! i) (ExitFailure 0)+    interpretStep _  _     (Delay i)   = threadDelay (fromIntegral i)+    interpretStep _  timer (Timeout tstep) = do+      timerVal <- readTVarIO timer+      case (timerVal,tstep) of+        (_,NewTimeout n)          -> do tout <- newTimeout (fromIntegral n)+                                        atomically $ writeTVar timer (Just tout)+        (Just tout,CancelTimeout) -> cancelTimeout tout+        (Just tout,AwaitTimeout)  -> atomically $ awaitTimeout tout >> return ()+        (Nothing,_)               -> return () -runTasks :: [Task] -> IOSim s (Int,Int)-runTasks tasks = do+runTasks :: Compare -> [Task] -> IOSim s (Int,Int)+runTasks cmp tasks = do   let m = maximum [maxTaskValue t | Task t <- tasks]-  r  <- atomically $ newTVar m-  t  <- atomically $ newTVar []+  r  <- newTVarIO m+  traceTVarIO r (\_ a -> return (TraceString (show a)))+  t  <- newTVarIO []   exploreRaces-  ts <- mapM (interpret r t) tasks-  atomically $ writeTVar t ts-  threadDelay 1000000000  -- allow the SUT threads to run-  a  <- atomically $ readTVar r+  ts <- mapM (interpret cmp r t) (zip [1..] tasks)+  atomically $ writeTVar t (asyncThreadId <$> ts)+  traverse_ wait ts -- allow the SUT threads to run+  a  <- readTVarIO r   return (m,a)  maxTaskValue :: [Step] -> Int@@ -296,100 +330,133 @@ maxTaskValue (_:t)           = maxTaskValue t maxTaskValue []              = 0 -propSimulates :: Tasks -> Property-propSimulates (Tasks tasks) =+propSimulates :: Compare -> Shrink2 Tasks -> Property+propSimulates cmp (Shrink2 (Tasks tasks)) =   any (not . null . (\(Task steps)->steps)) tasks ==>-    let Right (m,a) = runSim (runTasks tasks) in-    m>=a+    let trace = runSimTrace (runTasks cmp tasks) in+    case traceResult False trace of+      Right (m,a) -> property (m >= a)+      Left (FailureInternal msg)+                  -> counterexample msg False+      Left x      -> counterexample (ppTrace trace)+                   $ counterexample (show x) True  -propExploration :: Tasks -> Property-propExploration (Tasks tasks) =-  -- Debug.trace ("\nTasks:\n"++ show tasks) $+-- NOTE: This property needs to be executed sequentially, otherwise it fails+-- undeterministically, which `exploreSimTraceST` does.+--+propExploration :: Compare -> (Shrink2 Tasks) -> Property+propExploration cmp (Shrink2 ts@(Tasks tasks)) =   any (not . null . (\(Task steps)->steps)) tasks ==>-    traceNoDuplicates $ \addTrace ->-    --traceCounter $ \addTrace ->-    exploreSimTrace id (runTasks tasks) $ \_ trace ->-    --Debug.trace (("\nTrace:\n"++) . splitTrace . noExceptions $ show trace) $-    addTrace trace $-    counterexample (splitTrace . noExceptions $ show trace) $-    case traceResult False trace of-      Right (m,a) -> property $ m>=a-      Left e      -> counterexample (show e) False+    runST $+    -- traceNoDuplicates $ \addTrace->+    exploreSimTraceST (\a -> a { explorationDebugLevel = 0 })+                      (say (show ts) >> runTasks cmp tasks)+                    $ \_ trace -> do+    -- addTrace trace+    -- TODO: for now @coot is leaving `Trace.ppTrace`, but once we change all+    -- assertions into `FailureInternal`, we can use `ppTrace` instead+    return $ counterexample (Trace.ppTrace show (ppSimEvent 0 0 0) trace) $+             case traceResult False trace of+               Right (m,a) -> property (m >= a)+               Left (FailureInternal msg)+                           -> counterexample msg False+               Left _      -> property True ++-- | This is a counterexample for a fix included in the commit: "io-sim-por:+-- fixed counterexample in issue #113".  There was a missing wakeup effect when+-- a thread terminates, for threads that were blocked on `throwTo`.+--+unit_issue113_wakeup :: Property+unit_issue113_wakeup =+    propExploration AreEqual (Shrink2 tasks)+  where+    tasks = Tasks [Task [ThrowTo 1, WhenSet 0 0],+                   Task [Delay 1],+                   Task [WhenSet 0 0],+                   Task [WhenSet 1 0, ThrowTo 1]]+++-- | This test checks that we don't build a schedule which execute a racing+-- step that depends on something that wasn't added to `stepInfoNonDep`.+--+unit_issue113_racy :: Property+unit_issue113_racy =+    propExploration AreNotEqual (Shrink2 tasks)+  where+    tasks = Tasks [Task [WhenSet 1 0,ThrowTo 1],+                   Task [],+                   Task [ThrowTo 1,WhenSet 0 0],+                   Task [ThrowTo 1]]+++-- | This test checks that we don't build a schedule which execute a non+-- dependent step that depends on something that wasn't added to+-- `stepInfoNonDep`.+--+unit_issue113_nonDep :: Property+unit_issue113_nonDep =+    propExploration AreNotEqual (Shrink2 tasks)+  where+    tasks = Tasks [Task [WhenSet 0 0],+                   Task [],+                   Task [WhenSet 0 0],+                   Task [ThrowTo 1,WhenSet 1 1],+                   Task [ThrowTo 5],+                   Task [ThrowTo 1]]++ -- Testing propPermutations n should collect every permutation of [1..n] once only. -- Test manually, and supply a small value of n. propPermutations :: Int -> Property propPermutations n =+  runST $   traceNoDuplicates $ \addTrace ->-  exploreSimTrace (withScheduleBound 10000) (doit n) $ \_ trace ->-    addTrace trace $-    let Right result = traceResult False trace in-    tabulate "Result" [noExceptions $ show $ result] $-      True+  exploreSimTraceST (withScheduleBound 10000) (doit n) $ \_ trace -> do+    addTrace trace+    let Right result = traceResult False trace+    return $ tabulate "Result" [show $ result] $ True  doit :: Int -> IOSim s [Int] doit n = do-          r <- atomically $ newTVar []-          exploreRaces-          mapM_ (\i -> forkIO $ atomically $ modifyTVar r (++[i])) [1..n]-          threadDelay 1-          atomically $ readTVar r+  r <- atomically $ newTVar []+  exploreRaces+  mapM_ (\i -> forkIO $ atomically $ modifyTVar r (++[i])) [1..n]+  threadDelay 1+  atomically $ readTVar r -ordered :: Ord a => [a] -> Bool-ordered xs = and (zipWith (<) xs (drop 1 xs)) -noExceptions :: [Char] -> [Char]-noExceptions xs = unsafePerformIO $ try (evaluate xs) >>= \case-  Right []     -> return []-  Right (x:ys) -> return (x:noExceptions ys)-  Left e       -> return ("\n"++show (e :: SomeException))+traceNoDuplicates :: forall s a prop. (Show a, Testable prop)+                  => ((a -> ST s ()) -> ST s prop)+                  -> ST s Property+traceNoDuplicates k = do+  v <- newSTRef Map.empty :: ST s (STRef s (Map String Int))+  prop <- k (\a -> modifySTRef v (Map.insertWith (+) (show a) 1))+  m <- readSTRef v+  return $ prop .&&. counterexample (show (Map.keys $ Map.filter (> 1) m)) (maximum m === 1) -splitTrace :: [Char] -> [Char]-splitTrace [] = []-splitTrace (x:xs) | begins "(Trace" = "\n(" ++ splitTrace xs-                  | otherwise       = x:splitTrace xs-  where begins s = take (length s) (x:xs) == s -traceCounter :: (Testable prop1, Show a1) => ((a1 -> a2 -> a2) -> prop1) -> Property-traceCounter k = r `pseq` (k addTrace .&&.-                           tabulate "Trace repetitions" (map show $ traceCounts ()) True)-  where-    r = unsafePerformIO $ newIORef (Map.empty :: Map String Int)-    addTrace t x = unsafePerformIO $ do-      atomicModifyIORef r (\m->(Map.insertWith (+) (show t) 1 m,()))-      return x-    traceCounts () = unsafePerformIO $ Map.elems <$> readIORef r--traceNoDuplicates :: (Testable prop1, Show a1) => ((a1 -> a2 -> a2) -> prop1) -> Property-traceNoDuplicates k = r `pseq` (k addTrace .&&. maximum (traceCounts ()) == 1)-  where-    r = unsafePerformIO $ newIORef (Map.empty :: Map String Int)-    addTrace t x = unsafePerformIO $ do-      atomicModifyIORef r (\m->(Map.insertWith (+) (show t) 1 m,()))-      return x-    traceCounts () = unsafePerformIO $ Map.elems <$> readIORef r- -- -- IOSim reused properties -- - -- -- Read/Write graph --  prop_stm_graph_sim :: TestThreadGraph -> Property prop_stm_graph_sim g =+  runST $   traceNoDuplicates $ \addTrace ->-    exploreSimTrace id (prop_stm_graph g) $ \_ trace ->-      addTrace trace $-      counterexample (splitTrace . noExceptions $ show trace) $-      case traceResult False trace of-        Right () -> property True-        Left e   -> counterexample (show e) False-      -- TODO: Note that we do not use runSimStrictShutdown here to check-      -- that all other threads finished, but perhaps we should and structure-      -- the graph tests so that's the case.+    exploreSimTraceST id (prop_stm_graph g) $ \_ trace -> do+      addTrace trace+      return $ counterexample (Trace.ppTrace show show trace) $+        case traceResult False trace of+          Right () -> property True+          Left e   -> counterexample (show e) False+        -- TODO: Note that we do not use runSimStrictShutdown here to check+        -- that all other threads finished, but perhaps we should and structure+        -- the graph tests so that's the case.  prop_timers_ST :: TestMicro -> Property prop_timers_ST (TestMicro xs) =