Created
March 21, 2024 10:05
-
-
Save stevana/ad75c4aa9cbd7c526732c1114cb7dfb5 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{-# LANGUAGE DeriveFunctor #-} | |
{-# LANGUAGE GADTs #-} | |
{-# LANGUAGE Rank2Types #-} | |
{-# LANGUAGE RecordWildCards #-} | |
{-# LANGUAGE ScopedTypeVariables #-} | |
{-# LANGUAGE StandaloneDeriving #-} | |
{-# LANGUAGE StandaloneKindSignatures #-} | |
{-# LANGUAGE TypeApplications #-} | |
module MyLib where | |
import Control.Exception | |
import Data.Array.IO | |
import Data.Dynamic | |
import Data.Either | |
import Data.IntMap (IntMap) | |
import qualified Data.IntMap as Map | |
import Data.IORef | |
import Data.Kind | |
import Data.Typeable | |
import Test.QuickCheck | |
import Test.QuickCheck.Poly | |
------------------------------------------------------------------------ | |
-- Bounded queues, without any error checking. | |
data Queue a = | |
Queue { | |
queue_array :: IOArray Int a, | |
queue_peek :: IORef Int, | |
queue_poke :: IORef Int } | |
-- XXX: can we avoid having to define Eq and Show? | |
instance Eq (Queue a) where | |
(==) = undefined | |
instance Show (Queue a) where | |
show = undefined | |
new :: Int -> IO (Queue a) | |
new n = do | |
array <- newArray (0, n) undefined | |
peek <- newIORef 0 | |
poke <- newIORef 0 | |
return (Queue array peek poke) | |
put :: a -> Queue a -> IO () | |
put x Queue{..} = do | |
poke <- readIORef queue_poke | |
writeArray queue_array poke x | |
n <- arraySize queue_array | |
writeIORef queue_poke $! (poke + 1) `mod` n | |
get :: Queue a -> IO a | |
get Queue{..} = do | |
peek <- readIORef queue_peek | |
x <- readArray queue_array peek | |
n <- arraySize queue_array | |
writeIORef queue_peek $! (peek + 1) `mod` n | |
return x | |
size :: Queue a -> IO Int | |
size Queue{..} = do | |
peek <- readIORef queue_peek | |
poke <- readIORef queue_poke | |
n <- arraySize queue_array | |
return ((poke-peek) `mod` n) | |
arraySize :: (Num i, Ix i) => IOArray i a -> IO i | |
arraySize array = do | |
(lo, hi) <- getBounds array | |
return (hi-lo+1) | |
------------------------------------------------------------------------ | |
-- Mock | |
type MockOp a = Either Err a | |
data Err = NegativeSize Int | NoSpace | Empty | |
deriving Show | |
instance Exception Err | |
data MQueue a = MQueue | |
{ capacity :: Int | |
, content :: [a] | |
} | |
deriving Functor | |
mNew :: Int -> MockOp (MQueue a) | |
mNew n | n >= 0 = Right (MQueue n []) | |
| otherwise = Left (NegativeSize n) | |
mPut :: a -> MQueue a -> MockOp ((), MQueue a) | |
mPut x q | length (content q) < capacity q = Right ((), q { content = content q ++ [x] }) | |
| otherwise = Left NoSpace | |
mGet :: MQueue a -> MockOp (a, MQueue a) | |
mGet q = case content q of | |
[] -> Left Empty | |
(x : xs) -> Right (x, q { content = xs }) | |
mSize :: MQueue a -> MockOp (Int, MQueue a) | |
mSize q = Right (length (content q), q) | |
------------------------------------------------------------------------ | |
type State = IntMap SomeMQueue | |
type SomeMQueue = MQueue Dynamic | |
initialState :: State | |
initialState = Map.empty | |
lookupQueue :: Typeable a => Var (Queue a) -> State -> MQueue a | |
lookupQueue (Var i) m = fmap (flip fromDyn (error "lookupQueue")) (m Map.! i) | |
-- updateQueue :: Typeable a => Var (Queue a) -> (MQueue a -> MQueue a) -> State -> State | |
-- updateQueue (Var i) f s = | |
-- Map.adjust (fmap toDyn . f . fmap (flip fromDyn (error "updateQueue"))) i s | |
insertQueue :: Typeable a => MQueue a -> State -> (State, Var (Queue a)) | |
insertQueue q s = | |
let | |
i = Map.size s | |
in | |
(Map.insert i (fmap toDyn q) s, Var i) | |
type FakeOp a = State -> Either Err (State, a) | |
fakeOp :: MockOp a -> FakeOp a | |
fakeOp f s = undefined | |
fNew :: Typeable a => Int -> State -> (State, Var (Queue a)) | |
fNew n s = insertQueue (MQueue n []) s | |
fPut :: Typeable a => a -> Var (Queue a) -> State -> (State, ()) | |
fPut x q s = undefined | |
{- | |
m <- readIORef qs | |
let q = lookupMQueue i m | |
(r, q') <- runOp (mPut x q) | |
let m' = insertMQueue i q' m | |
writeIORef qs m' | |
return r | |
-} | |
fGet = undefined | |
------------------------------------------------------------------------ | |
data QueueI q = QueueI | |
{ iNew :: forall a. Typeable a => Int -> IO (q a) | |
, iPut :: forall a. Typeable a => a -> q a -> IO () | |
, iGet :: forall a. Typeable a => q a -> IO a | |
, iSize :: forall a. Typeable a => q a -> IO Int | |
} | |
data Compose g f x = Compose (g (f x)) | |
real :: IO (QueueI Queue) | |
real = return (QueueI new put get size) | |
runOp :: MockOp a -> IO a | |
runOp op = either throwIO return op | |
lookupMQueue :: Typeable a => Int -> IntMap SomeMQueue -> MQueue a | |
lookupMQueue i m = fmap (flip fromDyn (error "lookupMQueue")) (m Map.! i) | |
insertMQueue :: Typeable a => Int -> MQueue a -> IntMap SomeMQueue -> IntMap SomeMQueue | |
insertMQueue i q m = Map.insert i (fmap toDyn q) m | |
fake :: IO (QueueI (Compose Var Queue)) | |
fake = do | |
qs <- newIORef initialState :: IO (IORef State) | |
let | |
fNewIO :: forall a. Typeable a => Int -> IO (Compose Var Queue a) | |
fNewIO n = atomicModifyIORef' qs (fmap Compose . fNew n) | |
fPutIO :: Typeable a => a -> Compose Var Queue a -> IO () | |
fPutIO x (Compose q) = atomicModifyIORef' qs (fPut x q) | |
fGet :: Typeable a => Compose Var Queue a -> IO a | |
fGet (Compose (Var i)) = do | |
m <- readIORef qs | |
let q = lookupMQueue i m | |
(r, q') <- runOp (mGet q) | |
let m' = insertMQueue i q' m | |
writeIORef qs m' | |
return r | |
fSize :: forall a. Typeable a => Compose Var Queue a -> IO Int | |
fSize (Compose (Var i)) = do | |
m <- readIORef qs | |
let q = lookupMQueue i m :: MQueue a | |
(r, q') <- runOp (mSize q) | |
let m' = insertMQueue i (fmap toDyn q') m | |
writeIORef qs m' | |
return r | |
return (QueueI fNewIO fPutIO fGet fSize) | |
prog :: forall q. QueueI q -> IO () | |
prog i = do | |
q <- iNew i 3 | |
iPut i 'a' q | |
iPut i 'b' q | |
a <- iGet i q | |
sz <- iSize i q | |
print (a, sz) | |
q2 <- iNew i 2 | |
iPut i ("foo" :: String) q2 | |
iPut i "bar" q2 | |
foo <- iGet i q2 | |
bar <- iGet i q2 | |
print (foo, bar) | |
test :: IO () | |
test = do | |
real >>= prog | |
fake >>= prog | |
------------------------------------------------------------------------ | |
runFake :: forall a. State -> Command a -> Either Err (State, Either (Var a) a) | |
runFake s (New n) = do | |
mq <- mNew n | |
return (fmap Left (insertQueue mq s)) | |
runFake s (Put x q) = undefined | |
runFake s (Get q) = do | |
let mq :: MQueue a | |
mq = lookupQueue q s | |
(x, _q') <- mGet mq | |
return (s, Right x) | |
runFake s (Size q) = undefined | |
------------------------------------------------------------------------ | |
data Command a where | |
New :: Int -> Command (Queue A) | |
Put :: A -> Var (Queue A) -> Command () | |
Get :: Var (Queue A) -> Command A | |
Size :: Var (Queue A) -> Command Int | |
deriving instance Show (Command a) | |
runCommand :: Env -> Command a -> IO a | |
runCommand _ (New n) = new n | |
runCommand env (Put x ref) = put x (env ref) | |
runCommand env (Get ref) = get (env ref) | |
runCommand env (Size ref) = size (env ref) | |
shrinkCommand :: Command a -> [Command a] | |
shrinkCommand (New n) = map New (shrink n) | |
shrinkCommand (Put x q) = map (flip Put q) (shrink x) | |
shrinkCommand _ = [] | |
pre :: Command a -> State -> Bool | |
pre cmd s = isRight (runFake s cmd) | |
post :: (Eq a, Show a) => Command a -> a -> State -> Property | |
post cmd resp s = case runFake s cmd of | |
Left err -> counterexample (show err) (property False) | |
Right (_s', Right resp') -> resp === resp' | |
Right (_s', Left _) -> property True -- XXX? | |
nextState :: Command a -> Var a -> State -> State | |
nextState cmd q s = case runFake s cmd of | |
Left err -> error ("nextState: " ++ show err) | |
Right (s', _) -> s' | |
genCommand :: State -> Gen (Untyped Command) | |
genCommand s | |
| Map.null s = Untyped <$> (New <$> arbitrary) | |
| otherwise = | |
frequency | |
[ (3, Untyped <$> (Put <$> arbitrary <*> arbitraryQueue s)) | |
, (5, Untyped <$> (Get <$> arbitraryQueue s)) | |
, (1, Untyped <$> (New <$> arbitrary)) | |
] | |
arbitraryQueue :: Typeable a => State -> Gen (Var (Queue a)) | |
arbitraryQueue s = do | |
i <- choose (0, Map.size s - 1) | |
return (Var i) | |
{- | |
pre :: Command a -> State -> Bool | |
pre New{} HaveQueue{} = False | |
pre (New n) NoQueue = n >= 0 | |
pre Put{} HaveQueue{..} = | |
length state_contents < state_capacity | |
pre Get{} HaveQueue{state_contents = []} = False | |
pre _ _ = True | |
post :: Env -> Command a -> a -> State -> Property | |
post _ Get{} x HaveQueue{state_contents = y:_} = | |
counterexample "Wrong value returned from get" $ | |
x === y | |
post _ Size{} n HaveQueue{..} = | |
counterexample "Wrong length returned from size" $ | |
n === length state_contents | |
post _ _ _ _ = property True | |
nextState :: Command a -> Var a -> State -> State | |
nextState (New n) q NoQueue = | |
HaveQueue q n [] | |
nextState (Put x q) _ state@HaveQueue{..} = | |
state{state_contents = state_contents ++ [x]} | |
nextState (Get q) _ state@HaveQueue{state_contents = _:xs} = | |
state{state_contents = xs} | |
nextState (Size _) _ s = s | |
-} | |
prop_ok cmds = runCommands cmds | |
------------------------------------------------------------------------ | |
-- Testing, reusable code. | |
data Var a = Var Int deriving Show | |
type Env = forall a. Typeable a => Var a -> a | |
newtype Commands = Commands [Cmd] deriving Show | |
data Cmd where | |
-- A command together with its result | |
Cmd :: forall a. (Typeable a, Eq a, Show a) => Command a -> Var a -> Cmd | |
deriving instance Show Cmd | |
data Untyped f where | |
Untyped :: (Typeable a, Eq a, Show a) => f a -> Untyped f | |
instance Arbitrary Commands where | |
arbitrary = Commands <$> do | |
n <- sized $ \k -> choose (0, k) | |
let | |
-- First argument: number of commands to generate | |
-- Second argument: next variable index to use | |
loop :: Int -> Int -> State -> Gen [Cmd] | |
loop 0 _ _ = return [] | |
loop n i state = do | |
Untyped cmd <- genCommand state `suchThat` (\(Untyped cmd) -> pre cmd state) | |
let res = Var i | |
next = nextState cmd res state | |
fmap (Cmd cmd res:) (loop (n-1) (i+1) next) | |
loop n 0 initialState | |
shrink (Commands cmds) = | |
map (Commands . prune initialState []) (shrinkList shrinkCmd cmds) | |
where | |
shrinkCmd (Cmd cmd x) = | |
[ Cmd cmd' x | cmd' <- shrinkCommand cmd ] | |
-- Remove any commands which don't satisfy their precondition | |
prune :: State -> [Int] -> [Cmd] -> [Cmd] | |
prune _ _ [] = [] | |
prune state bound (Cmd cmd x@(Var res):cmds) | |
| pre cmd state = | |
Cmd cmd x: | |
prune (nextState cmd x state) (res:bound) cmds | |
| otherwise = prune state bound cmds | |
runCommands :: Commands -> Property | |
runCommands (Commands cmds) = runCmds initialState [] cmds | |
where | |
runCmds :: State -> [(Int, Dynamic)] -> [Cmd] -> Property | |
runCmds _ _ [] = property True | |
runCmds state vals (Cmd cmd var@(Var x):cmds) = | |
counterexample (show cmd) $ ioProperty $ do | |
res <- runCommand (sub vals) cmd | |
return $ | |
post cmd res state .&&. | |
let next = nextState cmd var state in | |
runCmds next ((x, toDyn res):vals) cmds | |
sub :: Typeable a => [(Int, Dynamic)] -> Var a -> a | |
sub vals (Var x) = | |
case lookup x vals of | |
Nothing -> discard | |
-- ^ this can happen if a shrink step makes a variable unbound | |
Just val -> | |
case fromDynamic val of | |
Nothing -> error $ "variable " ++ show x ++ " has wrong type" | |
Just val -> val |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment