{-# LANGUAGE OverloadedStrings #-}

module SMTLIB.Backends
  ( Backend (..),
    QueuingFlag (..),
    Solver,
    initSolver,
    command,
    command_,
    flushQueue,
  )
where

import Control.Monad ((<=<))
import Data.ByteString.Builder (Builder)
import qualified Data.ByteString.Lazy.Char8 as LBS
import Data.Char (isSpace)
import Data.IORef (IORef, modifyIORef, newIORef, readIORef, writeIORef)
import Data.List (intersperse)
import Prelude hiding (log)

-- | The type of solver backends. SMTLib2 commands are sent to a backend which
-- processes them and outputs the solver's response.
data Backend = Backend
  { -- | Send a command to the backend.
    -- While the implementation depends on the backend, this function is usually
    -- *not* thread-safe.
    Backend -> Builder -> IO ByteString
send :: Builder -> IO LBS.ByteString,
    -- | Send a command that doesn't produce any response to the backend.
    -- The backend may implement this by not reading the output and leaving it
    -- for a later read, or reading the output and discarding it immediately.
    -- Hence this method should only be used when the command does not produce
    -- any response to be outputted.
    -- Again, this function may not be thread-safe.
    Backend -> Builder -> IO ()
send_ :: Builder -> IO ()
  }

type Queue = IORef Builder

-- | A boolean-equivalent datatype indicating whether to enable queuing.
data QueuingFlag = Queuing | NoQueuing

-- | Push a command on the solver's queue of commands to evaluate.
-- The command must not produce any output when evaluated, unless it is the last
-- command added before the queue is flushed.
-- For a fixed queue, this function is *not* thread-safe.
put :: Queue -> Builder -> IO ()
put :: Queue -> Builder -> IO ()
put Queue
q Builder
cmd = forall a. IORef a -> (a -> a) -> IO ()
modifyIORef Queue
q (forall a. Semigroup a => a -> a -> a
<> Builder
cmd)

-- | Empty the queue of commands to evaluate and return its content as a bytestring
-- builder.
-- For a fixed queue, this function is *not* thread-safe.
flush :: Queue -> IO Builder
flush :: Queue -> IO Builder
flush Queue
q = do
  Builder
cmds <- forall a. IORef a -> IO a
readIORef Queue
q
  forall a. IORef a -> a -> IO ()
writeIORef Queue
q forall a. Monoid a => a
mempty
  forall (m :: * -> *) a. Monad m => a -> m a
return Builder
cmds

-- | A solver is essentially a wrapper around a solver backend. It also comes an
-- optional queue of commands to send to the backend.
--
-- A solver can either be in 'Queuing' mode or 'NoQueuing' mode. In 'NoQueuing'
-- mode, the queue of commands isn't used and the commands are sent to the
-- backend immediately. In 'Queuing' mode, commands whose output are not
-- strictly necessary for the rest of the computation (typically the ones whose
-- output should just be @success@) and that are sent through 'command_' are not
-- sent to the backend immediately, but rather written on the solver's queue.
-- When a command whose output is actually necessary needs to be sent, the queue
-- is flushed and sent as a batch to the backend.
--
-- 'Queuing' mode should be faster as there usually is a non-negligible constant
-- overhead in sending a command to the backend. But since the commands are sent
-- by batches, a command sent to the solver will only produce an error when the
-- queue is flushed, i.e. when a command with interesting output is sent. You
-- thus probably want to stick with 'NoQueuing' mode when debugging. Moreover,
-- when commands are sent by batches, only the last command in the batch may
-- produce an output for parsing to work properly. Hence the @:print-success@
-- option is disabled in 'Queuing' mode, and this should not be overriden
-- manually.
data Solver = Solver
  { -- | The backend processing the commands.
    Solver -> Backend
backend :: Backend,
    -- | An optional queue to write commands that are to be sent to the solver lazily.
    Solver -> Maybe Queue
queue :: Maybe Queue
  }

-- | Create a new solver and initialize it with some options so that it behaves
-- correctly for our use.
-- In particular, the "print-success" option is disabled in 'Queuing' mode. This
-- should not be overriden manually.
initSolver ::
  -- | whether to enable 'Queuing' mode (see 'Solver' for the meaning of this
  -- flag)
  QueuingFlag ->
  -- | the solver backend
  Backend ->
  IO Solver
initSolver :: QueuingFlag -> Backend -> IO Solver
initSolver QueuingFlag
queuing Backend
solverBackend = do
  Maybe Queue
solverQueue <- case QueuingFlag
queuing of
    QueuingFlag
Queuing -> do
      Queue
ref <- forall a. a -> IO (IORef a)
newIORef forall a. Monoid a => a
mempty
      forall (m :: * -> *) a. Monad m => a -> m a
return forall a b. (a -> b) -> a -> b
$ forall a. a -> Maybe a
Just Queue
ref
    QueuingFlag
NoQueuing -> forall (m :: * -> *) a. Monad m => a -> m a
return forall a. Maybe a
Nothing
  let solver :: Solver
solver = Backend -> Maybe Queue -> Solver
Solver Backend
solverBackend Maybe Queue
solverQueue
  case QueuingFlag
queuing of
    QueuingFlag
Queuing -> forall (m :: * -> *) a. Monad m => a -> m a
return ()
    QueuingFlag
NoQueuing ->
      -- this should not be enabled when the queue is used, as it messes with parsing
      -- the outputs of commands that are actually interesting
      -- TODO checking for correctness and enabling laziness can be made compatible
      -- but it would require the solver backends to return several outputs at once
      -- alternatively, we may consider that the user wanting both features should
      -- implement their own backend that deals with this
      Solver -> Builder -> Builder -> IO ()
setOption Solver
solver Builder
"print-success" Builder
"true"
  Solver -> Builder -> Builder -> IO ()
setOption Solver
solver Builder
"produce-models" Builder
"true"
  forall (m :: * -> *) a. Monad m => a -> m a
return Solver
solver

-- | Have the solver evaluate a SMT-LIB command.
-- This forces the queued commands to be evaluated as well, but their results are
-- *not* checked for correctness.
-- For a fixed backend, this function is *not* thread-safe.
command :: Solver -> Builder -> IO LBS.ByteString
command :: Solver -> Builder -> IO ByteString
command Solver
solver Builder
cmd = do
  Backend -> Builder -> IO ByteString
send (Solver -> Backend
backend Solver
solver)
    forall (m :: * -> *) a b. Monad m => (a -> m b) -> m a -> m b
=<< case Solver -> Maybe Queue
queue Solver
solver of
      Maybe Queue
Nothing -> forall (m :: * -> *) a. Monad m => a -> m a
return Builder
cmd
      Just Queue
q -> (forall a. Semigroup a => a -> a -> a
<> Builder
cmd) forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> Queue -> IO Builder
flush Queue
q

-- | A command with no interesting result.
-- In 'NoQueuing' mode, the result is checked for correctness. In 'Queuing'
-- mode, (unless the queue is flushed and evaluated right after) the command
-- must not produce any output when evaluated, and its output is thus in
-- particular not checked for correctness. For a fixed backend, this function is
-- *not* thread-safe.
command_ :: Solver -> Builder -> IO ()
command_ :: Solver -> Builder -> IO ()
command_ Solver
solver Builder
cmd =
  case Solver -> Maybe Queue
queue Solver
solver of
    Maybe Queue
Nothing -> do
      ByteString
res <- Backend -> Builder -> IO ByteString
send (Solver -> Backend
backend Solver
solver) Builder
cmd
      if ByteString -> ByteString
trim ByteString
res forall a. Eq a => a -> a -> Bool
== ByteString
"success"
        then forall (m :: * -> *) a. Monad m => a -> m a
return ()
        else
          forall (m :: * -> *) a. MonadFail m => [Char] -> m a
fail forall a b. (a -> b) -> a -> b
$
            [[Char]] -> [Char]
unlines
              [ [Char]
"Unexpected result from the SMT solver:",
                [Char]
"  Expected: success",
                [Char]
"  Got: " forall a. [a] -> [a] -> [a]
++ forall a. Show a => a -> [Char]
show ByteString
res
              ]
    Just Queue
q -> Queue -> Builder -> IO ()
put Queue
q Builder
cmd
  where
    trim :: ByteString -> ByteString
trim = (Char -> Bool) -> ByteString -> ByteString
LBS.dropWhile Char -> Bool
isSpace forall b c a. (b -> c) -> (a -> b) -> a -> c
. ByteString -> ByteString
LBS.reverse forall b c a. (b -> c) -> (a -> b) -> a -> c
. (Char -> Bool) -> ByteString -> ByteString
LBS.dropWhile Char -> Bool
isSpace forall b c a. (b -> c) -> (a -> b) -> a -> c
. ByteString -> ByteString
LBS.reverse

-- | Force the content of the queue to be sent to the solver.
-- Only useful in queuing mode, does nothing in non-queuing mode.
flushQueue :: Solver -> IO ()
flushQueue :: Solver -> IO ()
flushQueue Solver
solver = forall b a. b -> (a -> b) -> Maybe a -> b
maybe (forall (m :: * -> *) a. Monad m => a -> m a
return ()) (Backend -> Builder -> IO ()
send_ (Solver -> Backend
backend Solver
solver) forall (m :: * -> *) b c a.
Monad m =>
(b -> m c) -> (a -> m b) -> a -> m c
<=< Queue -> IO Builder
flush) forall a b. (a -> b) -> a -> b
$ Solver -> Maybe Queue
queue Solver
solver

setOption :: Solver -> Builder -> Builder -> IO ()
setOption :: Solver -> Builder -> Builder -> IO ()
setOption Solver
solver Builder
name Builder
value = Solver -> Builder -> IO ()
command_ Solver
solver forall a b. (a -> b) -> a -> b
$ [Builder] -> Builder
list [Builder
"set-option", Builder
":" forall a. Semigroup a => a -> a -> a
<> Builder
name, Builder
value]

list :: [Builder] -> Builder
list :: [Builder] -> Builder
list [Builder]
bs = Builder
"(" forall a. Semigroup a => a -> a -> a
<> forall a. Monoid a => [a] -> a
mconcat (forall a. a -> [a] -> [a]
intersperse Builder
" " [Builder]
bs) forall a. Semigroup a => a -> a -> a
<> Builder
")"