Simulation just hangs

I’m trying to include a pipeline inside the Df stream. I’m trying to write:

delayToDf' :: forall dom delay a b.
              (HasCallStack, HiddenClockResetEnable dom, KnownNat delay, NFDataX a, NFDataX b)
           => (HiddenClockResetEnable dom => DSignal dom 0 a -> DSignal dom delay b)
           -> (Signal dom (Maybe a), (Signal dom Ack, Signal dom ()))
           -> (Signal dom Ack, (Signal dom (Maybe b), Signal dom (DelayToDfDbg a b)))
delayToDf' f (input, (outputAck, _)) =
  let delayToDfDbgInput :: HasCallStack => Signal dom (DelayToDfDbgInput a)
      delayToDfDbgInput = DelayToDfDbgInput <$> input <*> fmap A.fromPAck outputAck

      inputAck :: HasCallStack => Signal dom Ack
      inputAck = proceedToAck <$> proceed

      proceed :: HasCallStack => Signal dom Proceed
      mask :: HasCallStack => Signal dom Mask
      (proceed, mask) = mealyB delayToDfSt Unmask (delayToDfDbgInput, pipelineOutput)

      pipelineInput :: HasCallStack => Signal dom a
      pipelineInput = fromMaybe (deepErrorX "Not from input") <$> input

      pipeline :: (HasCallStack, HiddenClockResetEnable dom) => Signal dom (Maybe b)
      pipeline = toSignal (aux <$> valid <*> f (fromSignal pipelineInput))
                 where aux :: Bool -> b -> Maybe b
                       aux True x = Just x
                       aux False _x = Nothing
                       valid :: DSignal dom delay Bool
                       valid = delayedI @delay @_ @_ @0 False (pure True)

      pipelineOutput :: HasCallStack => Signal dom (Maybe b)
      pipelineOutput = andEnable (proceedToBool <$> proceed) pipeline

      output :: HasCallStack => Signal dom (Maybe b)
      output = doMask <$> mask <*> pipelineOutput

      dbg :: HasCallStack => Signal dom (DelayToDfDbg a b)
      dbg =
        DelayToDfDbg <$>
        (DelayToDfDbgInput <$> input <*> fmap A.fromPAck outputAck) <*>
        (DelayToDfDbgOutput <$> fmap A.fromPAck inputAck <*> output) <*>
        (DelayToDfDbgState <$> mask <*> proceed <*> pipelineInput <*> pipelineOutput)
  in (inputAck, (output, dbg))

Now it contains some error that I try to debug but it shouldn’t hang. But when I try to use simulation it just hangs:

          lhsStallMode <- forAll $ P.genStallMode
          rhsStallMode <- forAll $ P.genStallMode
          input :: [C.BitVector 8] <- forAll $ Gen.list (Range.linear 1 100) genBitVector
          (lhsStallAck, lhsStalls) <- forAll $ P.genStalls (Gen.integral $ Range.linear 0 $ C.natToNum @delay) (length input) lhsStallMode
          (rhsStallAck, rhsStalls) <- forAll $ P.genStalls (Gen.integral $ Range.linear 0 $ C.natToNum @delay) (length input) rhsStallMode
          maps :: IORef Maps <- liftIO $ newIORef C.def
          let resetCycles = C.SNat @100
              simCfg = def { resetCycles = C.snatToNum resetCycles }
              lhsStall = Df.stall @C.System simCfg lhsStallAck lhsStalls
              rhsStall = Df.stall @C.System simCfg rhsStallAck rhsStalls
              f :: (C.HiddenClockResetEnable dom) => C.DSignal dom 0 (C.BitVector 8) -> C.DSignal dom delay (C.BitVector 8)
              f = C.delayedI C.undefined
              dut' :: C.HiddenClockResetEnable C.System => Circuit (Df C.System (C.BitVector 8)) (Df C.System (C.BitVector 8))
              dut' = lhsStall |> dut maps f |> rhsStall
              dut'' = C.exposeClockResetEnable dut' C.clockGen (C.resetGenN resetCycles) C.enableGen
              out = simulateC dut'' simCfg (Just <$> input)
              expOpt = P.defExpectOptions { P.eoTimeoutMs = Just 1000, P.eoResetCycles = C.snatToNum resetCycles }
          out' <- P.expectN (Proxy @(Df C.System (C.BitVector 8))) expOpt (take 100 out)

I’m stuck how to debug it. It doesn’t seem to hang when I feed it ‘by hand’ in ghci.

I think it’s because you have a loop in there:

You have a mealy:

(proceed, mask) = mealyB delayToDfSt Unmask (delayToDfDbgInput, pipelineOutput)

And then:

pipelineOutput = andEnable (proceedToBool <$> proceed) pipeline

So The loop is (arrow is depends on)

