packages feed

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

-- SPDX-FileCopyrightText: 2025-2026 Sören Tempel <soeren+git@soeren-tempel.net>
--
-- SPDX-License-Identifier: GPL-3.0-only

module Language.QBE.Simulator.Concolic.State
  ( Env (..),
    mkEnv,
    run,
    runPath,
    makeConcolic,
    ErrorState (..),
    ErrorPath (..),
    SimState (..),
  )
where

import Control.Exception (Exception, throwIO, try)
import Control.Monad.Error.Class (MonadError, catchError, throwError)
import Control.Monad.IO.Class (MonadIO, liftIO)
import Control.Monad.State.Strict
  ( MonadState,
    StateT (StateT),
    evalStateT,
    get,
    gets,
    modify,
    runStateT,
  )
import Data.Map qualified as Map
import Data.Word (Word8)
import Language.QBE (Program)
import Language.QBE.Backend.Store qualified as ST
import Language.QBE.Backend.Tracer qualified as T
import Language.QBE.Simulator.Concolic.Expression qualified as CE
import Language.QBE.Simulator.Default.Expression qualified as DE
import Language.QBE.Simulator.Default.Funcs (lookupSimFunc)
import Language.QBE.Simulator.Default.State qualified as DS
import Language.QBE.Simulator.Error (EvalError (FuncArgsMismatch, TypingError))
import Language.QBE.Simulator.Expression qualified as E
import Language.QBE.Simulator.Memory qualified as MEM
import Language.QBE.Simulator.State
import Language.QBE.Simulator.Symbolic.Expression qualified as SE
import Language.QBE.Types qualified as QBE
import System.Random (initStdGen, mkStdGen)

data Env
  = Env
  { envBase :: DS.Env (CE.Concolic DE.RegVal) (CE.Concolic Word8),
    envTracer :: T.ExecTrace,
    envStore :: ST.Store
  }

mkEnv ::
  Program ->
  MEM.Address ->
  MEM.Size ->
  Maybe Int ->
  IO Env
mkEnv prog memStart memSize maySeed = do
  initEnv <- DS.mkEnv prog memStart memSize
  randGen <-
    case maySeed of
      Just sd -> pure $ mkStdGen sd
      Nothing -> initStdGen
  pure $ Env initEnv T.newExecTrace (ST.empty randGen)

liftState ::
  (DS.SimState (CE.Concolic DE.RegVal) (CE.Concolic Word8)) a ->
  SimState a
liftState (DS.SimState toLift) = do
  defEnv <- gets envBase

  -- XXX: Since 'toLift' is run in the DS.SimState monad, it would
  -- use its 'throwError' implementation here. We want to use the
  -- implementation of our t'SimState' though, hence we need to handle
  -- IO exceptions here.
  result <- liftIO $ try (runStateT toLift defEnv)
  case result of
    Left (e :: EvalError) -> throwError e
    Right (a, s) -> do
      modify (\ps -> ps {envBase = s})
      pure a

makeConcolic :: String -> QBE.ExtType -> SimState (CE.Concolic DE.RegVal)
makeConcolic name ty = do
  st <- gets envStore
  let (ns, cv) = ST.getConcolic st name ty
  modify (\e -> e {envStore = ns})
  pure cv

modifyTracer :: (MonadState Env m) => (T.ExecTrace -> T.ExecTrace) -> m ()
modifyTracer f =
  modify (\s@Env {envTracer = t} -> s {envTracer = f t})

makeSymbolicArray ::
  QBE.GlobalIdent ->
  [CE.Concolic DE.RegVal] ->
  SimState (Maybe (CE.Concolic DE.RegVal))
