packages feed

crux-0.7.2: src/Crux/Config/Common.hs

{-# LANGUAGE ApplicativeDo #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE ImplicitParams #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE DataKinds #-}

module Crux.Config.Common (
  OutputOptions(..),
  CruxOptions(..),
  PathStrategy(..),
  cruxOptions,
  defaultOutputOptions,
  postprocessOptions,
) where

import Control.Lens (set)
import Data.Functor.Alt
import Data.Generics.Product.Fields (field)
import Data.Time(DiffTime, NominalDiffTime)
import Data.Maybe(fromMaybe)
import Data.Char(toLower)
import Data.Word (Word64)
import Data.Text (pack)
import GHC.Generics (Generic)
import System.Directory ( createDirectoryIfMissing )

import Crux.Config
import Crux.Config.Load (ColorOptions(..))
import Crux.Log as Log
import Config.Schema

import What4.ProblemFeatures

data PathStrategy
  = AlwaysMergePaths
  | SplitAndExploreDepthFirst

pathStrategySpec :: ValueSpec PathStrategy
pathStrategySpec =
  (AlwaysMergePaths <$ atomSpec "always-merge") <!>
  (SplitAndExploreDepthFirst <$ atomSpec "split-dfs")


postprocessOptions ::
  Logs msgs =>
  SupportsCruxLogMessage msgs =>
  CruxOptions -> IO CruxOptions
postprocessOptions =
  checkPathStrategyInteractions >>
  checkPathSatInteractions >>
  checkBldDir

checkPathStrategyInteractions ::
  Logs msgs =>
  SupportsCruxLogMessage msgs =>
  CruxOptions -> IO CruxOptions
checkPathStrategyInteractions crux =
  case pathStrategy crux of
    AlwaysMergePaths -> return crux
    SplitAndExploreDepthFirst
     | profileCrucibleFunctions crux || branchCoverage crux ->
         do sayCrux Log.DisablingProfilingIncompatibleWithPathSplitting
            return crux { profileCrucibleFunctions = False, branchCoverage = False }
     | otherwise -> return crux

checkPathSatInteractions ::
  Logs msgs =>
  SupportsCruxLogMessage msgs =>
  CruxOptions -> IO CruxOptions
checkPathSatInteractions crux =
  case checkPathSat crux of
    True -> return crux
    False
      | branchCoverage crux ->
          do sayCrux Log.DisablingBranchCoverageRequiresPathSatisfiability
             return crux { branchCoverage = False }
      | otherwise -> return crux

checkBldDir :: CruxOptions -> IO CruxOptions
checkBldDir crux = do
  createDirectoryIfMissing True $ bldDir crux
  return crux


data OutputOptions = OutputOptions
  { colorOptions :: ColorOptions

  , printFailures :: Bool
    -- ^ Print errors regarding failed verification goals

  , printSymbolicVars :: Bool
    -- ^ Print values assigned to symbolic variables
    --   when printing failed verification goals

  , simVerbose :: Int
    -- ^ Level of verbosity for the symbolic simulation

  , quietMode :: Bool
    -- ^ If true, produce minimal output

  }
  deriving (Generic)


defaultOutputOptions :: ColorOptions -> OutputOptions
defaultOutputOptions copts = OutputOptions
  { colorOptions = copts
  , printFailures = False
  , printSymbolicVars = False
  , quietMode = False
  , simVerbose = 0
  }


-- | Common options for Crux-based binaries.
data CruxOptions = CruxOptions
  { inputFiles              :: [FilePath]
    -- ^ the files to analyze

  , outDir                  :: FilePath
    -- ^ Write results in this location.
    -- If empty, then do not produce any analysis results.

  , bldDir     :: FilePath
    -- ^ Directory for writing files generated from the inputFiles set
    -- (e.g. building C sources into LLVM bitcode files) for analysis.

  , checkPathSat             :: Bool
    -- ^ Should we enable path satisfiability checking?

  , profileCrucibleFunctions :: Bool
  , profileSolver            :: Bool

  , branchCoverage           :: Bool

  , pathStrategy             :: PathStrategy

  , globalTimeout            :: Maybe NominalDiffTime
  , goalTimeout              :: Maybe DiffTime
  , profileOutputInterval    :: NominalDiffTime

  , loopBound                :: Maybe Word64
    -- ^ Should we artifically bound the number of loop iterations

  , recursionBound           :: Maybe Word64
    -- ^ Should we artifically bound the number of recursive calls to functions?

  , makeCexes                :: Bool
    -- ^ Should we construct counter-example executables

  , unsatCores               :: Bool
    -- ^ Should we attempt to compute unsatisfiable cores for successful
    --   proofs?
  , getNAbducts               :: Maybe Word64
    -- ^ How many abducts should we get from the solver for sat results?
    --   Only works with cvc5
  , solver                   :: String
    -- ^ Solver to use to discharge proof obligations
  , pathSatSolver            :: Maybe String
    -- ^ A separate solver to use for path satisfiability checking if that
    -- feature is enabled and if the path satisfiability checking solver should
    -- differ from the solver used to discharge proof obligations (default: use
    -- the proof obligation solver)
  , forceOfflineGoalSolving  :: Bool
    -- ^ Force goals to be verified using an offline solver instance, even if it
    -- would have been possible to use the same solver in online mode

  , pathSatSolverOutput      :: Maybe FilePath
    -- ^ The file to store the interaction session between the path
    -- satisfiability checker and the solver (if path satisfiability checking is
    -- enabled)
  , onlineSolverOutput       :: Maybe FilePath
    -- ^ The file to store the interaction with the online goal solver (if
    -- solving is being performed online)
  , offlineSolverOutput      :: Maybe FilePath
    -- ^ A template to use for files to store interactions with offline goal
    -- solvers (if solving is being performed offline).

  , yicesMCSat               :: Bool
    -- ^ Should the MC-SAT Yices solver be enabled (disables unsat cores; default: no)

  , floatMode                :: String
    -- ^ Tells the solver which representation to use for floating point values.

  , proofGoalsFailFast       :: Bool
    -- ^ If true, stop attempting to prove goals as soon as one is disproved

  , skipReport               :: Bool
    -- ^ Don't produce the HTML reports that describe the verification task

  , skipSuccessReports       :: Bool
    -- ^ Skip reporting on successful proof obligations

  , skipIncompleteReports    :: Bool
    -- ^ Skip reporting on goals that arise from resource exhaustion

  , hashConsing              :: Bool
    -- ^ Turn on hash-consing in the symbolic expression backend

  , onlineProblemFeatures    :: ProblemFeatures
    -- ^ Problem Features to force in online solvers

  , outputOptions            :: OutputOptions
    -- ^ Output options grouped together for easy transfer to the logger

  , debug                    :: Bool
    -- ^ Drop into the Crucible debugger before simulation begins

  }
  deriving (Generic)



cruxOptions :: Config CruxOptions
cruxOptions = Config
  { cfgFile =
       do inputFiles <-
            section "files" (oneOrList fileSpec) []
            "Input files to process."

          outDir <-
            section "output-directory" dirSpec ""
            "Save results in this directory."

          bldDir <-
            section "build-dir" dirSpec "crux-build"
            "Directory in which to create files generated from the inputFiles."

          checkPathSat <-
            section "path-sat" yesOrNoSpec False
            "Enable path satisfiability checking (default: no)."

          profileCrucibleFunctions <-
            section "profile-crucible" yesOrNoSpec False
            "Profile crucible function entry/exit. (default: no)"

          profileSolver <-
            section "profile-solver" yesOrNoSpec False
            "Profile solver events. (default: no)"

          branchCoverage <-
            section "branch-coverage" yesOrNoSpec False
            "Record branch coverage information. (default: no)"

          profileOutputInterval <-
            section "profiling-period" fractionalSpec 5
            "Time between intermediate profile data reports (default: 5 seconds)"

          globalTimeout <-
            sectionMaybe "timeout" fractionalSpec
            "Stop executing the simulator after this many seconds."

          goalTimeout <-
            sectionMaybe "goal-timeout" fractionalSpec
            "Stop trying to prove a goal after this many seconds."

          loopBound <-
            sectionMaybe "iteration-bound" numSpec
            "Bound all loops to at most this many iterations."

          recursionBound <-
            sectionMaybe "recursion-bound" numSpec
            "Bound the number of recursive calls to at most this many calls."

          pathStrategy <-
            section "path-strategy" pathStrategySpec AlwaysMergePaths
            "Simulator strategy for path exploration."

          makeCexes <-
            section "make-executables" yesOrNoSpec True
            "Should we generate counter-example executables. (default: yes)"

          unsatCores <-
            section "unsat-cores" yesOrNoSpec True
            "Should we attempt to compute unsatisfiable cores for successfult proofs (default: yes)"

          getNAbducts <-
            sectionMaybe "get-abducts" numSpec
            "How many abducts should we get from the solver for sat results? Only works with cvc5, 0 otherwise."
          solver <-
            section "solver" stringSpec "yices"
            (pack $ "Select the solver to use to discharge proof obligations. " ++
             "May be a single solver, a comma-separated list of solvers, or the string \"all\". " ++
             "Specifying multiple solvers requires the --force-offline-goal-solving option. " ++
             "(default: \"yices\")")

          pathSatSolver <-
            sectionMaybe "path-sat-solver" stringSpec
            "Select the solver to use for path satisfiability checking (if different from `solver` and required by the `path-sat` option). (default: use `solver`)"

          forceOfflineGoalSolving <-
            section "force-offline-goal-solving" yesOrNoSpec False
            "Force goals to be solved using an offline solver, even if the selected solver could have been used in online mode (default: no)"

          pathSatSolverOutput <-
            sectionMaybe "path-sat-solver-output" stringSpec
            "The file to store the interaction with the path satisfiability solver (if enabled and different from the main solver) (default: none)"

          onlineSolverOutput <-
            sectionMaybe "online-solver-output" stringSpec
            "The file to store the interaction with the online goal solver (if any) (default: none)"

          offlineSolverOutput <-
            sectionMaybe "offine-solver-output" stringSpec
            (pack offlineSolverOutputHelp)

          simVerbose <-
            section "sim-verbose" numSpec 1
            "Verbosity of simulators. (default: 1)"

          yicesMCSat <-
            section "mcsat" yesOrNoSpec False
            "Enable the MC-SAT solver in Yices (disables unsat cores) (default: no)"

          floatMode <-
            section "floating-point" stringSpec "default"
            (pack $ "Select floating point representation,"
             ++ " i.e. one of [real|ieee|uninterpreted|default]. "
             ++ "Default representation is solver specific: [cvc4|yices]=>real, z3=>ieee.")

          hashConsing <-
            section "hash-consing" yesOrNoSpec False
            "Enable hash-consing in the symbolic expression backend"

          printFailures <-
            section "print-failures" yesOrNoSpec True
            "Print error messages regarding failed verification goals"

          printSymbolicVars <-
            section "print-symbolic-vars" yesOrNoSpec False
            ("Print values assigned to symbolic variables " <>
             "when printing failed verification goals")

          skipReport <-
            section "skip-report" yesOrNoSpec False
            "Skip producing the HTML report after verification"

          skipSuccessReports <-
            section "skip-success-reports" yesOrNoSpec False
            "Skip reporting on successful proof obligations"

          skipIncompleteReports <-
            section "skip-incomplete-reports" yesOrNoSpec False
            "Skip reporting on proof obligations that arise from timeouts and resource exhaustion"

          noColors <-
            section "no-colors" yesOrNoSpec False
            "Suppress color codes in both the output and the errors"

          noColorsErr <-
            section "no-colors-err" yesOrNoSpec False
            "Suppress color codes in the errors"

          noColorsOut <-
            section "no-colors-out" yesOrNoSpec False
            "Suppress color codes in the output"

          quietMode <-
            section "quiet-mode" yesOrNoSpec False
            "If true, produce minimal output"

          proofGoalsFailFast <-
            section "proof-goals-fail-fast" yesOrNoSpec False
            "If true, stop attempting to prove goals as soon as one of them is disproved"

          onlineProblemFeatures <- pure noFeatures

          debug <-
            section "debug" yesOrNoSpec False
            "Drop into the Crucible debugger before simulation begins"

          pure CruxOptions
            { outputOptions = OutputOptions
              { colorOptions = ColorOptions
                { noColorsErr = noColorsErr || noColors
                , noColorsOut = noColorsOut || noColors
                },
                ..
              },
              ..
            }


  , cfgEnv =
      [
        EnvVar "BLDDIR" "Directory for writing build files generated from the input files."
        $ \v opts -> Right opts { bldDir = v }
      ]

  , cfgCmdLineFlag =

      [ Option "d" ["sim-verbose"]
        "Set simulator verbosity level."
        $ ReqArg "NUM" $ parsePosNum "NUM" $ \v -> set (field @"outputOptions" . field @"simVerbose") v

      , Option [] ["path-sat"]
        "Enable path satisfiability checking"
        $ NoArg $ \opts -> Right opts { checkPathSat = True }

      , Option [] ["output-directory"]
        "Location for reports. If unset, no reports will be generated."
        $ ReqArg "DIR" $ \v opts -> Right opts { outDir = v }

      , Option [] ["profile-crucible"]
        "Enable profiling of crucible function entry/exit"
        $ NoArg $ \opts -> Right opts { profileCrucibleFunctions = True }

      , Option [] ["profile-solver"]
        "Enable profiling of solver events"
        $ NoArg $ \opts -> Right opts { profileSolver = True }

      , Option [] ["branch-coverage"]
        "Record branch coverage information"
        $ NoArg $ \opts -> Right opts { branchCoverage = True }

      , Option "t" ["timeout"]
        "Stop executing the simulator after this many seconds (default: 60)"
        $ OptArg "SECS"
        $ dflt "60"
        $ parseNominalDiffTime "seconds"
        $ \v opts -> opts { globalTimeout = Just v }

      , Option [] ["goal-timeout"]
        "Stop trying to prove each goal after this many seconds (default: 10)."
        $ OptArg "SECS"
        $ dflt "10"
        $ parseDiffTime "seconds"
        $ \v opts -> opts { goalTimeout = Just v }

      , Option "" ["path-strategy"]
        "Strategy to use for exploring paths ('always-merge' or 'split-dfs')"
        $ ReqArg "strategy"
        $ parsePathStrategy
        $ \strat opts -> opts{ pathStrategy = strat }

      , Option "p" ["profiling-period"]
        "Time between intermediate profile data reports (default: 5 seconds)"
        $ OptArg "SECS"
        $ dflt "5"
        $ parseNominalDiffTime "seconds"
        $ \v opts -> opts { profileOutputInterval = v }

      , Option "i" ["iteration-bound"]
        "Bound all loops to at most this many iterations"
        $ ReqArg "ITER"
        $ parsePosNum "iterations"
        $ \v opts -> opts { loopBound = Just v }

      , Option "r" ["recursion-bound"]
        "Bound all recursive calls to at most this many calls"
        $ ReqArg "CALLS"
        $ parsePosNum "calls"
        $ \v opts -> opts { recursionBound = Just v }

      , Option "x" ["no-execs"]
        "Disable generating counter-example executables"
        $ NoArg $ \opts -> Right opts { makeCexes = False }

      , Option [] ["no-unsat-cores"]
        "Disable computing unsat cores for successful proofs"
        $ NoArg $ \opts -> Right opts { unsatCores = False }

      , Option "n" ["get-abducts"]
        "Get these many abducts. Only works with cvc5, 0 otherwise."
        $ ReqArg "ABDUCTS"
        $ parsePosNum "abducts"
        $ \v opts -> opts { getNAbducts = Just v }

      , Option "s" ["solver"]
        ("Select the solver to use to discharge proof obligations. " ++
         "May be a single solver, a comma-separated list of solvers, or the string \"all\". " ++
         "Specifying multiple solvers requires the --force-offline-goal-solving option. " ++
         "(default: \"yices\")")
        $ ReqArg "SOLVER" $ \v opts -> Right opts { solver = map toLower v }

      , Option [] ["path-sat-solver"]
        "Select the solver to use for path satisfiability checking (if different from `solver` and required by the `path-sat` option). (default: use `solver`)"
        $ OptArg "SOLVER" $ \ms opts -> Right opts { pathSatSolver = ms }

      , Option [] ["force-offline-goal-solving"]
        "Force goals to be solved using an offline solver, even if the selected solver could have been used in online mode (default: no)"
        $ NoArg $ \opts -> Right opts { forceOfflineGoalSolving = True }

      , Option [] ["path-sat-solver-output"]
        "The file to store the interaction with the path satisfiability solver (if enabled and different from the main solver) (default: none)"
        $ ReqArg "FILE" $ \f opts -> Right opts { pathSatSolverOutput = Just f }

      , Option [] ["online-solver-output"]
        "The file to store the interaction with the online goal solver (if any) (default: none)"
        $ ReqArg "FILE" $ \f opts -> Right opts { onlineSolverOutput = Just f }

      , Option [] ["offline-solver-output"]
        offlineSolverOutputHelp
        $ ReqArg "FILE" $ \f opts -> Right opts { offlineSolverOutput = Just f }

      , Option [] ["mcsat"]
        "Enable the MC-SAT solver in Yices (disables unsat cores)"
        $ NoArg $ \opts -> Right opts { yicesMCSat = True }

      , Option [] ["skip-report"]
        "Skip producing the HTML report following verificaion"
        $ NoArg $ \opts -> Right opts { skipReport = True }

      , Option [] ["skip-success-reports"]
        "Skip reporting on successful proof obligations"
        $ NoArg $ \opts -> Right opts { skipSuccessReports = True }

      , Option [] ["skip-incomplete-reports"]
        "Skip reporting on proof obligations that arise from timeouts and resource exhaustion"
        $ NoArg $ \opts -> Right opts { skipIncompleteReports = True }

      , Option [] ["hash-consing"]
        "Enable hash-consing in the symbolic expression backend"
        $ NoArg $ \opts -> Right opts{ hashConsing = True }

      , Option [] ["skip-print-failures"]
        "Skip printing messages related to failed verification goals"
        $ NoArg $ Right . set (field @"outputOptions" . field @"printFailures") False

      , Option [] ["fail-fast"]
        "Stop attempting to prove goals as soon as one of them is disproved"
        $ NoArg $ \opts -> Right opts { proofGoalsFailFast = True }

      , Option "q" ["quiet"]
        "Quiet mode; produce minimal output"
        $ NoArg $ Right . set (field @"outputOptions" . field @"quietMode") True

      , Option "f" ["floating-point"]
        ("Select floating point representation,"
         ++ " i.e. one of [real|ieee|uninterpreted|default]. "
         ++ "Default representation is solver specific: [cvc4|yices]=>real, z3=>ieee.")
        $ ReqArg "FPREP" $ \v opts -> Right opts { floatMode = map toLower v }

      , Option [] ["debug"]
        "Drop into the Crucible debugger before simulation begins"
        $ NoArg $ \opts -> Right opts { debug = True }
      ]
  }

offlineSolverOutputHelp :: String
offlineSolverOutputHelp = unlines
  [ "A template to use for files to store interactions with offline goal solvers"
  , "(if any) (default: none). For example, if the template is `offline-output.smt2`,"
  , "then each file will be named `offline-output-<goal number>-<solver name>.smt2,"
  , "where <goal number> is the number of the goal that was proven (starting at 0)"
  , "and <solver name> is the name of the solver used to dispatch that goal."
  ]

dflt :: String -> (String -> OptSetter opts) -> (Maybe String -> OptSetter opts)
dflt x p mb = p (fromMaybe x mb)

parseDiffTime ::
  String -> (DiffTime -> opts -> opts) -> String -> OptSetter opts
parseDiffTime thing mk =
  parsePosNum thing (\a opts -> mk (cvt a) opts)
  where cvt :: Double -> DiffTime
        cvt = fromRational . toRational

parseNominalDiffTime ::
  String -> (NominalDiffTime -> opts -> opts) -> String -> OptSetter opts
parseNominalDiffTime thing mk =
  parsePosNum thing (\a opts -> mk (cvt a) opts)
  where cvt :: Double -> NominalDiffTime
        cvt = fromRational . toRational

parsePathStrategy ::
  (PathStrategy -> opts -> opts) -> String -> OptSetter opts
parsePathStrategy mk "always-merge" opts = Right $ mk AlwaysMergePaths opts
parsePathStrategy mk "split-dfs"    opts = Right $ mk SplitAndExploreDepthFirst opts
parsePathStrategy _mk nm _opts = Left ("Unknown path strategy: " ++ show nm)