qute-0.1.0: src/Language/QBE/Simulator/State.hs
-- SPDX-FileCopyrightText: 2025-2026 Sören Tempel <soeren+git@soeren-tempel.net>
--
-- SPDX-License-Identifier: GPL-3.0-only
{-# LANGUAGE FunctionalDependencies #-}
-- | This module defines the abstract 'Simulator' monad and thus provides the primitives
-- used by "Language.QBE.Simulator" to describe the semantics of the QBE intermediate
-- representation.
module Language.QBE.Simulator.State
( -- * Abstract Monad
Simulator (..),
-- * Name Resolution
SomeFunc (..),
lookupFunc,
lookupArgs,
lookupGlobal,
lookupLocal,
lookupValue,
-- * Helper
liftMaybe,
subType,
runBinary,
returnFromFunc,
readNullArray,
-- * Stack
StackFrame (..),
newStackFrame,
storeLocal,
modifyFrame,
stackAlign,
stackAlloc,
stackSpill,
)
where
import Control.Monad.Error.Class (MonadError, throwError)
import Data.Functor ((<&>))
import Data.Map qualified as Map
import Data.Maybe (catMaybes)
import Data.Word (Word64)
import Language.QBE.Simulator.Error
import Language.QBE.Simulator.Expression qualified as E
import Language.QBE.Simulator.Memory qualified as MEM
import Language.QBE.Types qualified as QBE
-- | Representation of a stack frame on the function call stack.
data StackFrame v
= StackFrame
{ stkFunc :: QBE.FuncDef,
stkVars :: Map.Map QBE.LocalIdent v,
stkVarArgs :: [v],
stkFp :: v
}
-- | Create a new t'StackFrame' and push it onto the call stack.
newStackFrame ::
(Simulator m v) =>
-- | Definition of the functions to which this frame belongs.
QBE.FuncDef ->
-- | Named arguments passed to this function.
Map.Map QBE.LocalIdent v ->
-- | Optional, unnamed variadic arguments.
[v] ->
m (StackFrame v)
newStackFrame f args variadicArgs = do
frame <- getSP <&> StackFrame f args variadicArgs
pushStackFrame frame >> pure frame
{-# INLINEABLE newStackFrame #-}
-- | Store a local variable with a given name and value in the given t'StackFrame'.
storeLocal :: QBE.LocalIdent -> v -> StackFrame v -> StackFrame v
storeLocal ident value frame@(StackFrame {stkVars = v}) =
frame {stkVars = Map.insert ident value v}
-- | Lookup a local variable in the current t'StackFrame'.
lookupLocal :: StackFrame v -> QBE.LocalIdent -> Maybe v
lookupLocal (StackFrame {stkVars = v}) = flip Map.lookup v
{-# INLINEABLE lookupLocal #-}
------------------------------------------------------------------------
-- | Representation of a function.
data SomeFunc m v
= -- | A simulated function whose execution is intercepted by the Simulator.
SSimFunc ([v] -> m (Maybe v))
| -- | A QBE function defined in the input program.
SFuncDef QBE.FuncDef
-- | This is an “abstract monad” representing the Simulator and allowing
-- interaction with an encapsulated Simulator state @m@. Conceptually,
-- this monads describes the primitives based on which the semantics of
-- the QBE intermediate representation are abstractly described in
-- 'Language.QBE.Simulator'.
--
-- An instance of this monad then provides concrete semantics for these
-- primitives. For example, the module "Language.QBE.Simulator.Default.State"
-- provides an implementation of a polymorphic Simulator state implement over a
-- "Control.Monad.State" monad.
--
-- The idea is inspired by Bourgeat et al. <https://doi.org/10.1145/3607833>.
class (E.ValueRepr v, MonadError EvalError m) => Simulator m v | m -> v where
-- | Check if a value of type 'E.ValueRepr' evaluates to true. This is used
-- within "Language.QBE.Simulator" to implement conditional jumps.
isTrue :: v -> m Bool
-- | Convert a value of type 'E.ValueRepr' to a 'MEM.Address' that can be
-- used to index a "Language.QBE.Simulator.Memory".
toAddress :: v -> m MEM.Address
-- | Lookup the address of a data symbol.
lookupSymbol :: QBE.GlobalIdent -> m (Maybe MEM.Address)
-- | Find a function by name, required to implement [call instructions](https://c9x.me/compile/doc/il-v1.2.html#Call).
findFunc :: QBE.GlobalIdent -> m (Maybe (SomeFunc m v))
-- | Find a function by "text segment" address, used for the implementation of function pointers.
findFuncByAddr :: MEM.Address -> m (Maybe (SomeFunc m v))
-- | Return the t'StackFrame' of the currently executed function.
activeFrame :: m (StackFrame v)
-- | Push a new t'StackFrame' onto the function call stack.
pushStackFrame :: StackFrame v -> m ()
-- | Pop the current stack frame from the function call stack.
-- Should throw 'EmptyStack' when invoked on an empty function call stack.
popStackFrame :: m (StackFrame v)
-- | Get the current value of the stack pointer.
getSP :: m v
-- | Set the value of the stack pointer.
setSP :: v -> m ()
-- | Write a value to memory.
writeMemory :: MEM.Address -> QBE.ExtType -> v -> m () -- TODO: LoadType?
-- | Read a value from memory.
readMemory :: QBE.LoadType -> MEM.Address -> m v
-- | Extracts the element out of a 'Just' or throw the given 'EvalError' if
-- if its argument is 'Nothing'.
liftMaybe :: (MonadError EvalError m) => EvalError -> Maybe a -> m a
liftMaybe e Nothing = throwError e
liftMaybe _ (Just r) = pure r
{-# INLINE liftMaybe #-}
-- | Implements the subtyping rules of the QBE intermediate representation.
--
-- See <https://c9x.me/compile/doc/il-v1.2.html#Subtyping>.
subType :: (Simulator m v) => QBE.BaseType -> v -> m v
subType baseTy v = liftMaybe TypingError $ subType' baseTy (E.getType v)
where
subType' QBE.Word (QBE.Base QBE.Word) = Just v
subType' QBE.Word (QBE.Base QBE.Long) =
E.extract (QBE.Base QBE.Word) v
subType' QBE.Long (QBE.Base QBE.Long) = Just v
subType' QBE.Single (QBE.Base QBE.Single) = Just v
subType' QBE.Double (QBE.Base QBE.Double) = Just v
subType' _ _ = Nothing
{-# INLINEABLE subType #-}
-- | Invoke a binary operation and perform subtyping (see 'subType') on its
-- results for the provided 'QBE.BaseType'. If the operation returns a 'Nothing'
-- value a 'TypingError' is raised.
runBinary ::
(Simulator m v) =>
QBE.BaseType ->
(v -> v -> Maybe v) ->
v ->
v ->
m v
runBinary ty op lhs rhs =
liftMaybe TypingError (op lhs rhs) >>= subType ty
{-# INLINEABLE runBinary #-}
-- | Modify the current t'StackFrame', e.g. to add a new local variable to it.
-- If the function call stack is currently empty an 'EmptyStack' error is thrown.
modifyFrame :: (Simulator m v) => (StackFrame v -> StackFrame v) -> m ()
modifyFrame func = do
frame <- popStackFrame
pushStackFrame (func frame)
{-# INLINEABLE modifyFrame #-}
-- | Align a stack address. Contrary to 'MEM.alignAddr', this rounds down to
-- the nearest aligned addressed (not up) as the stack grows downward. Further,
-- since the SP representation is presently not fixed, it operates on 'E.ValueRepr'.
stackAlign :: (E.ValueRepr v) => v -> v -> Maybe v
stackAlign addr alignment =
addr `E.urem` alignment >>= (addr `E.sub`)
{-# INLINEABLE stackAlign #-}
-- | Allocate a given amount of bytes on the stack with the given alignment.
-- Advances the stack pointer accordingly.
stackAlloc :: (Simulator m v) => v -> Word64 -> m v
stackAlloc size align = do
stkPtr <- getSP
let newStkPtr = stkPtr `E.sub` size >>= (`stackAlign` E.fromLit (QBE.Base QBE.Long) align)
case newStkPtr of
Just ptr -> setSP ptr >> pure ptr
Nothing -> throwError InvalidAddressType
{-# INLINEABLE stackAlloc #-}
-- | Allocate space for the given value on the stack and store it there.
-- Returns a reference (i.e., a memory address) fore the allocated memory.
stackSpill :: (Simulator m v) => v -> m MEM.Address
stackSpill val = do
let ty = E.getType val
size = fromIntegral $ QBE.extTypeByteSize ty
sizeVal = E.fromLit (QBE.Base QBE.Long) size
ptr <- stackAlloc sizeVal size >>= toAddress
writeMemory ptr ty val
pure ptr
{-# INLINEABLE stackSpill #-}
-- | Trigger a function return, popping its t'StackFrame' from the call stack
-- and updating both the stack and frame pointer.
returnFromFunc :: (Simulator m v) => m ()
returnFromFunc = popStackFrame >>= setSP . stkFp
{-# INLINE returnFromFunc #-}
maybeLookup :: (Simulator m v) => String -> Maybe a -> m a
maybeLookup name = liftMaybe (UnknownVariable name)
{-# INLINE maybeLookup #-}
-- | Lookup a global variable, might throw an 'UnknownVariable' error.
lookupGlobal :: (Simulator m v) => QBE.BaseType -> QBE.GlobalIdent -> m v
lookupGlobal ty name = do
v <- lookupSymbol name >>= maybeLookup (show name)
subType ty (E.fromLit (QBE.Base QBE.Long) v)
{-# INLINEABLE lookupGlobal #-}
-- | Lookup a 'QBE.Value', invoking the correct lookup function. For example,
-- 'lookupGlobal' for globals or 'lookupLocal' for local variables.
lookupValue :: (Simulator m v) => QBE.BaseType -> QBE.Value -> m v
lookupValue ty (QBE.VConst (QBE.Const (QBE.Number v))) =
pure $ E.fromLit (QBE.Base ty) v
lookupValue ty (QBE.VConst (QBE.Const (QBE.SFP v))) =
subType ty (E.fromFloat v)
lookupValue ty (QBE.VConst (QBE.Const (QBE.DFP v))) =
subType ty (E.fromDouble v)
lookupValue ty (QBE.VConst (QBE.Const (QBE.Global k))) = lookupGlobal ty k
lookupValue ty (QBE.VConst (QBE.Thread k)) = lookupGlobal ty k
lookupValue ty (QBE.VConst (QBE.Extern k)) = lookupGlobal ty k
lookupValue ty (QBE.VConst (QBE.ExternThread k)) = lookupGlobal ty k
lookupValue ty (QBE.VLocal k) = do
v <- activeFrame >>= maybeLookup (show k) . flip lookupLocal k
subType ty v
{-# INLINEABLE lookupValue #-}
lookupFuncName :: (Simulator m v) => QBE.GlobalIdent -> m (SomeFunc m v)
lookupFuncName name = do
maybeFunc <- findFunc name
case maybeFunc of
Just def -> pure def
Nothing -> throwError (UnknownFunction name)
{-# INLINEABLE lookupFuncName #-}
-- | Interpret the given 'QBE.Value' as a function reference, either
-- looking it up by name or by address. If the function could not be
-- found by address an 'UnknownFunctionAddr' is thrown, otherwise an
-- 'UnknownFunction' error is thrown.
lookupFunc :: (Simulator m v) => QBE.Value -> m (SomeFunc m v)
lookupFunc (QBE.VConst (QBE.Extern n)) = lookupFuncName n
lookupFunc (QBE.VConst (QBE.Const (QBE.Global n))) = lookupFuncName n
lookupFunc value = do
addr <- lookupValue QBE.Long value >>= toAddress
maybeFunc <- findFuncByAddr addr
case maybeFunc of
Just def -> pure def
Nothing -> throwError (UnknownFunctionAddr addr)
{-# INLINEABLE lookupFunc #-}
lookupArg :: (Simulator m v) => QBE.FuncArg -> m (Maybe v)
lookupArg (QBE.ArgReg abity value) =
Just <$> lookupValue (QBE.abityToBase abity) value
lookupArg (QBE.ArgEnv _) = error "env function parameters not supported"
lookupArg QBE.ArgVar = pure Nothing
{-# INLINEABLE lookupArg #-}
-- | Lookup the arguments to a function.
lookupArgs :: (Simulator m v) => [QBE.FuncArg] -> m [v]
lookupArgs args = catMaybes <$> mapM lookupArg args
{-# INLINE lookupArgs #-}
-- | Read a null-terminated C string from memory at the given 'MEM.Address'.
-- The return value is a list of 8-bit values.
readNullArray :: (Simulator m v) => MEM.Address -> m [v]
readNullArray addr = go addr []
where
go a acc = do
byte <- readMemory (QBE.LSubWord QBE.SignedByte) a
if E.toWord64 byte == 0
then pure acc
else go (a + 1) (acc ++ [byte])
{-# INLINE readNullArray #-}