makeSymbolicArray _ [arrayPtr, numElem, elemSize, namePtr] = do
  name <- E.toString <$> (toAddress namePtr >>= readNullArray)
  vlty <- case E.toWord64 elemSize of
    1 -> pure QBE.Byte
    2 -> pure QBE.HalfWord
    4 -> pure (QBE.Base QBE.Word)
    8 -> pure (QBE.Base QBE.Long)
    _ -> throwError TypingError

  values <-
    mapM
      (\n -> makeConcolic (name ++ show n) vlty)
      [1 .. E.toWord64 numElem]

  arrayAddr <- toAddress arrayPtr
  liftState (DS.SimState $ DS.storeValues arrayAddr values) >> pure Nothing
makeSymbolicArray ident _ = throwError $ FuncArgsMismatch ident

findSimFunc :: QBE.GlobalIdent -> Maybe ([CE.Concolic DE.RegVal] -> SimState (Maybe (CE.Concolic DE.RegVal)))
findSimFunc i@(QBE.GlobalIdent "qute_make_symbolic") = Just (makeSymbolicArray i)
findSimFunc ident = lookupSimFunc ident

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

-- | State of the concolic executor with which an error was triggered
-- in the application code, which can be reproduced using this state.
data ErrorState
  = ErrorState
  { errTracer :: T.ExecTrace,
    errStore :: ST.Store
  }

-- | Exception thrown upon encountered an t'ErrorState'.
data ErrorPath
  = ErrorPath
  { pathInput :: ErrorState,
    pathError :: EvalError
  }

instance Exception ErrorPath

instance Show ErrorPath where
  show (ErrorPath _ err) = show err

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

newtype SimState a = SimState {unSimState :: StateT Env IO a}
  deriving (Functor, Applicative, Monad, MonadIO)

deriving instance MonadState Env SimState

-- Implements 'MonadError' in t'SimState' via 'IOException's. On throw,
-- it also returns the relevant executor state by encapsulting it in
-- an 'ErrorPath'.
--
-- See also: The instance for 'DS.SimState'.
instance MonadError EvalError SimState where
  throwError err = do
    Env {envTracer = t, envStore = s} <- get
    liftIO $ throwIO (ErrorPath (ErrorState t s) err)

  catchError (SimState st) handler =
    SimState $ DS.unliftCatch st (unSimState . handler)

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

instance Simulator SimState (CE.Concolic DE.RegVal) where
  isTrue value = do
    let condResult = E.toWord64 (CE.concrete value) /= 0
    case CE.symbolic value of
      Nothing -> pure condResult
      Just sexpr -> do
        -- Track the taken branch in the tracer.
        let branch = T.newBranch sexpr
        modifyTracer (\t -> T.appendBranch t condResult branch)

        pure condResult

  -- Implements address concretization as a memory model.
  toAddress CE.Concolic {CE.concrete = cv, CE.symbolic = svMaybe} =
    case svMaybe of
      Just sv ->
        case sv `E.eq` SE.fromReg cv of
          Just c -> do
            modifyTracer (`T.appendCons` c)
            pure $ E.toWord64 cv
          Nothing -> throwError TypingError
      Nothing -> pure $ E.toWord64 cv

  findFunc ident = do
    funcs <- gets (DS.envFuncs . envBase)
    pure $ case Map.lookup ident funcs of
      Just x -> Just $ SFuncDef x
      Nothing -> SSimFunc <$> findSimFunc ident
  findFuncByAddr addr = do
    fptrs <- gets (DS.envFuncAddrs . envBase)
    case Map.lookup addr fptrs of
      Just fn -> findFunc fn
      Nothing -> pure Nothing

  lookupSymbol = liftState . lookupSymbol
  activeFrame = liftState activeFrame
  pushStackFrame = liftState . pushStackFrame
  popStackFrame = liftState popStackFrame
  getSP = liftState getSP
  setSP = liftState . setSP

  writeMemory a t v = liftState (writeMemory a t v)
  readMemory t a = liftState (readMemory t a)

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

runPath :: SimState a -> SimState (T.ExecTrace, ST.Store)
runPath state = do
  _ <- state
  t <- gets envTracer
  s <- gets envStore
  pure (t, s)

run :: Env -> SimState a -> IO (T.ExecTrace, ST.Store)
run env state = evalStateT (unSimState $ runPath state) env