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.Map qualified as Map46import Data.Maybe (fromMaybe, mapMaybe)47import Data.Tuple (swap)48import Data.Word (Word8)49import Language.QBE (Definition (DefData), Program, globalFuncs)50import Language.QBE.Simulator.Default.Expression qualified as D51import Language.QBE.Simulator.Default.Funcs (lookupSimFunc)52import Language.QBE.Simulator.Error as Err53import Language.QBE.Simulator.Expression qualified as E54import Language.QBE.Simulator.Memory qualified as MEM55import Language.QBE.Simulator.State56import Language.QBE.Types qualified as QBE5758data Env v b59 = Env60 { envSyms :: Map.Map QBE.GlobalIdent MEM.Address,61 envFuncs :: Map.Map QBE.GlobalIdent QBE.FuncDef,62 envFuncAddrs :: Map.Map MEM.Address QBE.GlobalIdent, -- TODO: IntMap?63 envMem :: MEM.Memory IOArray b,64 envStk :: [StackFrame v],65 envStkPtr :: v,66 envDataPtr :: MEM.Address67 }6869allocText :: MEM.Address -> [QBE.FuncDef] -> Map.Map MEM.Address QBE.GlobalIdent70allocText _ [] = Map.empty71allocText addr (func : rest) =72 Map.insert addr (QBE.fName func) $73 allocText (addr + pointerSize) rest74 where75 pointerSize :: MEM.Size76 pointerSize = fromIntegral $ QBE.baseTypeByteSize QBE.Long7778mkEnv ::79 (MEM.Storable v b, E.ValueRepr v) =>80 Program ->81 MEM.Address ->82 MEM.Size ->83 IO (Env v b)84mkEnv prog a s = do85 -- Memory Layout: Data memory starts at address zero and grows upward. The86 -- stack starts at the maximum address and grows downward towards address87 -- zero.88 --89 -- The """text segment""" is located after the stack memory. So technically,90 -- beyond the defined memory range. This is useful because it means that91 -- reading/writing that memory errors.92 --93 -- TODO: Check for stack overflow.94 mem <- MEM.mkMemory a s95 let dataMem = allocData a (mapMaybe isData prog)96 fns = globalFuncs prog97 txt = allocText (fromIntegral $ a + s) fns98 env =99 Env100 { -- envSyms contains a mapping of function names to addresses101 -- (defined in envFuncAddrs) and addresses for all data defs.102 -- The latter is required here to enable forward references.103 envSyms = Map.union (getFuncPtr txt) (toSyms dataMem),104 envFuncs = makeFuncs fns,105 envFuncAddrs = txt,106 envMem = mem,107 envStk = [],108 envStkPtr = E.fromLit (QBE.Base QBE.Long) $ a + s - 1,109 envDataPtr = a110 }111 execStateT (initData dataMem) env112 where113 makeFuncs :: [QBE.FuncDef] -> Map.Map QBE.GlobalIdent QBE.FuncDef114 makeFuncs = Map.fromList . map (\f -> (QBE.fName f, f))115116 getFuncPtr ::117 Map.Map MEM.Address QBE.GlobalIdent ->118 Map.Map QBE.GlobalIdent MEM.Address119 getFuncPtr = Map.fromList . map swap . Map.toList120121 toSyms :: DataMem -> Map.Map QBE.GlobalIdent MEM.Address122 toSyms = Map.fromList . map (\(k, v) -> (QBE.name v, k))123124 isData :: Definition -> Maybe QBE.DataDef125 isData (DefData def) = Just def126 isData _ = Nothing127128------------------------------------------------------------------------129130-- TODO: Move this to Loader.hs131132storeBytes ::133 (MEM.Storable v b) =>134 MEM.Address ->135 [b] ->136 StateT (Env v b) IO MEM.Address137storeBytes addr bytes = do138 mem <- gets envMem139 liftIO $ MEM.storeBytes mem addr bytes140 pure $ addr + fromIntegral (length bytes)141{-# INLINEABLE storeBytes #-}142143storeValue ::144 (MEM.Storable v b) =>145 MEM.Address ->146 v ->147 StateT (Env v b) IO MEM.Address148storeValue addr = storeBytes addr . MEM.toBytes149{-# INLINE storeValue #-}150151storeValues ::152 (MEM.Storable v b) =>153 MEM.Address ->154 [v] ->155 StateT (Env v b) IO MEM.Address156storeValues addr = storeBytes addr . concatMap MEM.toBytes157{-# INLINE storeValues #-}158159loadItem ::160 forall v b.161 (MEM.Storable v b, E.ValueRepr v) =>162 MEM.Address ->163 QBE.ExtType ->164 QBE.DataItem ->165 StateT (Env v b) IO MEM.Address166loadItem addr QBE.Byte (QBE.DString str) = do167 storeValues addr $ E.fromString str168loadItem addr ty (QBE.DSymOff ident off) = do169 globals <- gets envSyms170 case Map.lookup ident globals of171 Nothing -> liftIO $ throwIO (Err.UnknownVariable $ show ident)172 Just symAddr ->173 storeValue addr $ E.fromLit @v ty (symAddr + off)174loadItem addr ty (QBE.DConst (QBE.Global ident)) =175 loadItem addr ty (QBE.DSymOff ident 0)176loadItem addr ty (QBE.DConst (QBE.Number num)) =177 storeValue addr $ E.fromLit @v ty num178loadItem addr (QBE.Base QBE.Single) (QBE.DConst (QBE.SFP num)) = do179 storeValue addr $ E.fromFloat @v num180loadItem addr (QBE.Base QBE.Double) (QBE.DConst (QBE.DFP num)) = do181 storeValue addr $ E.fromDouble @v num182loadItem _ _ item = error $ "unsupported DataItem: " ++ show item183{-# INLINEABLE loadItem #-}184185-- Load an object **without** inserting padding for data objects.186-- In QBE, the members of a struct will be packed. The frontend187-- is responsible for inserting padding between them when necessary.188loadObj ::189 forall v b.190 (MEM.Storable v b, E.ValueRepr v) =>191 MEM.Address ->192 QBE.DataObj ->193 StateT (Env v b) IO MEM.Address194loadObj addr (QBE.OZeroFill n) = do195 let zeroByte = E.fromLit @v QBE.Byte 0196 storeValues addr $ replicate (fromIntegral n) zeroByte197loadObj addr (QBE.OItem ty items) = do198 foldM (`loadItem` ty) addr items199{-# INLINEABLE loadObj #-}200201loadData ::202 (MEM.Storable v b, E.ValueRepr v) =>203 MEM.Address ->204 QBE.DataDef ->205 StateT (Env v b) IO ()206loadData addr dataDef = do207 newAddr <- foldM loadObj addr $ QBE.objs dataDef208209 -- The address calculations performed by 'loadObj' must be aligned210 -- with those performed by 'allocData' through 'QBE.dataSize'.211 assert (newAddr == addr + fromIntegral (QBE.dataSize dataDef)) $212 pure ()213{-# INLINEABLE loadData #-}214215initData ::216 (MEM.Storable v b, E.ValueRepr v) =>217 DataMem ->218 StateT (Env v b) IO ()219initData = mapM_ (uncurry loadData)220{-# SPECIALIZE initData :: DataMem -> StateT (Env D.RegVal Word8) IO () #-}221222------------------------------------------------------------------------223224-- This code implements the allocation of memory for 'DataDef's. In this225-- case, allocation means assigning a unique non-overlapping 'MEM.Address'.226-- This is separated from the initialization of the memory, which is227-- performed by 'initData'. Decoupling this enables forwards references.228--229-- For example:230--231-- data $a = { l $b }232-- data $b = { b 0 }233234-- Specifies the memory layout of the data memory. That is, for each235-- 'DataDef' defined in QBE, it specifies a start address in memory.236type DataMem = [(MEM.Address, QBE.DataDef)]237238allocDataDef ::239 QBE.DataDef ->240 (MEM.Address, DataMem) ->241 (MEM.Address, DataMem)242allocDataDef dataDef (startAddr, memMap) =243 let addr =244 MEM.alignAddr startAddr $245 fromMaybe maxAlign (QBE.align dataDef)246 size = fromIntegral $ QBE.dataSize dataDef247 in (addr + size, (addr, dataDef) : memMap)248 where249 -- The alignment of an aggregate type is the maximum alignment the members.250 maxAlign = maximum $ map QBE.objAlign (QBE.objs dataDef)251252allocData :: MEM.Address -> [QBE.DataDef] -> DataMem253allocData startAddr dataDefs =254 snd $ foldr allocDataDef (startAddr, []) dataDefs255256------------------------------------------------------------------------257258-- | Unlift 'Control.Exception.IOException' handling into a generic t'StateT' monad.259--260-- See also: <https://hackage.haskell.org/package/unliftio>.261unliftCatch ::262 (Exception t) =>263 StateT s IO a -> (t -> StateT s IO a) -> StateT s IO a264unliftCatch st handler = do265 StateT $ \s -> do266 let state = runStateT st s267 state `catch` (\e -> runStateT (handler e) s)268{-# INLINEABLE unliftCatch #-}269270-- | Simulator state, parameterized over a value and byte representation.271newtype SimState v b a = SimState {unSimState :: StateT (Env v b) IO a}272 deriving (Functor, Applicative, Monad, MonadIO)273274deriving instance MonadState (Env v b) (SimState v b)275276-- | Implements 'MonadError' in t'SimState' via 'Control.Exception.IOException's.277-- This should be more performant than using t'Control.Monad.Except.ExceptT'278-- monad transformer in conjunction with t'StateT'.279instance MonadError Err.EvalError (SimState v b) where280 throwError = liftIO . throwIO281 catchError (SimState st) handler =282 SimState $ unliftCatch st (unSimState . handler)283284------------------------------------------------------------------------285286-- | Like 'MEM.loadBytes' but receives a 'QBE.LoadType' as an argument, deducing287-- the size from it. Further, also catches any errors potentionally raised by288-- the memory and rethrows them as a 'EvalError'.289safeLoadBytes ::290 (NFData a) =>291 MEM.Memory IOArray a ->292 MEM.Address ->293 QBE.LoadType ->294 SimState v b [a]295safeLoadBytes mem addr ty = do296 let size = QBE.loadByteSize ty297 mayBytes <-298 liftIO $299 try (MEM.loadBytes mem addr size >>= evaluate . force)300301 case mayBytes of302 Left (ErrorCall msg) -> throwError $ Err.MemoryError msg303 Right bytes -> pure bytes304{-# INLINE safeLoadBytes #-}305306instance (MEM.Storable v b, E.ValueRepr v, NFData b) => Simulator (SimState v b) v where307 isTrue value = pure (E.toWord64 value /= 0)308 toAddress = pure . E.toWord64309310 lookupSymbol ident = gets (Map.lookup ident . envSyms)311312 findFunc ident = do313 funcs <- gets envFuncs314 pure $ case Map.lookup ident funcs of315 Just x -> Just $ SFuncDef x316 Nothing -> SSimFunc <$> lookupSimFunc ident317 findFuncByAddr addr = do318 fptrs <- gets envFuncAddrs319 case Map.lookup addr fptrs of320 Just fn -> findFunc fn321 Nothing -> pure Nothing322323 activeFrame = do324 stk <- gets envStk325 case stk of326 (x : _) -> pure x327 [] -> throwError Err.EmptyStack328 pushStackFrame frame =329 modify (\s -> s {envStk = frame : envStk s})330 popStackFrame = do331 stk <- gets envStk332 case stk of333 (x : xs) -> modify (\s -> s {envStk = xs}) >> pure x334 [] -> throwError Err.EmptyStack335336 getSP = gets envStkPtr337 setSP sp = modify (\s -> s {envStkPtr = sp})338339 writeMemory addr extType val = do340 mem <- gets envMem341342 -- Since halfwords and bytes are not first class in the IL, storeh and storeb343 -- take a word as argument. Only the first 16 or 8 bits of this word will be344 -- stored in memory at the address specified in the second argument.345 let bytes = MEM.toBytes val346 liftIO $347 MEM.storeBytes mem addr $348 case extType of349 QBE.Byte -> take 1 bytes350 QBE.HalfWord -> take 2 bytes351 QBE.Base _ -> bytes352 readMemory ty addr = do353 mem <- gets envMem354 bytes <- safeLoadBytes mem addr ty355356 case MEM.fromBytes ty bytes of357 Just x -> pure x358 Nothing -> throwError InvalidMemoryLoad359360------------------------------------------------------------------------361362run :: (E.ValueRepr v, MEM.Storable v b) => Env v b -> SimState v b a -> IO a363run env state = evalStateT (unSimState state) env364{-# SPECIALIZE run :: Env D.RegVal Word8 -> SimState D.RegVal Word8 a -> IO a #-}