packages feed

clash-prelude-1.10.0: tests/Clash/Tests/DumpVCD.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}

module Clash.Tests.DumpVCD where

import Control.Monad (foldM)
import Control.Monad.Morph (generalize, hoist)
import Data.Binary (encode)
import Data.Map.Strict (Map)
import Data.Maybe (isJust)
import GHC.Stack (HasCallStack, withFrozenCallStack)
import Test.Tasty
import Test.Tasty.Hedgehog (testPropertyNamed)
import Type.Reflection (typeRep)

import Clash.Signal.Trace
import Clash.Sized.BitVector (BitVector, size#)
import Hedgehog.Extra (LockstepWalk (..), combinations, lockstepWalk)

import qualified Data.List as L
import qualified Data.Map.Strict as Map
import qualified Hedgehog as H
import qualified Hedgehog.Gen as Gen
import qualified Hedgehog.Range as Range

type SignalData = BitVector 32

failWithMsg :: (H.MonadTest m, HasCallStack) => String -> m a
failWithMsg msg = withFrozenCallStack $ H.annotate msg >> H.failure

data Metadata
  = Metadata
  { timeScale :: VCDTime
  , nameMap :: Map String IDCode
  }
  deriving (Show)

parseMetadata ::
  [DeclarationCommand] ->
  Either String Metadata
parseMetadata dcs = go Nothing Nothing dcs
 where
  go Nothing m (TimeScale t u : dcs0) = do
    let
      scale u0 =
        case u0 of
          S -> (* 1000) <$> scale MS
          MS -> (* 1000) <$> scale US
          US -> (* 1000) <$> scale NS
          NS -> (* 1000) <$> scale PS
          PS -> Right t
          FS -> Left "$timescale fs unsupported; raw time in ps"
    t0 <- scale u
    go (Just t0) m dcs0
  go ts Nothing (Vars vs : dcs0) = do
    map0 <- goVars Map.empty vs
    go ts (Just map0) dcs0
  go (Just ts) (Just m) [] = Right $ Metadata ts m
  go Nothing _ [] = Left "No $timescale in declarations"
  go _ Nothing [] = Left "No $scope section in declarations"
  go _ _ (TimeScale _ _ : _) = Left "Multiple $timescale sections in declarations"
  go _ _ (Vars _ : _) =
    -- This is not an error in VCD files, but we don't emit it so we don't handle it.
    Left "Don't know what to do with multiple $scope sections in declarations"

  goVars map0 [] = Right map0
  goVars map0 (Var{..} : vs)
    | Map.member varReference map0 =
        Left $ "Duplicate $var section for variable named " <> show varReference
    | otherwise = goVars (Map.insert varReference varIDCode map0) vs

-- Are time stamps in order? Do they start at 0? Is every time stamp followed by actual
-- data?
--
-- We exploit knowledge about how @dumpVCD@ formats the resulting file. The @fixedStart@
-- is not mandated by VCD, it's just what @dumpVCD@ currently outputs.
saneTimestamps ::
  (H.MonadTest m) =>
  LockstepWalk m SimulationCommand
saneTimestamps = LockstepWalk $ fixedStart (0 :: Int) []
 where
  fixedStart 3 [SimulationTime t0, DumpVars _, SimulationTime t1] end sc = do
    H.diff t1 (>=) 0
    H.diff t0 (<) t1
    go True t1 end sc
  fixedStart 3 starts _ _ =
    failWithMsg $
      "Simulation doesn't start with proper $dumpvars\n\n"
        <> "The simulation starts with:\n"
        <> show starts
  fixedStart n starts False sc = pure $ LockstepWalk $ fixedStart (n + 1) (starts ++ [sc])
  fixedStart _ _ True _ = failWithMsg "Simulation ends prematurely"

  go _ t0 _ (SimulationValueChange _) = pure $ LockstepWalk $ go False t0
  go False t0 False (SimulationTime t1) = H.diff t0 (<) t1 >> pure (LockstepWalk $ go True t1)
  go True t0 _ (SimulationTime _) =
    failWithMsg $ "At time " <> show t0 <> ": Timestamp without change data"
  go _ _ True sc@(SimulationTime _) =
    -- A simulation can't end with a timestamp, a timestamp is always followed by values.
    failWithMsg $ "The final simulation command is " <> show sc
  go _ _ _ (DumpVars _) =
    -- This is not an error in VCD files, but we don't emit it so we don't handle it.
    failWithMsg $ "Don't know what to do with multiple $dumpvars sections"

-- The signal changes every period. Are all the samples there, and are there no extra
-- samples?
signalCorrect ::
  forall m.
  (H.MonadTest m, HasCallStack) =>
  String ->
  IDCode ->
  VCDTime ->
  VCDTime ->
  [Value] ->
  LockstepWalk m SimulationCommand
signalCorrect name idCode period end es = LockstepWalk initF
 where
  initF _ (SimulationTime t) =
    pure $ LockstepWalk $ go t (zip (-1 : [0, period .. end]) es)
  initF _ sc =
    failWithMsg $ "First simulation command is not SimulationTime but " <> show sc

  go t es0 lastC (SimulationValueChange (ValueChange _ idCode0 v))
    | idCode /= idCode0 = checkFinal lastC es0 >> pure (LockstepWalk $ go t es0)
    | otherwise = do
        es1 <- parseChange t es0 idCode0 v
        checkFinal lastC es1
        pure $ LockstepWalk $ go t es1
  go _ es0 _ (SimulationTime t) = pure $ LockstepWalk $ go t es0
  go t es0 _ (DumpVars vars) = do
    es2 <-
      foldM
        (\es1 (ValueChange _ idCode0 val) -> parseChange t es1 idCode0 val)
        es0
        vars
    pure $ LockstepWalk $ go t es2

  checkFinal False _ = pure ()
  checkFinal True ((t, _) : _) = do
    annotateSignal
    failWithMsg $ "Sample at time " <> show t <> " never came"
  checkFinal True _ = pure ()

  parseChange _ es0 idCode0 _
    | idCode /= idCode0 = pure es0
  parseChange t (e : es1) _ v
    | (t, v) == e = pure es1
    | otherwise = do
        annotateSignal
        (t, v) H.=== e
        H.failure
  parseChange _ [] _ _ = do
    annotateSignal
    failWithMsg $ "Too many samples in VCD file"

  annotateSignal :: (HasCallStack) => m ()
  annotateSignal =
    withFrozenCallStack $
      H.annotate $
        "Signal name: "
          <> show name
          <> "\n"
          <> "Signal identifier code: "
          <> show idCode

{- | Check timing of individual signals when tracing multiple domains

If we have multiple signals, each with their own period, that change every sample, does
the resulting VCD contain all the samples at the right times, and do timestamps and value
changes strictly alternate?

This test would have caught the issue that PR #3076 fixed, and serves as a good
basic sanity check of VCD generation.
-}
multiDomainTrace :: H.Property
multiDomainTrace =
  H.property $
    -- By default, property tests run in @PropertyT IO@. But we do a /lot/ of binds, and
    -- I'm unsure whether binds have an inherent cost in 'IO'. It doesn't seem like it,
    -- but running in 'IO' also doesn't give us anything.
    -- So we run in @PropertyT Identity@ instead.
    hoist generalize $ do
      -- It seems silly to test a single signal, but we do want it as a shrink option just
      -- in case the error occurs even for a single signal.
      k <-
        H.forAll $
          Gen.shrink (\x -> if x > 1 then [1] else []) $
            Gen.int $
              Range.linear 2 4
      -- @common@ scales all periods by a common power of ten factor
      common <- H.forAll $ fmap (10 ^) $ Gen.integral_ $ Range.constant (0 :: Int) 4
      -- Interesting behavior of multiple signals is captured by the relation between them.
      -- The relation is defined by the ratios between the periods, which depends only on
      -- the prime factors they do not share. Hence, primes are excellent candidates. It
      -- feels prudent to also include 1 as the other extreme, where for /every/ sample of
      -- a slower signal, there is also a new sample of the fastest signal.
      periodFactors <- H.forAll $ combinations [1, 2, 3, 5, 7, 11, 13, 17, 19, 23] k
      -- Maybe add one more signal in one of the domains
      duplSignal <- H.forAll $ Gen.maybe $ Gen.integral_ $ Range.constant 0 (k - 1)
      let
        -- After @product periods@ time, the timing starts to repeat, so this seems a good
        -- time to stop.
        --
        -- Maximum duration is about 10,000 samples with the list of primes above. That
        -- does make the test run pretty long, though. It seems unlikely that an anomaly
        -- would show up only after 1,000 samples.
        --
        -- The end of sampling is defined pretty weirdly in 'dumpVCD' currently, but with
        -- this definition of @sampleDuration@, we should have enough samples and not
        -- oversample too much.
        sampleDuration = min 1000 $ product periodFactors + 10
        signalNames =
          take k ["a", "b", "c", "d"] ++ if isJust duplSignal then ["e"] else []
        periods = map (* common) periodFactors
        periods0
          | Just i <- duplSignal = periods ++ [periods !! i]
          | otherwise = periods
        traceMap = Map.fromAscList $ zipWith (\n p -> (n, oneTrace p)) signalNames periods0
      VCDFile vcdDecs vcdSims <- H.evalEither $ dumpVCD1# (0, sampleDuration) traceMap
      Metadata{..} <- H.evalEither $ parseMetadata vcdDecs
      Map.keysSet nameMap H.=== Map.keysSet traceMap
      -- 'dumpVCD' currently outputs timestamp 0 twice: once for $dumpvars and again for
      -- the first real value changes. But this is probably a bug; the standard doesn't
      -- say, and GTKWave and Surfer accept it, but it sounds like it's not well-formed.
      -- For now, we alter the timestamp for $dumpvars. The timestamp -1 is not valid in a
      -- VCD file, but we can match on it in our tests.
      vcdSims0 <-
        case L.uncons vcdSims of
          Just (sc, vcdSims1) ->
            sc H.=== SimulationTime 0 >> pure (SimulationTime (-1) : vcdSims1)
          Nothing ->
            failWithMsg "VCDFile has no SimulationCommands"
      let
        -- The end of sampling is defined pretty weirdly in 'dumpVCD' currently
        end = sampleDuration - 2
        signalCorrects =
          map
            ( \(name, (idCode, period, vs)) ->
                signalCorrect name idCode (period `div` timeScale) end vs
            )
            $ Map.assocs
            $ Map.intersectionWith
              (\idCode (_, period, _, vs) -> (idCode, period, vs))
              nameMap
              traceMap
      lockstepWalk (saneTimestamps : signalCorrects) vcdSims0
 where
  oneTrace period =
    ( encode (typeRep @SignalData)
    , period
    , size# (0 :: SignalData)
    , map (0,) [0 ..]
    )

tests :: TestTree
tests =
  testGroup
    "DumpVCD"
    [ testPropertyNamed "multiDomainTrace" "multiDomainTrace" $ multiDomainTrace
    ]