ivory-quickcheck-0.2.0.3: src/Ivory/QuickCheck.hs
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ImplicitParams #-}
{-# LANGUAGE CPP #-}
{-# OPTIONS_GHC -fno-warn-name-shadowing #-}
-- | Check properties of Ivory programs using random inputs.
--
-- Example usage:
--
-- > [ivory|
-- > struct foo
-- > { foo_a :: Stored IFloat
-- > ; foo_b :: Stored Uint8
-- > }
-- > |]
-- >
-- > -- Function we want to generate inputs for.
-- > func :: Def ('[ Uint8
-- > , Ref s (Array 3 (Stored Uint8))
-- > , Ref s (Struct "foo")
-- > ] :-> ())
-- > func = proc "func" $ \u arr str ->
-- > ensures_ (checkStored (arr ! 0) (\r -> r >? u)) $
-- > body $
-- > arrayMap $ \ix -> do
-- > a <- deref (arr ! ix)
-- > b <- deref (str ~> foo_b)
-- > store (arr ! ix) (a + b + u)
-- >
-- > -- Module containing our function
-- > cmodule :: Module
-- > cmodule = package "module" $ do
-- > defStruct (Proxy :: Proxy "foo")
-- > incl func
-- >
-- > -- Running @mkTest@ will produce a C program in @<pwd>/test@ that will check
-- > -- @func@'s contract on 10 random inputs.
-- > mkTest :: IO ()
-- > mkTest = check 10 [] cmodule (contract func)
module Ivory.QuickCheck (check, checkWith, contract) where
import Prelude ()
import Prelude.Compat
import Control.Monad (replicateM,forM)
import Data.IORef (IORef,newIORef,readIORef,writeIORef)
import Data.List (transpose,find)
import Ivory.Compile.C.CmdlineFrontend
import qualified Ivory.Eval as E
import Ivory.Language
import Ivory.Language.Proc
import qualified Ivory.Language.Syntax as I
import System.IO.Unsafe (unsafeInterleaveIO)
import qualified Test.QuickCheck.Arbitrary as A
import qualified Test.QuickCheck.Gen as G
import Data.Int
import Data.Word
-- XXX: DEBUG
-- import Debug.Trace
-- | Generate a random C program to check that the property holds. The
-- generated program will be placed in the @test@ subdirectory.
checkWith :: Int -- ^ The number of inputs to generate.
-> Opts -- ^ Options to pass to the Ivory compiler.
-> [Module] -- ^ Modules we need to have in scope.
-> Module -- ^ The defining module.
-> Def (args ':-> IBool) -- ^ The property to check.
-> IO ()
checkWith n opts deps m prop@(DefProc p) = do
inputs <- sampleProc m p n
let main = DefProc I.Proc { I.procSym = "main"
, I.procRetTy = I.TyInt I.Int32
, I.procArgs = []
, I.procBody = concat inputs ++ [I.Return $ I.Typed (I.TyInt I.Int32) (I.ExpLit (I.LitInteger 0))]
, I.procRequires = []
, I.procEnsures = []
}
let test = package (I.modName m ++ "__test") $ do
depend m
incl prop
incl main
runCompiler ([m, test]++deps) [] opts -- initialOpts { outDir = Just "test" }
checkWith _ _ _ _ _ = error "I can only check normal Ivory procs!"
-- | Generate a random C program to check that the property holds. The
-- generated program will be placed in the @test@ subdirectory.
check :: Int -- ^ The number of inputs to generate.
-> [Module] -- ^ Modules we need to have in scope.
-> Module -- ^ The defining module.
-> Def (args ':-> IBool) -- ^ The property to check.
-> IO ()
check n deps m p = checkWith n (initialOpts { outDir = Just "test"}) deps m p
-- | Make a @check@able property from an arbitrary Ivory procedure. The
-- property will simply check that the contracts are satisfied.
contract :: Def (args ':-> res) -> Def (args ':-> IBool)
contract (DefProc (I.Proc {..}))
= DefProc I.Proc
{ I.procSym = procSym ++ "__contract_check"
, I.procRetTy = I.TyBool
, I.procArgs = procArgs
, I.procBody = [ I.Call procRetTy Nothing (I.NameSym procSym)
[ I.Typed t (I.ExpVar v) | I.Typed t v <- procArgs ]
, I.Return (I.Typed I.TyBool (I.ExpLit (I.LitBool True)))
]
, I.procRequires = procRequires
, I.procEnsures = []
}
contract _ = error "I can only check contracts of normal Ivory procs!"
mkUnique :: (?counter :: IORef Integer) => IO Integer
mkUnique = do
i <- readIORef ?counter
writeIORef ?counter (i+1)
return i
sampleProc :: Module -> I.Proc -> Int -> IO [I.Block]
sampleProc m@(I.Module {..}) p@(I.Proc {..}) n
= do c <- newIORef 0
let ?counter = c
allInits <- fmap transpose $ forM procArgs $ \ (I.Typed t _) ->
sampleType m t
allAreas <- fmap transpose $ forM (getVisible modAreas) $ \ I.Area {..} -> do
inits <- sampleType m areaType
lazyMapIO (\ (var, blck) -> do
let store = case areaType of
I.TyArr _ _ -> I.RefCopy
I.TyStruct _ -> I.RefCopy
I.TyRef _ -> I.RefCopy -- XXX: this shouldn't actually appear
_ -> I.Store
return $ blck ++ [ store areaType (I.ExpAddrOfGlobal areaSym) (I.ExpVar var)])
inits
--XXX: Refactor!
let validInits =
[ (inits, areas) | (inits, areas) <- zipLonger allInits allAreas
, let (vars, blcks) = unzip inits
, let asgnv = [ I.Assign ty arg (I.ExpVar var)
| (I.Typed ty arg, var) <- zip procArgs vars
]
, E.runEval (E.openModule m (do
E.evalBlock (concat blcks ++ asgnv ++ concat areas)
E.evalRequires procRequires))
== Right True
]
forM (take n validInits) $ \ (args, areas) -> do
let (vars, inits) = unzip args
chk <- mkCheck p vars
return (concat inits ++ concat areas ++ chk)
getVisible :: I.Visible a -> [a]
getVisible xs = I.public xs ++ I.private xs
mkCheck :: (?counter :: IORef Integer)
=> I.Proc -> [I.Var] -> IO I.Block
mkCheck (I.Proc {..}) args = do
n <- mkUnique
let b = mkVar n
let c = [ I.Call I.TyBool (Just b) (I.NameSym procSym)
[ I.Typed t (I.ExpVar v) | (I.Typed t _, v) <- zip procArgs args ]
, I.Assert (I.ExpVar b) ]
return c
sampleType :: (?counter :: IORef Integer)
=> Module -> I.Type -> IO [(I.Var, I.Block)]
sampleType m t = case t of
I.TyInt sz
-> lazyMapIO (mkLocal t) =<< sampleInt sz
I.TyWord sz
-> lazyMapIO (mkLocal t) =<< sampleWord sz
I.TyIndex i
-> lazyMapIO (mkLocal t) =<< sampleIndex i
I.TyBool
-> lazyMapIO (mkLocal t) =<< sampleBool
I.TyChar
-> lazyMapIO (mkLocal t) =<< sampleChar
I.TyFloat
-> lazyMapIO (mkLocal t) =<< sampleFloat
I.TyDouble
-> lazyMapIO (mkLocal t) =<< sampleDouble
I.TyRef ty
-> lazyMapIO (mkRef ty) =<< sampleType m ty
I.TyConstRef ty
-> lazyMapIO (mkRef ty) =<< sampleType m ty
I.TyArr len ty
-> sampleArray m len ty
I.TyStruct ty
-> sampleStruct m ty
I.TyProc _ _ -> err
I.TyVoid -> err
I.TyPtr _ -> err
I.TyCArray _ -> err
I.TyOpaque -> err
where
err = error $ "I don't know how to make values of type '" ++ show t ++ "'!"
mkLocal :: (?counter :: IORef Integer) => I.Type -> I.Init -> IO (I.Var, I.Block)
mkLocal ty init = do
n <- mkUnique
let v = mkVar n
return (v, [I.Local ty v init])
mkRef :: (?counter :: IORef Integer) => I.Type -> (I.Var, I.Block)
-> IO (I.Var, I.Block)
mkRef ty (v, init) = do
n <- mkUnique
let r = I.VarName ("ref" ++ show n)
return (r, init ++ [ I.AllocRef ty r (I.NameVar v) ])
mkVar :: Integer -> I.Var
mkVar n = I.VarName ("var" ++ show n)
mk :: G.Gen a -> IO [a]
mk g = G.generate (G.infiniteListOf g)
sampleInt :: I.IntSize -> IO [I.Init]
sampleInt sz = do
xs <- gen
return [ I.InitExpr (I.TyInt sz) (I.ExpLit (I.LitInteger x)) | x <- xs ]
where
gen = case sz of
I.Int8 -> fmap fromIntegral <$> mk (A.arbitrary :: G.Gen Int8)
I.Int16 -> fmap fromIntegral <$> mk (A.arbitrary :: G.Gen Int16)
I.Int32 -> fmap fromIntegral <$> mk (A.arbitrary :: G.Gen Int32)
I.Int64 -> fmap fromIntegral <$> mk (A.arbitrary :: G.Gen Int64)
sampleWord :: I.WordSize -> IO [I.Init]
sampleWord sz = do
xs <- gen
return [ I.InitExpr (I.TyWord sz) (I.ExpLit (I.LitInteger x))
| x <- xs
]
where
gen = case sz of
I.Word8 -> fmap fromIntegral <$> mk (A.arbitrary :: G.Gen Word8)
I.Word16 -> fmap fromIntegral <$> mk (A.arbitrary :: G.Gen Word16)
I.Word32 -> fmap fromIntegral <$> mk (A.arbitrary :: G.Gen Word32)
I.Word64 -> fmap fromIntegral <$> mk (A.arbitrary :: G.Gen Word64)
sampleIndex :: Integer -> IO [I.Init]
sampleIndex ix = do
xs <- mk (A.arbitrary :: G.Gen Word64)
return [ I.InitExpr (I.TyIndex ix)
(I.ExpLit (I.LitInteger (fromIntegral x `mod` ix)))
| x <- xs
]
sampleBool :: IO [I.Init]
sampleBool = do
bs <- mk A.arbitrary
return [I.InitExpr I.TyBool (I.ExpLit (I.LitBool b)) | b <- bs]
sampleChar :: IO [I.Init]
sampleChar = do
cs <- mk A.arbitrary
return [I.InitExpr I.TyChar (I.ExpLit (I.LitChar c)) | c <- cs]
sampleFloat :: IO [I.Init]
sampleFloat = do
cs <- mk A.arbitrary
return [I.InitExpr I.TyFloat (I.ExpLit (I.LitFloat c)) | c <- cs]
sampleDouble :: IO [I.Init]
sampleDouble = do
cs <- mk A.arbitrary
return [I.InitExpr I.TyDouble (I.ExpLit (I.LitDouble c)) | c <- cs]
sampleStruct :: (?counter :: IORef Integer)
=> I.Module -> String -> IO [(I.Var, I.Block)]
sampleStruct m@(I.Module {..}) ty
= case find (\s -> ty == I.structName s) structs of
Just (I.Struct _ fields) -> repeatIO $ do
(vars, blcks) <- fmap unzip $ forM fields $ \ (I.Typed t _) ->
head <$> sampleType m t
let init = zipWith (\ (I.Typed t f) v -> (f, I.InitExpr t (I.ExpVar v)))
fields vars
(v, blck) <- mkLocal (I.TyStruct ty) (I.InitStruct init)
return (v, concat blcks ++ blck)
_ -> error ("I don't know how to construct a '" ++ ty ++ "'!")
where
structs = I.public modStructs ++ I.private modStructs
sampleArray :: (?counter :: IORef Integer)
=> I.Module -> Int -> I.Type
-> IO [(I.Var, I.Block)]
sampleArray m len ty = repeatIO $ do
(vars, blcks) <- unzip <$> replicateM len (head <$> sampleType m ty)
let init = [ I.InitExpr ty (I.ExpVar v) | v <- vars ]
(v, blck) <- mkLocal (I.TyArr len ty) (I.InitArray init)
return (v, concat blcks ++ blck)
repeatIO :: IO a -> IO [a]
repeatIO doThis = do
x <- doThis
xs <- unsafeInterleaveIO (repeatIO doThis)
return (x : xs)
lazyMapIO :: (a -> IO b) -> [a] -> IO [b]
lazyMapIO _ [] = return []
lazyMapIO f (a:as) = do
b <- f a
bs <- unsafeInterleaveIO (lazyMapIO f as)
return (b : bs)
zipLonger :: [[a]] -> [[b]] -> [([a], [b])]
zipLonger as bs = zip (as ++ repeat []) (bs ++ repeat [])