packages feed

clash-protocols-0.1: tests/Tests/Protocols/Df.hs

{-# LANGUAGE CPP #-}
-- TODO: Fix warnings introduced by GHC 9.2 w.r.t. incomplete lazy pattern matches
{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-}
-- Hashable (Index n)
{-# OPTIONS_GHC -fno-warn-orphans #-}

module Tests.Protocols.Df where

-- base
import Data.Bifunctor (Bifunctor (first))
import Data.Coerce (coerce)
import Data.Foldable (fold)
import Data.Maybe (catMaybes, fromMaybe)
import Data.Tuple (swap)
import GHC.Stack (HasCallStack)
import Prelude
#if !MIN_VERSION_base(4,18,0)
import Control.Applicative (Applicative(liftA2))
#endif

-- clash-prelude

import Clash.Explicit.Prelude qualified as E
import Clash.Explicit.Reset (noReset)
import Clash.Prelude (Vec (Nil, (:>)), type (<=))
import Clash.Prelude qualified as C

-- containers
import Data.HashMap.Strict qualified as HashMap
import Data.HashSet qualified as HashSet

-- extra
import Data.Either (partitionEithers)
import Data.List (mapAccumL, partition, transpose)

-- deepseq
import Control.DeepSeq (NFData)

-- hashable
import Data.Hashable (Hashable)

-- hedgehog
import Hedgehog
import Hedgehog.Gen qualified as Gen
import Hedgehog.Range qualified as Range

-- tasty
import Test.Tasty
import Test.Tasty.HUnit (Assertion, testCase, (@?=))
import Test.Tasty.Hedgehog (HedgehogTestLimit (HedgehogTestLimit))
import Test.Tasty.Hedgehog.Extra (testProperty)
import Test.Tasty.TH (testGroupGenerator)

-- clash-protocols (me!)
import Protocols
import Protocols.Df qualified as Df
import Protocols.Experimental.Hedgehog

-- tests
import Util

newtype PlusInt = PlusInt Int
  deriving stock (C.Generic, Show)
  deriving anyclass (C.ShowX)
  deriving newtype (NFData, C.NFDataX, Eq)

instance Semigroup PlusInt where
  (<>) :: PlusInt -> PlusInt -> PlusInt
  PlusInt i <> PlusInt j = PlusInt (i + j)

instance Monoid PlusInt where
  mempty = PlusInt 0

instance Hashable (C.Index n)

genMaybe :: Gen a -> Gen (Maybe a)
genMaybe genA = Gen.choice [Gen.constant Nothing, Just <$> genA]

smallInt :: Range Int
smallInt = Range.linear 0 10

genSmallInt :: Gen Int
genSmallInt =
  Gen.frequency
    [ (90, Gen.integral smallInt)
    , (10, Gen.constant (Range.lowerBound 99 smallInt))
    ]

genSmallPlusInt :: Gen PlusInt
genSmallPlusInt = coerce <$> genSmallInt

genData :: Gen a -> Gen [a]
genData genA = do
  n <- genSmallInt
  Gen.list (Range.singleton n) genA

genVecData :: (C.KnownNat n, 1 <= n) => Gen a -> Gen (C.Vec n [a])
genVecData genA = do
  n <- genSmallInt
  genVec (Gen.list (Range.singleton n) genA)

-- Same as 'idWithModel', but specialized on 'Df'
idWithModelDf ::
  forall a b.
  (HasCallStack, TestType a, TestType b) =>
  -- | Options, see 'ExpectOptions'
  ExpectOptions ->
  {- | Test data generator, length of generated data is number of _valid_
  cycles. If an input consists of multiple input channels where the number
  of valid cycles differs, this should return the _maximum_ number of valid
  cycles of all channels.
  -}
  Gen [a] ->
  -- | Model
  ([a] -> [b]) ->
  -- | Implementation
  Circuit (Df C.System a) (Df C.System b) ->
  Property
idWithModelDf = idWithModel

-- | Same as 'idWithModelDf' but with hardcoded data generator
idWithModelDf' ::
  -- | Model
  ([Int] -> [Int]) ->
  -- | Implementation
  Circuit (Df C.System Int) (Df C.System Int) ->
  Property
idWithModelDf' = idWithModelDf defExpectOptions (genData genSmallInt)

---------------------------------------------------------------
---------------------------- TESTS ----------------------------
---------------------------------------------------------------
prop_id :: Property
prop_id = idWithModelDf' id idC

prop_map :: Property
prop_map = idWithModelDf' (map succ) (Df.map succ)

prop_filter :: Property
prop_filter = idWithModelDf' (filter (> 5)) (Df.filter (> 5))

prop_catMaybes :: Property
prop_catMaybes =
  idWithModelDf
    defExpectOptions
    (genData (genMaybe genSmallInt))
    catMaybes
    Df.catMaybes

-- A parameterized test definition validating that an expander which
-- simply releases a value downstream once every N cycles
-- does not otherwise change the contents of the stream.
testExpanderPassThrough :: forall n. (C.KnownNat n) => C.SNat n -> Property
testExpanderPassThrough _periodicity =
  idWithModelSingleDomain @C.System
    defExpectOptions
    (genData genSmallInt)
    (C.exposeClockResetEnable id)
    ( C.exposeClockResetEnable $
        passThroughExpander |> Df.catMaybes
    )
 where
  -- Just stares at a value for a few cycles and then forwards it
  passThroughExpander ::
    forall dom a.
    (C.HiddenClockResetEnable dom) =>
    Circuit (Df dom a) (Df dom (Maybe a))
  passThroughExpander = Df.expander (0 :: C.Index n) $ \count input ->
    let done = count == maxBound
     in ( if done then 0 else count + 1
        , if done then Just input else Nothing
        , done
        )

prop_expander_passthrough_linerate :: Property
prop_expander_passthrough_linerate = testExpanderPassThrough C.d1

prop_expander_passthrough_slow :: Property
prop_expander_passthrough_slow = testExpanderPassThrough C.d4

-- A parameterized test definition validating that an expander duplicates
-- input values N times and sends them downstream.
testExpanderDuplicate :: forall n. (C.KnownNat n) => C.SNat n -> Property
testExpanderDuplicate duplication =
  idWithModelSingleDomain @C.System
    defExpectOptions
    (genData genSmallInt)
    (C.exposeClockResetEnable (concatMap (replicate (C.snatToNum duplication))))
    ( C.exposeClockResetEnable
        duplicator
    )
 where
  -- Creates n copies of a value
  duplicator ::
    forall dom a.
    (C.HiddenClockResetEnable dom) =>
    Circuit (Df dom a) (Df dom a)
  duplicator = Df.expander (0 :: C.Index n) $ \count input ->
    let done = count == maxBound
     in ( if done then 0 else count + 1
        , input
        , done
        )

prop_expander_duplicate_linerate :: Property
prop_expander_duplicate_linerate = testExpanderDuplicate C.d1

prop_expander_duplicate_slow :: Property
prop_expander_duplicate_slow = testExpanderDuplicate C.d4

-- A paremterized test definition validating that a compressor correctly
-- sums up batches of N values.
testCompressorSum :: forall n. (C.KnownNat n) => C.SNat n -> Property
testCompressorSum batchSize =
  idWithModelSingleDomain @C.System
    defExpectOptions
    (genData genSmallInt)
    (C.exposeClockResetEnable referenceImpl)
    ( C.exposeClockResetEnable
        passThroughExpander
    )
 where
  chunk = C.snatToNum batchSize
  -- Given [a,b,c,d,e] and chunk = 2, yield [a+b,c+d]
  referenceImpl = map sum . takeWhile ((== chunk) . length) . map (take chunk) . iterate (drop chunk)
  -- Sum groups of `n` samples together
  passThroughExpander ::
    forall dom.
    (C.HiddenClockResetEnable dom) =>
    Circuit (Df dom Int) (Df dom Int)
  passThroughExpander = Df.compressor (0 :: C.Index n, 0 :: Int) $ \(count, total) input ->
    let done = count == maxBound
        total' = total + input
     in ( if done then (0, 0) else (count + 1, total')
        , if done then Just total' else Nothing
        )

prop_compressor_sum_linerate :: Property
prop_compressor_sum_linerate = testCompressorSum C.d1

prop_compressor_sum_slow :: Property
prop_compressor_sum_slow = testCompressorSum C.d4

prop_registerFwd :: Property
prop_registerFwd =
  idWithModelSingleDomain
    @C.System
    defExpectOptions
    (genData genSmallInt)
    (C.exposeClockResetEnable id)
    (C.exposeClockResetEnable Df.registerFwd)

prop_registerBwd :: Property
prop_registerBwd =
  idWithModelSingleDomain
    @C.System
    defExpectOptions
    (genData genSmallInt)
    (C.exposeClockResetEnable id)
    (C.exposeClockResetEnable Df.registerBwd)

prop_fanout1 :: Property
prop_fanout1 =
  idWithModelSingleDomain
    @C.System
    defExpectOptions
    (genData genSmallInt)
    (C.exposeClockResetEnable C.repeat)
    (C.exposeClockResetEnable @C.System (Df.fanout @1))

prop_fanout2 :: Property
prop_fanout2 =
  idWithModelSingleDomain
    @C.System
    defExpectOptions
    (genData genSmallInt)
    (C.exposeClockResetEnable C.repeat)
    (C.exposeClockResetEnable @C.System (Df.fanout @2))

prop_fanout7 :: Property
prop_fanout7 =
  idWithModelSingleDomain
    @C.System
    defExpectOptions
    (genData genSmallInt)
    (C.exposeClockResetEnable C.repeat)
    (C.exposeClockResetEnable @C.System (Df.fanout @7))

prop_roundrobin :: Property
prop_roundrobin =
  idWithModelSingleDomain
    @C.System
    defExpectOptions
    (genData genSmallInt)
    (C.exposeClockResetEnable chunksOf)
    (C.exposeClockResetEnable @C.System (Df.roundrobin @3))

prop_roundrobinCollectNoSkip :: Property
prop_roundrobinCollectNoSkip =
  idWithModelSingleDomain
    @C.System
    defExpectOptions
    (genVecData genSmallInt)
    (C.exposeClockResetEnable (concat . transpose . C.toList))
    (C.exposeClockResetEnable @C.System (Df.roundrobinCollect @3 Df.NoSkip))

prop_roundrobinCollectSkip :: Property
prop_roundrobinCollectSkip =
  propWithModelSingleDomain
    @C.System
    defExpectOptions
    (genVecData genSmallInt)
    (C.exposeClockResetEnable (concat . transpose . C.toList))
    (C.exposeClockResetEnable @C.System (Df.roundrobinCollect @3 Df.Skip))
    prop
 where
  prop :: [Int] -> [Int] -> PropertyT IO ()
  prop expected actual = HashSet.fromList expected === HashSet.fromList actual

prop_roundrobinCollectParallel :: Property
prop_roundrobinCollectParallel =
  propWithModelSingleDomain
    @C.System
    defExpectOptions
    (genVecData genSmallInt)
    (C.exposeClockResetEnable (concat . transpose . C.toList))
    (C.exposeClockResetEnable @C.System (Df.roundrobinCollect @3 Df.Parallel))
    prop
 where
  prop :: [Int] -> [Int] -> PropertyT IO ()
  prop expected actual = HashSet.fromList expected === HashSet.fromList actual

{- | Asserts that roundrobinCollect with Parallel mode behaves in a left-biased
fashion.
-}
case_roundrobinCollectParallel :: Assertion
case_roundrobinCollectParallel = do
  expected @?= actual
 where
  actual =
    E.sampleN 5
      . C.bundle
      . first C.bundle
      $ dut (input0 :> input1 :> input2 :> Nil, pure $ Ack True)

  expected =
    [ (Ack True :> Ack False :> Ack False :> Nil, Just (1 :: Int))
    , (Ack True :> Ack False :> Ack False :> Nil, Just 2)
    , (Ack False :> Ack True :> Ack False :> Nil, Just 10)
    , (Ack False :> Ack True :> Ack False :> Nil, Just 40)
    , (Ack False :> Ack False :> Ack True :> Nil, Just 100)
    ]

  input0 = C.fromList @_ @C.System [Just 1, Just 2, Nothing, Nothing, Nothing]
  input1 = C.fromList @_ @C.System [Just 10, Just 10, Just 10, Just 40, Nothing]
  input2 = C.fromList @_ @C.System [Just 100, Just 100, Just 100, Just 100, Just 100]

  dut =
    toSignals $
      C.withClockResetEnable @C.System C.clockGen noReset C.enableGen $
        Df.roundrobinCollect @3 Df.Parallel

prop_unbundleVec :: Property
prop_unbundleVec =
  idWithModelSingleDomain
    @C.System
    defExpectOptions
    (fmap C.repeat <$> genData genSmallInt)
    (C.exposeClockResetEnable (vecFromList . transpose . map C.toList))
    (C.exposeClockResetEnable (Df.unbundleVec @3 @C.System @Int))

prop_bundleVec :: Property
prop_bundleVec =
  idWithModel
    defExpectOptions
    (C.repeat <$> genData genSmallPlusInt)
    (map vecFromList . transpose . C.toList)
    (Df.bundleVec @3 @C.System @PlusInt)

prop_fanin :: Property
prop_fanin =
  idWithModel
    defExpectOptions
    (genVecData genSmallInt)
    (map sum . transpose . C.toList)
    (Df.fanin @3 @C.System @Int (+))

prop_mfanin :: Property
prop_mfanin =
  idWithModel
    defExpectOptions
    (genVecData genSmallPlusInt)
    (map fold . transpose . C.toList)
    (Df.mfanin @3 @C.System @PlusInt)

prop_zipWith :: Property
prop_zipWith =
  idWithModel
    defExpectOptions
    ( do
        as <- genData genSmallInt
        bs <- genData genSmallInt
        let n = min (length as) (length bs)
        pure (take n as, take n bs)
    )
    (uncurry (zipWith (+)))
    (Df.zipWith @C.System @Int @Int (+))

prop_zip :: Property
prop_zip =
  idWithModel
    defExpectOptions
    ( do
        as <- genData genSmallInt
        bs <- genData genSmallInt
        let n = min (length as) (length bs)
        pure (take n as, take n bs)
    )
    (uncurry zip)
    (Df.zip @Int @Int @C.System)

prop_partition :: Property
prop_partition =
  idWithModel
    defExpectOptions
    (genData genSmallInt)
    (partition (> 5))
    (Df.partition @C.System @Int (> 5))

prop_partitionEithers :: Property
prop_partitionEithers =
  idWithModel
    defExpectOptions
    (genData (Gen.either genSmallInt Gen.alphaNum))
    partitionEithers
    (Df.partitionEithers @C.System @Int @Char)

prop_route :: Property
prop_route =
  idWithModel
    defExpectOptions
    (zip <$> genData Gen.enumBounded <*> genData genSmallInt)
    (\inp -> C.map (\i -> map snd (filter ((== i) . fst) inp)) C.indicesI)
    (Df.route @3 @C.System @Int)

prop_select :: Property
prop_select =
  idWithModel
    defExpectOptions
    goGen
    (snd . uncurry (mapAccumL goModel))
    (Df.select @3 @C.System @Int)
 where
  goModel :: C.Vec 3 [Int] -> C.Index 3 -> (C.Vec 3 [Int], Int)
  goModel vec ix = let (i : is) = vec C.!! ix in (C.replace ix is vec, i)

  goGen :: Gen (C.Vec 3 [Int], [C.Index 3])
  goGen = do
    n <- genSmallInt
    ixs <- Gen.list (Range.singleton n) Gen.enumBounded
    let tall i = fromMaybe 0 (HashMap.lookup i (tally ixs))
    dats <- mapM (\i -> Gen.list (Range.singleton (tall i)) genSmallInt) C.indicesI
    pure (dats, ixs)

prop_selectN :: Property
prop_selectN =
  idWithModelSingleDomain
    @C.System
    defExpectOptions
    goGen
    (\_ _ _ -> concat . snd . uncurry (mapAccumL goModel))
    (C.exposeClockResetEnable (Df.selectN @3 @10 @C.System @Int))
 where
  goModel :: C.Vec 3 [Int] -> (C.Index 3, C.Index 10) -> (C.Vec 3 [Int], [Int])
  goModel vec (ix, len) =
    let (as, bs) = splitAt (fromIntegral len) (vec C.!! ix)
     in (C.replace ix bs vec, as)

  goGen :: Gen (C.Vec 3 [Int], [(C.Index 3, C.Index 10)])
  goGen = do
    n <- genSmallInt
    ixs <- Gen.list (Range.singleton n) Gen.enumBounded
    lenghts <- Gen.list (Range.singleton n) Gen.enumBounded
    let tallied = tallyOn fst (fromIntegral . snd) (zip ixs lenghts)
        tall i = fromMaybe 0 (HashMap.lookup i tallied)
    dats <- mapM (\i -> Gen.list (Range.singleton (tall i)) genSmallInt) C.indicesI
    pure (dats, zip ixs lenghts)

prop_selectUntil :: Property
prop_selectUntil =
  idWithModel
    defExpectOptions
    goGen
    (concat . snd . uncurry (mapAccumL goModel))
    (Df.selectUntil @3 @C.System @(Int, Bool) snd)
 where
  goModel :: C.Vec 3 [(Int, Bool)] -> C.Index 3 -> (C.Vec 3 [(Int, Bool)], [(Int, Bool)])
  goModel vec ix =
    let (as, (b : bs)) = break snd (vec C.!! ix)
     in (C.replace ix bs vec, as <> [b])

  goGen :: Gen (C.Vec 3 [(Int, Bool)], [C.Index 3])
  goGen = do
    n <- genSmallInt
    ixs <- Gen.list (Range.singleton n) Gen.enumBounded
    dats <- mapM (\i -> goChannelInput (HashMap.lookup i (tally ixs))) C.indicesI
    pure (dats, ixs)

  goChannelInput :: Maybe Int -> Gen [(Int, Bool)]
  goChannelInput Nothing = pure []
  goChannelInput (Just n) = do
    inputs0 <- Gen.list (Range.singleton n) (Gen.list (Range.linear 1 10) genSmallInt)
    let tagEnd xs = zip (init xs) (repeat False) <> [(last xs, True)]
    pure (concatMap tagEnd inputs0)

prop_fifo :: Property
prop_fifo =
  idWithModelDf'
    id
    (C.withClockResetEnable C.clockGen C.resetGen C.enableGen Df.fifo (C.SNat @10))

prop_toMaybeFromMaybe :: Property
prop_toMaybeFromMaybe =
  propWithModelSingleDomain
    @C.System
    defExpectOptions
    (genData genSmallInt)
    (\_ _ _ -> fmap (,0))
    (C.exposeClockResetEnable dut)
    (\sent received -> prop (fst <$> sent) received)
 where
  -- Calculate how many samples were dropped and use that to validate the received
  -- stream.
  prop :: [Int] -> [(Int, C.Unsigned 64)] -> PropertyT IO ()
  prop sents (unzip -> (receiveds, nDroppeds)) = do
    footnote ("sents: " <> show sents)
    footnote ("receiveds: " <> show receiveds)
    footnote ("nDroppeds: " <> show nDroppeds)
    footnote ("nDroppedSinceLasts: " <> show nDroppedSinceLasts)
    go sents (zip receiveds nDroppedSinceLasts)
   where
    nDroppedSinceLasts :: [C.Unsigned 64]
    nDroppedSinceLasts = 0 : zipWith (-) nDroppeds (0 : nDroppeds)

    go _ [] = pure ()
    go sent0 ((received, nDropped) : rest)
      | (s : ss) <- drop (fromIntegral nDropped) sent0 = do
          s === received
          go ss rest
      | otherwise = fail "Expected more sent values"

  -- XXX: This dut is a bit *meh*, because it inserts the dropped count into
  --      the Df stream, but it doesn't account for the rule that the data
  --      only "advances" when data is acked.
  dut ::
    (C.SystemClockResetEnable) =>
    Circuit
      (Df C.System Int)
      (Df C.System (Int, C.Unsigned 64))
  dut =
    Df.forceResetSanity
      |> Df.toMaybe
      |> Df.unsafeFromMaybe
      |> Circuit (first (,()) . swap . first (fmap go) . first C.bundle)
   where
    go :: (Maybe Int, C.Unsigned 64) -> Maybe (Int, C.Unsigned 64)
    go (a, b) = liftA2 (,) a (Just b)

tests :: TestTree
tests =
  -- TODO: Move timeout option to hedgehog for better error messages.
  -- TODO: Does not seem to work for combinatorial loops like @let x = x in x@??
  localOption (mkTimeout 12_000_000 {- 12 seconds -}) $
    localOption
      (HedgehogTestLimit (Just 1000))
      $(testGroupGenerator)

main :: IO ()
main = defaultMain tests