1-- SPDX-FileCopyrightText: 2025-2026 Sören Tempel <soeren+git@soeren-tempel.net>2--3-- SPDX-License-Identifier: GPL-3.0-only45module Explorer (exploreTests) where67import Data.Bifunctor (second)8import Data.List (partition, sort, uncons)9import Data.Map qualified as Map10import Data.Maybe (fromJust, isJust)11import Language.QBE (Program, parseAndFind)12import Language.QBE.Backend.Store qualified as ST13import Language.QBE.Simulator.Concolic.State (mkEnv)14import Language.QBE.Simulator.Default.Expression qualified as DE15import Language.QBE.Simulator.Explorer16 ( PathResult (..),17 defSolver,18 exploreFunc,19 newEngine,20 )21import Language.QBE.Types qualified as QBE22import System.FilePath ((</>))23import Test.Tasty24import Test.Tasty.HUnit2526branchPoints :: [PathResult] -> [[Bool]]27branchPoints lst = sort $ map (\(PathResult _ t _) -> map fst t) lst2829findAssign :: [PathResult] -> [Bool] -> Maybe ST.Assign30findAssign [] _ = Nothing31findAssign ((PathResult _ eTrace a) : xs) toFind32 | map fst eTrace == toFind = Just a33 | otherwise = findAssign xs toFind3435explore' :: Program -> QBE.FuncDef -> [(String, QBE.BaseType)] -> IO [PathResult]36explore' prog entry params = do37 defEnv <- mkEnv prog 0 128 Nothing38 engine <- newEngine defEnv <$> defSolver3940 exploreFunc engine entry $ map (second QBE.Base) params4142getFuncAndProg :: FilePath -> QBE.GlobalIdent -> IO (Program, QBE.FuncDef)43getFuncAndProg fileName funcName =44 let filePath = "test" </> "testdata" </> fileName45 in readFile filePath >>= parseAndFind funcName4647------------------------------------------------------------------------4849exploreTests :: TestTree50exploreTests =51 testGroup52 "Tests for Symbolic Program Exploration"53 [ testCase "Explore' program with four execution paths" $54 do55 let qbe =56 "function $branchOnInput(w %cond1, w %cond2) {\n\57 \@jump.1\n\58 \jnz %cond1, @branch.1, @branch.2\n\59 \@branch.1\n\60 \jmp @jump.2\n\61 \@branch.2\n\62 \jmp @jump.2\n\63 \@jump.2\n\64 \jnz %cond2, @branch.3, @branch.4\n\65 \@branch.3\n\66 \ret\n\67 \@branch.4\n\68 \ret\n\69 \}"7071 (prog, funcDef) <- parseAndFind (QBE.GlobalIdent "branchOnInput") qbe72 eTraces <- explore' prog funcDef [("cond1", QBE.Word), ("cond2", QBE.Word)]7374 let branches = branchPoints eTraces75 branches @?= [[False, False], [False, True], [True, False], [True, True]],76 testCase "Unsatisfiable branches" $77 do78 let qbe =79 "function $branchOnInput(w %cond1) {\n\80 \@jump.1\n\81 \jnz %cond1, @branch.1, @branch.2\n\82 \@branch.1\n\83 \jmp @jump.2\n\84 \@branch.2\n\85 \jmp @jump.2\n\86 \@jump.2\n\87 \jnz %cond1, @branch.3, @branch.4\n\88 \@branch.3\n\89 \ret\n\90 \@branch.4\n\91 \ret\n\92 \}"9394 (prog, funcDef) <- parseAndFind (QBE.GlobalIdent "branchOnInput") qbe95 eTraces <- explore' prog funcDef [("cond1", QBE.Word)]9697 let branches = branchPoints eTraces98 branches @?= [[False, False], [True, True]],99 testCase "Branch with overflow arithmetics" $100 do101 let qbe =102 "function $branchArithmetics(w %input) {\n\103 \@start\n\104 \%cond =w add %input, 1\n\105 \jnz %input, @branch.1, @branch.2\n\106 \@branch.1\n\107 \ret\n\108 \@branch.2\n\109 \ret\n\110 \}"111112 (prog, funcDef) <- parseAndFind (QBE.GlobalIdent "branchArithmetics") qbe113 eTraces <- explore' prog funcDef [("input", QBE.Word)]114115 let branches = branchPoints eTraces116 branches @?= [[False], [True]]117118 let assign = fromJust $ findAssign eTraces [False]119 Map.lookup "input" assign @?= Just (DE.VWord 0),120 testCase "Store symbolic value and memory, load it and pass it to function" $121 do122 let qbe =123 "function $branchOnInput(w %cond1) {\n\124 \@jump.1\n\125 \jnz %cond1, @branch.1, @branch.2\n\126 \@branch.1\n\127 \jmp @jump.2\n\128 \@branch.2\n\129 \jmp @jump.2\n\130 \@jump.2\n\131 \jnz %cond1, @branch.3, @branch.4\n\132 \@branch.3\n\133 \ret\n\134 \@branch.4\n\135 \ret\n\136 \}\n\137 \function $entry(w %in) {\n\138 \@start\n\139 \%a =l alloc4 4\n\140 \storew %in, %a\n\141 \%l =w loadw %a\n\142 \call $branchOnInput(w %l)\n\143 \ret\n\144 \}"145146 (prog, funcDef) <- parseAndFind (QBE.GlobalIdent "entry") qbe147 eTraces <- explore' prog funcDef [("x", QBE.Word)]148149 let branches = branchPoints eTraces150 branches @?= [[False, False], [True, True]],151 testCase "Branch on a specific concrete 64-bit value" $152 do153 let qbe =154 "function $f(l %input.0) {\n\155 \@start\n\156 \%input.1 =l sub %input.0, 42\n\157 \jnz %input.1, @not42, @is42\n\158 \@not42\n\159 \ret\n\160 \@is42\n\161 \ret\n\162 \}"163164 (prog, funcDef) <- parseAndFind (QBE.GlobalIdent "f") qbe165 eTraces <- explore' prog funcDef [("y", QBE.Long)]166167 branchPoints eTraces @?= [[False], [True]]168 let assign = fromJust $ findAssign eTraces [False]169 Map.lookup "y" assign @?= Just (DE.VLong 42),170 testCase "Branching with subtyping" $171 do172 let qbe =173 "function $f(l %input.0, w %input.1) {\n\174 \@start\n\175 \%added =w add %input.1, 1\n\176 \%subed =w sub %added, %input.1\n\177 \%result =w add %added, %subed\n\178 \jnz %result, @b1, @b2\n\179 \@b1\n\180 \jnz %input.0, @b2, @b2\n\181 \@b2\n\182 \ret\n\183 \}"184185 (prog, funcDef) <- parseAndFind (QBE.GlobalIdent "f") qbe186 eTraces <- explore' prog funcDef [("y", QBE.Long), ("x", QBE.Word)]187188 branchPoints eTraces @?= [[False], [True, False], [True, True]],189 testCase "make a single word symbolic" $190 do191 let qbe =192 "data $name = align 1 { b \"abcd\", b 0 }\n\193 \function w $main() {\n\194 \@start\n\195 \%ptr =l alloc4 4\n\196 \call $quebex_make_symbolic(l %ptr, l 1, l 4, l $name)\n\197 \%word =w loadw %ptr\n\198 \jnz %word, @b1, @b2\n\199 \@b1\n\200 \ret 0\n\201 \@b2\n\202 \ret 1\n\203 \}"204205 (prog, funcDef) <- parseAndFind (QBE.GlobalIdent "main") qbe206 eTraces <- explore' prog funcDef []207208 length eTraces @?= 2,209 testCase "make a range of memory symbolic" $210 do211 let qbe =212 "data $name = align 1 { b \"array\", b 0 }\n\213 \function w $main() {\n\214 \@start\n\215 \%ptr =l alloc4 32\n\216 \call $quebex_make_symbolic(l %ptr, l 8, l 4, l $name)\n\217 \%word =w loadw %ptr\n\218 \jnz %word, @b1, @b2\n\219 \@b1\n\220 \%ptr =l add %ptr, 4\n\221 \%word =w loadw %ptr\n\222 \jnz %word, @b3, @b4\n\223 \@b2\n\224 \ret 1\n\225 \@b3\n\226 \ret 0\n\227 \@b4\n\228 \ret 1\n\229 \}"230231 (prog, funcDef) <- parseAndFind (QBE.GlobalIdent "main") qbe232 eTraces <- explore' prog funcDef []233234 length eTraces @?= 3,235 testCase "explore a path with an error case" $236 do237 let qbe =238 "data $.Lstring.1 = align 1 { b \"a\", b 0 }\n\239 \export\n\240 \function w $main() {\n\241 \@body\n\242 \%.1 =l alloc4 4\n\243 \call $quebex_make_symbolic(l %.1, l 1, l 4, l $.Lstring.1)\n\244 \%.2 =w loadw %.1\n\245 \%.3 =w ceqw %.2, 42\n\246 \jnz %.3, @error, @okay\n\247 \@error\n\248 \hlt\n\249 \@okay\n\250 \ret 0\n\251 \}"252253 (prog, funcDef) <- parseAndFind (QBE.GlobalIdent "main") qbe254 (wErr, woErr) <-255 partition (isJust . pathErr)256 <$> explore' prog funcDef []257258 length wErr @?= 1259 length woErr @?= 1260261 let errorVars = pathVars $ fst $ fromJust $ uncons wErr262 errorVal = Map.lookup "a1" errorVars263 errorVal @?= Just (DE.VWord 42),264 testCase "explore program with multiple paths to error" $265 do266 (prog, funcDef) <-267 getFuncAndProg268 "insertion-sort-error-on-42.qbe"269 (QBE.GlobalIdent "main")270 (wErr, woErr) <-271 partition (isJust . pathErr)272 <$> explore' prog funcDef []273274 length wErr @?= 8275 length woErr @?= 6276277 -- every path on the error case must contain 42 in its input.278 let vals = map (Map.elems . pathVars) wErr279 let has42 = elem (DE.VWord 42)280 length (filter has42 vals) @?= length wErr,281 testCase "continue exploration after single path to error" $282 do283 (prog, funcDef) <-284 getFuncAndProg285 "single-error-case.qbe"286 (QBE.GlobalIdent "main")287 (wErr, woErr) <-288 partition (isJust . pathErr)289 <$> explore' prog funcDef []290291 length wErr @?= 1292 length woErr @?= 20293294 let errorVars = pathVars $ fst $ fromJust $ uncons wErr295 errorVal = Map.lookup "prime1" errorVars296 errorVal @?= Just (DE.VWord 43),297 testCase "exploration with memory error" $298 do299 (prog, funcDef) <-300 getFuncAndProg301 "out-of-bounds-error.qbe"302 (QBE.GlobalIdent "main")303 (wErr, woErr) <-304 partition (isJust . pathErr)305 <$> explore' prog funcDef []306307 length wErr @?= 1308 length woErr @?= 3309310 let errorVars = pathVars $ fst $ fromJust $ uncons wErr311 errorVal = Map.lookup "a1" errorVars312 errorVal @?= Just (DE.VWord 0x23523929)313 ]