1-- SPDX-FileCopyrightText: 2025-2026 Sören Tempel <soeren+git@soeren-tempel.net>2--3-- SPDX-License-Identifier: GPL-3.0-only45module Main (main) where67import Control.Monad (when)8import Control.Monad.State.Strict (evalStateT, gets, liftIO)9import Data.Binary (encodeFile)10import Data.KTest (KTest (KTest), KTestObj, fromAssign)11import Data.String (fromString)12import Language.QBE.Backend.Store (Assign)13import Language.QBE.CmdLine qualified as CMD14import Language.QBE.Simulator (execFunc)15import Language.QBE.Simulator.Concolic.State (mkEnv)16import Language.QBE.Simulator.Error (EvalError)17import Language.QBE.Simulator.Explorer18 ( Engine (expLastPath),19 PathResult (pathErr, pathVars),20 defSolver,21 explorePath,22 logSolver,23 newEngine,24 )25import Language.QBE.Types qualified as QBE26import Options.Applicative qualified as OPT27import System.Directory (createDirectoryIfMissing)28import System.Exit (die)29import System.FilePath (addExtension, (</>))30import System.IO (IOMode (WriteMode), hPutStrLn, stderr, withFile)31import Text.Printf (printf)3233data Opts = Opts34 { optLog :: Maybe FilePath,35 optSeed :: Maybe Int,36 optTestDir :: Maybe FilePath,37 optErrExit :: Bool,38 optWriteAll :: Bool,39 optVerbose :: Bool,40 optBase :: CMD.BasicArgs41 }4243optTestCases :: String44optTestCases = "test-cases"4546optsParser :: OPT.Parser Opts47optsParser =48 Opts49 <$> OPT.optional50 ( OPT.strOption51 ( OPT.long "dump-smt2"52 <> OPT.short 'd'53 <> OPT.metavar "FILE"54 <> OPT.help "Output queries as an SMT-LIB file"55 )56 )57 <*> OPT.optional58 ( OPT.option59 OPT.auto60 ( OPT.long "random-seed"61 <> OPT.short 'r'62 <> OPT.help "Initial seed to for the random number generator"63 )64 )65 <*> OPT.optional66 ( OPT.strOption67 ( OPT.long optTestCases68 <> OPT.short 't'69 <> OPT.metavar "FILE"70 <> OPT.help "Directory to write generate test inputs to"71 )72 )73 <*> OPT.switch74 ( OPT.long "exit-on-error"75 <> OPT.short 'e'76 <> OPT.help "Stop exploration after encountering the first error"77 )78 <*> OPT.switch79 ( OPT.long "write-all"80 <> OPT.short 'a'81 <> OPT.help "Write tests for all paths, not just those with errors"82 )83 <*> OPT.switch84 ( OPT.long "verbose"85 <> OPT.short 'v'86 <> OPT.help "Enable more verbose output"87 )88 <*> CMD.basicArgs8990------------------------------------------------------------------------9192data LogLevel = LogAll | LogErr93 deriving (Show, Eq, Ord)9495data KTestConf96 = KTestConf97 { confLevel :: LogLevel,98 confPath :: FilePath,99 confName :: String100 }101 deriving (Show)102103mkKTestConf :: LogLevel -> FilePath -> String -> IO KTestConf104mkKTestConf level directory name = do105 createDirectoryIfMissing True directory106 pure $ KTestConf level directory name107108writeAssign :: Maybe KTestConf -> LogLevel -> Int -> Assign -> IO ()109writeAssign Nothing _ _ _ = pure ()110writeAssign (Just conf) level pathID assign111 | level >= confLevel conf = writeKTest conf pathID (fromAssign assign)112 | otherwise = pure ()113114testCasePath :: KTestConf -> Int -> FilePath115testCasePath (KTestConf {confPath = directory}) n =116 addExtension117 (directory </> ("test" ++ printf "%06d" n))118 ".ktest"119120writeKTest :: KTestConf -> Int -> [KTestObj] -> IO ()121writeKTest conf@(KTestConf {confName = name}) pathID =122 writeKTest' pathID . KTest [fromString name]123 where124 writeKTest' :: Int -> KTest -> IO ()125 writeKTest' n ktest = do126 flip encodeFile ktest $127 testCasePath conf n128129------------------------------------------------------------------------130131showError :: Maybe KTestConf -> Int -> EvalError -> IO ()132showError ktest n err = printErr133 where134 printErr = do135 hPutStrLn stderr $136 "Encountered error on path #"137 ++ show n138 ++ ": "139 ++ show err140 ++ "\n"141 ++ "-> "142 ++ printPath ktest143144 printPath :: Maybe KTestConf -> String145 printPath Nothing = "Pass --" ++ optTestCases ++ " to generate test case"146 printPath (Just kt) =147 "Refer to the KTest file in " ++ show (testCasePath kt n)148149exploreEntry :: Opts -> Maybe KTestConf -> Engine -> QBE.FuncDef -> IO Int150exploreEntry opts ktest engine entry =151 evalStateT (go 1 $ execFunc entry []) engine152 where153 go n st = do154 when (optVerbose opts) $155 liftIO (hPutStrLn stderr $ "Exploring path " ++ show n ++ "...")156 morePaths <- explorePath st157158 lastPath <- gets expLastPath159 logLevel <- case pathErr lastPath of160 Just err -> liftIO $ do161 showError ktest n err162 pure LogErr163 Nothing -> pure LogAll164165 liftIO $ do166 writeAssign ktest logLevel n (pathVars lastPath)167 when (optErrExit opts && logLevel == LogErr) $168 die "Exiting due to encountered error"169170 if morePaths171 then go (n + 1) st172 else pure n173174exploreFile :: Opts -> IO Int175exploreFile opts@Opts {optBase = base} = do176 (prog, func) <- CMD.parseEntryFile $ CMD.optQBEFile base177178 let binName = CMD.optQBEFile $ optBase opts179 logLevel = if optWriteAll opts then LogAll else LogErr180 ktest <-181 case optTestDir opts of182 Just dir -> do183 Just <$> mkKTestConf logLevel dir binName184 Nothing -> pure Nothing185186 env <- mkEnv prog (CMD.optMemStart base) (CMD.optMemSize base) (optSeed opts)187 case optLog opts of188 Just fn -> withFile fn WriteMode (exploreWithHandle ktest env func)189 Nothing -> do190 engine <- newEngine env <$> defSolver191 exploreEntry opts ktest engine func192 where193 exploreWithHandle ktest env func handle = do194 engine <- newEngine env <$> logSolver handle195 exploreEntry opts ktest engine func196197------------------------------------------------------------------------198199cmd :: OPT.ParserInfo Opts200cmd =201 OPT.info202 (optsParser OPT.<**> OPT.helper)203 ( OPT.fullDesc204 <> OPT.progDesc "Symbolic execution of programs in the QBE intermediate language"205 )206207main :: IO ()208main = do209 numPaths <- OPT.execParser cmd >>= exploreFile210 putStrLn $ "\n---\nAmount of paths: " ++ show numPaths