packages feed

crux-0.8.0.0: src/Crux/Config/Solver.hs

{-# LANGUAGE DeriveFoldable #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE DeriveTraversable #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE RankNTypes #-}
module Crux.Config.Solver (
  parseSolverConfig,
  SolverConfig(..),
  SolverOnline(..),
  SolverOffline(..),
  HasDefaultFloatRepr(..),
  sameSolver
  ) where

import           Control.Applicative ( (<|>), empty, Alternative )
import           Data.List.Split (wordsBy)
import qualified Data.Foldable as F
import qualified Data.Traversable as T
import           Text.Printf ( printf )
import qualified What4.Expr.Builder as WEB
import qualified What4.InterpretedFloatingPoint as WIF

import qualified Crux.Config.Common as CCC

data SolverOnline = Yices | Z3 | CVC4 | CVC5 | STP | Bitwuzla
  deriving (Eq, Ord, Show)
data SolverOffline = SolverOnline SolverOnline | Boolector | DReal | RME
  deriving (Eq, Ord, Show)

class HasDefaultFloatRepr solver where
  withDefaultFloatRepr ::
    st s ->
    solver ->
    (forall fm. (WIF.IsInterpretedFloatExprBuilder (WEB.ExprBuilder s st (WEB.Flags fm))) => WEB.FloatModeRepr fm -> a) ->
    a

instance HasDefaultFloatRepr SolverOnline where
  withDefaultFloatRepr _ s k =
    case s of
      CVC4 -> k WEB.FloatRealRepr
      CVC5 -> k WEB.FloatRealRepr
      STP -> k WEB.FloatRealRepr
      Yices -> k WEB.FloatRealRepr
      Z3 -> k WEB.FloatIEEERepr
      Bitwuzla -> k WEB.FloatIEEERepr

instance HasDefaultFloatRepr SolverOffline where
  withDefaultFloatRepr st s k =
    case s of
      SolverOnline s' -> withDefaultFloatRepr st s' k
      Boolector -> k WEB.FloatUninterpretedRepr
      RME -> k WEB.FloatUninterpretedRepr
      DReal -> k WEB.FloatRealRepr

-- | Test to see if an online and offline solver are actually the same
sameSolver :: SolverOnline -> SolverOffline -> Bool
sameSolver o f =
  case f of
    SolverOnline s' -> o == s'
    _ -> False

data SolverConfig = SingleOnlineSolver SolverOnline
                  -- ^ Use a single online solver for both path satisfiability
                  -- checking and goal discharge

                  | OnlineSolverWithOfflineGoals SolverOnline SolverOffline
                  -- ^ Use an online solver for path satisfiability checking and
                  -- use an offline solver for goal discharge

                  | OnlineSolverWithSeparateOnlineGoals SolverOnline SolverOnline
                  -- ^ Use one online solver connection for path satisfiability
                  -- checking and a separate online solver connection for goal
                  -- discharge.  INVARIANT: the solvers must be distinct.

                  | OnlyOfflineSolvers [SolverOffline]
                  -- ^ Try any of a number of offline solvers with no support
                  -- for path satisfiability checking

-- | A type that contains a validated instance of a value or a list of messages
-- describing why it was not valid
--
-- This is basically `Either [String] a` but with instances that accumulate
-- error messages across alternatives unless there is a success.
data Validated a = Invalid [String] | Validated a
  deriving (Show, Functor, F.Foldable, T.Traversable)

validatedToEither :: Validated a -> Either [String] a
validatedToEither v =
  case v of
    Invalid rsns -> Left rsns
    Validated a -> Right a

instance Applicative Validated where
  pure = Validated
  Validated f <*> Validated a = Validated (f a)
  Validated _f <*> Invalid rsns = Invalid rsns
  Invalid rsns1 <*> Invalid rsns2 = Invalid (rsns1 <> rsns2)
  Invalid rsns <*> Validated _ = Invalid rsns

instance Alternative Validated where
  empty = Invalid []
  Validated a <|> _ = Validated a
  Invalid rsns1 <|> Invalid rsns2 = Invalid (rsns1 <> rsns2)
  Invalid _ <|> Validated a = Validated a

invalid :: String -> Validated a
invalid rsn = Invalid [rsn]

-- | Boolector, RME and DReal only support offline solving (for our purposes), so
-- attempt to parse them from the given string
asOnlyOfflineSolver :: String -> Validated SolverOffline
asOnlyOfflineSolver s =
  case s of
    "dreal" -> pure DReal
    "boolector" -> pure Boolector
    "rme" -> pure RME
    _ -> invalid (printf "%s is not an offline-only solver (expected dreal, rme or boolector)" s)

-- | Solvers that can be used in offline mode
asAnyOfflineSolver :: String -> Validated SolverOffline
asAnyOfflineSolver s = case s of
      "dreal" -> pure DReal
      "boolector" -> pure Boolector
      "rme" -> pure RME
      "z3" -> pure (SolverOnline Z3)
      "yices" -> pure (SolverOnline Yices)
      "cvc4" -> pure (SolverOnline CVC4)
      "cvc5" -> pure (SolverOnline CVC5)
      "stp" -> pure (SolverOnline STP)
      "bitwuzla" -> pure (SolverOnline Bitwuzla)
      _ -> invalid (printf "%s is not a valid solver (expected dreal, boolector, z3, yices, cvc4, cvc5, stp, rme, or bitwuzla)" s)

asManyOfflineSolvers :: String -> Validated [SolverOffline]
asManyOfflineSolvers s
  | s == "all"         = asManyOfflineSolvers "dreal,boolector,z3,yices,cvc4,cvc5,stp,bitwuzla,rme"
  | length solvers > 1 = traverse asAnyOfflineSolver solvers
  | otherwise          = invalid (printf "%s is not a valid solver list (expected 'all' or a comma separated list of solvers)" s)
  where
    solvers = wordsBy (== ',') s

-- | Attempt to parse the string as one of our solvers that supports online mode
asOnlineSolver :: String -> Validated SolverOnline
asOnlineSolver s =
  case s of
    "yices" -> pure Yices
    "cvc4" -> pure CVC4
    "cvc5" -> pure CVC5
    "z3" -> pure Z3
    "stp" -> pure STP
    "bitwuzla" -> pure Bitwuzla
    _ -> invalid (printf "%s is not a valid online solver (expected yices, cvc4, cvc5, z3, stp, or bitwuzla)" s)

-- | Examine a 'CCC.CruxOptions' and determine what solver configuration to use for
-- symbolic execution.  This can fail if an invalid solver configuration is
-- requested by the user.
--
-- The high level logic is that:
--
--  * If a user specifies only a single solver with the --solver option, that is
--    used as an online solver if possible for path sat checking (if requested)
--    and goal discharge.
--  * If the user specifies an explicit --path-sat-solver, that solver is used
--    for path satisfiability checking (if requested) while the solver specified
--    with --solver is used for goal discharge.
--  * The goal solver can be entirely offline (if it doesn't support online
--    mode) or if the user requests that it be used in offline mode with the
--    --force-offline-goal-solving option.
parseSolverConfig :: CCC.CruxOptions -> Either [String] SolverConfig
parseSolverConfig cruxOpts = validatedToEither $
  case (CCC.pathSatSolver cruxOpts, CCC.checkPathSat cruxOpts, CCC.forceOfflineGoalSolving cruxOpts) of
    (Nothing, True, False) ->
      -- No separate path sat checker was requested, but we do have to check
      -- path satisfiability.  That means that we have to use the one solver
      -- specified and it must be one that supports online solving
      trySingleOnline
    (Nothing, True, True) ->
      -- The user requested path satisfiability checking (using an online
      -- solver), but also wants to force goals to be discharged using the same
      -- solver in offline mode.
      OnlineSolverWithOfflineGoals <$> asOnlineSolver (CCC.solver cruxOpts) <*> asAnyOfflineSolver (CCC.solver cruxOpts)
    (Just _, False, False) ->
      -- The user specified a separate path sat checking solver, but did not
      -- request path satisfiability checking; we'll treat that as not asking
      -- for path satisfiability checking and just use a single online solver
      tryOnlyOffline <|> trySingleOnline
    (Just _, False, True) ->
      -- The user requested a separate path sat checking solver, but did not
      -- request path satisfiability checking.  They also requested that a
      -- separate solver be used for each goal (offline mode).  We'll use the
      -- specified solver as the only solver purely in offline mode.
      tryAnyOffline <|> tryManyOffline
    (Nothing, False, False) ->
      -- This is currently the same as the previous case, but the user
      -- explicitly selected no path sat checking
      tryOnlyOffline <|> trySingleOnline
    (Nothing, False, True) ->
      -- This is the same as the `(Just _, False, True)` case since we were just
      -- ignoring the unused path sat solver option.
      tryAnyOffline <|> tryManyOffline
    (Just onSolver, True, False) ->
      -- The user requested path sat checking and specified a solver
      --
      -- NOTE: We can still use the goal solver in online mode if it is supported

      -- In this case, the user requested a goal solver that is only usable as
      -- an offline solver
      (OnlineSolverWithOfflineGoals <$> asOnlineSolver onSolver <*> asOnlyOfflineSolver (CCC.solver cruxOpts)) <|>
        -- In this case, the requested solver can be used in online mode so we
        -- use it as such
        (onlineWithOnlineGoals <$> asOnlineSolver onSolver <*> asOnlineSolver (CCC.solver cruxOpts))

    (Just onSolver, True, True) ->
      -- In this case, the user requested separate solvers for path sat checking
      -- and goal discharge, but further requested that we force goals to be
      -- discharged with an offline solver.
      OnlineSolverWithOfflineGoals <$> asOnlineSolver onSolver <*> asAnyOfflineSolver (CCC.solver cruxOpts)
  where
    tryAnyOffline = OnlyOfflineSolvers . pure <$> asAnyOfflineSolver (CCC.solver cruxOpts)
    tryOnlyOffline = OnlyOfflineSolvers . pure <$> asOnlyOfflineSolver (CCC.solver cruxOpts)
    trySingleOnline = SingleOnlineSolver <$> asOnlineSolver (CCC.solver cruxOpts)
    tryManyOffline = OnlyOfflineSolvers <$> asManyOfflineSolvers (CCC.solver cruxOpts)

    onlineWithOnlineGoals s1 s2
      | s1 == s2  = SingleOnlineSolver s1
      | otherwise = OnlineSolverWithSeparateOnlineGoals s1 s2