qute-0.1.0: src/Language/QBE/Simulator/Default/State.hs
-- SPDX-FileCopyrightText: 2025-2026 Sören Tempel <soeren+git@soeren-tempel.net>
--
-- SPDX-License-Identifier: GPL-3.0-only
{-# LANGUAGE TypeApplications #-}
module Language.QBE.Simulator.Default.State
( -- * Interpreter State
Env (..),
DataMem, -- XXX: required by initData.
mkEnv,
initData,
loadObj, -- TODO: Don't export this.
storeValues,
-- * State Monad
SimState (..),
unliftCatch, -- TODO: Move this elsewhere.
run,
)
where
import Control.DeepSeq (NFData, force)
import Control.Exception
( ErrorCall (ErrorCall),
Exception,
assert,
catch,
evaluate,
throwIO,
try,
)
import Control.Monad (foldM)
import Control.Monad.Error.Class (MonadError, catchError, throwError)
import Control.Monad.IO.Class (MonadIO, liftIO)
import Control.Monad.State.Strict
( MonadState,
StateT (StateT),
evalStateT,
execStateT,
gets,
modify,
runStateT,
)
import Data.Array.IO (IOArray)
import Data.Map qualified as Map
import Data.Maybe (fromMaybe, mapMaybe)
import Data.Tuple (swap)
import Data.Word (Word8)
import Language.QBE (Definition (DefData), Program, globalFuncs)
import Language.QBE.Simulator.Default.Expression qualified as D
import Language.QBE.Simulator.Default.Funcs (lookupSimFunc)
import Language.QBE.Simulator.Error as Err
import Language.QBE.Simulator.Expression qualified as E
import Language.QBE.Simulator.Memory qualified as MEM
import Language.QBE.Simulator.State
import Language.QBE.Types qualified as QBE
data Env v b
= Env
{ envSyms :: Map.Map QBE.GlobalIdent MEM.Address,
envFuncs :: Map.Map QBE.GlobalIdent QBE.FuncDef,
envFuncAddrs :: Map.Map MEM.Address QBE.GlobalIdent, -- TODO: IntMap?
envMem :: MEM.Memory IOArray b,
envStk :: [StackFrame v],
envStkPtr :: v,
envDataPtr :: MEM.Address
}
allocText :: MEM.Address -> [QBE.FuncDef] -> Map.Map MEM.Address QBE.GlobalIdent
allocText _ [] = Map.empty
allocText addr (func : rest) =
Map.insert addr (QBE.fName func) $
allocText (addr + pointerSize) rest
where
pointerSize :: MEM.Size
pointerSize = fromIntegral $ QBE.baseTypeByteSize QBE.Long
mkEnv ::
(MEM.Storable v b, E.ValueRepr v) =>
Program ->
MEM.Address ->
MEM.Size ->
IO (Env v b)
mkEnv prog a s = do
-- Memory Layout: Data memory starts at address zero and grows upward. The
-- stack starts at the maximum address and grows downward towards address
-- zero.
--
-- The """text segment""" is located after the stack memory. So technically,
-- beyond the defined memory range. This is useful because it means that
-- reading/writing that memory errors.
--
-- TODO: Check for stack overflow.
mem <- MEM.mkMemory a s
let dataMem = allocData a (mapMaybe isData prog)
fns = globalFuncs prog
txt = allocText (fromIntegral $ a + s) fns
env =
Env
{ -- envSyms contains a mapping of function names to addresses
-- (defined in envFuncAddrs) and addresses for all data defs.
-- The latter is required here to enable forward references.
envSyms = Map.union (getFuncPtr txt) (toSyms dataMem),
envFuncs = makeFuncs fns,
envFuncAddrs = txt,
envMem = mem,
envStk = [],
envStkPtr = E.fromLit (QBE.Base QBE.Long) $ a + s - 1,
envDataPtr = a
}
execStateT (initData dataMem) env
where
makeFuncs :: [QBE.FuncDef] -> Map.Map QBE.GlobalIdent QBE.FuncDef
makeFuncs = Map.fromList . map (\f -> (QBE.fName f, f))
getFuncPtr ::
Map.Map MEM.Address QBE.GlobalIdent ->
Map.Map QBE.GlobalIdent MEM.Address
getFuncPtr = Map.fromList . map swap . Map.toList
toSyms :: DataMem -> Map.Map QBE.GlobalIdent MEM.Address
toSyms = Map.fromList . map (\(k, v) -> (QBE.name v, k))
isData :: Definition -> Maybe QBE.DataDef
isData (DefData def) = Just def
isData _ = Nothing
------------------------------------------------------------------------
-- TODO: Move this to Loader.hs
storeBytes ::
(MEM.Storable v b) =>
MEM.Address ->
[b] ->
StateT (Env v b) IO MEM.Address
storeBytes addr bytes = do
mem <- gets envMem
liftIO $ MEM.storeBytes mem addr bytes
pure $ addr + fromIntegral (length bytes)
{-# INLINEABLE storeBytes #-}
storeValue ::
(MEM.Storable v b) =>
MEM.Address ->
v ->
StateT (Env v b) IO MEM.Address
storeValue addr = storeBytes addr . MEM.toBytes
{-# INLINE storeValue #-}
storeValues ::
(MEM.Storable v b) =>
MEM.Address ->
[v] ->
StateT (Env v b) IO MEM.Address
storeValues addr = storeBytes addr . concatMap MEM.toBytes
{-# INLINE storeValues #-}
loadItem ::
forall v b.
(MEM.Storable v b, E.ValueRepr v) =>
MEM.Address ->
QBE.ExtType ->
QBE.DataItem ->
StateT (Env v b) IO MEM.Address
loadItem addr QBE.Byte (QBE.DString str) = do
storeValues addr $ E.fromString str
loadItem addr ty (QBE.DSymOff ident off) = do
globals <- gets envSyms
case Map.lookup ident globals of
Nothing -> liftIO $ throwIO (Err.UnknownVariable $ show ident)
Just symAddr ->
storeValue addr $ E.fromLit @v ty (symAddr + off)
loadItem addr ty (QBE.DConst (QBE.Global ident)) =
loadItem addr ty (QBE.DSymOff ident 0)
loadItem addr ty (QBE.DConst (QBE.Number num)) =
storeValue addr $ E.fromLit @v ty num
loadItem addr (QBE.Base QBE.Single) (QBE.DConst (QBE.SFP num)) = do
storeValue addr $ E.fromFloat @v num
loadItem addr (QBE.Base QBE.Double) (QBE.DConst (QBE.DFP num)) = do
storeValue addr $ E.fromDouble @v num
loadItem _ _ item = error $ "unsupported DataItem: " ++ show item
{-# INLINEABLE loadItem #-}
-- Load an object **without** inserting padding for data objects.
-- In QBE, the members of a struct will be packed. The frontend
-- is responsible for inserting padding between them when necessary.
loadObj ::
forall v b.
(MEM.Storable v b, E.ValueRepr v) =>
MEM.Address ->
QBE.DataObj ->
StateT (Env v b) IO MEM.Address
loadObj addr (QBE.OZeroFill n) = do
let zeroByte = E.fromLit @v QBE.Byte 0
storeValues addr $ replicate (fromIntegral n) zeroByte
loadObj addr (QBE.OItem ty items) = do
foldM (`loadItem` ty) addr items
{-# INLINEABLE loadObj #-}
loadData ::
(MEM.Storable v b, E.ValueRepr v) =>
MEM.Address ->
QBE.DataDef ->
StateT (Env v b) IO ()
loadData addr dataDef = do
newAddr <- foldM loadObj addr $ QBE.objs dataDef
-- The address calculations performed by 'loadObj' must be aligned
-- with those performed by 'allocData' through 'QBE.dataSize'.
assert (newAddr == addr + fromIntegral (QBE.dataSize dataDef)) $
pure ()
{-# INLINEABLE loadData #-}
initData ::
(MEM.Storable v b, E.ValueRepr v) =>
DataMem ->
StateT (Env v b) IO ()
initData = mapM_ (uncurry loadData)
{-# SPECIALIZE initData :: DataMem -> StateT (Env D.RegVal Word8) IO () #-}
------------------------------------------------------------------------
-- This code implements the allocation of memory for 'DataDef's. In this
-- case, allocation means assigning a unique non-overlapping 'MEM.Address'.
-- This is separated from the initialization of the memory, which is
-- performed by 'initData'. Decoupling this enables forwards references.
--
-- For example:
--
-- data $a = { l $b }
-- data $b = { b 0 }
-- Specifies the memory layout of the data memory. That is, for each
-- 'DataDef' defined in QBE, it specifies a start address in memory.
type DataMem = [(MEM.Address, QBE.DataDef)]
allocDataDef ::
QBE.DataDef ->
(MEM.Address, DataMem) ->
(MEM.Address, DataMem)
allocDataDef dataDef (startAddr, memMap) =
let addr =
MEM.alignAddr startAddr $
fromMaybe maxAlign (QBE.align dataDef)
size = fromIntegral $ QBE.dataSize dataDef
in (addr + size, (addr, dataDef) : memMap)
where
-- The alignment of an aggregate type is the maximum alignment the members.
maxAlign = maximum $ map QBE.objAlign (QBE.objs dataDef)
allocData :: MEM.Address -> [QBE.DataDef] -> DataMem
allocData startAddr dataDefs =
snd $ foldr allocDataDef (startAddr, []) dataDefs
------------------------------------------------------------------------
-- | Unlift 'Control.Exception.IOException' handling into a generic t'StateT' monad.
--
-- See also: <https://hackage.haskell.org/package/unliftio>.
unliftCatch ::
(Exception t) =>
StateT s IO a -> (t -> StateT s IO a) -> StateT s IO a
unliftCatch st handler = do
StateT $ \s -> do
let state = runStateT st s
state `catch` (\e -> runStateT (handler e) s)
{-# INLINEABLE unliftCatch #-}
-- | Simulator state, parameterized over a value and byte representation.
newtype SimState v b a = SimState {unSimState :: StateT (Env v b) IO a}
deriving (Functor, Applicative, Monad, MonadIO)
deriving instance MonadState (Env v b) (SimState v b)
-- | Implements 'MonadError' in t'SimState' via 'Control.Exception.IOException's.
-- This should be more performant than using t'Control.Monad.Except.ExceptT'
-- monad transformer in conjunction with t'StateT'.
instance MonadError Err.EvalError (SimState v b) where
throwError = liftIO . throwIO
catchError (SimState st) handler =
SimState $ unliftCatch st (unSimState . handler)
------------------------------------------------------------------------
-- | Like 'MEM.loadBytes' but receives a 'QBE.LoadType' as an argument, deducing
-- the size from it. Further, also catches any errors potentionally raised by
-- the memory and rethrows them as a 'EvalError'.
safeLoadBytes ::
(NFData a) =>
MEM.Memory IOArray a ->
MEM.Address ->
QBE.LoadType ->
SimState v b [a]
safeLoadBytes mem addr ty = do
let size = QBE.loadByteSize ty
mayBytes <-
liftIO $
try (MEM.loadBytes mem addr size >>= evaluate . force)
case mayBytes of
Left (ErrorCall msg) -> throwError $ Err.MemoryError msg
Right bytes -> pure bytes
{-# INLINE safeLoadBytes #-}
instance (MEM.Storable v b, E.ValueRepr v, NFData b) => Simulator (SimState v b) v where
isTrue value = pure (E.toWord64 value /= 0)
toAddress = pure . E.toWord64
lookupSymbol ident = gets (Map.lookup ident . envSyms)
findFunc ident = do
funcs <- gets envFuncs
pure $ case Map.lookup ident funcs of
Just x -> Just $ SFuncDef x
Nothing -> SSimFunc <$> lookupSimFunc ident
findFuncByAddr addr = do
fptrs <- gets envFuncAddrs
case Map.lookup addr fptrs of
Just fn -> findFunc fn
Nothing -> pure Nothing
activeFrame = do
stk <- gets envStk
case stk of
(x : _) -> pure x
[] -> throwError Err.EmptyStack
pushStackFrame frame =
modify (\s -> s {envStk = frame : envStk s})
popStackFrame = do
stk <- gets envStk
case stk of
(x : xs) -> modify (\s -> s {envStk = xs}) >> pure x
[] -> throwError Err.EmptyStack
getSP = gets envStkPtr
setSP sp = modify (\s -> s {envStkPtr = sp})
writeMemory addr extType val = do
mem <- gets envMem
-- Since halfwords and bytes are not first class in the IL, storeh and storeb
-- take a word as argument. Only the first 16 or 8 bits of this word will be
-- stored in memory at the address specified in the second argument.
let bytes = MEM.toBytes val
liftIO $
MEM.storeBytes mem addr $
case extType of
QBE.Byte -> take 1 bytes
QBE.HalfWord -> take 2 bytes
QBE.Base _ -> bytes
readMemory ty addr = do
mem <- gets envMem
bytes <- safeLoadBytes mem addr ty
case MEM.fromBytes ty bytes of
Just x -> pure x
Nothing -> throwError InvalidMemoryLoad
------------------------------------------------------------------------
run :: (E.ValueRepr v, MEM.Storable v b) => Env v b -> SimState v b a -> IO a
run env state = evalStateT (unSimState state) env
{-# SPECIALIZE run :: Env D.RegVal Word8 -> SimState D.RegVal Word8 a -> IO a #-}