crux-0.7: src/Crux/SVCOMP.hs
-- | This module provides various helper functionality
-- for participating in the SV-COMP comptetion, including
-- parsing comptetion set files, configuration data,
-- reporting results, etc.
{-# LANGUAGE ApplicativeDo #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
module Crux.SVCOMP where
import Config.Schema
import Control.Applicative
import Data.Aeson (ToJSON)
import qualified Data.Attoparsec.Text as Atto
import Data.Functor.Identity (Identity(..))
import Data.Kind (Type)
import Data.Map (Map)
import qualified Data.Map as Map
import Data.Text (Text)
import qualified Data.Text as Text
import qualified Data.Text.IO as Text
import qualified Data.Yaml as Yaml
import GHC.Generics (Generic)
import System.FilePath
import qualified System.FilePath.Glob as Glob
import qualified Data.Vector as V
#if MIN_VERSION_aeson(2,0,0)
import qualified Data.Aeson.KeyMap as KM
#else
import qualified Data.HashMap.Strict as HM
#endif
import Crux.Config
import Crux.Config.Common
data SVCOMPOptions (f :: Type -> Type) = SVCOMPOptions
{ svcompBlacklist :: [FilePath]
-- ^ Benchmarks to skip when evaluating in SV-COMP mode
, svcompArch :: f Arch
-- ^ The architecture assumed for the verification task
, svcompSpec :: f FilePath
-- ^ The file containing the specification text used to verify the program. Likely a .prp file.
, svcompWitnessOutput :: FilePath
-- ^ Write the witness automaton to this filename
}
data Arch = Arch32 | Arch64
deriving (Show,Eq,Ord)
archSpec :: ValueSpec Arch
archSpec =
(Arch32 <$ atomSpec "32bit") <!>
(Arch64 <$ atomSpec "64bit")
parseArch :: (Arch -> opts -> opts) -> String -> OptSetter opts
parseArch mk = \txt opts ->
case txt of
"32bit" -> Right (mk Arch32 opts)
"64bit" -> Right (mk Arch64 opts)
_ -> Left "Invalid architecture"
-- | We cannot verify an SV-COMP program without knowing the architecture and
-- the specification. Unfortunately, Crux's command-line argument parser makes
-- it somewhat awkward to require certain arguments (without defaults). As a
-- hacky workaround, we parse the command-line arguments for the
-- architecture/specification as if they were optional and later check here to
-- see if they were actually provided, throwing an error if they were not
-- provided.
processSVCOMPOptions :: SVCOMPOptions Maybe -> IO (SVCOMPOptions Identity)
processSVCOMPOptions
(SVCOMPOptions{ svcompArch, svcompSpec
, svcompBlacklist, svcompWitnessOutput}) = do
svcompArch' <- process "svcomp-arch" svcompArch
svcompSpec' <- process "svcomp-spec" svcompSpec
pure $ SVCOMPOptions{ svcompArch = svcompArch', svcompSpec = svcompSpec'
, svcompBlacklist, svcompWitnessOutput }
where
process :: String -> Maybe a -> IO (Identity a)
process optName = maybe (fail $ "A value for --" ++ optName ++ " must be provided") (pure . Identity)
svcompOptions :: Config (SVCOMPOptions Maybe)
svcompOptions = Config
{ cfgFile =
do svcompBlacklist <-
section "svcomp-blacklist" (listSpec stringSpec) []
"SV-COMP benchmark tasks to skip"
svcompArch <-
sectionMaybe "svcomp-arch" archSpec
"The architecture assumed for the verification task."
svcompSpec <-
sectionMaybe "svcomp-spec" fileSpec
"The file containing the specification text used to verify the program. Likely a .prp file."
svcompWitnessOutput <-
section "svcomp-witness-output" fileSpec "witness.graphml"
(Text.unwords [ "The file name to which the witness automaton file should be written"
, "(default: witness.graphml)."
])
return SVCOMPOptions{ .. }
, cfgEnv = []
, cfgCmdLineFlag =
[ Option [] ["svcomp-arch"]
"The architecture assumed for the verification task."
$ ReqArg "`32bit` or `64bit`"
$ parseArch
$ \a opts -> opts{ svcompArch = Just a }
, Option [] ["svcomp-spec"]
"The file containing the specification text used to verify the program. Likely a .prp file."
$ ReqArg "FILE"
$ \f opts -> Right opts{ svcompSpec = Just f }
, Option [] ["svcomp-witness-output"]
(unwords [ "The file name to which the witness automaton file should be written"
, "(default: witness.graphml)."
])
$ ReqArg "FILE"
$ \f opts -> Right opts{ svcompWitnessOutput = f }
]
}
propertyVerdict :: VerificationTask -> Maybe Bool
propertyVerdict task = foldl f Nothing (verificationProperties task)
where
comb b Nothing = Just b
comb b (Just x) = Just $! b && x
f v (CheckNoError _nm, Just b) = comb b v
f v (CheckValidFree, Just b) = comb b v
f v (CheckValidDeref, Just b) = comb b v
f v (CheckDefBehavior, Just b) = comb b v
f v (CheckNoOverflow, Just b) = comb b v
f v _ = v
data ComputedVerdict
= Verified
| Falsified
| Unknown
deriving (Eq, Generic, Ord, Show, ToJSON)
data BenchmarkSet =
BenchmarkSet
{ benchmarkName :: Text
, benchmarkTasks :: [VerificationTask]
}
deriving (Show,Eq,Ord)
data SVCompProperty
= CheckNoError Text
| CheckValidFree
| CheckValidDeref
| CheckValidMemtrack
| CheckValidMemCleanup
| CheckDefBehavior
| CheckNoOverflow
| CheckTerminates
| CoverageFQL Text
deriving (Show,Eq,Ord,Generic,ToJSON)
data SVCompLanguage
= C CDataModel
| Java
deriving (Show,Eq,Ord)
data CDataModel = ILP32 | LP64
deriving (Show,Eq,Ord)
data VerificationTask =
VerificationTask
{ verificationSourceFile :: FilePath
, verificationInputFiles :: [FilePath]
, verificationProperties :: [(SVCompProperty, Maybe Bool)]
, verificationLanguage :: SVCompLanguage
}
deriving (Show,Eq,Ord)
checkParse :: Atto.Parser SVCompProperty
checkParse = Atto.skipSpace *> Atto.string "init(main())," *> Atto.skipSpace *> ltl
where
ltl = Atto.string "LTL(" *> Atto.skipSpace *> go <* Atto.skipSpace <* Atto.string ")" <* Atto.skipSpace
go = Atto.choice
[ CheckNoError <$> (Atto.string "G ! call(" *> bracketedText <* ")")
, pure CheckTerminates <* Atto.string "F end"
, pure CheckValidFree <* Atto.string "G valid-free"
, pure CheckValidDeref <* Atto.string "G valid-deref"
, pure CheckValidMemtrack <* Atto.string "G valid-memtrack"
, pure CheckValidMemCleanup <* Atto.string "G valid-memcleanup"
, pure CheckNoOverflow <* Atto.string "G ! overflow"
, pure CheckDefBehavior <* Atto.string "G def-behavior"
]
coverParse :: Atto.Parser SVCompProperty
coverParse = CoverageFQL <$> (Atto.skipSpace *> Atto.string "init(main())," *> Atto.skipSpace *> bracketedText)
bracketedText :: Atto.Parser Text
bracketedText =
do xs <- Atto.takeTill (\x -> x `elem` ['(',')'])
(do _ <- Atto.char '('
ys <- bracketedText
_ <- Atto.char ')'
zs <- bracketedText
return (xs <> "(" <> ys <> ")" <> zs)
) <|> (return xs)
propParse :: Atto.Parser SVCompProperty
propParse = Atto.skipSpace *> Atto.choice
[ Atto.string "CHECK(" *> checkParse <* ")"
, Atto.string "COVER(" *> coverParse <* ")"
]
loadVerificationTask :: FilePath -> IO VerificationTask
loadVerificationTask fp =
Yaml.decodeFileEither fp >>= \case
Left err -> fail $ unlines ["Failure parsing YAML file: " ++ show fp, show err]
Right a -> decodeVT a
where
decodeVT :: Yaml.Value -> IO VerificationTask
decodeVT (Yaml.Object o) =
do case lookupKM "format_version" o of
Just (Yaml.String "2.0") -> return ()
_ -> fail $ unwords ["Expected verification task version 2.0 while parsing", show fp]
fs <- case lookupKM "input_files" o of
Just v -> getStrs v
Nothing -> fail $ unwords ["No 'input_files' section while parsing", show fp]
ps <- case lookupKM "properties" o of
Just v -> getProps v
Nothing -> fail $ unwords ["No 'properties' section while parsing", show fp]
lang <- case lookupKM "options" o of
Just v -> getLang v
Nothing -> fail $ unwords ["No 'options' section while parsing", show fp]
return VerificationTask
{ verificationSourceFile = fp
, verificationInputFiles = fs
, verificationProperties = ps
, verificationLanguage = lang
}
decodeVT v = fail $ unwords ["Expected Yaml object but got:", show v, "when parsing", show fp]
decodeProp p =
case Atto.parseOnly propParse p of
Left msg -> fail $ unlines [ "Could not parse property:"
, Text.unpack p
, msg
]
Right p' -> return p'
getStrs (Yaml.String f) = return [Text.unpack f]
getStrs (Yaml.Array xs) = concat <$> V.mapM getStrs xs
getStrs v = fail $ unwords ["expected strings, but got:", show v]
getProp (Yaml.Object o) =
do propf <- case lookupKM "property_file" o of
Just (Yaml.String f) -> return (Text.unpack f)
_ -> fail $ unwords ["expected string value in 'property_file' while parsing", show fp]
prop <- decodeProp =<< Text.readFile (takeDirectory fp </> propf)
verdict <- case lookupKM "expected_verdict" o of
Nothing -> return Nothing
Just (Yaml.Bool b) -> return (Just b)
Just (Yaml.String "true") -> return (Just True)
Just (Yaml.String "false") -> return (Just False)
Just _ -> fail $ unwords ["expected boolean value in 'expected_verdict' while parsing", show fp]
return (prop, verdict)
getProp _v = fail $ unwords ["expected object in 'properties' section when parsing", show fp]
getProps (Yaml.Array xs) = mapM getProp (V.toList xs)
getProps _v = fail $ unwords ["expected property array in 'properties' section when parsing", show fp]
getLang (Yaml.Object o) =
case lookupKM "language" o of
Just (Yaml.String "C") -> getCDataModel o
Just (Yaml.String "Java") -> return Java
_ -> fail $ unwords ["expected 'C' or 'Java' in 'language' while parsing", show fp]
getLang _v = fail $ unwords ["expected object in 'options' section when parsing", show fp]
getCDataModel o =
case lookupKM "data_model" o of
Just (Yaml.String "ILP32") -> return (C ILP32)
Just (Yaml.String "LP64") -> return (C LP64)
_ -> fail $ unwords ["expected 'ILP32' or 'LP64' in 'data_model' while parsing", show fp]
-- TODO: When the ecosystem widely uses aeson-2.0.0.0 or later, remove this CPP.
#if MIN_VERSION_aeson(2,0,0)
lookupKM = KM.lookup
#else
lookupKM = HM.lookup
#endif
compilePats :: [Text] -> [Glob.Pattern]
compilePats xs =
let xs' = filter (not . Text.null) $ map Text.strip xs
in map (Glob.compile . Text.unpack) xs'
loadSVCOMPBenchmarks :: CruxOptions -> IO [BenchmarkSet]
loadSVCOMPBenchmarks cruxOpts = mapM loadBenchmarkSet (inputFiles cruxOpts)
loadBenchmarkSet :: FilePath -> IO BenchmarkSet
loadBenchmarkSet fp =
do let dir = takeDirectory fp
base = takeBaseName fp
set = dir </> base <> ".set"
pats <- compilePats . Text.lines <$> Text.readFile set
fs <- concat <$> Glob.globDir pats dir
tasks <- mapM loadVerificationTask fs
return BenchmarkSet
{ benchmarkName = Text.pack base
, benchmarkTasks = tasks
}
type TaskMap = Map VerificationTask [BenchmarkSet]
-- | There is significant overlap between the various verification tasks found in
-- different benchmark sets in the SV-COMP repository. This operation collects
-- together all the potentially duplicated tasks found in a collection of benchmark sets
-- and places them in a map, ensuring there is at most one reference to each one.
-- The tasks are the keys of the map; the benchmark set(s) that referenced a given task
-- are stored in the elements the map.
deduplicateTasks :: [BenchmarkSet] -> TaskMap
deduplicateTasks = Map.unionsWith (++) . map f
where
f bs = Map.fromList
[ (t, [bs]) | t <- benchmarkTasks bs ]