packages feed

ersatz 0.4.13 → 0.5

raw patch · 14 files changed

+210/−27 lines, 14 filesdep +HUnitdep +test-frameworkdep +test-framework-hunitdep ~basedep ~containersdep ~data-defaultPVP ok

version bump matches the API change (PVP)

Dependencies added: HUnit, test-framework, test-framework-hunit

Dependency ranges changed: base, containers, data-default

API changes (from Hackage documentation)

- Ersatz.Variable: forall :: (Variable a, MonadQSAT s m) => m a
+ Ersatz.Variable: forall_ :: (Variable a, MonadQSAT s m) => m a
- Ersatz.Codec: decode :: (Codec a, MonadPlus f) => Solution -> a -> f (Decoded a)
+ Ersatz.Codec: decode :: Codec a => Solution -> a -> Maybe (Decoded a)
- Ersatz.Solver: solveWith :: (Monad m, MonadPlus n, HasSAT s, Default s, Codec a) => Solver s m -> StateT s m a -> m (Result, n (Decoded a))
+ Ersatz.Solver: solveWith :: (Monad m, HasSAT s, Default s, Codec a) => Solver s m -> StateT s m a -> m (Result, Maybe (Decoded a))

Files

.hlint.yaml view
@@ -7,3 +7,5 @@     name: Use lambda-case  - ignore: {name: Hoist not, within: [Ersatz.Bit]} # This is how we define `any`, ya dingus+- ignore: {name: Use ||, within: [Ersatz.Relation.Prop]} # This is a different `or`+- ignore: {name: Use &&, within: [Ersatz.Relation.Prop]} # This is a different `and`
CHANGELOG.md view
@@ -1,3 +1,40 @@+0.5 [2023.09.08]+----------------+* The `forall` function in `Ersatz.Variable` has been renamed to+  `forall_`, since a future version of GHC will make the use of `forall` as an+  identifier an error.+* The types of `decode` and `solveWith` have been slightly less general:++  ```diff+  -decode :: MonadPlus f => Solution -> a -> f     (Decoded a)+  +decode ::                Solution -> a -> Maybe (Decoded a)++  -solveWith :: (Monad m, MonadPlus n, HasSAT s, Default s, Codec a) => Solver s m -> StateT s m a -> m (Result, n     (Decoded a))+  +solveWith :: (Monad m,              HasSAT s, Default s, Codec a) => Solver s m -> StateT s m a -> m (Result, Maybe (Decoded a))+  ```++  That is, these functions now use `Maybe` instead of an arbitrary `MonadPlus`.+  This change came about because:++  1. Practically all uses of `solveWith` in the wild are already picking `n` to+     be `Maybe`, and+  2. The behavior of `decode` and `solveWith` for `MonadPlus` instances besides+     `Maybe` could  produce surprising results, as this behavior was not well+     specified.+* Fix a bug in which `decode` could return inconsistent results with solution+  that underconstrain variables. For instance:++  ```hs+  do b <- exists+     pure [b, not b]+  ```++  Previously, this could decode to `[False, False]` (an invalid assignment).+  `ersatz` now adopts the convention that unconstrained non-negative `Literal`s+  will always be assigned `False`, and unconstrained negative `Literal`s will+  always be assigned `True`. This means that the example above would now decode+  to `[False, True]`.+ 0.4.13 [2022.11.01] ------------------- * Make the examples compile with `mtl-2.3.*`.
README.md view
@@ -46,7 +46,7 @@ ```haskell verify_currying :: (MonadQSAT s m) => m () verify_currying = do-  (x::Bit, y::Bit, z::Bit) <- forall+  (x::Bit, y::Bit, z::Bit) <- forall_   assert $ ((x && y) ==> z) === (x ==> y ==> z) ``` 
ersatz.cabal view
@@ -1,5 +1,5 @@ name:           ersatz-version:        0.4.13+version:        0.5 license:        BSD3 license-file:   LICENSE author:         Edward A. Kmett, Eric Mertens, Johan Kiviniemi@@ -82,7 +82,9 @@               , GHC == 8.8.4               , GHC == 8.10.7               , GHC == 9.0.2-              , GHC == 9.2.2+              , GHC == 9.2.7+              , GHC == 9.4.5+              , GHC == 9.6.2 extra-source-files:   .gitignore   .hlint.yaml@@ -168,7 +170,7 @@   build-depends:     array                >= 0.2      && < 0.6,     base                 >= 4.9      && < 5,-    bytestring           >= 0.10.4.0 && < 0.12,+    bytestring           >= 0.10.4.0 && < 0.13,     containers           >= 0.2.0.1  && < 0.7,     data-default         >= 0.5      && < 0.8,     lens                 >= 4        && < 6,@@ -270,6 +272,21 @@ -- --   hs-source-dirs: tests --   Main-is: properties.hs++test-suite hunit+  type: exitcode-stdio-1.0+  main-is: HUnit.hs+  hs-source-dirs: tests+  build-depends:+    base,+    containers,+    data-default,+    ersatz,+    HUnit >= 1.2,+    test-framework >= 0.6,+    test-framework-hunit >= 0.2+  default-language: Haskell2010+  ghc-options: -Wall  test-suite speed   type: exitcode-stdio-1.0
src/Ersatz/Bit.hs view
@@ -27,7 +27,7 @@ import qualified Prelude  import Control.Applicative-import Control.Monad (MonadPlus(..), liftM2, liftM3)+import Control.Monad (liftM2, liftM3) import Data.Foldable (toList) import qualified Data.Foldable as Foldable import qualified Data.Traversable as Traversable@@ -123,8 +123,7 @@   type Decoded Bit = Bool   encode = bool   decode sol b-      = maybe (pure False <|> pure True) pure $-        solutionStableName sol (unsafePerformIO (makeStableName' b))+      = solutionStableName sol (unsafePerformIO (makeStableName' b))      -- The StableName didn’t have an associated literal with a solution,      -- recurse to compute the value.     <|> case b of@@ -135,7 +134,9 @@             decode sol $ if p then ct else cf           Not c'  -> not <$> decode sol c'           Var l   -> decode sol l-          Run _ -> mzero+          Run _ -> Nothing+      -- If all else false, just return False.+    <|> Just False     where       andMaybeBools :: [Maybe Bool] -> Maybe Bool       andMaybeBools mbs
src/Ersatz/Codec.hs view
@@ -30,13 +30,23 @@ -- | This class describes data types that can be marshaled to or from a SAT solver. class Codec a where   type Decoded a :: Type-  -- | Return a value based on the solution if one can be determined.-  decode :: MonadPlus f => Solution -> a -> f (Decoded a)+  -- | Return 'Just' a value based on the solution if one can be determined.+  -- Otherwise, return 'Nothing'.+  decode :: Solution -> a -> Maybe (Decoded a)   encode :: Decoded a -> a +-- | By convention, the 'decode' implementation will return 'False' for+-- unconstrained non-negative 'Literal's and 'True' for unconstrained negative+-- 'Literal's. instance Codec Literal where   type Decoded Literal = Bool-  decode s a = maybe (pure False <|> pure True) pure (solutionLiteral s a)+  decode s a = case solutionLiteral s a of+                 sol@(Just _) -> sol+                 Nothing+                   | i >= 0    -> Just False+                   | otherwise -> Just True+    where+      i = literalId a   encode True  = literalTrue   encode False = literalFalse 
src/Ersatz/Problem.hs view
@@ -53,6 +53,7 @@ import Data.IntSet (IntSet) import qualified Data.IntSet as IntSet import qualified Data.List as List+import qualified Data.List.NonEmpty as NE import Ersatz.Internal.Formula import Ersatz.Internal.Literal import Ersatz.Internal.StableName@@ -247,12 +248,12 @@                     , intDec (Seq.length tClauses)                     ]     quantified = foldMap go tQuantGroups-      where go ls = bLine0 (q (head ls) : map (intDec . getQuant) ls)+      where go ls = bLine0 (q (NE.head ls) : map (intDec . getQuant) (NE.toList ls))             q Exists{} = char7 'e'             q Forall{} = char7 'a'     clauses = foldMap bClause tClauses -    tQuantGroups = List.groupBy eqQuant (qdimacsQuantified t)+    tQuantGroups = NE.groupBy eqQuant (qdimacsQuantified t)       where         eqQuant :: Quant -> Quant -> Bool         eqQuant Exists{} Exists{} = True@@ -311,9 +312,10 @@   -- "Unbound atoms are to be considered as being existentially quantified in   -- the first (i.e., the outermost) quantified set." per QDIMACS standard.   -- Skip the implicit first existentials.-  qdimacsQuantified q-    | IntSet.null (q^.universals) = []-    | otherwise = quants [head qUniversals..lastAtom'] qUniversals+  qdimacsQuantified q =+    case qUniversals of+      []    -> []+      (i:_) -> quants [i..lastAtom'] qUniversals     where       lastAtom'   = qdimacsNumVariables q       qUniversals = IntSet.toAscList (q^.universals)
src/Ersatz/Solution.hs view
@@ -24,6 +24,9 @@  data Solution = Solution   { solutionLiteral    :: Literal -> Maybe Bool+    -- ^ If a 'Literal' is uniquely assigned to a particular value, this will+    -- return 'Just' of that value. If a 'Literal' is unconstrained (i.e., it+    -- can be assigned either 'True' or 'False'), this will return 'Nothing'.   , solutionStableName :: StableName () -> Maybe Bool   } 
src/Ersatz/Solver.hs view
@@ -14,7 +14,6 @@   , solveWith   ) where -import Control.Monad import Control.Monad.State import Data.Default import Ersatz.Codec@@ -24,9 +23,39 @@ import Ersatz.Solver.Minisat import Ersatz.Solver.Z3 +-- | @'solveWith' solver prob@ solves a SAT problem @prob@ with the given+-- @solver@. It returns a pair consisting of:+--+-- 1. A 'Result' that indicates if @prob@ is satisfiable ('Satisfied'),+--    unsatisfiable ('Unsatisfied'), or if the solver could not determine any+--    results ('Unsolved').+--+-- 2. A 'Decoded' answer that was decoded using the solution to @prob@. Note+--    that this answer is only meaningful if the 'Result' is 'Satisfied' and+--    the answer value is in a 'Just'.+--+-- Here is a small example of how to use 'solveWith':+--+-- @+-- import Ersatz+--+-- main :: IO ()+-- main = do+--   res <- 'solveWith' minisat $ do+--     (b1 :: Bit) <- exists+--     (b2 :: Bit) <- exists+--     assert (b1 === b2)+--     pure [b1, b2]+--   case res of+--     (Satisfied, Just answer) -> print answer+--     _ -> fail "Could not solve problem"+-- @+--+-- Depending on the whims of @minisat@, this program may print either+-- @[False, False]@ or @[True, True]@. solveWith ::-  (Monad m, MonadPlus n, HasSAT s, Default s, Codec a) =>-  Solver s m -> StateT s m a -> m (Result, n (Decoded a))+  (Monad m, HasSAT s, Default s, Codec a) =>+  Solver s m -> StateT s m a -> m (Result, Maybe (Decoded a)) solveWith solver m = do   (a, problem) <- runStateT m def   (res, litMap) <- solver problem
src/Ersatz/Solver/DepQBF.hs view
@@ -32,6 +32,20 @@ parseLiteral ('-':xs) = (read xs, False) parseLiteral xs = (read xs, True) +-- Parse the QDIMACS output format, which is described in+-- http://www.qbflib.org/qdimacs.html#output+parseOutput :: String -> [(Int, Bool)]+parseOutput out =+  case lines out of+    (_preamble:certLines) -> map parseCertLine certLines+    [] -> error "QDIMACS output without preamble"+  where+    parseCertLine :: String -> (Int, Bool)+    parseCertLine certLine =+      case words certLine of+        (_v:lit:_) -> parseLiteral lit+        _ -> error $ "Malformed QDIMACS certificate line: " ++ certLine+ -- | This is a 'Solver' for 'QSAT' problems that lets you specify the path to the @depqbf@ executable. depqbfPath :: MonadIO m => FilePath -> Solver QSAT m depqbfPath path problem = liftIO $@@ -47,6 +61,6 @@     return $ (,) result $       case result of         Satisfied ->-          I.fromList $ map (parseLiteral . head . tail . words) $ tail $ lines out+          I.fromList $ parseOutput out         _ ->           I.empty
src/Ersatz/Variable.hs view
@@ -15,7 +15,7 @@ module Ersatz.Variable   ( Variable(..) #ifndef HLINT-  , forall+  , forall_ #endif   , exists @@ -32,8 +32,8 @@ exists = literally literalExists  #ifndef HLINT-forall :: (Variable a, MonadQSAT s m)  => m a-forall = literally literalForall+forall_ :: (Variable a, MonadQSAT s m) => m a+forall_ = literally literalForall #endif  class GVariable f where
+ tests/HUnit.hs view
@@ -0,0 +1,38 @@+-- | @HUnit@-based unit tests for @ersatz@.+module Main where++import Prelude hiding ((||), (&&), not)++import Data.Default+import qualified Data.IntMap.Strict as IntMap++import Test.Framework+import Test.Framework.Providers.HUnit+import Test.HUnit (Assertion, (@?=))++import Ersatz+import Ersatz.Internal.Literal++main :: IO ()+main = defaultMain tests++tests :: [Test]+tests =+  [ testGroup "unit tests"+      [ testCase "unconstrained literals" case_unconstrained_literals+      ]+  ]++-- A regression test for #60 and #76.+case_unconstrained_literals :: Assertion+case_unconstrained_literals =+    decode sol [b1, not b1, b2, not b2, b1 || b2, b1 && b2] @?=+      Just [False, True, False, True, False, False]+      -- There are other valid answers, but in practice, ersatz will choose this+      -- one due to the convention that the Codec Literal instance always+      -- assigns non-negative unconstrained Literals to False and negative+      -- unconstrained Literals to True.+  where+    sol = solutionFrom (IntMap.fromList [(1, True)]) (def :: SAT)+    b1 = Var $ Literal 2+    b2 = Var $ Literal 3
tests/Speed.hs view
@@ -16,8 +16,12 @@   putStrLn $ unwords [ "n",  show n ]   (s, mgs) <- solveWith anyminisat $ do       gs <- replicateM n exists-      forM_ (zip gs $ tail gs) $ \ (x,y) -> assert ( x /== y )+      forM_ (zipTail gs) $ \ (x,y) -> assert ( x /== y )       return (gs :: [Bit])   case (s, mgs) of      (Satisfied, Just gs) -> do print $ length $ filter id gs      _ -> do return ()++zipTail :: [a] -> [(a, a)]+zipTail []         = []+zipTail xss@(_:xs) = zip xss xs
tests/Z001.hs view
@@ -41,6 +41,8 @@   unknown :: MonadSAT s m => m a  -- | square matrices+--+-- Invariant: @dim >= 2@ newtype Matrix (dim::Nat)  a = Matrix [[a]]   deriving ( Show, Equatable, Orderable ) @@ -68,9 +70,33 @@  for = flip map -topleft (Matrix xss) = head (head xss)-botright (Matrix xss) = last (last xss)-topright (Matrix xss) = last (head xss)+topleft (Matrix xss) =+  case xss of+    ((x:_):_) -> x+    _ -> matrixInvariantViolated++botright (Matrix xss)+  | Just (_, xs) <- unsnoc xss+  , Just (_, x)  <- unsnoc xs+  = x+  | otherwise+  = matrixInvariantViolated++topright (Matrix xss)+  | xs:_ <- xss+  , Just (_, x) <- unsnoc xs+  = x+  | otherwise+  = matrixInvariantViolated++unsnoc :: [a] -> Maybe ([a], a)+unsnoc []     = Nothing+unsnoc (x:xs) = case unsnoc xs of+                  Nothing    -> Just ([], x)+                  Just (a,b) -> Just (x:a, b)++matrixInvariantViolated :: a+matrixInvariantViolated = error "Matrix invariant violated"  monotone m = positive (topleft m) && positive (botright m)