packages feed

io-sim 1.4.1.0 → 1.5.0.0

raw patch · 10 files changed

+335/−256 lines, 10 filesdep +paralleldep ~basedep ~criteriondep ~io-classesnew-uploaderPVP ok

version bump matches the API change (PVP)

Dependencies added: parallel

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

API changes (from Hackage documentation)

- Control.Monad.IOSim: controlSimTraceST :: Maybe Int -> ScheduleControl -> IOSim s a -> ST s (SimTrace a)
- 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
+ Data.List.Trace: drop :: Int -> Trace a b -> Trace a b
+ Data.List.Trace: dropWhile :: (b -> Bool) -> Trace a b -> Trace a b
+ Data.List.Trace: take :: Int -> Trace a b -> Trace (Maybe a) b
+ Data.List.Trace: takeWhile :: (b -> Bool) -> Trace a b -> Trace (Maybe a) b
- Control.Monad.IOSim: traceSelectTraceEvents :: (Time -> SimEventType -> Maybe b) -> SimTrace a -> Trace (SimResult a) b
+ Control.Monad.IOSim: traceSelectTraceEvents :: (Time -> SimEventType -> Maybe b) -> Trace a SimEvent -> Trace a b
- Control.Monad.IOSim: traceSelectTraceEventsDynamic :: forall a b. Typeable b => SimTrace a -> Trace (SimResult a) b
+ Control.Monad.IOSim: traceSelectTraceEventsDynamic :: forall a b. Typeable b => Trace a SimEvent -> Trace a b
- Control.Monad.IOSim: traceSelectTraceEventsSay :: forall a. SimTrace a -> Trace (SimResult a) String
+ Control.Monad.IOSim: traceSelectTraceEventsSay :: forall a. Trace a SimEvent -> Trace a String

Files

