packages feed

clash-prelude-1.6.0: tests/Clash/Tests/Laws/SaturatingNum.hs

{-# LANGUAGE RankNTypes #-}

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

module Clash.Tests.Laws.SaturatingNum (tests) where

import Test.Tasty
import Test.Tasty.Hedgehog
import Test.Tasty.HUnit
import Test.Tasty.HUnit.Extra

import Clash.Class.Num
import Clash.Sized.Index (Index)
import Clash.Sized.Signed (Signed)
import Clash.Sized.Fixed (SFixed, UFixed)
import Clash.Sized.Unsigned (Unsigned)

import Control.DeepSeq (NFData)
import GHC.TypeLits (KnownNat)

import Hedgehog
import qualified Hedgehog.Range as Range
import qualified Hedgehog.Gen as Gen

type TestWrap = Bool

type SaturationLaw a =
  (Ord a, Show a, Eq a, SaturatingNum a) =>
  Gen a ->
  Assertion

-- Check that all modes apart from SatError are total. SatError cannot be total
-- as it throws an XException on overflow/underflow.
isTotal ::
  forall a.
  (NFData a, Show a, Eq a) =>
  (SaturationMode -> a -> a -> a) ->
  Gen a ->
  Property
isTotal f genA = property $ do
  satMode <- forAll Gen.enumBounded
  a <- forAll genA
  b <- forAll genA

  if isTotalMode satMode
     then evalNF (f satMode a b) *> success
     else success
 where
  isTotalMode SatError = False
  isTotalMode _ = True

satWrapOverflowLaw :: forall a. SaturationLaw a
satWrapOverflowLaw _ = satSucc @a SatWrap maxBound @?= minBound

satWrapUnderflowLaw :: forall a. SaturationLaw a
satWrapUnderflowLaw _ = satPred @a SatWrap minBound @?= maxBound

satBoundOverflowLaw :: forall a. SaturationLaw a
satBoundOverflowLaw _ = satSucc @a SatBound maxBound @?= maxBound

satBoundUnderflowLaw :: forall a. SaturationLaw a
satBoundUnderflowLaw _ = satPred @a SatBound minBound @?= minBound

satZeroOverflowLaw :: forall a. SaturationLaw a
satZeroOverflowLaw _ = satSucc @a SatZero maxBound @?= 0

satZeroUnderflowLaw :: forall a. SaturationLaw a
satZeroUnderflowLaw _ = satPred @a SatZero minBound @?= 0

satErrorOverflowLaw :: forall a. SaturationLaw a
satErrorOverflowLaw _ = expectXException (satSucc @a SatError maxBound)

satErrorUnderflowLaw :: forall a. SaturationLaw a
satErrorUnderflowLaw _ = expectXException (satPred @a SatError minBound)

satSymmetricOverflow :: forall a. SaturationLaw a
satSymmetricOverflow _ = satSucc @a SatSymmetric maxBound @?= maxBound

satSymmetricUnderflow :: forall a. SaturationLaw a
satSymmetricUnderflow _ =
  if minBound @a < 0 then
    -- Signed number
    satPred @a SatSymmetric minBound @?= satSucc SatWrap minBound
  else
    -- Unsigned number (or zero-width)
    satPred @a SatSymmetric minBound @?= minBound

saturatingNumLaws ::
  (NFData a, Ord a, Show a, Eq a, SaturatingNum a) =>
  TestWrap ->
  Gen a ->
  [TestTree]
saturatingNumLaws testEnum genA =
  (if testEnum then
    [ testCase "SatWrap: Wrap around on overflow" (satWrapOverflowLaw genA)
    , testCase "SatWrap: Wrap around on underflow" (satWrapUnderflowLaw genA)
    , testCase "SatSymmetric: Become maxBound on overflow"
               (satSymmetricOverflow genA)
    , testCase "SatSymmetric: Become minBound or minBound+1 on underflow"
               (satSymmetricUnderflow genA)
    , testCase "SatBound: Become maxBound on overflow"
               (satBoundOverflowLaw genA)
    , testCase "SatBound: Become minBound on underflow"
               (satBoundUnderflowLaw genA)
    , testCase "SatZero: Become 0 on overflow" (satZeroOverflowLaw genA)
    , testCase "SatZero: Become 0 on underflow" (satZeroUnderflowLaw genA)
    , testCase "SatError: Error on overflow" (satErrorOverflowLaw genA)
    , testCase "SatError: Error on underflow" (satErrorUnderflowLaw genA)
    ]
  else
    []) <>
  [ testProperty "satAddTotal" (isTotal satAdd genA)
  , testProperty "satSubTotal" (isTotal satSub genA)
  , testProperty "satMulTotal" (isTotal satMul genA)
  ]

testSaturationLaws ::
  (NFData a, Ord a, Show a, Eq a, SaturatingNum a) =>
  TestWrap ->
  String ->
  Gen a ->
  TestTree
testSaturationLaws testEnum typeName genA =
  testGroup typeName (saturatingNumLaws testEnum genA)

-- | Generates a bounded integral with a bias towards extreme values:
--
--      5%: minBound
--      5%: maxBound
--      5%: 0
--     85%: uniform [minBound, maxBound]
--
genBoundedIntegral :: (Integral a, Bounded a) => Gen a
genBoundedIntegral = Gen.frequency
  [ (5,  pure minBound)
  , (5,  pure maxBound)
  , (5,  pure 0)
  , (85, Gen.integral (Range.linear minBound maxBound)) ]

genIndex :: forall n. KnownNat n => Gen (Index n)
genIndex = genBoundedIntegral

genUnsigned :: forall n. KnownNat n => Gen (Unsigned n)
genUnsigned = genBoundedIntegral

genSigned :: forall n. KnownNat n => Gen (Signed n)
genSigned = genBoundedIntegral

-- | Generates a bounded fractional with a bias towards extreme values:
--
--      5%: minBound
--      5%: maxBound
--      5%: 0.0
--     85%: uniform [minBound, maxBound]
--
genBoundedFractional :: forall a. (Real a, Fractional a, Bounded a) => Gen a
genBoundedFractional = Gen.frequency
  [ (5,  pure minBound)
  , (5,  pure maxBound)
  , (5,  pure 0.0)
  , (85,   fmap (fromRational . toRational)
         $ Gen.double
         $ fmap fromRational
         $ Range.linearFrac
             (toRational (minBound @a))
             (toRational (maxBound @a))) ]

genSFixed :: forall a b. (KnownNat a, KnownNat b) => Gen (SFixed a b)
genSFixed = genBoundedFractional

genUFixed :: forall a b. (KnownNat a, KnownNat b) => Gen (UFixed a b)
genUFixed = genBoundedFractional

tests :: TestTree
tests = testGroup "SaturatingNum"
  [ testSaturationLaws True "Index 1" (genIndex @1)
  , testSaturationLaws True "Index 2" (genIndex @2)
  , testSaturationLaws True "Index 128" (genIndex @128)

  , testSaturationLaws True "Unsigned 0" (genUnsigned @0)
  , testSaturationLaws True "Unsigned 1" (genUnsigned @1)
  , testSaturationLaws True "Unsigned 32" (genUnsigned @32)
  , testSaturationLaws True "Unsigned 127" (genUnsigned @127)
  , testSaturationLaws True "Unsigned 128" (genUnsigned @128)

  , testSaturationLaws True "Signed 0" (genSigned @0)
  , testSaturationLaws True "Signed 1" (genSigned @1)
  , testSaturationLaws True "Signed 32" (genSigned @32)
  , testSaturationLaws True "Signed 127" (genSigned @127)
  , testSaturationLaws True "Signed 128" (genSigned @128)

  , testSaturationLaws False "SFixed 0 0" (genSFixed @0 @0)
  , testSaturationLaws False "SFixed 0 1" (genSFixed @0 @1)
  , testSaturationLaws False "SFixed 1 0" (genSFixed @1 @0)
  , testSaturationLaws False "SFixed 0 2" (genSFixed @0 @2)
  , testSaturationLaws False "SFixed 1 1" (genSFixed @1 @1)
  , testSaturationLaws False "SFixed 2 0" (genSFixed @2 @0)
  , testSaturationLaws False "SFixed 1 2" (genSFixed @1 @2)
  , testSaturationLaws False "SFixed 2 1" (genSFixed @2 @1)
  , testSaturationLaws False "SFixed 2 2" (genSFixed @2 @2)
  , testSaturationLaws False "SFixed 7 7" (genSFixed @7 @7)
  , testSaturationLaws False "SFixed 121 121" (genSFixed @121 @121)
  , testSaturationLaws False "SFixed 128 128" (genSFixed @128 @128)

  , testSaturationLaws False "UFixed 0 0" (genUFixed @0 @0)
  , testSaturationLaws False "UFixed 0 1" (genUFixed @0 @1)
  , testSaturationLaws False "UFixed 1 0" (genUFixed @1 @0)
  , testSaturationLaws False "UFixed 0 2" (genUFixed @0 @2)
  , testSaturationLaws False "UFixed 1 1" (genUFixed @1 @1)
  , testSaturationLaws False "UFixed 2 0" (genUFixed @2 @0)
  , testSaturationLaws False "UFixed 1 2" (genUFixed @1 @2)
  , testSaturationLaws False "UFixed 2 1" (genUFixed @2 @1)
  , testSaturationLaws False "UFixed 2 2" (genUFixed @2 @2)
  , testSaturationLaws False "UFixed 7 7" (genUFixed @7 @7)
  , testSaturationLaws False "UFixed 121 121" (genUFixed @121 @121)
  , testSaturationLaws False "UFixed 128 128" (genUFixed @128 @128)
  ]