packages feed

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

{-# LANGUAGE NumericUnderscores #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE TypeFamilies #-}

{-# OPTIONS_GHC -fplugin=GHC.TypeLits.Extra.Solver #-}
{-# OPTIONS_GHC -fplugin=GHC.TypeLits.Normalise #-}
{-# OPTIONS_GHC -fplugin=GHC.TypeLits.KnownNat.Solver #-}

module Clash.Tests.Resize (tests, main) where

import Clash.Prelude (Unsigned, Signed, BitVector, Index)
import Clash.Promoted.Nat (SNat(..))
import Clash.XException (XException)
import Control.DeepSeq (NFData)
import Control.Exception (SomeException, try, evaluate)
import Data.Either (isLeft)
import Data.Proxy (Proxy(Proxy))
import GHC.TypeNats (KnownNat, SomeNat(..), type (<=), type (+), Nat, someNatVal)
import Hedgehog ((===))
import Numeric.Natural (Natural)
import Test.Tasty (TestTree, testGroup, defaultMain)
import Test.Tasty.Hedgehog (testPropertyNamed)
import Test.Tasty.QuickCheck (testProperty, discard, ioProperty, counterexample)

import qualified Clash.Class.Resize as Resize
import qualified Hedgehog as H
import qualified Hedgehog.Gen as Gen
import qualified Hedgehog.Range as Range
import qualified Test.Tasty.QuickCheck as Q

withSomeSNat :: Natural -> (forall (n :: Nat). SNat n -> r) -> r
withSomeSNat n f = case someNatVal n of
  SomeNat (_ :: Proxy n) -> f (SNat @n)

-- | Anything that's in bounds should not cause an error
indexProp ::
  forall a b.
  ((a <= b), KnownNat a, KnownNat b) =>
  Proxy b -> Index a -> Bool
indexProp Proxy v =
  Resize.resize v == Resize.checkedResize @a @b v

-- | Anything that's out of bounds should cause an error
indexFailProp ::
  forall a b.
  ((b <= a), KnownNat a, KnownNat b) =>
  Proxy b -> Index a -> Q.Property
indexFailProp Proxy v =
  let checked = Resize.checkedResize @a @b v in
  if toInteger v > toInteger (maxBound @(Index b)) then
    expectExceptionNoX checked
  else
    discard

maybeResizePropT ::
  forall f a b .
  ( Integral (f a)
  , Bounded (f a)
  , Show (f a)
  , Integral (f b)
  , Bounded (f b)
  , Show (f b)
  , Resize.Resize f
  , KnownNat a
  , KnownNat b
  ) =>
  Proxy (f a) ->
  Proxy (f b) ->
  H.PropertyT IO ()
maybeResizePropT _ _ = do
  let
    minFa = fromIntegral (minBound @(f a)) :: Integer
    maxFa = fromIntegral (maxBound @(f a)) :: Integer
    minFb = fromIntegral (minBound @(f b)) :: Integer
    maxFb = fromIntegral (maxBound @(f b)) :: Integer

  H.footnote $ "minFa: " <> show minFa
  H.footnote $ "maxFa: " <> show maxFa
  H.footnote $ "minFb: " <> show minFb
  H.footnote $ "maxFb: " <> show maxFb

  input <- H.forAll $ Gen.integral (Range.constant minFa maxFa)
  let output = Resize.maybeResize @a @b @f (fromIntegral input)
  if minFb <= input && input <= maxFb
  then output === Just (fromIntegral input)
  else output === Nothing

maybeResizeUnsignedProp :: H.Property
maybeResizeUnsignedProp = H.property $ do
  a <- H.forAll $ Gen.integral (Range.linear 0 100)
  b <- H.forAll $ Gen.integral (Range.linear 0 100)
  withSomeSNat a $ \(SNat :: SNat a) -> do
    withSomeSNat b $ \(SNat :: SNat b) -> do
      maybeResizePropT @Unsigned @a @b Proxy Proxy

maybeResizeSignedProp :: H.Property
maybeResizeSignedProp = H.property $ do
  a <- H.forAll $ Gen.integral (Range.linear 0 100)
  b <- H.forAll $ Gen.integral (Range.linear 0 100)
  withSomeSNat a $ \(SNat :: SNat a) -> do
    withSomeSNat b $ \(SNat :: SNat b) -> do
      maybeResizePropT @Signed @a @b Proxy Proxy

maybeResizeIndexProp :: H.Property
maybeResizeIndexProp = H.property $ do
  a <- H.forAll $ Gen.integral (Range.linear 1 (2^(128::Natural)))
  b <- H.forAll $ Gen.integral (Range.linear 1 (2^(128::Natural)))
  withSomeSNat a $ \(SNat :: SNat a) -> do
    withSomeSNat b $ \(SNat :: SNat b) -> do
      maybeResizePropT @Index @a @b Proxy Proxy

maybeResizeBitVectorProp :: H.Property
maybeResizeBitVectorProp = H.property $ do
  a <- H.forAll $ Gen.integral (Range.linear 0 100)
  b <- H.forAll $ Gen.integral (Range.linear 0 100)
  withSomeSNat a $ \(SNat :: SNat a) -> do
    withSomeSNat b $ \(SNat :: SNat b) -> do
      maybeResizePropT @BitVector @a @b Proxy Proxy

maybeTruncatePropT ::
  forall f a b .
  ( Integral (f a)
  , Bounded (f a)
  , Show (f a)
  , Integral (f (a + b))
  , Bounded (f (a + b))
  , Show (f (a + b))
  , Resize.Resize f
  , KnownNat a
  , KnownNat b
  ) =>
  Proxy (f a) ->
  Proxy (f (a + b)) ->
  H.PropertyT IO ()
maybeTruncatePropT _ _ = do
  let
    minFa = fromIntegral (minBound @(f a)) :: Integer
    maxFa = fromIntegral (maxBound @(f a)) :: Integer
    minFab = fromIntegral (minBound @(f (a + b))) :: Integer
    maxFab = fromIntegral (maxBound @(f (a + b))) :: Integer

  H.footnote $ "minFa: " <> show minFa
  H.footnote $ "maxFa: " <> show maxFa
  H.footnote $ "minFab: " <> show minFab
  H.footnote $ "maxFab: " <> show maxFab

  input <- H.forAll $ Gen.integral (Range.constant minFa maxFa)
  let output = Resize.maybeTruncateB @a @b @f (fromIntegral input)
  if minFab <= input && input <= maxFab
  then output === Just (fromIntegral input)
  else output === Nothing

maybeTruncateUnsignedProp :: H.Property
maybeTruncateUnsignedProp = H.property $ do
  a <- H.forAll $ Gen.integral (Range.linear 0 100)
  b <- H.forAll $ Gen.integral (Range.linear 0 100)
  withSomeSNat a $ \(SNat :: SNat a) -> do
    withSomeSNat b $ \(SNat :: SNat b) -> do
      maybeTruncatePropT @Unsigned @a @b Proxy Proxy

maybeTruncateSignedProp :: H.Property
maybeTruncateSignedProp = H.property $ do
  a <- H.forAll $ Gen.integral (Range.linear 0 100)
  b <- H.forAll $ Gen.integral (Range.linear 0 100)
  withSomeSNat a $ \(SNat :: SNat a) -> do
    withSomeSNat b $ \(SNat :: SNat b) -> do
      maybeTruncatePropT @Signed @a @b Proxy Proxy

maybeTruncateIndexProp :: H.Property
maybeTruncateIndexProp = H.property $ do
  a <- H.forAll $ Gen.integral (Range.linear 1 (2^(128::Natural)))
  b <- H.forAll $ Gen.integral (Range.linear 1 (2^(128::Natural)))
  withSomeSNat a $ \(SNat :: SNat a) -> do
    withSomeSNat b $ \(SNat :: SNat b) -> do
      maybeTruncatePropT @Index @a @b Proxy Proxy

maybeTruncateBitVectorProp :: H.Property
maybeTruncateBitVectorProp = H.property $ do
  a <- H.forAll $ Gen.integral (Range.linear 0 100)
  b <- H.forAll $ Gen.integral (Range.linear 0 100)
  withSomeSNat a $ \(SNat :: SNat a) -> do
    withSomeSNat b $ \(SNat :: SNat b) -> do
      maybeTruncatePropT @BitVector @a @b Proxy Proxy

-- | Succeed if evaluating leads to a non-XException Exception
expectExceptionNoX :: (Show a, NFData a) => a -> Q.Property
expectExceptionNoX a0 = ioProperty $ do
  a1 <- try @SomeException (try @XException (evaluate a0))
  pure $
    counterexample
      ("Expected non-XException Exception, got: " <> show a1)
      (isLeft a1)

tests :: TestTree
tests = testGroup "Resize"
  [ testGroup "checkedResize"
    [ testProperty "indexProp @17 @19" (indexProp @17 @19 Proxy)
    , testProperty "indexProp @19 @19" (indexProp @19 @19 Proxy)
    , testProperty "indexFailProp @37 @7" (indexFailProp @37 @7 Proxy)
    , testPropertyNamed "maybeResizeUnsignedProp" "maybeResizeUnsignedProp" maybeResizeUnsignedProp
    , testPropertyNamed "maybeResizeSignedProp" "maybeResizeSignedProp" maybeResizeSignedProp
    , testPropertyNamed "maybeResizeIndexProp" "maybeResizeIndexProp" maybeResizeIndexProp
    , testPropertyNamed "maybeResizeBitVectorProp" "maybeResizeBitVectorProp" maybeResizeBitVectorProp
    , testPropertyNamed "maybeTruncateUnsignedProp" "maybeTruncateUnsignedProp" maybeTruncateUnsignedProp
    , testPropertyNamed "maybeTruncateSignedProp" "maybeTruncateSignedProp" maybeTruncateSignedProp
    , testPropertyNamed "maybeTruncateIndexProp" "maybeTruncateIndexProp" maybeTruncateIndexProp
    , testPropertyNamed "maybeTruncateBitVectorProp" "maybeTruncateBitVectorProp" maybeTruncateBitVectorProp
    ]
  ]

main :: IO ()
main = defaultMain
  -- $ adjustOption (\_ -> HedgehogTestLimit (Just 1_000_000))
  $ tests