CHANGELOG.md view
@@ -1,4 +1,18 @@-# Revsion history of io-sim+# Revision history of io-sim++## 1.5.0.0++### Breaking changes++- Generalised the type of `traceSelectTraceEvents` & co.++### Non-breaking changes++- Added `writeTMVar` to `MonadSTM` instance for `(IOSim s)`.+- Fixes IOSimPOR test failure (see issue #154).+- Reverted commit 4534b6eae64072a87bd81584f479a123681358a3 which uses+  `unsafePerformIO` instead of ST, to regain lazyness on infinite simulations.+- Added a test to check for lazyness on infinite simulations  ## 1.4.1.0 
io-sim.cabal view
@@ -1,6 +1,6 @@ cabal-version:       3.0 name:                io-sim-version:             1.4.1.0+version:             1.5.0.0 synopsis:            A pure simulator for monadic concurrency with STM. description:   A pure simulator monad with support of concurency (base & async style), 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, 9.8 }+tested-with:         GHC == { 8.10, 9.2, 9.4, 9.6, 9.8, 9.10 }  flag asserts   description: Enable assertions@@ -75,19 +75,20 @@                        RankNTypes,                        ScopedTypeVariables,                        TypeFamilies-  build-depends:       base              >=4.9 && <4.20,-                       io-classes       ^>=1.4.1,+  build-depends:       base              >=4.9 && <4.21,+                       io-classes       ^>=1.5,                        exceptions        >=0.10,                        containers,                        deepseq,                        nothunks,-                       primitive         >=0.7 && <0.10,+                       primitive         >=0.7 && <0.11,                        psqueues          >=0.2 && <0.3,-                       strict-stm       ^>=1.4,-                       si-timers        ^>=1.4,+                       strict-stm       ^>=1.5,+                       si-timers        ^>=1.5,                        time              >=1.9.1 && <1.13,                        quiet,                        QuickCheck,+                       parallel     if flag(asserts)@@ -130,7 +131,7 @@   default-language:    Haskell2010   default-extensions:  ImportQualifiedPost   build-depends:       base,-                       criterion,+                       criterion ^>= 1.6,                         io-classes,                        io-sim,
src/Control/Monad/IOSim.hs view
@@ -1,15 +1,11 @@-{-# LANGUAGE BangPatterns          #-} {-# LANGUAGE ExplicitNamespaces    #-}-{-# LANGUAGE LambdaCase            #-} {-# LANGUAGE NamedFieldPuns        #-}-{-# LANGUAGE QuantifiedConstraints #-} {-# LANGUAGE RankNTypes            #-} {-# LANGUAGE ScopedTypeVariables   #-} {-# LANGUAGE TupleSections         #-}  {-# OPTIONS_GHC -Wno-name-shadowing #-}-{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-}-+{-# LANGUAGE QuantifiedConstraints #-} module Control.Monad.IOSim   ( -- * Simulation monad     IOSim@@ -28,9 +24,7 @@     -- ** Explore races using /IOSimPOR/     -- $iosimpor   , exploreSimTrace-  , exploreSimTraceST   , controlSimTrace-  , controlSimTraceST   , ScheduleMod (..)   , ScheduleControl (..)     -- *** Exploration options@@ -99,22 +93,17 @@ 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 Data.Set qualified as Set import Data.Typeable (Typeable)  import Data.List.Trace (Trace (..))-import Data.List.Trace qualified as Trace  import Control.Exception (throw)  import Control.Monad.ST.Lazy-import Data.STRef.Lazy  import Control.Monad.Class.MonadThrow as MonadThrow @@ -127,10 +116,13 @@ import Test.QuickCheck.Gen.Unsafe (Capture (..), capture) import Test.QuickCheck.Monadic (PropertyM, monadic') -import Debug.Trace qualified as Debug import System.IO.Unsafe +import Data.Functor (void)+import Data.IORef+import Debug.Trace qualified as Debug + -- | Select events according to the predicate function.  It throws an error if -- the simulation ends with 'Failure'. --@@ -187,38 +179,29 @@ -- unsafe, of course, since that function may return different results -- at different times. --- | 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+detachTraceRaces :: forall a. Int -> SimTrace a -> (() -> [ScheduleControl], SimTrace a)+detachTraceRaces debugLevel trace = unsafePerformIO $ do+  races <- newIORef []+  let readRaces :: () -> [ScheduleControl]+      readRaces () = concat . reverse . unsafePerformIO $ readIORef races -  trace <- go trace0-  return (readRaces, trace)+      saveRaces :: [ScheduleControl] -> x -> x+      saveRaces rs t = unsafePerformIO $ modifyIORef races (rs:)+                                      >> return t +      go :: SimTrace a -> SimTrace a+      go (SimTrace a b c d trace)      = traceDebugLog debugLevel (Left (SimEvent a b c d))+                                       $ SimTrace a b c d $ go trace+      go (SimPORTrace a b c d e trace) = traceDebugLog debugLevel (Left (SimPOREvent a b c d e))+                                       $ SimPORTrace a b c d e $ go trace+      go (TraceRacesFound rs trace)    = traceDebugLog debugLevel (Left (SimRacesFound rs))+                                       $ saveRaces rs $ go trace+      go (Cons a as)                   = traceDebugLog debugLevel (Left a)+                                       $ Cons a (go as)+      go (Nil a)                       = traceDebugLog debugLevel (Right a)+                                       $ Nil a --- | 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)+  return (readRaces, go trace)  -- | Select all the traced values matching the expected type.  It relies on the -- sim's dynamic trace facility.@@ -313,8 +296,8 @@ -- traceSelectTraceEvents     :: (Time -> SimEventType -> Maybe b)-    -> SimTrace a-    -> Trace (SimResult a) b+    -> Trace a SimEvent+    -> Trace a b traceSelectTraceEvents fn = bifoldr ( \ v _acc -> Nil v )                                     ( \ eventCtx acc                                      -> case eventCtx of@@ -333,7 +316,8 @@ -- | Select dynamic events.  It is a /total function/. -- traceSelectTraceEventsDynamic :: forall a b. Typeable b-                              => SimTrace a -> Trace (SimResult a) b+                              => Trace a SimEvent+                              -> Trace a b traceSelectTraceEventsDynamic = traceSelectTraceEvents fn   where     fn :: Time -> SimEventType -> Maybe b@@ -343,7 +327,7 @@  -- | Select say events.  It is a /total function/. ---traceSelectTraceEventsSay :: forall a.  SimTrace a -> Trace (SimResult a) String+traceSelectTraceEventsSay :: forall a. Trace a SimEvent -> Trace a String traceSelectTraceEventsSay = traceSelectTraceEvents fn   where     fn :: Time -> SimEventType -> Maybe String@@ -531,77 +515,57 @@   -- ^ a callback which receives the previous trace (e.g. before reverting   -- a race condition) and current trace   -> Property-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.----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+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)   where     opts = optsf stdExplorationOptions -    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) ]-        ]+    explore :: Int -- schedule bound+            -> Int -- branching factor+            -> 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 (explorationDebugLevel opts)+                              $ 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) ]+     bucket :: Int -> String     bucket n | n<10      = show n              | otherwise = buck n 1@@ -619,42 +583,69 @@       ppIOSimThreadId tid ++ (case lab of Nothing -> ""                                           Just l  -> " ("++l++")") +    -- cache of explored schedules+    cache :: IORef (Set ScheduleControl)+    cache = unsafePerformIO cacheIO+     -- insert a schedule into the cache-    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+    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+     ---    -- Caching in ST monad+    -- Caching in IO 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.-    createCacheST :: ST s (STRef s (Set ScheduleControl))-    createCacheST = newSTRef $+    cacheIO :: IO (IORef (Set ScheduleControl))+    cacheIO = newIORef $               -- 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" -    cacheSizeST :: STRef s (Set ScheduleControl) -> ST s Int-    cacheSizeST = fmap Set.size . readSTRef+    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 + -- |  Trace `SimTrace` to `stderr`. ---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)+-- An internal function.+--+traceDebugLog :: Int -> Either SimEvent (SimResult a) -> SimTrace a -> SimTrace a+traceDebugLog logLevel _event trace | logLevel <= 0 = trace+-- Discard races if log level is 1+traceDebugLog 1 (Left SimPOREvent { seType = EventRaces {} }) trace = trace+traceDebugLog 1 (Left event) trace  = Debug.trace (ppSimEvent 0 0 0 event) trace+traceDebugLog 1 (Right event) trace = Debug.trace (ppSimResult 0 0 0 (void event)) trace+-- Otherwise, show races+traceDebugLog _ (Left event) trace  = Debug.trace (ppSimEvent 0 0 0 event) trace+traceDebugLog _ (Right event) trace = Debug.trace (ppSimResult 0 0 0 (void event)) trace +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 (explorationDebugLevel opts) $+                  controlSimTrace (explorationStepTimelimit opts) control mainAction+  in property (k trace)  -- | Run a simulation using a given schedule.  This is useful to reproduce -- failing cases without exploring the races.@@ -671,22 +662,12 @@                 -- ^ a simulation to run                 -> SimTrace a 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-+    runST (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@@ -702,62 +683,41 @@ -- this far, then we collect its identity only if it is reached using -- `unsafePerformIO`. --- 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')+compareTraces :: Maybe (SimTrace a1)+              -> SimTrace a2+              -> (Maybe ((Time, IOSimThreadId, Maybe ThreadLabel),+                         Set.Set (IOSimThreadId, 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')                    (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 = 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+                $ 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 -        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 =-              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+              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  -- -- QuickCheck monadic combinators
src/Control/Monad/IOSim/Types.hs view
@@ -16,7 +16,7 @@ {-# LANGUAGE TypeFamilies              #-}  -- Needed for `SimEvent` type.-{-# OPTIONS_GHC -Wno-partial-fields     #-}+{-# OPTIONS_GHC -Wno-partial-fields    #-}  module Control.Monad.IOSim.Types   ( IOSim (..)@@ -75,8 +75,8 @@   ) where  import Control.Applicative-import Control.Exception (ErrorCall (..), asyncExceptionFromException,-           asyncExceptionToException)+import Control.Exception (ErrorCall (..))+import Control.Exception qualified as IO import Control.Monad import Control.Monad.Fix (MonadFix (..)) @@ -340,6 +340,9 @@  instance MonadThrow (IOSim s) where   throwIO e = IOSim $ oneShot $ \_ -> Throw (toException e)+#if __GLASGOW_HASKELL__ >= 910+  annotateIO ann io = io `catch` \e -> throwIO (IO.addExceptionContext ann e)+#endif  instance MonadEvaluate (IOSim s) where   evaluate a = IOSim $ oneShot $ \k -> Evaluate a k@@ -354,6 +357,9 @@  instance MonadThrow (STM s) where   throwIO e = STM $ oneShot $ \_ -> ThrowStm (toException e)+#if __GLASGOW_HASKELL__ >= 910+  annotateIO ann io = io `catch` \e -> throwIO (IO.addExceptionContext ann e)+#endif    -- Since these involve re-throwing the exception and we don't provide   -- CatchSTM at all, then we can get away with trivial versions:@@ -516,6 +522,7 @@   readTMVar         = MonadSTM.readTMVarDefault   tryReadTMVar      = MonadSTM.tryReadTMVarDefault   swapTMVar         = MonadSTM.swapTMVarDefault+  writeTMVar        = MonadSTM.writeTMVarDefault   isEmptyTMVar      = MonadSTM.isEmptyTMVarDefault    newTQueue         = newTQueueDefault@@ -644,8 +651,7 @@   primitive st = IOSim $ oneShot $ \k -> LiftST (Prim.primitive st) k  instance MonadST (IOSim s) where-  stToIO f = IOSim $ oneShot $ \k -> LiftST f k-  withLiftST f = f liftST+  stToIO = liftST  -- | Lift an 'StrictST.ST' computation to 'IOSim'. --@@ -742,8 +748,8 @@     show (TimeoutException tmid) = "<<timeout " ++ show tmid ++ " >>"  instance Exception TimeoutException where-  toException   = asyncExceptionToException-  fromException = asyncExceptionFromException+  toException   = IO.asyncExceptionToException+  fromException = IO.asyncExceptionFromException  -- | Wrapper for Eventlog events so they can be retrieved from the trace with -- 'selectTraceEventsDynamic'.
src/Control/Monad/IOSimPOR/Internal.hs view
@@ -1771,17 +1771,27 @@                                            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 = concurrent Set.\\ effectWakeup newEffect in+          let !lessConcurrent = concurrent Set.\\ effectWakeup newEffect +              -- `step` happened before `newStep` (`newStep` happened after+              -- `step`)+              happensBefore   = step `happensBeforeStep` newStep++              !stepInfoNonDep'+                -- `newStep` happened after `step`+                | happensBefore =           stepInfoNonDep+                -- `newStep` did not happen after `step`+                | otherwise     = newStep : stepInfoNonDep in+           if tid `notElem` concurrent-            then stepInfo { stepInfoConcurrent = lessConcurrent }+            then let+                  in stepInfo { stepInfoConcurrent = lessConcurrent+                              , stepInfoNonDep     = stepInfoNonDep'+                              }              -- 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 @@ -1794,12 +1804,6 @@                        | 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
src/Control/Monad/IOSimPOR/QuickCheckUtils.hs view
@@ -4,19 +4,32 @@  module Control.Monad.IOSimPOR.QuickCheckUtils where -import Control.Monad.ST.Lazy+import Control.Parallel 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 _ -> (case ok result of { Nothing -> (); Just !_ -> (); })+                                `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 @@ -84,7 +97,7 @@   propertyNoCatch = MkProperty . return . MkProp . return  instance TestableNoCatch Prop where-  propertyNoCatch = MkProperty . return+  propertyNoCatch p = MkProperty . return $ p  instance TestableNoCatch prop => TestableNoCatch (Gen prop) where   propertyNoCatch mp = MkProperty $ do p <- mp; unProperty (againNoCatch $ propertyNoCatch p)
src/Data/Deque/Strict.hs view
@@ -1,10 +1,15 @@+{-# LANGUAGE CPP        #-} {-# LANGUAGE LambdaCase #-}  -- | A minimal implementation of a strict deque. -- module Data.Deque.Strict where +#if __GLASGOW_HASKELL__ >= 910+import Data.Foldable (foldr')+#else import Data.Foldable (foldl', foldr')+#endif import Data.List qualified as List import Prelude hiding (head, init, tail) 
src/Data/List/Trace.hs view
@@ -10,9 +10,14 @@   , tail   , filter   , length+  , take+  , takeWhile+  , drop+  , dropWhile   ) where -import Prelude hiding (filter, head, length, tail)+import Prelude hiding (drop, dropWhile, filter, head, length, tail, take,+           takeWhile)  import Control.Applicative (Alternative (..)) import Control.Monad (MonadPlus (..))@@ -64,6 +69,32 @@ ppTrace :: (a -> String) -> (b -> String) -> Trace a b -> String ppTrace sa  sb (Cons b bs) = sb b ++ "\n" ++ ppTrace sa sb bs ppTrace sa _sb (Nil a)     = sa a++-- | Take the first n elements of a Trace, converting each to ().+take :: Int -> Trace a b -> Trace (Maybe a) b+take n _ | n <= 0 = Nil Nothing+take _ (Nil a)    = Nil (Just a)+take n (Cons b o) = Cons b (take (n - 1) o)++-- | Take elements from the Trace while the predicate holds, converting each to ().+takeWhile :: (b -> Bool) -> Trace a b -> Trace (Maybe a) b+takeWhile _ (Nil a)    = Nil (Just a)+takeWhile p (Cons b o)+  | p b                = Cons b (takeWhile p o)+  | otherwise          = Nil Nothing++-- | Drop the first n elements of a Trace.+drop :: Int -> Trace a b -> Trace a b+drop n o | n <= 0 = o+drop _ (Nil a)    = Nil a+drop n (Cons _ o) = drop (n - 1) o++-- | Drop elements from the Trace while the predicate holds.+dropWhile :: (b -> Bool) -> Trace a b -> Trace a b+dropWhile _ o@Nil {}        = o+dropWhile p o@(Cons b o')+  | p b                     = dropWhile p o'+  | otherwise               = o  instance Bifunctor Trace where     bimap f g (Cons b bs) = Cons (g b) (bimap f g bs)
test/Test/Control/Monad/IOSim.hs view
@@ -24,7 +24,9 @@  import Data.Either (isLeft) import Data.Fixed (Micro)+#if __GLASGOW_HASKELL__ < 910 import Data.Foldable (foldl')+#endif import Data.Functor (($>)) import Data.Time.Clock (picosecondsToDiffTime) 
test/Test/Control/Monad/IOSimPOR.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE CPP                   #-} {-# LANGUAGE DeriveGeneric         #-} {-# LANGUAGE FlexibleContexts      #-} {-# LANGUAGE LambdaCase            #-}@@ -11,12 +12,15 @@ module Test.Control.Monad.IOSimPOR (tests) where  import Data.Fixed (Micro)+#if __GLASGOW_HASKELL__ >= 910+import Data.Foldable (traverse_)+#else import Data.Foldable (foldl', traverse_)+#endif import Data.Functor (($>)) import Data.List qualified as List import Data.Map (Map) import Data.Map qualified as Map-import Data.STRef.Lazy  import System.Exit import System.IO.Error (ioeGetErrorString, isUserError)@@ -24,7 +28,6 @@ import Control.Exception (ArithException (..), AsyncException) import Control.Monad import Control.Monad.Fix-import Control.Monad.ST.Lazy (ST, runST)  import Control.Concurrent.Class.MonadSTM import Control.Monad.Class.MonadAsync@@ -49,6 +52,13 @@ import Test.Tasty (TestTree, testGroup) import Test.Tasty.QuickCheck +import System.IO.Unsafe++import Data.IORef+import Data.Time (secondsToDiffTime)+import Data.Void (Void)+import GHC.Conc (pseq)+ tests :: TestTree tests =   testGroup "IOSimPOR"@@ -72,6 +82,7 @@       , testProperty "stacked timeouts"       prop_stacked_timeouts       , testProperty "{register,thread}Delay" unit_registerDelay_threadDelay       ]+    , testProperty "infinite simulation"      prop_explore_endless_simulation     , testProperty "threadId order (IOSim)"   (withMaxSuccess 1000 prop_threadId_order_order_Sim)     , testProperty "forkIO order (IOSim)"     (withMaxSuccess 1000 prop_fork_order_ST)     , testGroup "throw/catch unit tests"@@ -344,18 +355,17 @@ -- NOTE: This property needs to be executed sequentially, otherwise it fails -- undeterministically, which `exploreSimTraceST` does. ---propExploration :: Compare -> (Shrink2 Tasks) -> Property+propExploration :: Compare -> Shrink2 Tasks -> Property propExploration cmp (Shrink2 ts@(Tasks tasks)) =-  any (not . null . (\(Task steps)->steps)) tasks ==>-    runST $-    -- traceNoDuplicates $ \addTrace->-    exploreSimTraceST (\a -> a { explorationDebugLevel = 0 })-                      (say (show ts) >> runTasks cmp tasks)-                    $ \_ trace -> do-    -- addTrace trace+  not (all (null . (\(Task steps)->steps)) tasks) ==>+    traceNoDuplicates $ \addTrace->+    exploreSimTrace (\a -> a { explorationDebugLevel = 0 })+                    (say (show ts) >> runTasks cmp tasks)+                    $ \_ trace ->+    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) $+    counterexample (Trace.ppTrace show (ppSimEvent 0 0 0) trace) $              case traceResult False trace of                Right (m,a) -> property (m >= a)                Left (FailureInternal msg)@@ -410,32 +420,66 @@ -- Test manually, and supply a small value of n. propPermutations :: Int -> Property propPermutations n =-  runST $   traceNoDuplicates $ \addTrace ->-  exploreSimTraceST (withScheduleBound 10000) (doit n) $ \_ trace -> do-    addTrace trace+  exploreSimTrace (withScheduleBound 10000) (doit n) $ \_ trace ->+    addTrace trace $     let Right result = traceResult False trace-    return $ tabulate "Result" [show $ result] $ True+     in tabulate "Result" [show result] True  doit :: Int -> IOSim s [Int] doit n = do-  r <- atomically $ newTVar []+  r <- newTVarIO []   exploreRaces   mapM_ (\i -> forkIO $ atomically $ modifyTVar r (++[i])) [1..n]   threadDelay 1-  atomically $ readTVar r+  readTVarIO r  -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)+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 +-- | Checks that IOSimPOR is capable of analysing an infinite simulation+-- lazily.+--+-- If the test fails with "outer timeout" error it means that 'exploreSimTrace'+-- is not lazy and there's something wrong with IOSimPOR implementation and+-- the trace is being forced way too soon. If, on the other hand, this property+-- fails with "inner timeout" error, it means that 'exploreSimTrace' is somewhat+-- lazy, however the trace is not being lazily generated.+--+--+prop_explore_endless_simulation :: Positive (Small Integer) -> Property+prop_explore_endless_simulation (Positive (Small finalTime)) =+    counterexample "outer timeout"+  $ label (show (secondsToDiffTime finalTime))+  $ within 15000000 -- 15 seconds+  $ exploreSimTrace+      id sim $ \_ trace -> do+        let l = takeWhile (\(t, _, _, _) -> t < Time (secondsToDiffTime finalTime))+              . traceEvents+              $ trace+         in ioProperty $ do+           r <- timeout 10 $ evaluate (foldl' (flip seq) True l)+           case r of+             Nothing -> return $ counterexample "inner timeout" False+             Just _  -> return (property True)+  where+    thread :: forall s. IOSim s Void+    thread = do+      threadDelay 1+      thread +    sim :: forall s. IOSim s Void+    sim = do+      exploreRaces+      withAsync thread wait+ -- -- IOSim reused properties --@@ -446,11 +490,10 @@  prop_stm_graph_sim :: TestThreadGraph -> Property prop_stm_graph_sim g =-  runST $   traceNoDuplicates $ \addTrace ->-    exploreSimTraceST id (prop_stm_graph g) $ \_ trace -> do+    exploreSimTrace id (prop_stm_graph g) $ \_ trace -> do       addTrace trace-      return $ counterexample (Trace.ppTrace show show trace) $+      $ counterexample (Trace.ppTrace show show trace) $         case traceResult False trace of           Right () -> property True           Left e   -> counterexample (show e) False@@ -498,7 +541,7 @@        Left e  -> counterexample (show e) False   where     factorial :: (Int -> Int) -> Int -> Int-    factorial = \rec_ k -> if k <= 1 then 1 else k * rec_ (k - 1)+    factorial rec_ k = if k <= 1 then 1 else k * rec_ (k - 1)  prop_mfix_purity_2 :: [Positive Int] -> Property prop_mfix_purity_2 as =