packages feed

ersatz-0.6: src/Ersatz/Solver/DepQBF.hs

--------------------------------------------------------------------
-- |
-- Copyright :  © Edward Kmett 2010-2014, Johan Kiviniemi 2013
-- License   :  BSD3
-- Maintainer:  Edward Kmett <ekmett@gmail.com>
-- Stability :  experimental
-- Portability: non-portable
--
-- <http://lonsing.github.io/depqbf/ DepQBF> is a solver capable of
-- solving quantified boolean formulae ('QBF').
--------------------------------------------------------------------
module Ersatz.Solver.DepQBF
  ( depqbf
  , depqbfPath
  , depqbfPathArgs
  ) where

import Control.Monad.IO.Class
import Data.Version (Version, makeVersion, parseVersion)
import Ersatz.Problem ( QSAT, writeQdimacs' )
import Ersatz.Solution
import Ersatz.Solver.Common
import qualified Data.IntMap as I
import System.Exit (ExitCode(..))
import System.Process (readProcessWithExitCode)
import qualified Text.ParserCombinators.ReadP as P

-- | This is a 'Solver' for 'QSAT' problems that runs the @depqbf@ solver using
-- the current @PATH@, it tries to run an executable named @depqbf@.
depqbf :: MonadIO m => Solver QSAT m
depqbf = depqbfPath "depqbf"

parseLiteral :: String -> (Int, Bool)
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 filter (not . comment) $ lines out of
    (_preamble:certLines) -> map parseCertLine certLines
    [] -> error "QDIMACS output without preamble"
  where
    comment [] = True
    comment ('c' : _) = True
    comment _ = False

    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.
-- This passes different arguments to @depqbf@ depending on its version:
--
-- * If using version 6.03 or later, this passes @[\"--qdo\", \"--no-dynamic-nenofex\"]@.
--
-- * Otherwise, this passes @[\"--qdo\"]@.
depqbfPath :: MonadIO m => FilePath -> Solver QSAT m
depqbfPath path problem = do
  ver <- liftIO $ depqbfVersion path
  let args | ver >= makeVersion [6,03]
           = [ "--qdo", "--no-dynamic-nenofex" ]
           | otherwise
           = [ "--qdo" ]
  depqbfPathArgs path args problem

-- | This is a 'Solver' for 'QSAT' problems that lets you specify the path to the @depqbf@ executable
-- as well as a list of command line arguments. They will appear after the problem file name.
depqbfPathArgs :: MonadIO m => FilePath -> [String] -> Solver QSAT m
depqbfPathArgs path args problem = liftIO $
  withTempFiles ".cnf" "" $ \problemPath _ -> do
    writeQdimacs' problemPath problem

    (exit, out, _err) <-
      readProcessWithExitCode path (problemPath : args) []

    let result = resultOf exit

    return $ (,) result $
      case result of
        Satisfied ->
          I.fromList $ parseOutput out
        _ ->
          I.empty

-- | Query @depqbf@'s 'Version' by invoking @depqbf --version@ and parsing the
-- output. This assumes that the output can be parsed as a valid 'Version' and
-- that @depqbf@ versions increase in a way that is compatible with the
-- 'Ord Version' instance (see 'depqbfPath', which compares 'Version's using
-- ('>=')).
depqbfVersion :: FilePath -> IO Version
depqbfVersion path = do
  (exit, out, err) <-
    readProcessWithExitCode path ["--version"] []

  let parseError reason =
        fail $ unlines
          [ "Could not query depqbf version (" ++ reason ++ ")"
          , "Standard output:"
          , out
          , ""
          , "Standard error:"
          , err
          ]

  case exit of
    ExitSuccess -> do
      -- Should be something like "DepQBF 6.03"
      verStrLine <-
        case lines err of
          line:_ -> pure line
          [] -> parseError "no lines of standard error"
      -- Should be something like "6.03"
      verStr <-
        case words verStrLine of
          _depQBF:ver:_ -> pure ver
          _ -> parseError $ "unexpected version number " ++ verStrLine
      -- Convert the string to a full Version
      case readEitherP parseVersion verStr of
        Left reason -> parseError reason
        Right v -> pure v
    ExitFailure i ->
      parseError $ "exit code " ++ show i ++ ")"

-- | Like @readEither@ from "Text.Read", but accepting an arbitrary 'P.ReadP'
-- argument instead of requiring a 'Read' constraint.
readEitherP :: P.ReadP a -> String -> Either String a
readEitherP rp s =
  case [ x | (x,"") <- P.readP_to_S read' s ] of
    [x] -> Right x
    []  -> Left "no parse"
    _   -> Left "ambiguous parse"
 where
  read' = do
    x <- rp
    P.skipSpaces
    pure x