1-- SPDX-FileCopyrightText: 2025-2026 Sören Tempel <soeren+git@soeren-tempel.net>2--3-- SPDX-License-Identifier: GPL-3.0-only45-- | This module describes the semantics of the [QBE](https://c9x.me/compile/)6-- intermediate representation using an abstract 'Simulator' monad.7-- Specifically, it abstractly describes the semantics of QBE's control-flow8-- constructs (such as functions, statements, and blocks) and instructions9-- using the primitives of this monad. The semantics can then be concretely10-- instantiated (refer to the instance of the 'Simulator' monad). This idea11-- is inspired by the paper [Flexible Instruction-Set Semantics via Abstract Monads]12-- (https://dl.acm.org/doi/10.1145/3607833).13module Language.QBE.Simulator14 ( BlockResult,15 execInstr,16 execStmt,17 execBlock,18 execFunc,19 )20where2122import Control.Monad (unless, void, when)23import Control.Monad.Error.Class (throwError)24import Data.Functor ((<&>))25import Data.List (elemIndex, uncons)26import Data.Map qualified as Map27import Data.Maybe (fromMaybe, isJust, isNothing)28import Data.Word (Word8)29import Language.QBE.Simulator.Default.Expression qualified as DE30import Language.QBE.Simulator.Default.State31import Language.QBE.Simulator.Error32import Language.QBE.Simulator.Expression qualified as E33import Language.QBE.Simulator.Memory (addrOverlap)34import Language.QBE.Simulator.State35import Language.QBE.Types qualified as QBE3637-- | Execution of a 'QBE.Block' can either return (with an optional return38-- value) or it can jump to another 'QBE.Block' which will then be executed.39type BlockResult v = (Either (Maybe v) QBE.Block)4041------------------------------------------------------------------------4243execVolatile :: (Simulator m v) => QBE.VolatileInstr -> m ()44execVolatile (QBE.Store valTy valReg addrReg) = do45 -- Since byte and half are not first-class types in the IL, they are46 -- stored as words and have to be looked up as such.47 val <- case valTy of48 QBE.Byte -> lookupValue QBE.Word valReg49 QBE.HalfWord -> lookupValue QBE.Word valReg50 (QBE.Base bt) -> lookupValue bt valReg5152 addr <- lookupValue QBE.Long addrReg >>= toAddress53 writeMemory addr valTy val54execVolatile (QBE.Blit src dst toCopy) = do55 srcAddrVal <- lookupValue QBE.Long src56 dstAddrVal <- lookupValue QBE.Long dst5758 -- TODO: Check for invalid BLITs59 srcAddr <- toAddress srcAddrVal60 dstAddr <- toAddress dstAddrVal61 when (srcAddr /= dstAddr && addrOverlap srcAddr dstAddr toCopy) $62 throwError $63 OverlappingBlit srcAddr dstAddr6465 -- Somehow allow specialization of memory copies, e.g. for qute-symex.66 when (toCopy > 0) $67 mapM_68 ( \off -> do69 srcByte <- readMemory (QBE.LSubWord QBE.UnsignedByte) (srcAddr + off)70 writeMemory (dstAddr + off) QBE.Byte srcByte71 )72 [0 .. toCopy - 1]73execVolatile (QBE.VAStart val) = do74 ptr <- lookupValue QBE.Long val >>= toAddress75 stk <- activeFrame7677 addrs <- mapM (\v -> (v,) <$> stackSpill v) (stkVarArgs stk)78 case uncons addrs of79 Just ((firstValue, firstAddr), _) -> do80 let valType = E.getType firstValue81 valSize = fromIntegral $ QBE.extTypeByteSize valType8283 -- Initially, the pointer stored in our representation of the “variable84 -- argument list” points one element beyond the argument list. This85 -- allows us to determine the element pointer in `vaarg` by always86 -- substracting the size of the requested element from the pointer.87 writeMemory ptr (QBE.Base QBE.Long) $88 E.fromLit (QBE.Base QBE.Long) (firstAddr + valSize)89 Nothing -> pure ()90execVolatile (QBE.DBGLoc {}) = pure ()91{-# INLINEABLE execVolatile #-}9293execBinaryTy ::94 (Simulator m v) =>95 QBE.BaseType ->96 (v -> v -> Maybe v) ->97 (QBE.BaseType, QBE.Value) ->98 (QBE.BaseType, QBE.Value) ->99 m v100execBinaryTy retTy op (lty, lhs) (rty, rhs) = do101 v1 <- lookupValue lty lhs102 v2 <- lookupValue rty rhs103 runBinary retTy op v1 v2104105execBinary ::106 (Simulator m v) =>107 QBE.BaseType ->108 (v -> v -> Maybe v) ->109 QBE.Value ->110 QBE.Value ->111 m v112execBinary retTy op lhs rhs =113 execBinaryTy retTy op (retTy, lhs) (retTy, rhs)114{-# INLINE execBinary #-}115116execShift ::117 (Simulator m v) =>118 QBE.BaseType ->119 (v -> v -> Maybe v) ->120 QBE.Value ->121 QBE.Value ->122 m v123execShift retTy op lhs amount =124 execBinaryTy retTy op (retTy, lhs) (QBE.Word, amount)125{-# INLINE execShift #-}126127-- | Execute a single 'QBE.Instr'. The 'QBE.BaseType' denotes the return value type.128-- For example, as provided in the enclosing 'QBE.Assign'.129execInstr :: (Simulator m v) => QBE.BaseType -> QBE.Instr -> m v130execInstr retTy (QBE.Neg op) = do131 v <- lookupValue retTy op132 liftMaybe TypingError (E.neg v)133execInstr retTy (QBE.Add lhs rhs) = execBinary retTy E.add lhs rhs134execInstr retTy (QBE.Sub lhs rhs) = execBinary retTy E.sub lhs rhs135execInstr retTy (QBE.Mul lhs rhs) = execBinary retTy E.mul lhs rhs136execInstr retTy (QBE.Div lhs rhs) = execBinary retTy E.div lhs rhs137execInstr retTy (QBE.Or lhs rhs) = execBinary retTy E.or lhs rhs138execInstr retTy (QBE.Xor lhs rhs) = execBinary retTy E.xor lhs rhs139execInstr retTy (QBE.And lhs rhs) = execBinary retTy E.and lhs rhs140execInstr retTy (QBE.URem lhs rhs) = execBinary retTy E.urem lhs rhs141execInstr retTy (QBE.Rem lhs rhs) = execBinary retTy E.srem lhs rhs142execInstr retTy (QBE.UDiv lhs rhs) = execBinary retTy E.udiv lhs rhs143execInstr retTy (QBE.Sar lhs rhs) = execShift retTy E.sar lhs rhs144execInstr retTy (QBE.Shr lhs rhs) = execShift retTy E.shr lhs rhs145execInstr retTy (QBE.Shl lhs rhs) = execShift retTy E.shl lhs rhs146execInstr retTy (QBE.Load ty addrVal) = do147 addr <- lookupValue QBE.Long addrVal >>= toAddress148 val <- readMemory ty addr149 subType retTy val150execInstr QBE.Long (QBE.Alloc align sizeValue) = do151 size <- lookupValue QBE.Long sizeValue152 stackAlloc size (fromIntegral $ QBE.getSize align)153execInstr _ QBE.Alloc {} = throwError InvalidAddressType154execInstr retTy (QBE.CompareInt intArg cmpOp lhs rhs) = do155 let cmpTy = QBE.i2BaseType intArg156 v1 <- lookupValue cmpTy lhs157 v2 <- lookupValue cmpTy rhs158159 let exprOp = E.compareIntExpr cmpOp160 runBinary retTy exprOp v1 v2161execInstr retTy (QBE.CompareFloat floatArg cmpOp lhs rhs) = do162 let cmpTy = QBE.f2BaseType floatArg163 v1 <- lookupValue cmpTy lhs164 v2 <- lookupValue cmpTy rhs165166 let exprOp = E.compareFloatExpr cmpOp167 runBinary retTy exprOp v1 v2168-- exts is only valid with a double return type.169execInstr QBE.Double (QBE.Ext QBE.ExtSingle value) = do170 v <- lookupValue QBE.Single value171 liftMaybe TypingError $ E.extendFloat v172execInstr retTy (QBE.Ext extArg value) = do173 v <- lookupValue QBE.Word value174 let (isSigned, extTy) = QBE.toExtType extArg175 liftMaybe176 TypingError177 (E.extract extTy v >>= E.extend (QBE.Base retTy) isSigned)178execInstr QBE.Single (QBE.TruncDouble value) = do179 v <- lookupValue QBE.Double value180 liftMaybe TypingError $ E.truncFloat v181-- truncd is only valid with a single return type.182execInstr _ (QBE.TruncDouble _) = throwError TypingError183execInstr retTy (QBE.Copy value) = lookupValue retTy value184execInstr retTy (QBE.FloatToInt floatArg isSigned value) = do185 v <- lookupValue (QBE.f2BaseType floatArg) value186 liftMaybe TypingError $ E.floatToInt (QBE.Base retTy) isSigned v187execInstr retTy (QBE.IntToFloat intArg isSigned value) = do188 v <- lookupValue (QBE.i2BaseType intArg) value189 liftMaybe TypingError $ E.intToFloat (QBE.Base retTy) isSigned v190execInstr retTy (QBE.Cast value) = do191 -- We must deduce the value type to use for lookup from192 -- the return type as manadated by the cast type string.193 let valueType =194 case retTy of195 QBE.Word -> QBE.Single196 QBE.Long -> QBE.Double197 QBE.Single -> QBE.Word198 QBE.Double -> QBE.Long199200 -- TODO: Consider adding an explicit operation for casting201 -- of floating points to the expression language abstraction.202 v <- lookupValue valueType value203 pure (E.fromLit (QBE.Base retTy) $ E.toWord64 v)204execInstr retTy (QBE.VAArg argLst) = do205 -- 'argsCtx' represents the “variable argument list”. Currently,206 -- it is not modeled after a specific ABI but simply contains a207 -- pointer to the previous argument. This pointer is updated by208 -- each invocation of the `vaarg` instruction.209 argsCtx <- lookupValue QBE.Long argLst >>= toAddress210211 prevPtr <- readMemory (QBE.LBase QBE.Long) argsCtx212 let retTySize =213 E.fromLit214 (QBE.Base QBE.Long)215 (fromIntegral $ QBE.baseTypeByteSize retTy)216217 -- Obtain current pointer by subtracting size from 'prevPtr'218 -- and align the pointer down to the nearest aligned address.219 ptrAligned <-220 liftMaybe InvalidAddressType $221 (prevPtr `E.sub` retTySize) >>= (`stackAlign` retTySize)222223 val <- toAddress ptrAligned >>= readMemory (QBE.LBase retTy)224 writeMemory argsCtx (QBE.Base QBE.Long) ptrAligned225 pure val226{-# INLINEABLE execInstr #-}227228-- | Execute a 'QBE.Statement', usually a sequence of 'QBE.Instruction'.229-- Therefore, this function iteratively calls 'execInstr' in the common case.230execStmt :: (Simulator m v) => QBE.Statement -> m ()231execStmt (QBE.Assign name ty inst) = do232 newVal <- execInstr ty inst233 modifyFrame (storeLocal name newVal)234execStmt (QBE.Volatile v) = execVolatile v235execStmt (QBE.Call ret toCall params) = do236 function <- lookupFunc toCall237 funcArgs <- lookupArgs params238 -- Sanity chekcs on funcArgs are performed by execFunc.239240 mayRetVal <- case function of241 SFuncDef funcDef -> execFunc funcDef funcArgs242 SSimFunc simFunc -> simFunc funcArgs243244 case mayRetVal of245 Nothing ->246 -- XXX: Could also check funcDef for the return value.247 if isNothing ret248 then pure ()249 else throwError FunctionReturnIgnored250 Just retVal ->251 case ret of252 Nothing -> throwError AssignedVoidReturnValue253 Just (ident, abity) -> do254 let baseTy = QBE.abityToBase abity255 subTyped <- subType baseTy retVal256 modifyFrame (storeLocal ident subTyped)257{-# INLINEABLE execStmt #-}258259execJump :: (Simulator m v) => QBE.JumpInstr -> m (BlockResult v)260execJump QBE.Halt = throwError EncounteredHalt261execJump (QBE.Jump ident) = do262 blocks <- QBE.fBlock <$> (activeFrame <&> stkFunc)263 case Map.lookup ident blocks of264 Just bl -> pure $ Right bl265 Nothing -> throwError (UnknownBlock ident)266execJump (QBE.Jnz cond ifT ifF) = do267 condValue <- lookupValue QBE.Word cond268 condResult <- isTrue condValue269 execJump $ QBE.Jump (if condResult then ifT else ifF)270execJump (QBE.Return v) = do271 func <- activeFrame <&> stkFunc272 case QBE.fAbity func of273 Just abity -> do274 retVal <-275 case v of276 Nothing -> throwError InvalidReturnValue277 Just x -> pure x278 lookupValue (QBE.abityToBase abity) retVal <&> (Left . Just)279 Nothing ->280 if isNothing v281 then pure (Left Nothing)282 else throwError InvalidReturnValue283{-# INLINEABLE execJump #-}284285execPhi :: (Simulator m v) => Maybe QBE.BlockIdent -> QBE.Phi -> m ()286execPhi Nothing _ = throwError InvalidPhiPosition287execPhi (Just prevIdent) (QBE.Phi name ty labels) =288 case Map.lookup prevIdent labels of289 Nothing -> throwError (UnknownBlock prevIdent)290 Just v -> do291 retVal <- lookupValue ty v292 modifyFrame (storeLocal name retVal)293{-# INLINEABLE execPhi #-}294295-- | Execute a BasicBlock, as represented by 'QBE.Block', by iteratively296-- invoking 'execStmt'. If this isn't the first executed BasicBlock within a a297-- 'QBE.Function', then the 'QBE.BlockIdent' of the previously executed298-- BasicBlock should be provided. This is required to properly execute [phi299-- instructions](https://c9x.me/compile/doc/il-v1.2.html#Phi).300execBlock :: (Simulator m v) => Maybe QBE.BlockIdent -> QBE.Block -> m (BlockResult v)301execBlock prevIdent block = do302 mapM_ (execPhi prevIdent) (QBE.phi block)303 mapM_ execStmt (QBE.stmt block)304 execJump (QBE.term block)305{-# INLINEABLE execBlock #-}306307execTilRet :: (Simulator m v) => Maybe QBE.BlockIdent -> QBE.Block -> m (BlockResult v)308execTilRet prevIdent block = go prevIdent (Right block)309 where310 go _ retValue@(Left _) = pure retValue311 go prevIdent' (Right nextBlock) =312 execBlock prevIdent' nextBlock >>= go (Just $ QBE.label nextBlock)313{-# INLINEABLE execTilRet #-}314315-- | Execute a 'QBE.FuncDef' until function return. If the function requires arguments to316-- be passed to it, these must be provided as a list. Limited sanity checking is performed317-- to ensure that the provided arguments match the declared function parameters. The return318-- value of 'execFunc' is the return value of the executed 'QBE.FuncDef'. If the function319-- has no return value, 'Nothing' is returned here.320execFunc :: (Simulator m v) => QBE.FuncDef -> [v] -> m (Maybe v)321execFunc func@(QBE.FuncDef {QBE.fParams = params}) args = do322 -- Assumption: Variadic argument has been filtered from args (see lookupArgs).323 let varIdxMay = elemIndex QBE.Variadic params324 numNamed = fromMaybe (length args) varIdxMay325 argsSane =326 if isJust varIdxMay327 then length args + 1 >= length params -- +1 for filtered '...'328 else length params == length args329 unless argsSane $330 throwError (FuncArgsMismatch $ QBE.fName func)331332 -- Separate name and unnamed variadic arguments using 'numNamed'333 -- and create a 'StackFrame' for 'func' that captures both.334 let vars =335 Map.fromList $336 zip (map paramName $ take numNamed params) args337 void $ newStackFrame func vars (drop numNamed args)338339 blockResult <- execTilRet Nothing (QBE.fEntry func) <* returnFromFunc340 case blockResult of341 Right _block -> throwError MissingFunctionReturn342 Left maybeValue -> pure maybeValue343 where344 paramName :: QBE.FuncParam -> QBE.LocalIdent345 paramName (QBE.Regular _ n) = n346 paramName (QBE.Env n) = n347 paramName QBE.Variadic = error "unreachable"348{-# SPECIALIZE execFunc :: QBE.FuncDef -> [DE.RegVal] -> SimState DE.RegVal Word8 (Maybe DE.RegVal) #-}349{-# INLINEABLE execFunc #-}