为什么内衬会在此构造上窒息?

Why does the inliner choke on this construct?

我正在尝试在模拟器和 CPUs 的 CLaSH 实现之间共享尽可能多的代码。作为其中的一部分,我正在按照

的方式编写指令获取和解码
fetchInstr :: (Monad m) => m Word8 -> m Instr

这对于模拟器中的 运行 来说是微不足道的,使用在其状态中具有程序计数器并直接访问内存的 monad。对于硬件版本,我制作了一个固定大小的缓冲区(因为指令字节长度是有界的)并且在每个周期中,如果缓冲区中还没有足够的数据,则将获取短路。

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 存储在 CPU 的状态中来使用,并且 remembering 从内存中获取值时运行:

下的缓冲区
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 实现中,this commit 开始使用上述 FetchM。在此更改之前,仅 100 的内联深度就足以通过 CLaSH 合成器;进行此更改后,300 是不够的,1000 会导致 CLaSH 不断变化,直到 运行 内存不足。

FetchM 有什么不对,内衬会卡住?

原来真正的罪魁祸首不是 FetchM,而是我的代码的其他部分需要内联很多函数(我的主 CPU monad 中每个 monadic 绑定一个!) , FetchM 只是增加了绑定的数量。

真正的问题是我的 CPU monad was, among other things, a Writer (Endo CPUOut) 和所有那些 CPUOut -> CPUOut 函数都需要完全内联,因为 CLaSH 不能将函数表示为信号。

所有这些都在 the related CLaSH bug ticket 中有更详细的解释。