packages feed

crux-0.7.2: src/Crux.hs

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeOperators #-}
{-# Language RankNTypes, ImplicitParams, TypeApplications, MultiWayIf #-}
{-# Language OverloadedStrings, FlexibleContexts, ScopedTypeVariables #-}

{-# OPTIONS_GHC -Wno-simplifiable-class-constraints #-}

module Crux
  ( runSimulator
  , postprocessSimResult
  , loadOptions
  , mkOutputConfig
  , defaultOutputConfig
  , SimulatorCallbacks(..)
  , SimulatorHooks(..)
  , RunnableState(..)
  , pattern RunnableState
  , Explainer
  , CruxOptions(..)
  , SomeOnlineSolver(..)
  , Crux(..)
  , module Crux.Config
  , module Crux.Log
  ) where

import qualified Control.Applicative as Applicative
import qualified Control.Exception as Ex
import           Control.Lens
import           Control.Monad ( unless, void, when )
import qualified Data.Aeson as JSON
import           Data.Foldable
import           Data.Functor.Contravariant ( (>$<) )
import           Data.Functor.Contravariant.Divisible ( divide )
import           Data.Generics.Product.Fields (field)
import           Data.IORef
import           Data.Maybe ( fromMaybe )
import qualified Data.Sequence as Seq
import           Data.Text (Text)
import qualified Data.Text as T
import           Data.Version (Version)
import           Data.Void (Void)
import qualified Lumberjack as LJ
import           Prettyprinter
import qualified System.Console.ANSI as AC
import           System.Console.Terminal.Size (Window(Window), size)
import           System.Directory (createDirectoryIfMissing)
import           System.Exit (exitSuccess, ExitCode(..), exitFailure, exitWith)
import           System.FilePath ((</>))
import           System.IO ( Handle, hPutStr, stdout, stderr )

import           Data.Parameterized.Classes
import qualified Data.Parameterized.Map as MapF
import           Data.Parameterized.Nonce (newIONonceGenerator, NonceGenerator)
import           Data.Parameterized.Some ( Some(..) )

import           Lang.Crucible.Backend
import           Lang.Crucible.Backend.Online
import qualified Lang.Crucible.Backend.Simple as CBS
import           Lang.Crucible.CFG.Extension
import qualified Lang.Crucible.Debug as Debug
import           Lang.Crucible.Simulator
import           Lang.Crucible.Simulator.BoundedExec
import           Lang.Crucible.Simulator.BoundedRecursion
import           Lang.Crucible.Simulator.PathSatisfiability
import           Lang.Crucible.Simulator.PathSplitting
import           Lang.Crucible.Simulator.PositionTracking
import           Lang.Crucible.Simulator.Profiling
import qualified Lang.Crucible.Syntax.Concrete as Syn
import           Lang.Crucible.Types


import           What4.Config (setOpt, getOptionSetting, verbosity, extendConfig)
import qualified What4.Expr as WE
import           What4.FunctionName (FunctionName)
import           What4.Interface (IsExprBuilder, getConfiguration)
import           What4.Protocol.Online (OnlineSolver)
import qualified What4.Solver as WS
import           What4.Solver.Bitwuzla (bitwuzlaTimeout)
import           What4.Solver.CVC4 (cvc4Timeout)
import           What4.Solver.CVC5 (cvc5Timeout)
import           What4.Solver.Yices (yicesEnableMCSat, yicesGoalTimeout)
import           What4.Solver.Z3 (z3Timeout)

import           Crux.Config
import           Crux.Config.Common
import           Crux.Config.Doc
import qualified Crux.Config.Load as Cfg
import qualified Crux.Config.Solver as CCS
import           Crux.FormatOut ( sayWhatFailedGoals, sayWhatResultStatus )
import           Crux.Goal
import           Crux.Log -- for the export list
import           Crux.Log as Log
import           Crux.Report
import           Crux.Types

pattern RunnableState :: forall sym . () => forall ext personality . (IsSyntaxExtension ext) => ExecState (personality sym) sym ext (RegEntry sym UnitType) -> RunnableState sym
pattern RunnableState es = RunnableStateWithExtensions es []

-- | A crucible @ExecState@ that is ready to be passed into the simulator.
--   This will usually, but not necessarily, be an @InitialState@.
data RunnableState sym where
  RunnableStateWithExtensions :: (IsSyntaxExtension ext)
                              => ExecState (personality sym) sym ext (RegEntry sym UnitType)
                              -> [ExecutionFeature (personality sym) sym ext (RegEntry sym UnitType)]
                              -> RunnableState sym

-- | Individual crux tools will generally call the @runSimulator@ combinator to
--   handle the nitty-gritty of setting up and running the simulator. Tools
--   provide @SimulatorCallbacks@ to hook into the simulation process at three
--   points:
--
--   * Before simulation begins, to set up global variables, register override
--     functions, construct the initial program entry point, and generally do
--     any necessary language-specific setup (i.e., to produce a 'RunnableState')
--   * When simulation ends with an assertion failure ('Explainer')
--   * When simulation ends, regardless of the outcome, to interpret the results.
--
--   All of these callbacks have access to the symbolic backend.
newtype SimulatorCallbacks msgs r
  = SimulatorCallbacks
    { getSimulatorCallbacks ::
        forall sym bak t st fs.
          ( IsSymBackend sym bak
          , Logs msgs
          , sym ~ WE.ExprBuilder t st fs
          ) =>
          IO (SimulatorHooks sym bak t r)
    }


-- | A GADT to capture the online solver constraints when we need them
data SomeOnlineSolver sym bak where
  SomeOnlineSolver :: ( sym ~ WE.ExprBuilder scope st fs
                      , bak ~ OnlineBackend solver scope st fs
                      , IsSymBackend sym bak
                      , OnlineSolver solver
                      ) => bak -> SomeOnlineSolver sym bak

-- | See 'SimulatorCallbacks'
data SimulatorHooks sym bak t r =
  SimulatorHooks
    { setupHook   :: bak -> Maybe (SomeOnlineSolver sym bak) -> IO (RunnableState sym)
    , onErrorHook :: bak -> IO (Explainer sym t Void)
    , resultHook  :: bak -> CruxSimulationResult -> IO r
    }

-- | Given the result of a simulation and proof run, report the overall
--   status, generate user-consumable reports and compute the exit code.
postprocessSimResult ::
  Logs msgs =>
  Bool -> CruxOptions -> CruxSimulationResult -> IO ExitCode
postprocessSimResult showFailedGoals opts res =
  do -- print goals that failed and overall result
     logSimResult showFailedGoals res

     -- Generate report
     generateReport opts res

     return $! computeExitCode res


-- | Load crux generic and the provided options, and provide them to
--   the given continuation.
--
--   IMPORTANT:  This processes options like @help@ and @version@, which
--   just print something and exit, so this function may never call
--   its continuation.
loadOptions ::
  SupportsCruxLogMessage msgs =>
  (Maybe OutputOptions -> OutputConfig msgs) ->
  Text {- ^ Name -} ->
  Version ->
  Config opts ->
  (Logs msgs => (CruxOptions, opts) -> IO a) ->
  IO a
loadOptions mkOutCfg nm ver config cont =
  do let optSpec = cfgJoin cruxOptions config
     (copts, opts) <- Cfg.loadConfig nm optSpec
     case opts of
       Cfg.ShowHelp ->
          do let ?outputConfig = mkOutCfg (Just (defaultOutputOptions copts))
             showHelp nm optSpec
             exitSuccess
       Cfg.ShowVersion ->
          do let ?outputConfig = mkOutCfg (Just (defaultOutputOptions copts))
             showVersion nm ver
             exitSuccess
       Cfg.Options (cruxWithoutColorOptions, os) files ->
          do let crux = set (field @"outputOptions" . field @"colorOptions") copts cruxWithoutColorOptions
             let ?outputConfig = mkOutCfg (Just (outputOptions crux))
             crux' <- postprocessOptions crux { inputFiles = files ++ inputFiles crux }
             cont (crux', os)

 `Ex.catch` \(e :: Ex.SomeException) ->
   do let ?outputConfig = mkOutCfg Nothing
      case (Ex.fromException e :: Maybe ExitCode) of
        Just exitCode -> exitWith exitCode
        Nothing -> logException e >> exitFailure


showHelp ::
  Logs msgs =>
  SupportsCruxLogMessage msgs =>
  Text -> Config opts -> IO ()
showHelp nm cfg = do
  -- Ideally, this would be done in the log handler.  We will soon try to change
  -- the logging mechanism so thaat it prefers 'Doc' over 'Text' so that layout
  -- decisions can be delayed to the log handlers.
  outWidth <- maybe 80 (\(Window _ w) -> w) <$> size
  let opts = LayoutOptions $ AvailablePerLine outWidth 0.98
  sayCrux (Log.Help (LogDoc (layoutPretty opts (configDocs nm cfg))))


showVersion ::
  Logs msgs =>
  SupportsCruxLogMessage msgs =>
  Text -> Version -> IO ()
showVersion nm ver = sayCrux (Log.Version nm ver)


-- | Create an OutputConfig for Crux, based on an indication of whether colored
-- output should be used, the normal and error output handles, and the parsed
-- CruxOptions.
--
-- If no CruxOptions are available (i.e. Nothing, as used in the preliminary
-- portions of loadOptions), then a default output stance is applied; the
-- default stance is described along with the individual option below.
--
-- The following CruxOptions affect the generated OutputConfig:
--
--  * noColorsErr    (default stance: False when the error handle supports
--    colors, as reported by System.Console.ANSI.hSupportsANSIColor)
--  * noColorsOut    (default stance: False when the output handle supports
--    colors, as reported by System.Console.ANSI.hSupportsANSIColor)
--  * printFailures  (default stance: True)
--  * quietMode      (default stance: False)
--  * simVerbose     (default stance: False)
mkOutputConfig ::
  JSON.ToJSON msgs =>
  (Handle, Bool) -> (Handle, Bool) ->
  (msgs -> SayWhat) -> Maybe OutputOptions ->
  OutputConfig msgs
mkOutputConfig (outHandle, outWantsColor) (errHandle, errWantsColor) logMessageToSayWhat opts =
  let consensusBetween wants maybeRefuses = wants && not (fromMaybe False maybeRefuses)
      copts = colorOptions <$> opts
      outSpec = (outHandle, consensusBetween outWantsColor (Cfg.noColorsOut <$> copts))
      errSpec@(_, errShouldColor) = (errHandle, consensusBetween errWantsColor (Cfg.noColorsErr <$> copts))
      lgWhat = let la = LJ.LogAction $ logToStd outSpec errSpec
                   -- TODO simVerbose may not be the best setting to use here...
                   baseline = if maybe False ((> 1) . simVerbose) opts
                              then Noisily
                              else Simply
               in if beQuiet
                  then logfltr OK >$< la
                  else logfltr baseline >$< la
      beQuiet = maybe False quietMode opts
      logfltr allowed = \case
        SayNothing -> SayNothing
        w@(SayWhat lvl _ _) -> if lvl >= allowed then w else SayNothing
        SayMore m1 m2 -> case (logfltr allowed m1, logfltr allowed m2) of
          (SayNothing, SayNothing) -> SayNothing
          (SayNothing, m) -> m
          (m, SayNothing) -> m
          (m1', m2') -> SayMore m1' m2'

      printFails = maybe True printFailures opts
      printVars = maybe False printSymbolicVars opts
      lgGoal = sayWhatFailedGoals printFails printVars >$< lgWhat
      splitResults r@(CruxSimulationResult _cmpl gls) = (snd <$> gls, r)
  in OutputConfig
     { _outputHandle = outHandle
     , _quiet = beQuiet
     , _logMsg = logMessageToSayWhat >$< lgWhat
     , _logExc = let seeRed = AC.hSetSGR errHandle
                              [ AC.SetConsoleIntensity AC.BoldIntensity
                              , AC.SetColor AC.Foreground AC.Vivid AC.Red]
                     seeCalm = AC.hSetSGR errHandle [AC.Reset]
                     dispExc = hPutStr errHandle . Ex.displayException
                 in if errShouldColor
                    then LJ.LogAction $ \e -> Ex.bracket_ seeRed seeCalm $ dispExc e
                    else LJ.LogAction $ dispExc
     , _logSimResult = \showDetails ->
                         if showDetails
                         then divide splitResults
                              lgGoal
                              (sayWhatResultStatus >$< lgWhat)
                         else sayWhatResultStatus >$< lgWhat
     , _logGoal = Seq.singleton >$< lgGoal
     }

defaultOutputConfig ::
  JSON.ToJSON msgs =>
  (msgs -> SayWhat) -> IO (Maybe OutputOptions -> OutputConfig msgs)
defaultOutputConfig logMessagesToSayWhat = do
  outWantsColor <- AC.hSupportsANSIColor stdout
  errWantsColor <- AC.hSupportsANSIColor stderr
  return $ Crux.mkOutputConfig (stdout, outWantsColor) (stderr, errWantsColor) logMessagesToSayWhat


--------------------------------------------------------------------------------

-- | For a given configuration, compute the 'FloatModeRepr'
--
-- Note that this needs to be CPS-ed because of the type parameter to the float
-- mode.  Also note that we can't use this function in
-- 'withSelectedOnlineBackend', unfortunately, because that function requires a
-- 'IsInterpretedFloatExprBuilder' constraint that we don't seem to have a way
-- to capture in this CPS-ed style.
withFloatRepr ::
  CCS.HasDefaultFloatRepr solver =>
  st s ->
  CruxOptions ->
  [solver] ->
  (forall fm .
    IsInterpretedFloatExprBuilder (WE.ExprBuilder s st (WE.Flags fm)) =>
    WE.FloatModeRepr fm ->
    IO a) ->
  IO a
withFloatRepr st cruxOpts selectedSolvers k =
  case floatMode cruxOpts of
    "real" -> k WE.FloatRealRepr
    "ieee" -> k WE.FloatIEEERepr
    "uninterpreted" -> k WE.FloatUninterpretedRepr
    "default" -> case selectedSolvers of
                   [oneSolver] -> CCS.withDefaultFloatRepr st oneSolver k
                   _           -> k WE.FloatUninterpretedRepr
    fm -> fail ("Unknown floating point mode: " ++ fm ++ "; expected one of [real|ieee|uninterpreted|default]")

-- | Parse through the options structure to determine which online backend to
-- instantiate (including the chosen floating point mode).
--
-- The choice of solver is provided as a separate argument (the
-- 'CCS.SolverOnline').  This function dispatches primarily on floating point
-- mode.  An explicit floating point mode can be provided if it has to match
-- another solver that has already started.  This code is slightly complicated
-- and duplicated because it is very hard to quantify over 'FloatModeRepr's in
-- such a way that captures the necessary 'IsInterpretedFloatExprBuilder'
-- constraints.
withSelectedOnlineBackend :: forall msgs scope st a.
  Logs msgs =>
  SupportsCruxLogMessage msgs =>
  CruxOptions ->
  NonceGenerator IO scope ->
  CCS.SolverOnline ->
  Maybe String ->
  st scope ->
  -- The string is an optional explicitly-requested float mode that supersedes the choice in
  -- the configuration (probably due to using two different online connections)
  (forall solver fm .
    ( OnlineSolver solver
    , IsInterpretedFloatExprBuilder (WE.ExprBuilder scope st (WE.Flags fm))
    ) =>
    (OnlineBackend solver scope st (WE.Flags fm) -> IO a)) ->
    IO a
withSelectedOnlineBackend cruxOpts nonceGen selectedSolver maybeExplicitFloatMode initSt k =
  case fromMaybe (floatMode cruxOpts) maybeExplicitFloatMode of
    "real" -> withOnlineBackendFM WE.FloatRealRepr
    "ieee" -> withOnlineBackendFM WE.FloatIEEERepr
    "uninterpreted" -> withOnlineBackendFM WE.FloatUninterpretedRepr
    "default" ->
      case selectedSolver of
        CCS.Yices -> withOnlineBackendFM WE.FloatRealRepr
        CCS.CVC4 -> withOnlineBackendFM WE.FloatRealRepr
        CCS.CVC5 -> withOnlineBackendFM WE.FloatRealRepr
        CCS.STP -> withOnlineBackendFM WE.FloatRealRepr
        CCS.Z3 -> withOnlineBackendFM WE.FloatIEEERepr
        CCS.Bitwuzla -> withOnlineBackendFM WE.FloatIEEERepr
    fm -> fail ("Unknown floating point mode: " ++ fm ++ "; expected one of [real|ieee|uninterpreted|default]")

  where
    withOnlineBackendFM ::
      IsInterpretedFloatExprBuilder (WE.ExprBuilder scope st (WE.Flags fm)) =>
      WE.FloatModeRepr fm ->
      IO a
    withOnlineBackendFM fm =
      do sym <- WE.newExprBuilder fm initSt nonceGen
         withSelectedOnlineBackend' cruxOpts selectedSolver sym k

withSelectedOnlineBackend' ::
  Logs msgs =>
  SupportsCruxLogMessage msgs =>
  IsInterpretedFloatExprBuilder (WE.ExprBuilder scope st fs) =>

  CruxOptions ->
  CCS.SolverOnline ->
  WE.ExprBuilder scope st fs ->
  (forall solver.
    OnlineSolver solver =>
    (OnlineBackend solver scope st fs -> IO a)) ->
    IO a
withSelectedOnlineBackend' cruxOpts selectedSolver sym k =
  let unsatCoreFeat | unsatCores cruxOpts
                    , not (yicesMCSat cruxOpts) = ProduceUnsatCores
                    | otherwise                 = NoUnsatFeatures
      extraFeatures = onlineProblemFeatures cruxOpts

   in case selectedSolver of
     CCS.Yices -> withYicesOnlineBackend sym unsatCoreFeat extraFeatures $ \bak -> do
       symCfg sym yicesEnableMCSat (yicesMCSat cruxOpts)
       case goalTimeout cruxOpts of
         Just s -> symCfg sym yicesGoalTimeout (floor s)
         Nothing -> return ()
       k bak
     CCS.CVC4 -> withCVC4OnlineBackend sym unsatCoreFeat extraFeatures $ \bak -> do
       case goalTimeout cruxOpts of
         Just s -> symCfg sym cvc4Timeout (floor (s * 1000))
         Nothing -> return ()
       k bak
     CCS.CVC5 -> withCVC5OnlineBackend sym unsatCoreFeat extraFeatures $ \bak -> do
       case goalTimeout cruxOpts of
         Just s -> symCfg sym cvc5Timeout (floor (s * 1000))
         Nothing -> return ()
       k bak
     CCS.Z3 -> withZ3OnlineBackend sym unsatCoreFeat extraFeatures $ \bak -> do
       case goalTimeout cruxOpts of
         Just s -> symCfg sym z3Timeout (floor (s * 1000))
         Nothing -> return ()
       k bak
     CCS.Bitwuzla -> withBitwuzlaOnlineBackend sym unsatCoreFeat extraFeatures $ \bak -> do
       case goalTimeout cruxOpts of
         Just s -> symCfg sym bitwuzlaTimeout (floor (s * 1000))
         Nothing -> return ()
       k bak
     CCS.STP -> do
       -- We don't have a timeout option for STP
       case goalTimeout cruxOpts of
         Just _ -> sayCrux (Log.UnsupportedTimeoutFor "STP")
         Nothing -> return ()
       withSTPOnlineBackend sym k

data ProfData sym = ProfData
  { inFrame          :: forall a. FunctionName -> IO a -> IO a
  , profExecFeatures :: [GenericExecutionFeature sym]
  , writeProf        :: IO ()
  }

noProfiling :: ProfData sym
noProfiling = ProfData
  { inFrame           = \_ x -> x
  , profExecFeatures  = []
  , writeProf         = pure ()
  }

setupProfiling :: IsSymInterface sym => sym -> CruxOptions -> IO (ProfData sym)
setupProfiling sym cruxOpts =
  do tbl <- newProfilingTable

     when (profileSolver cruxOpts) $
       startRecordingSolverEvents sym tbl

     let profSource = case inputFiles cruxOpts of
                        [f] -> f
                        _ -> "multiple files"

         profOutFile = outDir cruxOpts </> "report_data.js"
         saveProf = writeProfileReport profOutFile "crux profile" profSource
         profOpts = ProfilingOptions
                      { periodicProfileInterval = profileOutputInterval cruxOpts
                      , periodicProfileAction = saveProf
                      }

         profFilt = EventFilter
                      { recordProfiling = profileCrucibleFunctions cruxOpts
                      , recordCoverage = branchCoverage cruxOpts
                      }

     pfs <- execFeatureIf (profileCrucibleFunctions cruxOpts || branchCoverage cruxOpts)
                          (profilingFeature tbl profFilt (Just profOpts))

     pure ProfData
       { inFrame = \str -> inProfilingFrame tbl str Nothing
       , profExecFeatures = pfs
       , writeProf = saveProf tbl
       }

execFeatureIf ::
  Bool ->
  IO (GenericExecutionFeature sym) ->
  IO [GenericExecutionFeature sym]
execFeatureIf b m = if b then (:[]) <$> m else pure []

execFeatureMaybe ::
  Maybe a ->
  (a -> IO (GenericExecutionFeature sym)) ->
  IO [GenericExecutionFeature sym]
execFeatureMaybe mb m =
  case mb of
    Nothing -> pure []
    Just a  -> (:[]) <$> m a


-- | Common setup for all solver connections
setupSolver :: (IsExprBuilder sym, sym ~ WE.ExprBuilder t st fs) => CruxOptions -> Maybe FilePath -> sym -> IO ()
setupSolver cruxOpts mInteractionFile sym = do
  mapM_ (symCfg sym solverInteractionFile) (fmap T.pack mInteractionFile)

  -- Turn on hash-consing, if enabled
  when (hashConsing cruxOpts) (WE.startCaching sym)

  let outOpts = outputOptions cruxOpts
  -- The simulator verbosity is one less than our verbosity.
  -- In this way, we can say things, without the simulator also
  -- being verbose
  symCfg sym verbosity $ toInteger $ if simVerbose outOpts > 1
                                       then simVerbose outOpts - 1
                                       else 0

-- | Common code for initializing all of the requested execution features
--
-- This function is a bit funny because one feature, path satisfiability
-- checking, requires on online solver while the others do not.  In order to
-- maximally reuse code, we pass in the necessary online constraints as an extra
-- argument when we have them available (i.e., when we build an online solver)
-- and elide them otherwise.
setupExecutionFeatures :: IsSymBackend sym bak
                       => CruxOptions
                       -> bak
                       -> Maybe (SomeOnlineSolver sym bak)
                       -> IO
                       ([GenericExecutionFeature sym], ProfData sym)
setupExecutionFeatures cruxOpts bak maybeOnline = do
  let sym = backendGetSym bak
  -- Setup profiling
  let profiling = isProfiling cruxOpts
  profInfo <- if profiling then setupProfiling sym cruxOpts
                           else pure noProfiling

  -- Global timeout
  tfs <- execFeatureMaybe (globalTimeout cruxOpts) timeoutFeature

  -- Loop bound
  bfs <- execFeatureMaybe (loopBound cruxOpts) $ \i ->
          boundedExecFeature (\_ -> return (Just i)) True {- side cond: yes -}

  -- Recursion bound
  rfs <- execFeatureMaybe (recursionBound cruxOpts) $ \i ->
          boundedRecursionFeature (\_ -> return (Just i)) True {- side cond: yes -}

  -- Check path satisfiability
  psat_fs <- case maybeOnline of
    Just (SomeOnlineSolver _bak) ->
      do enableOpt <- getOptionSetting enableOnlineBackend (getConfiguration sym)
         _ <- setOpt enableOpt (checkPathSat cruxOpts)
         execFeatureIf (checkPathSat cruxOpts)
           $ pathSatisfiabilityFeature sym (considerSatisfiability bak)
    Nothing -> return []

  -- Position tracking
  trackfs <- positionTrackingFeature sym

  return (concat [tfs, profExecFeatures profInfo, bfs, rfs, psat_fs, [trackfs]], profInfo)

-- | Select the What4 solver adapter for the user's solver choice (used for
-- offline solvers)
withSolverAdapter :: CCS.SolverOffline -> (WS.SolverAdapter st -> a) -> a
withSolverAdapter solverOff k =
  case solverOff of
    CCS.Boolector -> k WS.boolectorAdapter
    CCS.DReal -> k WS.drealAdapter
    CCS.SolverOnline CCS.CVC4 -> k WS.cvc4Adapter
    CCS.SolverOnline CCS.CVC5 -> k WS.cvc5Adapter
    CCS.SolverOnline CCS.STP -> k WS.stpAdapter
    CCS.SolverOnline CCS.Yices -> k WS.yicesAdapter
    CCS.SolverOnline CCS.Z3 -> k WS.z3Adapter
    CCS.SolverOnline CCS.Bitwuzla -> k WS.bitwuzlaAdapter

withSolverAdapters :: [CCS.SolverOffline] -> ([WS.SolverAdapter st] -> a) -> a
withSolverAdapters solverOffs k =
  foldr go base solverOffs $ []
  where
    base adapters = k adapters
    go nextOff withAdapters adapters = withSolverAdapter nextOff (\adapter -> withAdapters (adapter:adapters))

-- | Parse through all of the user-provided options and start up the verification process
--
-- This figures out which solvers need to be run, and in which modes.  It takes
-- as arguments some of the results of common setup code.  It also tries to
-- minimize code duplication between the different verification paths (e.g.,
-- online vs offline solving).
runSimulator ::
  Logs msgs =>
  SupportsCruxLogMessage msgs =>
  CruxOptions ->
  SimulatorCallbacks msgs r ->
  IO r
runSimulator cruxOpts simCallback = do
  sayCrux (Log.Checking (inputFiles cruxOpts))
  createDirectoryIfMissing True (outDir cruxOpts)
  Some (nonceGen :: NonceGenerator IO s) <- newIONonceGenerator
  case CCS.parseSolverConfig cruxOpts of

    Right (CCS.SingleOnlineSolver onSolver) ->
      withSelectedOnlineBackend cruxOpts nonceGen onSolver Nothing WE.EmptyExprBuilderState $ \bak -> do
        let monline = Just (SomeOnlineSolver bak)
        setupSolver cruxOpts (pathSatSolverOutput cruxOpts) (backendGetSym bak)
        (execFeatures, profInfo) <- setupExecutionFeatures cruxOpts bak monline
        doSimWithResults cruxOpts simCallback bak execFeatures profInfo monline (proveGoalsOnline bak)

    Right (CCS.OnlineSolverWithOfflineGoals onSolver offSolver) ->
      withSelectedOnlineBackend cruxOpts nonceGen onSolver Nothing WE.EmptyExprBuilderState $ \bak -> do
        let monline = Just (SomeOnlineSolver bak)
        setupSolver cruxOpts (pathSatSolverOutput cruxOpts) (backendGetSym bak)
        (execFeatures, profInfo) <- setupExecutionFeatures cruxOpts bak monline
        withSolverAdapter offSolver $ \adapter -> do
          -- We have to add the configuration options from the solver adapter,
          -- since they weren't included in the symbolic backend configuration
          -- with the initial setup of the online solver (since it could have
          -- been a different solver)
          unless (CCS.sameSolver onSolver offSolver) $
            extendConfig (WS.solver_adapter_config_options adapter) (getConfiguration (backendGetSym bak))
          doSimWithResults cruxOpts simCallback bak execFeatures profInfo monline (proveGoalsOffline [adapter])

    Right (CCS.OnlyOfflineSolvers offSolvers) ->
      withFloatRepr (WE.EmptyExprBuilderState @s) cruxOpts offSolvers $ \floatRepr -> do
        withSolverAdapters offSolvers $ \adapters -> do
          sym <- WE.newExprBuilder floatRepr WE.EmptyExprBuilderState nonceGen
          bak <- CBS.newSimpleBackend sym
          setupSolver cruxOpts Nothing sym
          -- Since we have a bare SimpleBackend here, we have to initialize it
          -- with the options taken from the solver adapter (e.g., solver path)
          extendConfig (WS.solver_adapter_config_options =<< adapters) (getConfiguration sym)
          (execFeatures, profInfo) <- setupExecutionFeatures cruxOpts bak Nothing
          doSimWithResults cruxOpts simCallback bak execFeatures profInfo Nothing (proveGoalsOffline adapters)

    Right (CCS.OnlineSolverWithSeparateOnlineGoals pathSolver goalSolver) ->
      -- This case is probably the most complicated because it needs two
      -- separate online solvers.  The two must agree on the floating point
      -- mode.
      withSelectedOnlineBackend cruxOpts nonceGen pathSolver Nothing WE.EmptyExprBuilderState $ \pathSatBak -> do
        let sym = backendGetSym pathSatBak
        setupSolver cruxOpts (pathSatSolverOutput cruxOpts) sym
        (execFeatures, profInfo) <- setupExecutionFeatures cruxOpts pathSatBak (Just (SomeOnlineSolver pathSatBak))
        withSelectedOnlineBackend' cruxOpts goalSolver sym $ \goalBak -> do
          doSimWithResults cruxOpts simCallback pathSatBak execFeatures profInfo (Just (SomeOnlineSolver pathSatBak)) (proveGoalsOnline goalBak)

    Left rsns -> fail ("Invalid solver configuration:\n" ++ unlines rsns)


-- | Core invocation of the symbolic execution engine
--
-- This is separated out so that we don't have to duplicate the code sequence in
-- 'runSimulator'.  The strategy used to ultimately discharge proof obligations
-- is a parameter to allow this code to use either an online or offline solver
-- connection.
--
-- The main work in this function is setting up appropriate solver frames and
-- traversing the goals tree, as well as handling some reporting.
doSimWithResults ::
  forall sym bak r t st fs msgs.
  sym ~ WE.ExprBuilder t st fs =>
  IsSymBackend sym bak =>
  Logs msgs =>
  SupportsCruxLogMessage msgs =>
  CruxOptions ->
  SimulatorCallbacks msgs r ->
  bak ->
  [GenericExecutionFeature sym] ->
  ProfData sym ->
  Maybe (SomeOnlineSolver sym bak) ->
  ProverCallback sym
    {- ^ The function to use to prove goals; this is intended to be
         one of 'proveGoalsOffline' or 'proveGoalsOnline' -} ->
  IO r
doSimWithResults cruxOpts simCallback bak execFeatures profInfo monline goalProver = do
  compRef <- newIORef ProgramComplete
  glsRef <- newIORef Seq.empty

  frm <- pushAssumptionFrame bak

  SimulatorHooks setup onError interpretResult <-
    getSimulatorCallbacks simCallback
  inFrame profInfo "<Crux>" $ do
    -- perform tool-specific setup
    RunnableStateWithExtensions initSt exts <- setup bak monline
    explainFailure <- onError bak

    -- This can't be initialized with the rest of the execution features,
    -- because it is not a 'GenericExecutionFeature'.
    --
    -- Ideally, we would use language-extension-specific command extensions
    -- (e.g., LLVM commands for Crux-LLVM) and parser hooks, but that would
    -- require some refactoring to pass those in...
    let debugging = debug cruxOpts
    debugger <-
      if debugging
      then do
        let ?parserHooks = Syn.ParserHooks Applicative.empty Applicative.empty
        let cExts = Debug.voidExts
        inps <- Debug.defaultDebuggerInputs cExts
        dbg <-
          Debug.debugger
            cExts
            Debug.voidImpl
            (Debug.IntrinsicPrinters MapF.empty)
            inps
            Debug.defaultDebuggerOutputs
            UnitRepr
        pure [dbg]
      else pure []

    -- execute the simulator
    case pathStrategy cruxOpts of
      AlwaysMergePaths ->
        do res <- executeCrucible (map genericToExecutionFeature execFeatures ++ exts ++ debugger) initSt
           void $ resultCont compRef glsRef frm explainFailure (Result res)
      SplitAndExploreDepthFirst ->
        do (i,ws) <- executeCrucibleDFSPaths
                         (map genericToExecutionFeature execFeatures ++ exts ++ debugger)
                         initSt
                         (resultCont compRef glsRef frm explainFailure . Result)
           sayCrux (Log.TotalPathsExplored i)
           unless (null ws) $
             sayCrux (Log.PathsUnexplored (Seq.length ws))

  sayCrux Log.SimulationComplete
  when (isProfiling cruxOpts) $ writeProf profInfo
  result <- CruxSimulationResult <$> readIORef compRef <*> readIORef glsRef
  interpretResult bak result

 where
 failfast = proofGoalsFailFast cruxOpts

 resultCont ::
      IORef ProgramCompleteness
   -> IORef (Seq.Seq (ProcessedGoals, ProvedGoals))
   -> FrameIdentifier
   -> (Maybe (WE.GroundEvalFn t) -> LabeledPred (WE.Expr t BaseBoolType) SimError -> IO (Doc Void))
   -> Result personality (WE.ExprBuilder t st fs)
   -> IO Bool
 resultCont compRef glsRef frm explainFailure (Result res) =
   do timedOut <-
        case res of
          TimeoutResult {} ->
            do sayCrux Log.SimulationTimedOut
               writeIORef compRef ProgramIncomplete
               return True
          _ -> return False
      popUntilAssumptionFrame bak frm
      let ctx = execResultContext res
      inFrame profInfo "<Prove Goals>" $ do
        todo <- getProofObligations bak
        sayCrux $ Log.ProofObligations (LogProofObligation <$> maybe [] goalsToList todo)
        when (isJust todo) $ sayCrux Log.AttemptingProvingVCs
        (nms, proved) <- goalProver cruxOpts ctx explainFailure todo
        mgt <- provedGoalsTree (backendGetSym bak) proved
        case mgt of
          Nothing -> return (not timedOut)
          Just gt ->
            do modifyIORef glsRef (Seq.|> (nms,gt))
               return (not (timedOut || (failfast && disprovedGoals nms > 0)))

isProfiling :: CruxOptions -> Bool
isProfiling cruxOpts =
  profileCrucibleFunctions cruxOpts || profileSolver cruxOpts || branchCoverage cruxOpts

computeExitCode :: CruxSimulationResult -> ExitCode
computeExitCode (CruxSimulationResult cmpl gls) = maximum . (base:) . fmap f . toList $ gls
 where
 base = case cmpl of
          ProgramComplete   -> ExitSuccess
          ProgramIncomplete -> ExitFailure 1
 f (nms,_gl) =
  let tot = totalProcessedGoals nms
      proved = provedGoals nms
  in if proved == tot then
       ExitSuccess
     else
       ExitFailure 1