dejafu-0.2.0.0: Test/DejaFu/STM/Internal.hs
{-# LANGUAGE CPP #-}
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
-- | 'MonadSTM' testing implementation, internal types and
-- definitions.
module Test.DejaFu.STM.Internal where
import Control.Exception (Exception, SomeException(..), fromException)
import Control.Monad.Cont (Cont, runCont)
import Data.List (nub)
import Data.Maybe (fromMaybe)
import Data.Typeable (cast)
import Test.DejaFu.Internal
#if __GLASGOW_HASKELL__ < 710
import Data.Foldable (Foldable(..))
import Data.Monoid (mempty)
#endif
--------------------------------------------------------------------------------
-- The @STMLike@ monad
-- | The underlying monad is based on continuations over primitive
-- actions.
type M n r a = Cont (STMAction n r) a
-- | Dict of methods for implementations to override.
type Fixed n r = Ref n r (Cont (STMAction n r))
--------------------------------------------------------------------------------
-- * Primitive actions
-- | STM transactions are represented as a sequence of primitive
-- actions.
data STMAction n r
= forall a e. Exception e => SCatch (e -> M n r a) (M n r a) (a -> STMAction n r)
| forall a. SRead (CTVar r a) (a -> STMAction n r)
| forall a. SWrite (CTVar r a) a (STMAction n r)
| forall a. SOrElse (M n r a) (M n r a) (a -> STMAction n r)
| forall a. SNew a (CTVar r a -> STMAction n r)
| SLift (n (STMAction n r))
| forall e. Exception e => SThrow e
| SRetry
| SStop
--------------------------------------------------------------------------------
-- * @CTVar@s
-- | A 'CTVar' is a tuple of a unique ID and the value contained. The
-- ID is so that blocked transactions can be re-run when a 'CTVar'
-- they depend on has changed.
newtype CTVar r a = CTVar (CTVarId, r a)
-- | The unique ID of a 'CTVar'. Only meaningful within a single
-- concurrent computation.
type CTVarId = Int
--------------------------------------------------------------------------------
-- * Output
-- | The result of an STM transaction, along with which 'CTVar's it
-- touched whilst executing.
data Result a =
Success [CTVarId] [CTVarId] a
-- ^ The transaction completed successfully, reading the first list
-- 'CTVar's and writing to the second.
| Retry [CTVarId]
-- ^ The transaction aborted by calling 'retry', and read the
-- returned 'CTVar's. It should be retried when at least one of the
-- 'CTVar's has been mutated.
| Exception SomeException
-- ^ The transaction aborted by throwing an exception.
deriving Show
instance Functor Result where
fmap f (Success rs ws a) = Success rs ws $ f a
fmap _ (Retry rs) = Retry rs
fmap _ (Exception e) = Exception e
instance Foldable Result where
foldMap f (Success _ _ a) = f a
foldMap _ _ = mempty
--------------------------------------------------------------------------------
-- * Execution
-- | Run a STM transaction, returning an action to undo its effects.
doTransaction :: Monad n => Fixed n r -> M n r a -> CTVarId -> n (Result a, n (), CTVarId)
doTransaction fixed ma newctvid = do
ref <- newRef fixed Nothing
let c = runCont (ma >>= liftN fixed . writeRef fixed ref . Just . Right) $ const SStop
(newctvid', undo, readen, written) <- go ref c (return ()) newctvid [] []
res <- readRef fixed ref
case res of
Just (Right val) -> return (Success (nub readen) (nub written) val, undo, newctvid')
Just (Left exc) -> undo >> return (Exception exc, return (), newctvid)
Nothing -> undo >> return (Retry $ nub readen, return (), newctvid)
where
go ref act undo nctvid readen written = do
(act', undo', nctvid', readen', written') <- stepTrans fixed act nctvid
let ret = (nctvid', undo >> undo', readen' ++ readen, written' ++ written)
case act' of
SStop -> return ret
SRetry -> writeRef fixed ref Nothing >> return ret
SThrow exc -> writeRef fixed ref (Just . Left $ wrap exc) >> return ret
_ -> go ref act' (undo >> undo') nctvid' (readen' ++ readen) (written' ++ written)
-- | This wraps up an uncaught exception inside a @SomeException@,
-- unless it already is a @SomeException@. This is because
-- multiple levels of @SomeException@ do not play nicely with
-- @fromException@.
wrap e = fromMaybe (SomeException e) $ cast e
-- | Run a transaction for one step.
stepTrans :: Monad n => Fixed n r -> STMAction n r -> CTVarId -> n (STMAction n r, n (), CTVarId, [CTVarId], [CTVarId])
stepTrans fixed act newctvid = case act of
SCatch h stm c -> stepCatch h stm c
SRead ref c -> stepRead ref c
SWrite ref a c -> stepWrite ref a c
SNew a c -> stepNew a c
SOrElse a b c -> stepOrElse a b c
SLift na -> stepLift na
halt -> return (halt, nothing, newctvid, [], [])
where
nothing = return ()
stepCatch h stm c = onFailure stm c
(\readen -> return (SRetry, nothing, newctvid, readen, []))
(\exc -> case fromException exc of
Just exc' -> transaction (h exc') c
Nothing -> return (SThrow exc, nothing, newctvid, [], []))
stepRead (CTVar (ctvid, ref)) c = do
val <- readRef fixed ref
return (c val, nothing, newctvid, [ctvid], [])
stepWrite (CTVar (ctvid, ref)) a c = do
old <- readRef fixed ref
writeRef fixed ref a
return (c, writeRef fixed ref old, newctvid, [], [ctvid])
stepNew a c = do
let newctvid' = newctvid + 1
ref <- newRef fixed a
let ctvar = CTVar (newctvid, ref)
return (c ctvar, nothing, newctvid', [], [newctvid])
stepOrElse a b c = onFailure a c
(\_ -> transaction b c)
(\exc -> return (SThrow exc, nothing, newctvid, [], []))
stepLift na = do
a <- na
return (a, nothing, newctvid, [], [])
onFailure stm onSuccess onRetry onException = do
(res, undo, newctvid') <- doTransaction fixed stm newctvid
case res of
Success readen written val -> return (onSuccess val, undo, newctvid', readen, written)
Retry readen -> onRetry readen
Exception exc -> onException exc
transaction stm onSuccess = onFailure stm onSuccess
(\readen -> return (SRetry, nothing, newctvid, readen, []))
(\exc -> return (SThrow exc, nothing, newctvid, [], []))