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.ABI.C (writeVAList)30import Language.QBE.Simulator.Default.Expression qualified as DE31import Language.QBE.Simulator.Default.State32import Language.QBE.Simulator.Error33import Language.QBE.Simulator.Expression qualified as E34import Language.QBE.Simulator.Memory (addrOverlap)35import Language.QBE.Simulator.State36import Language.QBE.Types qualified as QBE3738-- | Execution of a 'QBE.Block' can either return (with an optional return39-- value) or it can jump to another 'QBE.Block' which will then be executed.40type BlockResult v = (Either (Maybe v) QBE.Block)4142------------------------------------------------------------------------4344execVolatile :: (Simulator m v) => QBE.VolatileInstr -> m ()45execVolatile (QBE.Store valTy valReg addrReg) = do46 -- Since byte and half are not first-class types in the IL, they are47 -- stored as words and have to be looked up as such.48 val <- case valTy of49 QBE.Byte -> lookupValue QBE.Word valReg50 QBE.HalfWord -> lookupValue QBE.Word valReg51 (QBE.Base bt) -> lookupValue bt valReg5253 addr <- lookupValue QBE.Long addrReg >>= toAddress54 writeMemory addr valTy val55execVolatile (QBE.Blit src dst toCopy) = do56 srcAddrVal <- lookupValue QBE.Long src57 dstAddrVal <- lookupValue QBE.Long dst5859 -- TODO: Check for invalid BLITs60 srcAddr <- toAddress srcAddrVal61 dstAddr <- toAddress dstAddrVal62 when (srcAddr /= dstAddr && addrOverlap srcAddr dstAddr toCopy) $63 throwError (OverlappingBlit srcAddr dstAddr)6465 -- 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 -- The pointer stored in our representation of the `overflow_arg_area` of84 -- `va_list` points one element beyond the argument list. This allows us85 -- to determine the element pointer in `vaarg` by always substracting the86 -- size of the requested element from the pointer.87 --88 -- TODO: This is incompatible with the x86 System V ABI, which requires89 -- `overflow_arg_area` to be „initialized with the address of the first90 -- argument passed on the stack“. However, we can't do that because91 -- `stackSpill` places elements from low→high addresses on the stack and92 -- the x86 System V ABI assumes they are instead stored from high→low.93 let overflowPtr = firstAddr + valSize94 writeVAList ptr overflowPtr95 Nothing -> writeVAList ptr 096execVolatile (QBE.DBGLoc {}) = pure ()97{-# INLINEABLE execVolatile #-}9899execBinaryTy ::100 (Simulator m v) =>101 QBE.BaseType ->102 (v -> v -> Maybe v) ->103 (QBE.BaseType, QBE.Value) ->104 (QBE.BaseType, QBE.Value) ->105 m v106execBinaryTy retTy op (lty, lhs) (rty, rhs) = do107 v1 <- lookupValue lty lhs108 v2 <- lookupValue rty rhs109 runBinary retTy op v1 v2110111execBinary ::112 (Simulator m v) =>113 QBE.BaseType ->114 (v -> v -> Maybe v) ->115 QBE.Value ->116 QBE.Value ->117 m v118execBinary retTy op lhs rhs =119 execBinaryTy retTy op (retTy, lhs) (retTy, rhs)120{-# INLINE execBinary #-}121122execShift ::123 (Simulator m v) =>124 QBE.BaseType ->125 (v -> v -> Maybe v) ->126 QBE.Value ->127 QBE.Value ->128 m v129execShift retTy op lhs amount =130 execBinaryTy retTy op (retTy, lhs) (QBE.Word, amount)131{-# INLINE execShift #-}132133-- | Execute a single 'QBE.Instr'. The 'QBE.BaseType' denotes the return value type.134-- For example, as provided in the enclosing 'QBE.Assign'.135execInstr :: (Simulator m v) => QBE.BaseType -> QBE.Instr -> m v136execInstr retTy (QBE.Neg op) = do137 v <- lookupValue retTy op138 liftMaybe TypingError (E.neg v)139execInstr retTy (QBE.Add lhs rhs) = execBinary retTy E.add lhs rhs140execInstr retTy (QBE.Sub lhs rhs) = execBinary retTy E.sub lhs rhs141execInstr retTy (QBE.Mul lhs rhs) = execBinary retTy E.mul lhs rhs142execInstr retTy (QBE.Div lhs rhs) = execBinary retTy E.div lhs rhs143execInstr retTy (QBE.Or lhs rhs) = execBinary retTy E.or lhs rhs144execInstr retTy (QBE.Xor lhs rhs) = execBinary retTy E.xor lhs rhs145execInstr retTy (QBE.And lhs rhs) = execBinary retTy E.and lhs rhs146execInstr retTy (QBE.URem lhs rhs) = execBinary retTy E.urem lhs rhs147execInstr retTy (QBE.Rem lhs rhs) = execBinary retTy E.srem lhs rhs148execInstr retTy (QBE.UDiv lhs rhs) = execBinary retTy E.udiv lhs rhs149execInstr retTy (QBE.Sar lhs rhs) = execShift retTy E.sar lhs rhs150execInstr retTy (QBE.Shr lhs rhs) = execShift retTy E.shr lhs rhs151execInstr retTy (QBE.Shl lhs rhs) = execShift retTy E.shl lhs rhs152execInstr retTy (QBE.Load ty addrVal) = do153 addr <- lookupValue QBE.Long addrVal >>= toAddress154 val <- readMemory ty addr155 subType retTy val156execInstr QBE.Long (QBE.Alloc align sizeValue) = do157 size <- lookupValue QBE.Long sizeValue158 stackAlloc size (fromIntegral $ QBE.getSize align)159execInstr _ QBE.Alloc {} = throwError InvalidAddressType160execInstr retTy (QBE.CompareInt intArg cmpOp lhs rhs) = do161 let cmpTy = QBE.i2BaseType intArg162 v1 <- lookupValue cmpTy lhs163 v2 <- lookupValue cmpTy rhs164165 let exprOp = E.compareIntExpr cmpOp166 runBinary retTy exprOp v1 v2167execInstr retTy (QBE.CompareFloat floatArg cmpOp lhs rhs) = do168 let cmpTy = QBE.f2BaseType floatArg169 v1 <- lookupValue cmpTy lhs170 v2 <- lookupValue cmpTy rhs171172 let exprOp = E.compareFloatExpr cmpOp173 runBinary retTy exprOp v1 v2174-- exts is only valid with a double return type.175execInstr QBE.Double (QBE.Ext QBE.ExtSingle value) = do176 v <- lookupValue QBE.Single value177 liftMaybe TypingError $ E.extendFloat v178execInstr retTy (QBE.Ext extArg value) = do179 v <- lookupValue QBE.Word value180 let (isSigned, extTy) = QBE.toExtType extArg181 liftMaybe182 TypingError183 (E.extract extTy v >>= E.extend (QBE.Base retTy) isSigned)184execInstr QBE.Single (QBE.TruncDouble value) = do185 v <- lookupValue QBE.Double value186 liftMaybe TypingError $ E.truncFloat v187-- truncd is only valid with a single return type.188execInstr _ (QBE.TruncDouble _) = throwError TypingError189execInstr retTy (QBE.Copy value) = lookupValue retTy value190execInstr retTy (QBE.FloatToInt floatArg isSigned value) = do191 v <- lookupValue (QBE.f2BaseType floatArg) value192 liftMaybe TypingError $ E.floatToInt (QBE.Base retTy) isSigned v193execInstr retTy (QBE.IntToFloat intArg isSigned value) = do194 v <- lookupValue (QBE.i2BaseType intArg) value195 liftMaybe TypingError $ E.intToFloat (QBE.Base retTy) isSigned v196execInstr retTy (QBE.Cast value) = do197 -- We must deduce the value type to use for lookup from198 -- the return type as manadated by the cast type string.199 let valueType =200 case retTy of201 QBE.Word -> QBE.Single202 QBE.Long -> QBE.Double203 QBE.Single -> QBE.Word204 QBE.Double -> QBE.Long205206 -- TODO: Consider adding an explicit operation for casting207 -- of floating points to the expression language abstraction.208 v <- lookupValue valueType value209 pure (E.fromLit (QBE.Base retTy) $ E.toWord64 v)210execInstr retTy (QBE.VAArg argLst) = do211 -- 'argsCtx' represents a `va_list` with the memory layout as212 -- defined in the x86 System V ABI. Below, we extract the213 -- `overflow_arg_area` pointer. The value of this pointer is214 -- currently not aligned with the System V ABI. As such, the215 -- implementation of `vaarg` is also incompatible with the216 -- description of the `va_arg` macro in the System V ABI.217 --218 -- See the `vastart` implementation for more information.219 argsCtx <- lookupValue QBE.Long argLst >>= toAddress220 let overflowArgArea = argsCtx + 8221222 prevPtr <- readMemory (QBE.LBase QBE.Long) overflowArgArea223 let retTySize =224 E.fromLit225 (QBE.Base QBE.Long)226 (fromIntegral $ QBE.baseTypeByteSize retTy)227228 -- Obtain current pointer by subtracting size from 'prevPtr'229 -- and align the pointer down to the nearest aligned address.230 ptrAligned <-231 liftMaybe InvalidAddressType $232 (prevPtr `E.sub` retTySize) >>= (`stackAlign` retTySize)233234 val <- toAddress ptrAligned >>= readMemory (QBE.LBase retTy)235 writeMemory overflowArgArea (QBE.Base QBE.Long) ptrAligned236 pure val237{-# INLINEABLE execInstr #-}238239-- | Execute a 'QBE.Statement', usually a sequence of 'QBE.Instruction'.240-- Therefore, this function iteratively calls 'execInstr' in the common case.241execStmt :: (Simulator m v) => QBE.Statement -> m ()242execStmt (QBE.Assign name ty inst) = do243 newVal <- execInstr ty inst244 modifyFrame (storeLocal name newVal)245execStmt (QBE.Volatile v) = execVolatile v246execStmt (QBE.Call ret toCall params) = do247 function <- lookupFunc toCall248 funcArgs <- lookupArgs params249 -- Sanity chekcs on funcArgs are performed by execFunc.250251 mayRetVal <- case function of252 SFuncDef funcDef -> execFunc funcDef funcArgs253 SSimFunc simFunc -> simFunc funcArgs254255 case mayRetVal of256 Nothing ->257 -- XXX: Could also check funcDef for the return value.258 if isNothing ret259 then pure ()260 else throwError FunctionReturnIgnored261 Just retVal ->262 case ret of263 Nothing -> throwError AssignedVoidReturnValue264 Just (ident, abity) -> do265 let baseTy = QBE.abityToBase abity266 subTyped <- subType baseTy retVal267 modifyFrame (storeLocal ident subTyped)268{-# INLINEABLE execStmt #-}269270execJump :: (Simulator m v) => QBE.JumpInstr -> m (BlockResult v)271execJump QBE.Halt = throwError EncounteredHalt272execJump (QBE.Jump ident) = do273 blocks <- QBE.fBlock <$> (activeFrame <&> stkFunc)274 case Map.lookup ident blocks of275 Just bl -> pure $ Right bl276 Nothing -> throwError (UnknownBlock ident)277execJump (QBE.Jnz cond ifT ifF) = do278 condValue <- lookupValue QBE.Word cond279 condResult <- isTrue condValue280 execJump $ QBE.Jump (if condResult then ifT else ifF)281execJump (QBE.Return v) = do282 func <- activeFrame <&> stkFunc283 case QBE.fAbity func of284 Just abity -> do285 retVal <-286 case v of287 Nothing -> throwError InvalidReturnValue288 Just x -> pure x289 lookupValue (QBE.abityToBase abity) retVal <&> (Left . Just)290 Nothing ->291 if isNothing v292 then pure (Left Nothing)293 else throwError InvalidReturnValue294{-# INLINEABLE execJump #-}295296execPhi :: (Simulator m v) => Maybe QBE.BlockIdent -> QBE.Phi -> m ()297execPhi Nothing _ = throwError InvalidPhiPosition298execPhi (Just prevIdent) (QBE.Phi name ty labels) =299 case Map.lookup prevIdent labels of300 Nothing -> throwError (UnknownBlock prevIdent)301 Just v -> do302 retVal <- lookupValue ty v303 modifyFrame (storeLocal name retVal)304{-# INLINEABLE execPhi #-}305306-- | Execute a BasicBlock, as represented by 'QBE.Block', by iteratively307-- invoking 'execStmt'. If this isn't the first executed BasicBlock within a a308-- 'QBE.Function', then the 'QBE.BlockIdent' of the previously executed309-- BasicBlock should be provided. This is required to properly execute [phi310-- instructions](https://c9x.me/compile/doc/il-v1.2.html#Phi).311execBlock :: (Simulator m v) => Maybe QBE.BlockIdent -> QBE.Block -> m (BlockResult v)312execBlock prevIdent block = do313 mapM_ (execPhi prevIdent) (QBE.phi block)314 mapM_ execStmt (QBE.stmt block)315 execJump (QBE.term block)316{-# INLINEABLE execBlock #-}317318execTilRet :: (Simulator m v) => Maybe QBE.BlockIdent -> QBE.Block -> m (BlockResult v)319execTilRet prevIdent block = go prevIdent (Right block)320 where321 go _ retValue@(Left _) = pure retValue322 go prevIdent' (Right nextBlock) =323 execBlock prevIdent' nextBlock >>= go (Just $ QBE.label nextBlock)324{-# INLINEABLE execTilRet #-}325326-- | Execute a 'QBE.FuncDef' until function return. If the function requires arguments to327-- be passed to it, these must be provided as a list. Limited sanity checking is performed328-- to ensure that the provided arguments match the declared function parameters. The return329-- value of 'execFunc' is the return value of the executed 'QBE.FuncDef'. If the function330-- has no return value, 'Nothing' is returned here.331execFunc :: (Simulator m v) => QBE.FuncDef -> [v] -> m (Maybe v)332execFunc func@(QBE.FuncDef {QBE.fParams = params}) args = do333 -- Assumption: Variadic argument has been filtered from args (see lookupArgs).334 let varIdxMay = elemIndex QBE.Variadic params335 numNamed = fromMaybe (length args) varIdxMay336 argsSane =337 if isJust varIdxMay338 then length args + 1 >= length params -- +1 for filtered '...'339 else length params == length args340 unless argsSane $341 throwError (FuncArgsMismatch $ QBE.fName func)342343 -- Separate name and unnamed variadic arguments using 'numNamed'344 -- and create a 'StackFrame' for 'func' that captures both.345 let vars =346 Map.fromList $347 zip (map paramName $ take numNamed params) args348 void $ newStackFrame func vars (drop numNamed args)349350 blockResult <- execTilRet Nothing (QBE.fEntry func) <* returnFromFunc351 case blockResult of352 Right _block -> throwError MissingFunctionReturn353 Left maybeValue -> pure maybeValue354 where355 paramName :: QBE.FuncParam -> QBE.LocalIdent356 paramName (QBE.Regular _ n) = n357 paramName (QBE.Env n) = n358 paramName QBE.Variadic = error "unreachable"359{-# SPECIALIZE execFunc :: QBE.FuncDef -> [DE.RegVal] -> SimState DE.RegVal Word8 (Maybe DE.RegVal) #-}360{-# INLINEABLE execFunc #-}