-- | 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) -- We import the first naive implementation as the specification. import qualified Parser1 as Spec (P, symbol, (+++), pfail, parse) {- -- This instance is included in QuickCheck-2.1.0.2 but not earlier instance Applicative Gen where pure = return (<*>) = ap -} -- | 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 Parser = Plus Parser Parser | Fail | Return Bool | Symbol | Bind Parser Fun -- | Instead of arbitrary functions (which quickCheck can handle) we -- build a datatype modelling a few interesting functions. data Fun = K Parser -- \_ -> p | If Parser Parser -- \x -> if x then p1 else p2 -- Applying a function to an argument. apply :: Fun -> Bool -> Parser 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 Parser 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 Fun 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. genParser :: Int -> Gen Parser genParser 0 = oneof [ return Fail , Return <$> arbitrary , return Symbol ] genParser n = frequency $ [ (1, genParser 0) , (3, Plus <$> gen2 <*> gen2) , (5, Bind <$> gen2 <*> genFun2) ] where gen2 = genParser (n `div` 2) genFun2 = genFun (n `div` 2) -- | Generating arbitrary functions. genFun :: Int -> Gen Fun genFun n = oneof $ [ K <$> genParser n , If <$> gen2 <*> gen2 ] where gen2 = genParser (div n 2) instance Arbitrary Parser where arbitrary = sized genParser -- 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 Fun 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 :: Parser -> 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 :: Parser -> 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 :: Fun -> (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 shrink $ \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 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 = lhs `bagEq` rhs where lhs = parse (compile p) s rhs = Spec.parse (spec p) s ---------------- main = do 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 :: Parser -> [[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 Parser) return $ zip ps (map language ps)