packages feed

qute-symex-0.1.0: src/Language/QBE/Simulator/Explorer.hs

-- SPDX-FileCopyrightText: 2025-2026 Sören Tempel <soeren+git@soeren-tempel.net>
--
-- SPDX-License-Identifier: GPL-3.0-only
module Language.QBE.Simulator.Explorer
  ( defSolver,
    logSolver,
    PathResult (..),
    Engine (expLastPath),
    newEngine,
    explorePath,
    exploreFunc,
  )
where

import Control.Applicative (empty, (<|>))
import Control.Monad.Catch (try)
import Control.Monad.IO.Class (liftIO)
import Control.Monad.State.Strict (StateT, evalStateT, get, lift, modify, put)
import Data.Map qualified as Map
import Language.QBE.Backend.DFS (PathSel, findUnexplored, newPathSel, trackTrace)
import Language.QBE.Backend.Model (Model)
import Language.QBE.Backend.Store qualified as ST
import Language.QBE.Backend.Tracer qualified as T
import Language.QBE.Simulator (execFunc)
import Language.QBE.Simulator.Concolic.State
  ( Env (envStore),
    ErrorPath (pathError, pathInput),
    ErrorState (errStore, errTracer),
    SimState (..),
    makeConcolic,
    runPath,
  )
import Language.QBE.Simulator.Error (EvalError)
import Language.QBE.Types qualified as QBE
import SimpleBV qualified as SMT
import System.Directory (findExecutable)
import System.IO (Handle)

logic :: String
logic = "QF_BV"

findSolver :: IO (String, [String])
findSolver =
  solver "bitwuzla" []
    <|> solver "z3" ["-smt2", "-in"]
    <|> solver "cvc5" ["--incremental"]
    <|> fail "no suitable sover found in PATH"
  where
    solver :: String -> [String] -> IO (String, [String])
    solver exec args = do
      r <- findExecutable exec
      maybe empty (\_ -> pure (exec, args)) r

defSolver :: IO SMT.Solver
defSolver = do
  -- l <- SMT.newLogger 0
  (solver, args) <- findSolver
  s <- SMT.newSolver solver args Nothing
  SMT.setLogic s logic
  return s

logSolver :: Handle -> IO SMT.Solver
logSolver handle = do
  l <- SMT.newLoggerWithHandle handle 0
  (solver, args) <- findSolver
  s <-
    SMT.newSolverWithConfig
      (SMT.defaultConfig solver args)
        { SMT.solverLogger = SMT.smtSolverLogger l
        }
  SMT.setLogic s logic
  return s

------------------------------------------------------------------------

data PathResult
  = PathResult
  { pathErr :: Maybe EvalError,
    pathTrace :: T.ExecTrace,
    pathVars :: ST.Assign
  }
  deriving (Show, Eq)

initPath :: PathResult
initPath = PathResult Nothing [] Map.empty

data Engine
  = Engine
  { expSolver :: SMT.Solver,
    expPathSel :: PathSel,
    expEnv :: Env,
    expLastPath :: PathResult
  }

newEngine :: Env -> SMT.Solver -> Engine
newEngine env solver =
  Engine
    { expSolver = solver,
      expPathSel = newPathSel,
      expEnv = env,
      expLastPath = initPath
    }

findNext :: [SMT.SExpr] -> T.ExecTrace -> StateT Engine IO (Maybe Model)
findNext symVars eTrace = do
  engine <- get

  let pathSel = trackTrace (expPathSel engine) eTrace
  (model, nextPathSel) <-
    liftIO $ findUnexplored (expSolver engine) symVars pathSel

  put $ engine {expPathSel = nextPathSel}
  pure model

-- TODO: Consider modelling changes of the PathSel (via findNext) and
-- changes of the Store (via ST.finalize and ST.setModel) as a StateT.
explorePath :: SimState a -> StateT Engine IO Bool
explorePath simState = do
  engine@(Engine {expEnv = env}) <- get
  maybePath <- try $ run env
  let (mayErr, eTrace, nStore) =
        case maybePath of
          Left (err :: ErrorPath) ->
            let st = pathInput err
             in (Just $ pathError err, errTracer st, errStore st)
          Right (t, s) -> (Nothing, t, s)

  -- Before finalizing the store, we can extract the variables we encountered
  -- during this concrete execution, as well as the concrete values used for
  -- these variables during the execution.
  let inputVars = ST.sexprs nStore
      varAssign = ST.cValues nStore
  put $ engine {expLastPath = PathResult mayErr eTrace varAssign}

  -- Finalize the store (declare new symbolic vars in solver) and then,
  -- based on the new solver state, solve constraints to find a new input.
  store <- liftIO $ ST.finalize (expSolver engine) nStore
  model <- findNext inputVars eTrace
  case model of
    Nothing -> pure False
    Just newModel -> do
      let nEnv = env {envStore = ST.setModel store newModel}
       in modify (\e -> e {expEnv = nEnv})
      pure True
  where
    run env = lift $ evalStateT (unSimState $ runPath simState) env

------------------------------------------------------------------------

exploreFunc ::
  Engine ->
  QBE.FuncDef ->
  [(String, QBE.ExtType)] ->
  IO [PathResult]
exploreFunc engine entry params = do
  let funcState = mapM (uncurry makeConcolic) params >>= execFunc entry
  evalStateT (exploreFunc' funcState) engine
  where
    exploreFunc' st = do
      morePaths <- explorePath st
      curEngine <- get

      let ret = expLastPath curEngine
       in if morePaths
            then (ret :) <$> exploreFunc' st
            else pure [ret]