Я пытаюсь разделить как можно больше кода между эмуляторами и реализациями CLaSH для процессоров.Как часть этого, я пишу выборку и декодирование инструкций как что-то вроде
fetchInstr :: (Monad m) => m Word8 -> m Instr
. Это тривиально для запуска в эмуляторе с использованием монады, в которой находится счетчик программ, и прямой доступ кобъем памяти.Для аппаратной версии я создаю буфер фиксированного размера (поскольку длина байта инструкции ограничена) и в каждом цикле закорачиваю выборку, если в буфере еще недостаточно данных.
data Failure
= Underrun
| Overrun
deriving Show
data Buffer n dat = Buffer
{ bufferContents :: Vec n dat
, bufferNext :: Index (1 + n)
}
deriving (Show, Generic, Undefined)
instance (KnownNat n, Default dat) => Default (Buffer n dat) where
def = Buffer (pure def) 0
remember :: (KnownNat n) => Buffer n dat -> dat -> Buffer n dat
remember Buffer{..} x = Buffer
{ bufferContents = replace bufferNext x bufferContents
, bufferNext = bufferNext + 1
}
newtype FetchM n dat m a = FetchM{ unFetchM :: ReaderT (Buffer n dat) (StateT (Index (1 + n)) (ExceptT Failure m)) a }
deriving newtype (Functor, Applicative, Monad)
runFetchM :: (Monad m, KnownNat n) => Buffer n dat -> FetchM n dat m a -> m (Either Failure a)
runFetchM buf act = runExceptT $ evalStateT (runReaderT (unFetchM act) buf) 0
fetch :: (Monad m, KnownNat n) => FetchM n dat m dat
fetch = do
Buffer{..} <- FetchM ask
idx <- FetchM get
when (idx == maxBound) overrun
when (idx >= bufferNext) underrun
FetchM $ modify (+ 1)
return $ bufferContents !! idx
where
overrun = FetchM . lift . lift . throwE $ Overrun
underrun = FetchM . lift . lift . throwE $ Underrun
Идея состоит в том, что это будет использоваться для сохранения Buffer n dat
в состоянии ЦП во время выборки команд и remember
извлечения значений, поступающих из памяти, когда есть переполнение буфера:
case cpuState of
Fetching buf -> do
buf' <- remember buf <$> do
modify $ \s -> s{ pc = succ pc }
return cpuInMem
instr_ <- runFetchM buf' $ fetchInstr fetch
instr <- case instr_ of
Left Underrun -> goto (Fetching buf') >> abort
Left Overrun -> errorX "Overrun"
Right instr -> return instr
goto $ Fetching def
exec instr
Это прекрасно работает в симуляторе CLaSH.
Проблема в том, что если я начну использовать его таким образом, ему потребуется гораздо больший лимит встраивания, чтобы CLaSH мог его синтезировать.Например, в реализации CHIP-8 этот коммит начинает использовать вышеописанный FetchM
.Перед этим изменением достаточно глубины встраивания всего 100, чтобы пройти через синтезатор CLaSH;после этого изменения 300 недостаточно, а 1000 заставляют CLaSH просто взбалтывать, пока не кончится память.
Что такого плохого в FetchM
, что инлайнер задыхается от него?