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------------------------------------------------------------------------130131handleError :: Opts -> Maybe KTestConf -> Int -> EvalError -> IO ()132handleError opts ktest n err = do133 printErr134 when (optErrExit opts) $135 die "Exiting due to encountered error"136 where137 printErr = do138 hPutStrLn stderr $139 "Encoundered error on path #"140 ++ show n141 ++ ": "142 ++ show err143 ++ "\n"144 ++ "↳ "145 ++ printPath ktest146147 printPath :: Maybe KTestConf -> String148 printPath Nothing = "Pass --" ++ optTestCases ++ " to generate test case"149 printPath (Just kt) =150 "Refer to the KTest file in " ++ show (testCasePath kt n)151152exploreEntry :: Opts -> Maybe KTestConf -> Engine -> QBE.FuncDef -> IO Int153exploreEntry opts ktest engine entry =154 evalStateT (go 1 $ execFunc entry []) engine155 where156 go n st = do157 when (optVerbose opts) $158 liftIO (putStrLn $ "Exploring path " ++ show n ++ "...")159 morePaths <- explorePath st160161 lastPath <- gets expLastPath162 logLevel <- case pathErr lastPath of163 Just err -> liftIO $ do164 handleError opts ktest n err165 pure LogErr166 Nothing -> pure LogAll167168 liftIO $ writeAssign ktest logLevel n (pathVars lastPath)169 if morePaths170 then go (n + 1) st171 else pure n172173exploreFile :: Opts -> IO Int174exploreFile opts@Opts {optBase = base} = do175 (prog, func) <- CMD.parseEntryFile $ CMD.optQBEFile base176177 let binName = CMD.optQBEFile $ optBase opts178 logLevel = if optWriteAll opts then LogAll else LogErr179 ktest <-180 case optTestDir opts of181 Just dir -> do182 Just <$> mkKTestConf logLevel dir binName183 Nothing -> pure Nothing184185 env <- mkEnv prog (CMD.optMemStart base) (CMD.optMemSize base) (optSeed opts)186 case optLog opts of187 Just fn -> withFile fn WriteMode (exploreWithHandle ktest env func)188 Nothing -> do189 engine <- newEngine env <$> defSolver190 exploreEntry opts ktest engine func191 where192 exploreWithHandle ktest env func handle = do193 engine <- newEngine env <$> logSolver handle194 exploreEntry opts ktest engine func195196------------------------------------------------------------------------197198cmd :: OPT.ParserInfo Opts199cmd =200 OPT.info201 (optsParser OPT.<**> OPT.helper)202 ( OPT.fullDesc203 <> OPT.progDesc "Symbolic execution of programs in the QBE intermediate language"204 )205206main :: IO ()207main = do208 numPaths <- OPT.execParser cmd >>= exploreFile209 putStrLn $ "\n---\nAmount of paths: " ++ show numPaths