pipeLineOutput → proceed → delayToDfSt -> pipelineOutput

This looks like classic combinational loop. It’s probably fine via ghci because you didn’t hit the case where the loop is evaluated.

Hmm. Yeah - I guess it is this loop. My question about how to debug it still stands though. I would expect that I get loop exception or I can at least debug it somehow.

For example I rewrote it as;

delayToDf' :: forall dom delay a b dbg.
              (HasCallStack, HiddenClockResetEnable dom, KnownNat delay)
           => (HiddenClockResetEnable dom => DSignal dom 0 a -> (DSignal dom delay b, Signal dom dbg))
           -> (Signal dom (Maybe a), (Signal dom Ack, Signal dom ()))
           -> (Signal dom Ack, (Signal dom (Maybe b), Signal dom (DelayToDfDbg delay a b dbg)))
delayToDf' f (input, (outputAck, _)) =
  let pipelineFilled :: Signal dom Bool
      pipelineFilledSt :: Signal dom (Vec delay Bool)
      (pipelineFilled, pipelineFilledSt) =
        let pipelineFilledSM :: Vec delay Bool -> Maybe a -> (Vec delay Bool, (Bool, Vec delay Bool))
            pipelineFilledSM st (Just _) =
              let (st', pF :> _) = shiftInAt0 st (True :> Nil) in (st', (pF, st))
            pipelineFilledSM st Nothing =
              let (_, pF :> _) = shiftInAt0 st (True :> Nil) in (st, (pF, st))
        in mealyB pipelineFilledSM (repeat False) input

      pipelineInput :: Signal dom a
      pipelineInput = fromMaybe undefined <$> input

      inputAck :: Signal dom Bool
      inputAck = mux pipelineFilled ((\(Ack oA) -> oA) <$> outputAck) (pure True)

      enablePipeline :: Signal dom Bool
      enablePipeline = (isJust <$> input) .&&. inputAck

      pipelineOutput :: Signal dom b
      pipelineDbg :: Signal dom dbg
      (pipelineOutput, pipelineDbg) = andEnable enablePipeline (first toSignal . f . fromSignal $ pipelineInput)

      hasOutput :: Signal dom Bool
      hasOutput = (isJust <$> input) .&&. pipelineFilled

      output :: Signal dom (Maybe b)
      output =  mux hasOutput (Just <$> pipelineOutput) (pure Nothing)

      dbg :: Signal dom (DelayToDfDbg delay a b dbg)
      dbg = DelayToDfDbg <$> (DelayToDfDbgInput <$> input <*> fmap A.fromPAck outputAck)
                         <*> (DelayToDfDbgOutput <$> fmap (A.fromPAck . Ack) inputAck <*> output)
                         <*> (DelayToDfDbgSt <$> pipelineFilled
                                             <*> pipelineFilledSt
                                             <*> pipelineInput
                                             <*> pipelineOutput
                                             <*> pipelineDbg
                                             <*> hasOutput
                                             <*> enablePipeline)
  in (Ack <$> inputAck, (output, dbg))

Which doesn’t have loop as far as I can tell.

Ok. I narrowed it down to:

traceDt :: forall dom b dbg. (C.KnownDomain dom, C.NFDataX dbg, Waveform dbg) => String -> IORef Maps -> Circuit (Df dom b, CSignal dom dbg) (Df dom b)
traceDt name maps =
      fromSignals $
      \((b, _dbg), ack) -> ((ack, pure ()), b)

dut :: forall dom delay a b dbg. (a ~ b, C.HiddenClockResetEnable dom, KnownNat delay, C.NFDataX a, C.NFDataX b, C.NFDataX dbg, Waveform a, Waveform b, Waveform dbg, Show a, Show b)
    => IORef Maps
    -> (C.HiddenClockResetEnable dom => C.DSignal dom 0 a -> (C.DSignal dom delay b, C.Signal dom dbg))
    -> Circuit (Df dom a) (Df dom b)
dut maps f = aux |> traceDt "dbg" maps
             where aux :: Circuit  (Df dom a) (Df dom b, CSignal dom dbg)
                   aux = fromSignals $ \(l, (a, _v)) -> (a, (l, pure undefined))

They are suppose to add tracing before/after but I removed it to simplify code. If I change dut maps f to idC it works.

Ok. Making the signals lazy fixes the problem. Might be good to include in documentation?

Yes you are right. This is one of the real dark corners of clash debugging. Currently there is not really any good way to debug this. The main options are adding debug prints, removing/adding expressions until you have an idea what’s wrong.

Recently a GHC dev mentioned in Non-looping circuit loops in Clash simulation · Issue #3006 · clash-lang/clash-compiler · GitHub (see last comment) that certain runtime flags + a profiling build should help narrow done where it is. But I did not yet try that.