qute-0.1.0: src/Language/QBE/Simulator.hs
-- SPDX-FileCopyrightText: 2025-2026 Sören Tempel <soeren+git@soeren-tempel.net>
--
-- SPDX-License-Identifier: GPL-3.0-only
-- | This module describes the semantics of the [QBE](https://c9x.me/compile/)
-- intermediate representation using an abstract 'Simulator' monad.
-- Specifically, it abstractly describes the semantics of QBE's control-flow
-- constructs (such as functions, statements, and blocks) and instructions
-- using the primitives of this monad. The semantics can then be concretely
-- instantiated (refer to the instance of the 'Simulator' monad). This idea
-- is inspired by the paper [Flexible Instruction-Set Semantics via Abstract Monads]
-- (https://dl.acm.org/doi/10.1145/3607833).
module Language.QBE.Simulator
( BlockResult,
execInstr,
execStmt,
execBlock,
execFunc,
)
where
import Control.Monad (unless, void, when)
import Control.Monad.Error.Class (throwError)
import Data.Functor ((<&>))
import Data.List (elemIndex, uncons)
import Data.Map qualified as Map
import Data.Maybe (fromMaybe, isJust, isNothing)
import Data.Word (Word8)
import Language.QBE.Simulator.Default.Expression qualified as DE
import Language.QBE.Simulator.Default.State
import Language.QBE.Simulator.Error
import Language.QBE.Simulator.Expression qualified as E
import Language.QBE.Simulator.Memory (addrOverlap)
import Language.QBE.Simulator.State
import Language.QBE.Types qualified as QBE
-- | Execution of a 'QBE.Block' can either return (with an optional return
-- value) or it can jump to another 'QBE.Block' which will then be executed.
type BlockResult v = (Either (Maybe v) QBE.Block)
------------------------------------------------------------------------
execVolatile :: (Simulator m v) => QBE.VolatileInstr -> m ()
execVolatile (QBE.Store valTy valReg addrReg) = do
-- Since byte and half are not first-class types in the IL, they are
-- stored as words and have to be looked up as such.
val <- case valTy of
QBE.Byte -> lookupValue QBE.Word valReg
QBE.HalfWord -> lookupValue QBE.Word valReg
(QBE.Base bt) -> lookupValue bt valReg
addr <- lookupValue QBE.Long addrReg >>= toAddress
writeMemory addr valTy val
execVolatile (QBE.Blit src dst toCopy) = do
srcAddrVal <- lookupValue QBE.Long src
dstAddrVal <- lookupValue QBE.Long dst
-- TODO: Check for invalid BLITs
srcAddr <- toAddress srcAddrVal
dstAddr <- toAddress dstAddrVal
when (srcAddr /= dstAddr && addrOverlap srcAddr dstAddr toCopy) $
throwError $
OverlappingBlit srcAddr dstAddr
-- Somehow allow specialization of memory copies, e.g. for qute-symex.
when (toCopy > 0) $
mapM_
( \off -> do
srcByte <- readMemory (QBE.LSubWord QBE.UnsignedByte) (srcAddr + off)
writeMemory (dstAddr + off) QBE.Byte srcByte
)
[0 .. toCopy - 1]
execVolatile (QBE.VAStart val) = do
ptr <- lookupValue QBE.Long val >>= toAddress
stk <- activeFrame
addrs <- mapM (\v -> (v,) <$> stackSpill v) (stkVarArgs stk)
case uncons addrs of
Just ((firstValue, firstAddr), _) -> do
let valType = E.getType firstValue
valSize = fromIntegral $ QBE.extTypeByteSize valType
-- Initially, the pointer stored in our representation of the “variable
-- argument list” points one element beyond the argument list. This
-- allows us to determine the element pointer in `vaarg` by always
-- substracting the size of the requested element from the pointer.
writeMemory ptr (QBE.Base QBE.Long) $
E.fromLit (QBE.Base QBE.Long) (firstAddr + valSize)
Nothing -> pure ()
execVolatile (QBE.DBGLoc {}) = pure ()
{-# INLINEABLE execVolatile #-}
execBinaryTy ::
(Simulator m v) =>
QBE.BaseType ->
(v -> v -> Maybe v) ->
(QBE.BaseType, QBE.Value) ->
(QBE.BaseType, QBE.Value) ->
m v
execBinaryTy retTy op (lty, lhs) (rty, rhs) = do
v1 <- lookupValue lty lhs
v2 <- lookupValue rty rhs
runBinary retTy op v1 v2
execBinary ::
(Simulator m v) =>
QBE.BaseType ->
(v -> v -> Maybe v) ->
QBE.Value ->
QBE.Value ->
m v
execBinary retTy op lhs rhs =
execBinaryTy retTy op (retTy, lhs) (retTy, rhs)
{-# INLINE execBinary #-}
execShift ::
(Simulator m v) =>
QBE.BaseType ->
(v -> v -> Maybe v) ->
QBE.Value ->
QBE.Value ->
m v
execShift retTy op lhs amount =
execBinaryTy retTy op (retTy, lhs) (QBE.Word, amount)
{-# INLINE execShift #-}
-- | Execute a single 'QBE.Instr'. The 'QBE.BaseType' denotes the return value type.
-- For example, as provided in the enclosing 'QBE.Assign'.
execInstr :: (Simulator m v) => QBE.BaseType -> QBE.Instr -> m v
execInstr retTy (QBE.Neg op) = do
v <- lookupValue retTy op
liftMaybe TypingError (E.neg v)
execInstr retTy (QBE.Add lhs rhs) = execBinary retTy E.add lhs rhs
execInstr retTy (QBE.Sub lhs rhs) = execBinary retTy E.sub lhs rhs
execInstr retTy (QBE.Mul lhs rhs) = execBinary retTy E.mul lhs rhs
execInstr retTy (QBE.Div lhs rhs) = execBinary retTy E.div lhs rhs
execInstr retTy (QBE.Or lhs rhs) = execBinary retTy E.or lhs rhs
execInstr retTy (QBE.Xor lhs rhs) = execBinary retTy E.xor lhs rhs
execInstr retTy (QBE.And lhs rhs) = execBinary retTy E.and lhs rhs
execInstr retTy (QBE.URem lhs rhs) = execBinary retTy E.urem lhs rhs
execInstr retTy (QBE.Rem lhs rhs) = execBinary retTy E.srem lhs rhs
execInstr retTy (QBE.UDiv lhs rhs) = execBinary retTy E.udiv lhs rhs
execInstr retTy (QBE.Sar lhs rhs) = execShift retTy E.sar lhs rhs
execInstr retTy (QBE.Shr lhs rhs) = execShift retTy E.shr lhs rhs
execInstr retTy (QBE.Shl lhs rhs) = execShift retTy E.shl lhs rhs
execInstr retTy (QBE.Load ty addrVal) = do
addr <- lookupValue QBE.Long addrVal >>= toAddress
val <- readMemory ty addr
subType retTy val
execInstr QBE.Long (QBE.Alloc align sizeValue) = do
size <- lookupValue QBE.Long sizeValue
stackAlloc size (fromIntegral $ QBE.getSize align)
execInstr _ QBE.Alloc {} = throwError InvalidAddressType
execInstr retTy (QBE.CompareInt intArg cmpOp lhs rhs) = do
let cmpTy = QBE.i2BaseType intArg
v1 <- lookupValue cmpTy lhs
v2 <- lookupValue cmpTy rhs
let exprOp = E.compareIntExpr cmpOp
runBinary retTy exprOp v1 v2
execInstr retTy (QBE.CompareFloat floatArg cmpOp lhs rhs) = do
let cmpTy = QBE.f2BaseType floatArg
v1 <- lookupValue cmpTy lhs
v2 <- lookupValue cmpTy rhs
let exprOp = E.compareFloatExpr cmpOp
runBinary retTy exprOp v1 v2
-- exts is only valid with a double return type.
execInstr QBE.Double (QBE.Ext QBE.ExtSingle value) = do
v <- lookupValue QBE.Single value
liftMaybe TypingError $ E.extendFloat v
execInstr retTy (QBE.Ext extArg value) = do
v <- lookupValue QBE.Word value
let (isSigned, extTy) = QBE.toExtType extArg
liftMaybe
TypingError
(E.extract extTy v >>= E.extend (QBE.Base retTy) isSigned)
execInstr QBE.Single (QBE.TruncDouble value) = do
v <- lookupValue QBE.Double value
liftMaybe TypingError $ E.truncFloat v
-- truncd is only valid with a single return type.
execInstr _ (QBE.TruncDouble _) = throwError TypingError
execInstr retTy (QBE.Copy value) = lookupValue retTy value
execInstr retTy (QBE.FloatToInt floatArg isSigned value) = do
v <- lookupValue (QBE.f2BaseType floatArg) value
liftMaybe TypingError $ E.floatToInt (QBE.Base retTy) isSigned v
execInstr retTy (QBE.IntToFloat intArg isSigned value) = do
v <- lookupValue (QBE.i2BaseType intArg) value
liftMaybe TypingError $ E.intToFloat (QBE.Base retTy) isSigned v
execInstr retTy (QBE.Cast value) = do
-- We must deduce the value type to use for lookup from
-- the return type as manadated by the cast type string.
let valueType =
case retTy of
QBE.Word -> QBE.Single
QBE.Long -> QBE.Double
QBE.Single -> QBE.Word
QBE.Double -> QBE.Long
-- TODO: Consider adding an explicit operation for casting
-- of floating points to the expression language abstraction.
v <- lookupValue valueType value
pure (E.fromLit (QBE.Base retTy) $ E.toWord64 v)
execInstr retTy (QBE.VAArg argLst) = do
-- 'argsCtx' represents the “variable argument list”. Currently,
-- it is not modeled after a specific ABI but simply contains a
-- pointer to the previous argument. This pointer is updated by
-- each invocation of the `vaarg` instruction.
argsCtx <- lookupValue QBE.Long argLst >>= toAddress
prevPtr <- readMemory (QBE.LBase QBE.Long) argsCtx
let retTySize =
E.fromLit
(QBE.Base QBE.Long)
(fromIntegral $ QBE.baseTypeByteSize retTy)
-- Obtain current pointer by subtracting size from 'prevPtr'
-- and align the pointer down to the nearest aligned address.
ptrAligned <-
liftMaybe InvalidAddressType $
(prevPtr `E.sub` retTySize) >>= (`stackAlign` retTySize)
val <- toAddress ptrAligned >>= readMemory (QBE.LBase retTy)
writeMemory argsCtx (QBE.Base QBE.Long) ptrAligned
pure val
{-# INLINEABLE execInstr #-}
-- | Execute a 'QBE.Statement', usually a sequence of 'QBE.Instruction'.
-- Therefore, this function iteratively calls 'execInstr' in the common case.
execStmt :: (Simulator m v) => QBE.Statement -> m ()
execStmt (QBE.Assign name ty inst) = do
newVal <- execInstr ty inst
modifyFrame (storeLocal name newVal)
execStmt (QBE.Volatile v) = execVolatile v
execStmt (QBE.Call ret toCall params) = do
function <- lookupFunc toCall
funcArgs <- lookupArgs params
-- Sanity chekcs on funcArgs are performed by execFunc.
mayRetVal <- case function of
SFuncDef funcDef -> execFunc funcDef funcArgs
SSimFunc simFunc -> simFunc funcArgs
case mayRetVal of
Nothing ->
-- XXX: Could also check funcDef for the return value.
if isNothing ret
then pure ()
else throwError FunctionReturnIgnored
Just retVal ->
case ret of
Nothing -> throwError AssignedVoidReturnValue
Just (ident, abity) -> do
let baseTy = QBE.abityToBase abity
subTyped <- subType baseTy retVal
modifyFrame (storeLocal ident subTyped)
{-# INLINEABLE execStmt #-}
execJump :: (Simulator m v) => QBE.JumpInstr -> m (BlockResult v)
execJump QBE.Halt = throwError EncounteredHalt
execJump (QBE.Jump ident) = do
blocks <- QBE.fBlock <$> (activeFrame <&> stkFunc)
case Map.lookup ident blocks of
Just bl -> pure $ Right bl
Nothing -> throwError (UnknownBlock ident)
execJump (QBE.Jnz cond ifT ifF) = do
condValue <- lookupValue QBE.Word cond
condResult <- isTrue condValue
execJump $ QBE.Jump (if condResult then ifT else ifF)
execJump (QBE.Return v) = do
func <- activeFrame <&> stkFunc
case QBE.fAbity func of
Just abity -> do
retVal <-
case v of
Nothing -> throwError InvalidReturnValue
Just x -> pure x
lookupValue (QBE.abityToBase abity) retVal <&> (Left . Just)
Nothing ->
if isNothing v
then pure (Left Nothing)
else throwError InvalidReturnValue
{-# INLINEABLE execJump #-}
execPhi :: (Simulator m v) => Maybe QBE.BlockIdent -> QBE.Phi -> m ()
execPhi Nothing _ = throwError InvalidPhiPosition
execPhi (Just prevIdent) (QBE.Phi name ty labels) =
case Map.lookup prevIdent labels of
Nothing -> throwError (UnknownBlock prevIdent)
Just v -> do
retVal <- lookupValue ty v
modifyFrame (storeLocal name retVal)
{-# INLINEABLE execPhi #-}
-- | Execute a BasicBlock, as represented by 'QBE.Block', by iteratively
-- invoking 'execStmt'. If this isn't the first executed BasicBlock within a a
-- 'QBE.Function', then the 'QBE.BlockIdent' of the previously executed
-- BasicBlock should be provided. This is required to properly execute [phi
-- instructions](https://c9x.me/compile/doc/il-v1.2.html#Phi).
execBlock :: (Simulator m v) => Maybe QBE.BlockIdent -> QBE.Block -> m (BlockResult v)
execBlock prevIdent block = do
mapM_ (execPhi prevIdent) (QBE.phi block)
mapM_ execStmt (QBE.stmt block)
execJump (QBE.term block)
{-# INLINEABLE execBlock #-}
execTilRet :: (Simulator m v) => Maybe QBE.BlockIdent -> QBE.Block -> m (BlockResult v)
execTilRet prevIdent block = go prevIdent (Right block)
where
go _ retValue@(Left _) = pure retValue
go prevIdent' (Right nextBlock) =
execBlock prevIdent' nextBlock >>= go (Just $ QBE.label nextBlock)
{-# INLINEABLE execTilRet #-}
-- | Execute a 'QBE.FuncDef' until function return. If the function requires arguments to
-- be passed to it, these must be provided as a list. Limited sanity checking is performed
-- to ensure that the provided arguments match the declared function parameters. The return
-- value of 'execFunc' is the return value of the executed 'QBE.FuncDef'. If the function
-- has no return value, 'Nothing' is returned here.
execFunc :: (Simulator m v) => QBE.FuncDef -> [v] -> m (Maybe v)
execFunc func@(QBE.FuncDef {QBE.fParams = params}) args = do
-- Assumption: Variadic argument has been filtered from args (see lookupArgs).
let varIdxMay = elemIndex QBE.Variadic params
numNamed = fromMaybe (length args) varIdxMay
argsSane =
if isJust varIdxMay
then length args + 1 >= length params -- +1 for filtered '...'
else length params == length args
unless argsSane $
throwError (FuncArgsMismatch $ QBE.fName func)
-- Separate name and unnamed variadic arguments using 'numNamed'
-- and create a 'StackFrame' for 'func' that captures both.
let vars =
Map.fromList $
zip (map paramName $ take numNamed params) args
void $ newStackFrame func vars (drop numNamed args)
blockResult <- execTilRet Nothing (QBE.fEntry func) <* returnFromFunc
case blockResult of
Right _block -> throwError MissingFunctionReturn
Left maybeValue -> pure maybeValue
where
paramName :: QBE.FuncParam -> QBE.LocalIdent
paramName (QBE.Regular _ n) = n
paramName (QBE.Env n) = n
paramName QBE.Variadic = error "unreachable"
{-# SPECIALIZE execFunc :: QBE.FuncDef -> [DE.RegVal] -> SimState DE.RegVal Word8 (Maybe DE.RegVal) #-}
{-# INLINEABLE execFunc #-}