-- | Testing the parser library from lecture 4. module Main where import Control.Applicative((<$>), (<*>)) import Control.Arrow(second) import Data.List(sort) import Test.QuickCheck -- import Parsers (P, symbol, (+++), pfail, parse) import PolyParser (P, symbol, (+++), pfail, parse) -- We import the first naive implementation as the specification. import qualified Parser1 as Spec (P, symbol, (+++), pfail, parse) -- | We want to generate and show arbitrary parsers. To do this -- we restrict ourselves to parsers of type P Bool Bool and build -- a datatype to model these. data ParsBB = Plus ParsBB ParsBB | Fail | Return Bool | Symbol | Bind ParsBB B2ParsBB -- | Instead of arbitrary functions (which quickCheck can handle) we -- build a datatype modelling a few interesting functions. data B2ParsBB = K ParsBB -- \_ -> p | If ParsBB ParsBB -- \x -> if x then p1 else p2 -- Applying a function to an argument. apply :: B2ParsBB -> Bool -> ParsBB apply (K p) _ = p apply (If p1 p2) x = if x then p1 else p2 -- | We can show elements in our model, but not the parsers from the -- implementation. instance Show ParsBB where showsPrec n p = case p of Fail -> showString "pfail" Symbol -> showString "symbol" Return x -> showParen (n > 2) $ showString "return " . shows x Plus p q -> showParen (n > 0) $ showsPrec 1 p . showString " +++ " . showsPrec 1 q Bind p f -> showParen (n > 1) $ showsPrec 2 p . showString " >>= " . shows f -- and we can show our functions. That would have been harder if -- we had used real functions. instance Show B2ParsBB where show (K p) = "\\_ -> " ++ show p show (If p1 p2) = "\\x -> if x then " ++ show p1 ++ " else " ++ show p2 -- | Generating an arbitrary parser. Parameterised by a size argument -- to ensure that we don't generate infinite parsers. genParsBB :: Int -> Gen ParsBB genParsBB 0 = oneof [ return Fail , Return <$> arbitrary , return Symbol ] genParsBB n = frequency $ [ (1, genParsBB 0) , (3, Plus <$> gen2 <*> gen2) , (5, Bind <$> gen2 <*> genFun2) ] where gen2 = genParsBB (n `div` 2) genFun2 = genFun (n `div` 2) -- | Generating arbitrary functions. genFun :: Int -> Gen B2ParsBB genFun n = oneof $ [ K <$> genParsBB n , If <$> gen2 <*> gen2 ] where gen2 = genParsBB (n `div` 2) instance Arbitrary ParsBB where arbitrary = sized genParsBB -- Shrinking is used to get minimal counter examples and is very -- handy. The shrink function returns a list of things that are -- smaller (in some way) than the argument. shrink (Plus p1 p2) = p1 : p2 : [ Plus p1' p2 | p1' <- shrink p1 ] ++ [ Plus p1 p2' | p2' <- shrink p2 ] shrink Fail = [ Return False ] shrink (Return x) = [] shrink Symbol = [ Return False ] shrink (Bind p k) = p : apply k False : apply k True : [ Bind p' k | p' <- shrink p ] ++ [ Bind p k' | k' <- shrink k ] instance Arbitrary B2ParsBB where arbitrary = sized genFun shrink (K p) = [ K p | p <- shrink p ] shrink (If p1 p2) = K p1 : K p2 : [ If p1 p2 | p1 <- shrink p1 ] ++ [ If p1 p2 | p2 <- shrink p2 ] -- | We can turn a parser in our model into its specification... spec :: ParsBB -> Spec.P Bool Bool spec Symbol = Spec.symbol spec (Return x) = return x spec (Plus p1 p2) = spec p1 Spec.+++ spec p2 spec Fail = Spec.pfail spec (Bind p k) = spec p >>= \x -> spec (apply k x) -- | ... or we can compile to a parser from the implementation we're -- testing. compile :: ParsBB -> P Bool Bool compile Symbol = symbol compile (Return x) = return x compile (Plus p1 p2) = compile p1 +++ compile p2 compile Fail = pfail compile (Bind p k) = compile p >>= compileFun k compileFun :: B2ParsBB -> (Bool -> P Bool Bool) compileFun k = \x -> compile (apply k x) -- Tests infix 0 =~= -- | When are two parsers equal? Remember that we don't care -- about the order of results so we sort the result lists -- before comparing. -- (=~=) :: P Bool Bool -> P Bool Bool -> Property p =~= q = -- forAllShrink arbitrary shrinkNothing $ \s -> parse p s `bagEq` parse q s bagEq :: Ord a => [a] -> [a] -> Bool bagEq xs ys = sort xs == sort ys -- We can turn all the laws we had into properties. -- Exercise: check all the laws L1 .. L10. law1' x f = return x >>= f =~= f x law1 x f0 = return x >>= f =~= f x where f = compileFun f0 law2 p0 = p >>= return =~= p where p = compile p0 law3 p0 f0 g0 = (p >>= f) >>= g =~= p >>= (\x -> f x >>= g) where p = compile p0 f = compileFun f0 g = compileFun g0 law5 p0 q0 f0 = (p +++ q) >>= f =~= (p >>= f) +++ (q >>= f) where p = compile p0 q = compile q0 f = compileFun f0 law9 p0 q0 = p +++ q =~= q +++ p where p = compile p0 q = compile q0 -- | We can also check that the implementation behaves as the -- specification. prop_spec p s = whenFail debug $ lhs `bagEq` rhs where lhs = parse (compile p) s rhs = Spec.parse (spec p) s debug = do putStrLn ("parse (compile p) s = " ++ show lhs) putStrLn ("Spec.parse (spec p) s = " ++ show rhs) ---------------- main = do quickCheck law3 quickCheck prop_spec quickCheck law1 quickCheck law2 quickCheck law5 quickCheck law9 ---------------- -- Some unfinished experiments below language' :: (Bounded s, Enum s) => P s a -> [[s]] language' p = filter (parseOK p) allLists language :: ParsBB -> [[Bool]] language = language' . compile parseOK :: P s a -> [s] -> Bool parseOK p = not . null . parse p allLists :: (Bounded a, Enum a) => [[a]] allLists = [] : [ x : xs | xs <- allLists, x <- allA ] where allA = [minBound .. maxBound] test = do ps <- sample' (arbitrary :: Gen ParsBB) return $ zip ps (map language ps)