1-- SPDX-FileCopyrightText: 2025-2026 Sören Tempel <soeren+git@soeren-tempel.net>2--3-- SPDX-License-Identifier: GPL-3.0-only4{-# LANGUAGE TypeApplications #-}56module Language.QBE.Simulator.Default.State7 ( -- * Interpreter State8 Env (..),9 DataMem, -- XXX: required by initData.10 mkEnv,11 initData,12 loadObj, -- TODO: Don't export this.13 storeValues,1415 -- * State Monad16 SimState (..),17 unliftCatch, -- TODO: Move this elsewhere.18 run,19 )20where2122import Control.DeepSeq (NFData, force)23import Control.Exception24 ( ErrorCall (ErrorCall),25 Exception,26 assert,27 catch,28 evaluate,29 throwIO,30 try,31 )32import Control.Monad (foldM)33import Control.Monad.Error.Class (MonadError, catchError, throwError)34import Control.Monad.IO.Class (MonadIO, liftIO)35import Control.Monad.State.Strict36 ( MonadState,37 StateT (StateT),38 evalStateT,39 execStateT,40 gets,41 modify,42 runStateT,43 )44import Data.Array.IO (IOArray)45import Data.List (uncons)46import Data.Map qualified as Map47import Data.Maybe (fromMaybe, mapMaybe)48import Data.Tuple (swap)49import Data.Word (Word8)50import Language.QBE (Definition (DefData), Program, globalFuncs)51import Language.QBE.Simulator.Default.Expression qualified as D52import Language.QBE.Simulator.Default.Funcs (lookupSimFunc)53import Language.QBE.Simulator.Error as Err54import Language.QBE.Simulator.Expression qualified as E55import Language.QBE.Simulator.Memory qualified as MEM56import Language.QBE.Simulator.State57import Language.QBE.Types qualified as QBE5859data Env v b60 = Env61 { envSyms :: Map.Map QBE.GlobalIdent MEM.Address,62 envFuncs :: Map.Map QBE.GlobalIdent QBE.FuncDef,63 envFuncAddrs :: Map.Map MEM.Address QBE.GlobalIdent, -- TODO: IntMap?64 envMem :: MEM.Memory IOArray b,65 envStk :: [StackFrame v],66 envStkPtr :: v,67 envDataPtr :: MEM.Address68 }6970allocText :: MEM.Address -> [QBE.FuncDef] -> Map.Map MEM.Address QBE.GlobalIdent71allocText _ [] = Map.empty72allocText addr (func : rest) =73 Map.insert addr (QBE.fName func) $74 allocText (addr + pointerSize) rest75 where76 pointerSize :: MEM.Size77 pointerSize = fromIntegral $ QBE.baseTypeByteSize QBE.Long7879mkEnv ::80 forall v b.81 (MEM.Storable v b, E.ValueRepr v) =>82 Program ->83 MEM.Address ->84 MEM.Size ->85 Bool ->86 IO (Env v b)87mkEnv prog a s initMemory = do88 -- Memory Layout: Data memory starts at address zero and grows upward. The89 -- stack starts at the maximum address and grows downward towards address90 -- zero.91 --92 -- The """text segment""" is located after the stack memory. So technically,93 -- beyond the defined memory range. This is useful because it means that94 -- reading/writing that memory errors.95 --96 -- TODO: Check for stack overflow.9798 let zeroByte =99 case uncons $ MEM.toBytes (E.fromLit @v QBE.Byte 0) of100 Just (firstByte, _) -> firstByte101 _ -> error "unrechable"102 zeroByteMay = if initMemory then Just zeroByte else Nothing103 mem <- MEM.mkMemory a zeroByteMay s104105 let dataMem = allocData a (mapMaybe isData prog)106 fns = globalFuncs prog107 txt = allocText (fromIntegral $ a + s) fns108 env =109 Env110 { -- envSyms contains a mapping of function names to addresses111 -- (defined in envFuncAddrs) and addresses for all data defs.112 -- The latter is required here to enable forward references.113 envSyms = Map.union (getFuncPtr txt) (toSyms dataMem),114 envFuncs = makeFuncs fns,115 envFuncAddrs = txt,116 envMem = mem,117 envStk = [],118 envStkPtr = E.fromLit (QBE.Base QBE.Long) $ a + s - 1,119 envDataPtr = a120 }121 execStateT (initData dataMem) env122 where123 makeFuncs :: [QBE.FuncDef] -> Map.Map QBE.GlobalIdent QBE.FuncDef124 makeFuncs = Map.fromList . map (\f -> (QBE.fName f, f))125126 getFuncPtr ::127 Map.Map MEM.Address QBE.GlobalIdent ->128 Map.Map QBE.GlobalIdent MEM.Address129 getFuncPtr = Map.fromList . map swap . Map.toList130131 toSyms :: DataMem -> Map.Map QBE.GlobalIdent MEM.Address132 toSyms = Map.fromList . map (\(k, v) -> (QBE.name v, k))133134 isData :: Definition -> Maybe QBE.DataDef135 isData (DefData def) = Just def136 isData _ = Nothing137138------------------------------------------------------------------------139140-- TODO: Move this to Loader.hs141142storeBytes ::143 (MEM.Storable v b) =>144 MEM.Address ->145 [b] ->146 StateT (Env v b) IO MEM.Address147storeBytes addr bytes = do148 mem <- gets envMem149 liftIO $ MEM.storeBytes mem addr bytes150 pure $ addr + fromIntegral (length bytes)151{-# INLINEABLE storeBytes #-}152153storeValue ::154 (MEM.Storable v b) =>155 MEM.Address ->156 v ->157 StateT (Env v b) IO MEM.Address158storeValue addr = storeBytes addr . MEM.toBytes159{-# INLINE storeValue #-}160161storeValues ::162 (MEM.Storable v b) =>163 MEM.Address ->164 [v] ->165 StateT (Env v b) IO MEM.Address166storeValues addr = storeBytes addr . concatMap MEM.toBytes167{-# INLINE storeValues #-}168169loadItem ::170 forall v b.171 (MEM.Storable v b, E.ValueRepr v) =>172 MEM.Address ->173 QBE.ExtType ->174 QBE.DataItem ->175 StateT (Env v b) IO MEM.Address176loadItem addr QBE.Byte (QBE.DString str) = do177 storeValues addr $ E.fromString str178loadItem addr ty (QBE.DSymOff ident off) = do179 globals <- gets envSyms180 case Map.lookup ident globals of181 Nothing -> liftIO $ throwIO (Err.UnknownVariable $ show ident)182 Just symAddr ->183 storeValue addr $ E.fromLit @v ty (symAddr + off)184loadItem addr ty (QBE.DConst (QBE.Global ident)) =185 loadItem addr ty (QBE.DSymOff ident 0)186loadItem addr ty (QBE.DConst (QBE.Number num)) =187 storeValue addr $ E.fromLit @v ty num188loadItem addr (QBE.Base QBE.Single) (QBE.DConst (QBE.SFP num)) = do189 storeValue addr $ E.fromFloat @v num190loadItem addr (QBE.Base QBE.Double) (QBE.DConst (QBE.DFP num)) = do191 storeValue addr $ E.fromDouble @v num192loadItem _ _ item = error $ "unsupported DataItem: " ++ show item193{-# INLINEABLE loadItem #-}194195-- Load an object **without** inserting padding for data objects.196-- In QBE, the members of a struct will be packed. The frontend197-- is responsible for inserting padding between them when necessary.198loadObj ::199 forall v b.200 (MEM.Storable v b, E.ValueRepr v) =>201 MEM.Address ->202 QBE.DataObj ->203 StateT (Env v b) IO MEM.Address204loadObj addr (QBE.OZeroFill n) = do205 let zeroByte = E.fromLit @v QBE.Byte 0206 storeValues addr $ replicate (fromIntegral n) zeroByte207loadObj addr (QBE.OItem ty items) = do208 foldM (`loadItem` ty) addr items209{-# INLINEABLE loadObj #-}210211loadData ::212 (MEM.Storable v b, E.ValueRepr v) =>213 MEM.Address ->214 QBE.DataDef ->215 StateT (Env v b) IO ()216loadData addr dataDef = do217 newAddr <- foldM loadObj addr $ QBE.objs dataDef218219 -- The address calculations performed by 'loadObj' must be aligned220 -- with those performed by 'allocData' through 'QBE.dataSize'.221 assert (newAddr == addr + fromIntegral (QBE.dataSize dataDef)) $222 pure ()223{-# INLINEABLE loadData #-}224225initData ::226 (MEM.Storable v b, E.ValueRepr v) =>227 DataMem ->228 StateT (Env v b) IO ()229initData = mapM_ (uncurry loadData)230{-# SPECIALIZE initData :: DataMem -> StateT (Env D.RegVal Word8) IO () #-}231232------------------------------------------------------------------------233234-- This code implements the allocation of memory for 'DataDef's. In this235-- case, allocation means assigning a unique non-overlapping 'MEM.Address'.236-- This is separated from the initialization of the memory, which is237-- performed by 'initData'. Decoupling this enables forwards references.238--239-- For example:240--241-- data $a = { l $b }242-- data $b = { b 0 }243244-- Specifies the memory layout of the data memory. That is, for each245-- 'DataDef' defined in QBE, it specifies a start address in memory.246type DataMem = [(MEM.Address, QBE.DataDef)]247248allocDataDef ::249 QBE.DataDef ->250 (MEM.Address, DataMem) ->251 (MEM.Address, DataMem)252allocDataDef dataDef (startAddr, memMap) =253 let addr =254 MEM.alignAddr startAddr $255 fromMaybe maxAlign (QBE.align dataDef)256 size = fromIntegral $ QBE.dataSize dataDef257 in (addr + size, (addr, dataDef) : memMap)258 where259 -- The alignment of an aggregate type is the maximum alignment the members.260 maxAlign = maximum $ map QBE.objAlign (QBE.objs dataDef)261262allocData :: MEM.Address -> [QBE.DataDef] -> DataMem263allocData startAddr dataDefs =264 snd $ foldr allocDataDef (startAddr, []) dataDefs265266------------------------------------------------------------------------267268-- | Unlift 'Control.Exception.IOException' handling into a generic t'StateT' monad.269--270-- See also: <https://hackage.haskell.org/package/unliftio>.271unliftCatch ::272 (Exception t) =>273 StateT s IO a -> (t -> StateT s IO a) -> StateT s IO a274unliftCatch st handler = do275 StateT $ \s -> do276 let state = runStateT st s277 state `catch` (\e -> runStateT (handler e) s)278{-# INLINEABLE unliftCatch #-}279280-- | Simulator state, parameterized over a value and byte representation.281newtype SimState v b a = SimState {unSimState :: StateT (Env v b) IO a}282 deriving (Functor, Applicative, Monad, MonadIO)283284deriving instance MonadState (Env v b) (SimState v b)285286-- | Implements 'MonadError' in t'SimState' via 'Control.Exception.IOException's.287-- This should be more performant than using t'Control.Monad.Except.ExceptT'288-- monad transformer in conjunction with t'StateT'.289instance MonadError Err.EvalError (SimState v b) where290 throwError = liftIO . throwIO291 catchError (SimState st) handler =292 SimState $ unliftCatch st (unSimState . handler)293294------------------------------------------------------------------------295296-- | Like 'MEM.loadBytes' but receives a 'QBE.LoadType' as an argument, deducing297-- the size from it. Further, also catches any errors potentionally raised by298-- the memory and rethrows them as a 'EvalError'.299safeLoadBytes ::300 (NFData a) =>301 MEM.Memory IOArray a ->302 MEM.Address ->303 QBE.LoadType ->304 SimState v b [a]305safeLoadBytes mem addr ty = do306 let size = QBE.loadByteSize ty307 mayBytes <-308 liftIO $309 try (MEM.loadBytes mem addr size >>= evaluate . force)310311 case mayBytes of312 Left (ErrorCall msg) -> throwError $ Err.MemoryError msg313 Right bytes -> pure bytes314{-# INLINE safeLoadBytes #-}315316instance (MEM.Storable v b, E.ValueRepr v, NFData b) => Simulator (SimState v b) v where317 isTrue value = pure (E.toWord64 value /= 0)318 toAddress = pure . E.toWord64319320 lookupSymbol ident = gets (Map.lookup ident . envSyms)321322 findFunc ident = do323 funcs <- gets envFuncs324 pure $ case Map.lookup ident funcs of325 Just x -> Just $ SFuncDef x326 Nothing -> SSimFunc <$> lookupSimFunc ident327 findFuncByAddr addr = do328 fptrs <- gets envFuncAddrs329 case Map.lookup addr fptrs of330 Just fn -> findFunc fn331 Nothing -> pure Nothing332333 activeFrame = do334 stk <- gets envStk335 case stk of336 (x : _) -> pure x337 [] -> throwError Err.EmptyStack338 pushStackFrame frame =339 modify (\s -> s {envStk = frame : envStk s})340 popStackFrame = do341 stk <- gets envStk342 case stk of343 (x : xs) -> modify (\s -> s {envStk = xs}) >> pure x344 [] -> throwError Err.EmptyStack345346 getSP = gets envStkPtr347 setSP sp = modify (\s -> s {envStkPtr = sp})348349 writeMemory addr extType val = do350 mem <- gets envMem351352 -- Since halfwords and bytes are not first class in the IL, storeh and storeb353 -- take a word as argument. Only the first 16 or 8 bits of this word will be354 -- stored in memory at the address specified in the second argument.355 let bytes = MEM.toBytes val356 liftIO $357 MEM.storeBytes mem addr $358 case extType of359 QBE.Byte -> take 1 bytes360 QBE.HalfWord -> take 2 bytes361 QBE.Base _ -> bytes362 readMemory ty addr = do363 mem <- gets envMem364 bytes <- safeLoadBytes mem addr ty365366 case MEM.fromBytes ty bytes of367 Just x -> pure x368 Nothing -> throwError InvalidMemoryLoad369370------------------------------------------------------------------------371372run :: (E.ValueRepr v, MEM.Storable v b) => Env v b -> SimState v b a -> IO a373run env state = evalStateT (unSimState state) env374{-# SPECIALIZE run :: Env D.RegVal Word8 -> SimState D.RegVal Word8 a -> IO a #-}