picologic 0.1.1 → 0.1.2
raw patch · 8 files changed
+534/−61 lines, 8 filesdep +QuickCheckdep +picologicdep ~basedep ~containersdep ~mtl
Dependencies added: QuickCheck, picologic
Dependency ranges changed: base, containers, mtl, parsec, picosat, pretty
Files
- picologic.cabal +35/−9
- src/Picologic/AST.hs +10/−8
- src/Picologic/Lexer.hs +74/−0
- src/Picologic/Pretty.hs +73/−0
- src/Picologic/Solver.hs +58/−44
- src/Picologic/Tseitin.hs +182/−0
- tests/PrettyPrintTest.hs +17/−0
- tests/tests.hs +85/−0
picologic.cabal view
@@ -1,5 +1,5 @@ name: picologic-version: 0.1.1+version: 0.1.2 synopsis: Utilities for symbolic predicate logic expressions homepage: https://github.com/sdiehl/picologic license: MIT@@ -28,16 +28,20 @@ Picologic, Picologic.Solver, Picologic.AST,- Picologic.Parser+ Picologic.Parser,+ Picologic.Pretty,+ Picologic.Tseitin+ other-modules:+ Picologic.Lexer hs-source-dirs: src other-extensions: DeriveDataTypeable, BangPatterns build-depends: - base >= 4.6 && <4.8,- picosat >= 0.1 && <0.2,- containers >= 0.5 && <0.6,- mtl >= 2.1 && <2.2,- pretty >= 1.1 && <1.2,- parsec >= 3.1 && <3.2+ base >= 2 && <5,+ picosat,+ containers,+ mtl,+ pretty,+ parsec default-language: Haskell2010 executable picologic@@ -47,7 +51,7 @@ other-modules: Picologic.Repl other-extensions: DeriveDataTypeable, BangPatterns build-depends: - base >= 4.6 && <4.8,+ base >= 2 && <5, picosat >= 0.1 && <0.2, containers >= 0.5 && <0.6, mtl >= 2.1 && <2.2,@@ -58,3 +62,25 @@ default-language: Haskell2010 else buildable: False++--Todo: how to fail 'cabal test' on a QuickCheck error?+Test-Suite picologic-quickcheck+ type: exitcode-stdio-1.0+ main-is: tests.hs+ hs-source-dirs: tests+ build-depends: + base >= 2 && <5,+ QuickCheck,+ containers,+ picosat,+ mtl,+ pretty,+ picologic++Test-Suite pretty-print-test+ type: exitcode-stdio-1.0+ main-is: PrettyPrintTest.hs+ hs-source-dirs: tests+ build-depends: + base >= 2 && <5,+ picologic
src/Picologic/AST.hs view
@@ -34,7 +34,7 @@ | Disj Expr Expr -- ^ Logical disjunction | Iff Expr Expr -- ^ Logical biconditional | Implies Expr Expr -- ^ Material implication- deriving (Eq, Ord, Show, Data, Typeable)+ deriving (Eq, Ord, Data, Typeable) -- | Evaluate expression. eval :: Ctx -> Expr -> Bool@@ -57,14 +57,15 @@ go (Implies e1 e2) !vs = go e1 vs ++ go e2 vs -- | Negation normal form.+-- (May result in exponential growth) nnf :: Expr -> Expr nnf ex = case ex of e@(Var _) -> e e@(Neg (Var _)) -> e- Neg (Neg e) -> e+ Neg (Neg e) -> nnf e Conj e1 e2 -> nnf e1 `Conj` nnf e2- Neg (Conj e1 e2) -> nnf $ Neg e1 `Conj` Neg e2+ Neg (Conj e1 e2) -> nnf $ Neg e1 `Disj` Neg e2 Disj e1 e2 -> nnf e1 `Disj` nnf e2 Neg (Disj e1 e2) -> nnf $ Neg e1 `Conj` Neg e2@@ -72,15 +73,16 @@ Implies e1 e2 -> nnf $ Neg e1 `Disj` e2 Neg (Implies e1 e2) -> nnf $ e1 `Conj` Neg e2 - Iff e1 e2 -> let a = e1 `Conj` e2- b = Neg e1 `Conj` Neg e2- in nnf $ a `Disj` b+ Iff e1 e2 -> let a = e1 `Disj` Neg e2+ b = Neg e1 `Disj` e2+ in nnf $ a `Conj` b Neg (Iff e1 e2) -> let a = e1 `Disj` e2 b = Neg e1 `Disj` Neg e2 in nnf $ a `Conj` b -- | Conjunctive normal form.+-- (May result in exponential growth) cnf :: Expr -> Expr cnf = simp . cnf' . nnf where@@ -97,8 +99,8 @@ -- | Remove tautologies. simp :: Expr -> Expr simp ex = case ex of- Disj e1 (Neg e2) | e1 == e2 -> e1- Disj (Neg e1) e2 | e1 == e2 -> e1+ -- Disj e1 (Neg e2) | e1 == e2 -> True+ -- Disj (Neg e1) e2 | e1 == e2 -> True Disj e1 e2 -> Disj (simp e1) (simp e2) Conj e1 e2 | e1 == e2 -> e1 | otherwise -> Conj (simp e1) (simp e2)
+ src/Picologic/Lexer.hs view
@@ -0,0 +1,74 @@+module Picologic.Lexer (+ Parser,+ Op,+ contents,+ parens,+ reservedOp,+ reserved,+ identifier,+) where++import Control.Monad.Identity++import Text.Parsec+import Text.Parsec.Language (haskellStyle)+import qualified Text.Parsec.Token as Tok+import qualified Text.Parsec.Expr as Ex++type Parser = ParsecT String () Identity+type Lexer = Tok.GenTokenParser String () Identity+type Language = Tok.GenLanguageDef String () Identity+type Op = Ex.Operator String () Identity++-------------------------------------------------------------------------------+-- Lexer+-------------------------------------------------------------------------------++reservedOps :: [String]+reservedOps = [+ "->",+ "&",+ "|",+ "<->",+ "~"+ ]++reservedNames :: [String]+reservedNames = []++lexerStyle :: Language+lexerStyle = haskellStyle+ { Tok.commentStart = "{-"+ , Tok.commentEnd = "-}"+ , Tok.commentLine = "--"+ , Tok.nestedComments = True+ , Tok.identStart = letter+ , Tok.identLetter = alphaNum+ , Tok.opStart = Tok.opLetter lexerStyle+ , Tok.opLetter = oneOf "`~!@$%^&*-+=;:<>./?#"+ , Tok.reservedOpNames = reservedOps+ , Tok.reservedNames = reservedNames+ , Tok.caseSensitive = True+ }++lexer :: Lexer+lexer = Tok.makeTokenParser lexerStyle++reservedOp :: String -> Parser ()+reservedOp = Tok.reservedOp lexer++reserved :: String -> Parser ()+reserved = Tok.reserved lexer++identifier :: Parser String+identifier = Tok.identifier lexer++parens :: Parser a -> Parser a+parens = Tok.parens lexer++contents :: Parser a -> Parser a+contents p = do+ Tok.whiteSpace lexer+ r <- p+ eof+ return r
+ src/Picologic/Pretty.hs view
@@ -0,0 +1,73 @@+module Picologic.Pretty (+ ppExprU,+ ppExprA,+ ppExprLisp,+ ppSolutions,+) where++import Picologic.AST (Expr(..), Ident(..), Solutions(..))+import Text.PrettyPrint+import Data.List (intersperse)++-- | Pretty print with unicode symbols.+ppExprU :: Expr -> Doc+ppExprU ex = case ex of+ Var (Ident n) -> text n+ Neg expr -> char '¬' <> ppExprU expr+ Conj a b -> con '∧' a b+ Disj a b -> con '∨' a b+ Implies a b -> con '→' a b+ Iff a b -> con '↔' a b+ where con c a b =+ parens $ sep [ppExprU a, char c <+> ppExprU b]++-- | Pretty print with ascii symbols.+ppExprA :: Expr -> Doc+ppExprA ex = case ex of+ Var (Ident n) -> text n+ Neg expr -> char '~' <> ppExprA expr+ Conj e1 e2 -> parens $ ppExprA e1 <+> char '&' <+> ppExprA e2+ Disj e1 e2 -> parens $ ppExprA e1 <+> char '|' <+> ppExprA e2+ Implies e1 e2 -> parens $ ppExprA e1 <+> text "->" <+> ppExprA e2+ Iff e1 e2 -> parens $ ppExprA e1 <+> text "<->" <+> ppExprA e2++-- | Pretty print into S-Expressions+ppExprLisp :: Expr -> Doc+ppExprLisp ex = case ex of+ Var (Ident n) -> text n+ Conj a b -> con "and" $ ands [a, b]+ Disj a b -> con "or" $ ors [a, b]+ Implies a b -> con "==>" [a, b]+ Iff a b -> con "==" $ iffs [a, b]+ Neg (Var (Ident n)) -> text $ "-" ++ n+ Neg (Conj a b) -> con "nand" $ ands [a, b]+ Neg (Disj a b) -> con "nor" $ ors [a, b]+ Neg (Iff a b) -> con "xor" $ iffs [a, b]+ Neg expr -> parens $ text "not" <+> ppExprLisp expr+ where con c xs =+ parens $+ sep [text c,+ nest 1 $ sep $ map ppExprLisp xs]++ands [] = []+ands (Conj a b : xs) = ands [a] ++ ands [b] ++ ands xs+ands (x:xs) = x : ands xs++ors [] = []+ors (Disj a b : xs) = ors [a] ++ ors [b] ++ ors xs+ors (x:xs) = x : ors xs++iffs [] = []+iffs (Iff a b : xs) = iffs [a] ++ iffs [b] ++ iffs xs+iffs (x:xs) = x : iffs xs++instance Show Expr where+ show = show . ppExprLisp+++ppSolutions :: Solutions -> String+ppSolutions (Solutions xs) =+ concat (concat $ intersperse ["\n"] (fmap showExprs xs))++showExprs :: [Expr] -> [String]+showExprs xs = intersperse " " $ fmap (render . ppExprU) xs
src/Picologic/Solver.hs view
@@ -1,38 +1,81 @@ module Picologic.Solver ( solveProp,- clausesExpr,+ solveCNF,+ solveOneCNF,+ clausesExpr ) where import Picologic.AST+import Picologic.Pretty import Picosat import Data.List import qualified Data.Map as M import Control.Monad.Writer -data Clause- = CV Int -- ^Clause variable ( a or -a )- | CL [Clause] -- ^Set of clause under disjuntion+-- | Yield the solutions for an expression using the PicoSAT solver.+solveProp :: Expr -> IO Solutions+solveProp p = solveCNF $ cnf p -instance Show Clause where- show (CV i) = show i- show (CL xs) = concat (intersperse " " (fmap show xs))+-- | Yield the solutions for an expression using the PicoSAT+-- solver. The Expression must be in CNF form already.+solveCNF :: Expr -> IO Solutions+solveCNF p = do+ solutions <- solveAll ds+ return $ Solutions $ fmap (backSubst vs') solutions+ where+ cs = clausesFromCNF p+ ds = cnfToDimacs vs cs+ vs = M.fromList $ zip vars [1..]+ vs' = M.fromList $ zip [1..] vars+ vars = variables p --- | Yield the soutions for an expressions using the PicosSAT solver.-solveProp :: Expr -> IO Solutions-solveProp p = solveAll cs >>= return . Solutions . fmap (backSubst vs')+-- | Yield one single solution for an expression using the PicoSAT+-- solver. The Expression must be in CNF form already.+solveOneCNF :: Expr -> IO [Expr]+solveOneCNF p = do+ solution <- solve ds+ return $ backSubst vs' solution where- cs = filter (not . null) $ fmap toInts $ execWriter $ clauses vs (cnf p)+ cs = clausesFromCNF p+ ds = cnfToDimacs vs cs vs = M.fromList $ zip vars [1..] vs' = M.fromList $ zip [1..] vars- vars = variables (cnf p)+ vars = variables p +clausesFromCNF :: Expr -> [[Expr]]+clausesFromCNF p =+ [ [ case lit of+ v@(Var name) -> v+ v@(Neg (Var name)) -> v+ x -> error $ "input not in CNF: \n" ++ show p+ | lit <- ors [clause] ]+ | clause <- ands [p]]++ands :: [Expr] -> [Expr]+ands [] = []+ands (Conj a b : xs) = ands [a] ++ ands [b] ++ ands xs+ands (x:xs) = x : ands xs++ors :: [Expr] -> [Expr]+ors [] = []+ors (Disj a b : xs) = ors [a] ++ ors [b] ++ ors xs+ors (x:xs) = x : ors xs++cnfToDimacs :: M.Map Ident Int -> [[Expr]] -> [[Int]]+cnfToDimacs vs cs = map (map encode) cs+ where encode (Var ident) = vs M.! ident+ encode (Neg (Var ident)) = negate $ vs M.! ident+ + -- | Yield the integer clauses given to the SAT solver. clausesExpr :: Expr -> [[Int]]-clausesExpr p = filter (not . null) $ fmap toInts $ execWriter $ clauses vs (cnf p)+clausesExpr p = ds where- vs = M.fromList $ zip vars [1..]- vars = variables (cnf p)+ cs = clausesFromCNF p+ vs = M.fromList $ zip vars [1..]+ vars = variables p+ ds = cnfToDimacs vs cs backSubst :: M.Map Int Ident -> Solution -> [Expr] backSubst env (Solution xs) = fmap go xs@@ -42,32 +85,3 @@ backSubst _ Unsatisfiable = [] backSubst _ Unknown = [] -toInts :: Clause -> [Int]-toInts (CL xs) = fmap (\(CV n) -> n) xs-toInts (CV x) = [x]--neg :: Clause -> Clause-neg (CV n) = CV (-n)-neg (CL xs) = CL (fmap neg xs)--combine :: Clause -> Clause -> Clause-combine (CL x) (CL y) = CL (x++y)-combine (CL x) y = combine (CL x) (CL [y])-combine x (CL y) = combine (CL [x]) (CL y)-combine x y = CL [x, y]--clauses :: M.Map Ident Int -> Expr -> Writer [Clause] Clause-clauses env ex = case ex of- Var v -> return $ CV (env M.! v)- Neg x -> do- cs <- clauses env x- return (neg cs)- Conj e1 e2 -> do- cs1 <- clauses env e1- cs2 <- clauses env e2- tell [cs1, cs2]- return (CL [])- Disj e1 e2 -> do- cs1 <- clauses env e1- cs2 <- clauses env e2- return (combine cs1 cs2)
+ src/Picologic/Tseitin.hs view
@@ -0,0 +1,182 @@+module Picologic.Tseitin+ (tseitinCNF,+ dropTseitinVarsInSolutions,+ dropTseitinVars,+ + ) where++-- TODO: How efficient is the `mappend` used by Writer?+-- TODO: For cases like (Conj (Conj a b) c) the introduction+-- of one Tseitin var can be enough.+-- TODO: The outermost (Conj (Conj ...) ...) needn't be+-- Tseitin encoded at all.+-- Also, the (Disj (Disj ...) ...) below, needn't be encoded+-- when they use only variables or negated variables.+-- Tseitin transformation can be used specifically there to+-- turn other expressions into variables.++import Prelude hiding (or, and)++import Picologic.AST+import Control.Monad.State.Strict+import Control.Monad.Writer.Strict++type TS a = StateT Int (Writer [Expr]) a++evalTS :: TS a -> (a, [Expr])+evalTS action = + runWriter (evalStateT action 1)++var :: TS Expr+var = do+ n <- get+ put $ succ n+ return $ Var $ Ident $ "ts*" ++ show n++or xs = foldl1 Disj xs+and xs = foldl1 Conj xs++tseitinCNF :: Expr -> Expr+tseitinCNF e =+ let (var, clauses) = evalTS $ tseitin $ simplify e+ in and (var : clauses)++neg (Neg x) = x+neg x = Neg x++tseitin :: Expr -> TS Expr++tseitin lit@(Var _) = return lit++tseitin lit@(Neg (Var _)) = return lit++tseitin (Conj x y) = do+ a <- tseitin x+ b <- tseitin y+ c <- var+ tell [or [neg a, neg b, c],+ or [a, neg c],+ or [b, neg c]]+ return c++tseitin (Neg (Conj x y)) = do+ a <- tseitin x+ b <- tseitin y+ c <- var+ tell [or [neg a, neg b, neg c],+ or [a, c],+ or [b, c]]+ return c++tseitin (Disj x y) = do+ a <- tseitin x+ b <- tseitin y+ c <- var+ tell [or [a, b, neg c],+ or [neg a, c],+ or [neg b, c]]+ return c++tseitin (Neg (Disj x y)) = do+ a <- tseitin x+ b <- tseitin y+ c <- var+ tell [or [a, b, c],+ or [neg a, neg c],+ or [neg b, neg c]]+ return c+++-- |+-- @+-- (c -> (a -> b)) & (-c -> -(a->b))+-- (-c | (-a | b)) & (c | (a&-b))+-- (-a|b|-c) & (a|c) & (-b|c)+-- @+tseitin (Implies x y) = do+ a <- tseitin x+ b <- tseitin y+ c <- var+ tell [or [neg a, b, neg c],+ or [a, c],+ or [neg b, c]]+ return c++-- |+-- @+-- (c -> -(a -> b)) & (-c -> (a->b))+-- (-c | (a&-b)) & (c | (-a|b))+-- (a|-c) & (-b|-c) & (-a|b|c)+-- @+tseitin (Neg (Implies x y)) = do+ a <- tseitin x+ b <- tseitin y+ c <- var+ tell [or [a, neg c],+ or [neg b, neg c],+ or [neg a, b, c]]+ return c++-- |+-- @+-- (c -> a == b) & (-c -> a /= b)+-- (-c | ((-a|b) & (a|-b))) & (c | ((a|b) & (-a|-b)))+-- (-a|b|-c) & (a|-b|-c) & (a|b|c) & (-a|-b|c)+-- @+tseitin (Iff x y) = do+ a <- tseitin x+ b <- tseitin y+ c <- var+ tell [or [neg a, b, neg c],+ or [a, neg b, neg c],+ or [a, b, c],+ or [neg a, neg b, c]]+ return c++-- |+-- @+-- (c -> a /= b) & (-c -> a == b)+-- (-c | ((a|b) & (-a|-b))) & (c | ((-a|b) & (a|-b)))+-- (a|b|-c) & (-a|-b|-c) & (-a|b|c) & (a|-b|c)+-- @+tseitin (Neg (Iff x y)) = do+ a <- tseitin x+ b <- tseitin y+ c <- var+ tell [or [a, b, neg c],+ or [neg a, neg b, neg c],+ or [neg a, b, c],+ or [a, neg b, c]]+ return c++tseitin (Neg x) = do+ a <- tseitin x+ c <- var+ tell [or [neg a, neg c],+ or [a, c]]+ return c++dropTseitinVarsInSolutions (Solutions xs) =+ Solutions $ map dropTseitinVars xs++dropTseitinVars :: [Expr] -> [Expr]+dropTseitinVars = filter (\x -> not $ isTseitinLiteral x)++isTseitinLiteral :: Expr -> Bool+isTseitinLiteral lit =+ case lit of+ (Var (Ident nm)) -> tseitinName nm+ (Neg (Var (Ident nm))) -> tseitinName nm++tseitinName ('t':'s':'*':_) = True+tseitinName _ = False++simplify :: Expr -> Expr+simplify (Neg (Neg x)) = simplify x+simplify v@(Var _) = v+simplify (Neg a) = neg $ simplify a+simplify (Conj a b) = Conj (simplify a) (simplify b)+simplify (Disj a b) = Disj (simplify a) (simplify b)+simplify (Implies a b) = Implies (simplify a) (simplify b)+simplify (Iff a b) = Iff (simplify a) (simplify b)+
+ tests/PrettyPrintTest.hs view
@@ -0,0 +1,17 @@+import Picologic++main = print $ ppExprLisp d+ where a =+ Disj (Var (Ident "some-var"))+ (Disj (Var (Ident "another-var"))+ (Iff (Var (Ident "ggg"))+ (Neg (Iff (Neg (Var (Ident "var-x")))+ (Var (Ident "origin"))))))+ b = + Conj (Var (Ident "var-y"))+ (Conj (Var (Ident "ccc"))+ (Iff (Var (Ident "ddddd"))+ (Neg (Iff (Neg (Var (Ident "abcdefg")))+ (Var (Ident "ggg"))))))+ c = Disj a b+ d = Conj c c
+ tests/tests.hs view
@@ -0,0 +1,85 @@+import Picologic.AST+import Picologic.Tseitin+import Picologic.Solver+import Picologic.Pretty++import Test.QuickCheck+import qualified Data.Map as M+import qualified Data.Set as S+import System.Exit (exitFailure)+import System.IO.Unsafe (unsafePerformIO)++instance Arbitrary Expr where+ arbitrary = sized $ \n ->+ tree (round $ sqrt $ fromIntegral n :: Int)+ where tree 0 = elements $ map (Var . Ident) ["a", "b", "c", "d"]+ tree n =+ oneof+ [do a <- tree (pred n)+ return $ Neg a,+ do l <- arbitrary+ let n2 = l `mod` n+ a <- tree n2+ b <- tree (n-n2)+ con <- elements [Conj, Disj, Implies, Iff]+ return $ con a b]+ shrink (Var _) = []+ shrink (Neg (Neg x)) = [x, Neg x] ++ map Neg (shrink x)+ shrink (Neg x) = [x] ++ map Neg (shrink x)+ shrink (Conj a b) = [a, b]+ ++ map (Conj a) (shrink b)+ ++ map (\aa-> Conj aa b) (shrink a)+ shrink (Disj a b) = [a, b]+ ++ map (Disj a) (shrink b)+ ++ map (\aa-> Disj aa b) (shrink a)+ shrink (Implies a b) = [a, b]+ ++ map (Implies a) (shrink b)+ ++ map (\aa-> Implies aa b) (shrink a)+ shrink (Iff a b) = [a, b]+ ++ map (Iff a) (shrink b)+ ++ map (\aa-> Iff aa b) (shrink a)+++env = M.fromList [(Ident "a", True),+ (Ident "b", True),+ (Ident "c", False),+ (Ident "d", False)]++test_nnf :: Expr -> Bool+test_nnf e = eval env e == eval env (nnf e)++test_cnf :: Expr -> Bool+test_cnf e = eval env e == eval env (cnf e)++test_tseitin :: Expr -> Bool+test_tseitin e = unsafePerformIO test+ where test = do+ let ts = tseitinCNF e+ -- putStrLn "\nexpr"+ -- print $ ppExprLisp e+ -- putStrLn "tseitin"+ -- print $ ppExprLisp ts+ -- putStrLn "tseitin clauses"+ -- mapM_ print $ clausesExpr ts+ as <- solveCNF $ cnf e+ bs0 <- solveCNF ts+ let bs = dropTseitinVarsInSolutions bs0+ case (as, bs) of+ (Solutions av, Solutions bv) -> do+ --print ("as", av)+ --print ("bs", bv)+ return $ S.fromList av == S.fromList bv++qc = verboseCheckWith (stdArgs { maxSuccess = 1000 })++-- how to make an error fail a 'cabal test'?+qcwf p = verboseCheckWith (stdArgs { maxSuccess = 1000 })+ (whenFail exitFailure p)++main = do+ putStrLn "nnf"+ qc test_nnf+ putStrLn "cnf"+ qc test_cnf+ putStrLn "tseitin"+ qc test_tseitin