{-# LANGUAGE ExistentialQuantification, TypeOperators #-}
module Test.IOSpec.VirtualMachine
(
VM
, Data
, Loc
, Scheduler
, Store
, ThreadId
, initialStore
, alloc
, emptyLoc
, freshThreadId
, finishThread
, lookupHeap
, mainTid
, printChar
, readChar
, updateHeap
, updateSoup
, Effect (..)
, roundRobin
, singleThreaded
, Executable(..)
, Step(..)
, runIOSpec
, evalIOSpec
, execIOSpec
)
where
import Control.Monad.State
import Data.Dynamic
import Data.List
import qualified Data.Stream as Stream
import Test.IOSpec.Types
import Test.QuickCheck
import Control.Monad (ap)
type Data = Dynamic
type Loc = Int
type Heap = Loc -> Maybe Data
newtype ThreadId = ThreadId Int deriving (Eq, Show)
instance Arbitrary ThreadId where
arbitrary = liftM ThreadId arbitrary
instance CoArbitrary ThreadId where
coarbitrary (ThreadId k) = coarbitrary k
newtype Scheduler =
Scheduler (Int -> (Int, Scheduler))
instance Arbitrary Scheduler where
arbitrary = liftM streamSched arbitrary
instance Show Scheduler where
show _ = "Test.IOSpec.Scheduler"
data ThreadStatus =
forall f b . Executable f => Running (IOSpec f b)
| Finished
type ThreadSoup = ThreadId -> ThreadStatus
data Store =
Store { fresh :: Loc
, heap :: Heap
, nextTid :: ThreadId
, blockedThreads :: [ThreadId]
, finishedThreads :: [ThreadId]
, scheduler :: Scheduler
, threadSoup :: ThreadSoup
}
initialStore :: Scheduler -> Store
initialStore sch =
Store { fresh = 0
, heap = internalError "Access of unallocated memory "
, nextTid = ThreadId 1
, blockedThreads = []
, finishedThreads = []
, scheduler = sch
, threadSoup = internalError "Unknown thread scheduled"
}
modifyFresh :: (Loc -> Loc) -> VM ()
modifyFresh f = do s <- get
put (s {fresh = f (fresh s)})
modifyHeap :: (Heap -> Heap) -> VM ()
modifyHeap f = do s <- get
put (s {heap = f (heap s)})
modifyNextTid :: (ThreadId -> ThreadId) -> VM ()
modifyNextTid f = do s <- get
put (s {nextTid = f (nextTid s)})
modifyBlockedThreads :: ([ThreadId] -> [ThreadId]) -> VM ()
modifyBlockedThreads f = do s <- get
put (s {blockedThreads = f (blockedThreads s)})
modifyFinishedThreads :: ([ThreadId] -> [ThreadId]) -> VM ()
modifyFinishedThreads f = do s <- get
put (s {finishedThreads = f (finishedThreads s)})
modifyScheduler :: (Scheduler -> Scheduler) -> VM ()
modifyScheduler f = do s <- get
put (s {scheduler = f (scheduler s)})
modifyThreadSoup :: (ThreadSoup -> ThreadSoup) -> VM ()
modifyThreadSoup f = do s <- get
put (s {threadSoup = f (threadSoup s)})
type VM a = StateT Store Effect a
alloc :: VM Loc
alloc = do loc <- gets fresh
modifyFresh ((+) 1)
return loc
emptyLoc :: Loc -> VM ()
emptyLoc l = modifyHeap (update l Nothing)
freshThreadId :: VM ThreadId
freshThreadId = do
t <- gets nextTid
modifyNextTid (\(ThreadId n) -> ThreadId (n+1))
return t
finishThread :: ThreadId -> VM ()
finishThread tid = do
modifyFinishedThreads (tid:)
modifyThreadSoup (update tid Finished)
blockThread :: ThreadId -> VM ()
blockThread tid = modifyBlockedThreads (tid:)
resetBlockedThreads :: VM ()
resetBlockedThreads = modifyBlockedThreads (const [])
lookupHeap :: Loc -> VM (Maybe Data)
lookupHeap l = do h <- gets heap
return (h l)
mainTid :: ThreadId
mainTid = ThreadId 0
readChar :: VM Char
readChar = StateT (\s -> (ReadChar (\c -> (Done (c,s)))))
printChar :: Char -> VM ()
printChar c = StateT (\s -> (Print c (Done ((),s))))
updateHeap :: Loc -> Data -> VM ()
updateHeap l d = modifyHeap (update l (Just d))
updateSoup :: Executable f => ThreadId -> IOSpec f a -> VM ()
updateSoup tid p = modifyThreadSoup (update tid (Running p))
update :: Eq a => a -> b -> (a -> b) -> (a -> b)
update l d h k
| l == k = d
| otherwise = h k
data Effect a =
Done a
| ReadChar (Char -> Effect a)
| Print Char (Effect a)
| Fail String
instance Functor Effect where
fmap f (Done x) = Done (f x)
fmap f (ReadChar t) = ReadChar (\c -> fmap f (t c))
fmap f (Print c t) = Print c (fmap f t)
fmap _ (Fail msg) = Fail msg
instance Applicative Effect where
pure = Done
(<*>) = ap
instance Monad Effect where
return = Done
(Done x) >>= f = f x
(ReadChar t) >>= f = ReadChar (\c -> t c >>= f)
(Print c t) >>= f = Print c (t >>= f)
(Fail msg) >>= _ = Fail msg
instance Eq a => Eq (Effect a) where
(Done x) == (Done y) = x == y
(ReadChar f) == (ReadChar g) =
all (\x -> f x == g x) [minBound .. maxBound]
(Print c t) == (Print d u) = c == d && t == u
(Fail s) == (Fail t) = s == t
_ == _ = False
roundRobin :: Scheduler
roundRobin = streamSched (Stream.unfold (\k -> (k, k+1)) 0)
singleThreaded :: Scheduler
singleThreaded = streamSched (Stream.repeat 0)
streamSched :: Stream.Stream Int -> Scheduler
streamSched (Stream.Cons x xs) =
Scheduler (\k -> (x `mod` k, streamSched xs))
class Functor f => Executable f where
step :: f a -> VM (Step a)
data Step a = Step a | Block
instance (Executable f, Executable g) => Executable (f :+: g) where
step (Inl x) = step x
step (Inr y) = step y
execVM :: Executable f => IOSpec f a -> VM a
execVM main = do
(tid,t) <- schedule main
case t of
(Main (Pure x)) -> resetBlockedThreads >> return x
(Main (Impure p)) -> do x <- step p
case x of
Step y -> resetBlockedThreads >> execVM y
Block -> blockThread mainTid >> execVM main
(Aux (Pure _)) -> do finishThread tid
execVM main
(Aux (Impure p)) -> do x <- step p
case x of
Step y -> resetBlockedThreads >>
updateSoup tid y >>
execVM main
Block -> blockThread tid >>
execVM main
data Process a =
forall f . Executable f => Main (IOSpec f a)
| forall f b . Executable f => Aux (IOSpec f b)
getNextThreadId :: VM ThreadId
getNextThreadId = do
Scheduler sch <- gets scheduler
(ThreadId total) <- gets nextTid
let allTids = [ThreadId i | i <- [0 .. total - 1]]
blockedTids <- gets blockedThreads
finishedTids <- gets finishedThreads
let activeThreads = allTids \\ (blockedTids `union` finishedTids)
let (i,s) = sch (length activeThreads)
modifyScheduler (const s)
return (activeThreads !! i)
schedule :: Executable f => IOSpec f a -> VM (ThreadId, Process a)
schedule main = do tid <- getNextThreadId
if tid == mainTid
then return (mainTid, Main main)
else do
tsoup <- gets threadSoup
case tsoup tid of
Finished -> internalError
"Scheduled finished thread."
Running p -> return (tid, Aux p)
runIOSpec :: Executable f => IOSpec f a -> Scheduler -> Effect (a, Store)
runIOSpec io sched = runStateT
(execVM io)
(initialStore sched)
execIOSpec :: Executable f => IOSpec f a -> Scheduler -> Store
execIOSpec io sched =
case runIOSpec io sched of
Done (_,s) -> s
_ -> error $ "Failed application of Test.IOSpec.execIOSpec.\n" ++
"Probable cause: your function uses functions such as " ++
"putChar and getChar. Check the preconditions for calling " ++
"this function in the IOSpec documentation."
evalIOSpec :: Executable f => IOSpec f a -> Scheduler -> Effect a
evalIOSpec io sched = fmap fst (runIOSpec io sched)
internalError :: String -> a
internalError msg = error ("IOSpec.VirtualMachine: " ++ msg)