Я пытаюсь разделить как можно больше кода между эмуляторами и реализациями 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
, что вкладыш задыхается от него?
Оказалось, что настоящим виновником был не FetchM
, а другие части моего кода, которые требовали встраивания большого количества функций (по одной на каждый монадический бинд в моей основной CPU
монаде!), а FetchM
просто увеличили количество биндов.
Настоящая проблема заключалась в том, что мой CPU
монада была, помимо прочего, Writer (Endo CPUOut)
и все эти функции CPUOut -> CPUOut
нужно было полностью встроить, поскольку CLaSH не может представлять функции как сигналы.
Все это более подробно объясняется в соответствующий билет об ошибке CLaSH.