From 530541f19be680b31d0a1102fdf8bf0915cd86f2 Mon Sep 17 00:00:00 2001 From: Martijn Bastiaan Date: Sun, 30 Mar 2025 20:42:47 +0200 Subject: [PATCH 1/2] Add `Clash.Class.NumConvert` Utilities for safely converting between various Clash number types --- ...20_42_00+02_00_add_clash_class_num_convert | 1 + clash-prelude/clash-prelude.cabal | 5 + clash-prelude/src/Clash/Class/NumConvert.hs | 33 ++ .../NumConvert/Internal/MaybeNumConvert.hs | 181 ++++++++++ .../Class/NumConvert/Internal/NumConvert.hs | 178 +++++++++ clash-prelude/src/Clash/Explicit/Prelude.hs | 2 + clash-prelude/src/Clash/Prelude.hs | 2 + .../tests/Clash/Tests/MaybeNumConvert.hs | 326 +++++++++++++++++ clash-prelude/tests/Clash/Tests/NumConvert.hs | 339 ++++++++++++++++++ clash-prelude/tests/unittests.hs | 6 +- 10 files changed, 1072 insertions(+), 1 deletion(-) create mode 100644 changelog/2025-03-30T20_42_00+02_00_add_clash_class_num_convert create mode 100644 clash-prelude/src/Clash/Class/NumConvert.hs create mode 100644 clash-prelude/src/Clash/Class/NumConvert/Internal/MaybeNumConvert.hs create mode 100644 clash-prelude/src/Clash/Class/NumConvert/Internal/NumConvert.hs create mode 100644 clash-prelude/tests/Clash/Tests/MaybeNumConvert.hs create mode 100644 clash-prelude/tests/Clash/Tests/NumConvert.hs diff --git a/changelog/2025-03-30T20_42_00+02_00_add_clash_class_num_convert b/changelog/2025-03-30T20_42_00+02_00_add_clash_class_num_convert new file mode 100644 index 0000000000..e89cd73a23 --- /dev/null +++ b/changelog/2025-03-30T20_42_00+02_00_add_clash_class_num_convert @@ -0,0 +1 @@ +ADD: `Clash.Class.NumConvert`: Utilities for safely converting between various Clash number types diff --git a/clash-prelude/clash-prelude.cabal b/clash-prelude/clash-prelude.cabal index 8871d82623..259e23936c 100644 --- a/clash-prelude/clash-prelude.cabal +++ b/clash-prelude/clash-prelude.cabal @@ -189,6 +189,9 @@ Library Clash.Class.HasDomain.CodeGen Clash.Class.HasDomain.Common Clash.Class.Num + Clash.Class.NumConvert + Clash.Class.NumConvert.Internal.NumConvert + Clash.Class.NumConvert.Internal.MaybeNumConvert Clash.Class.Parity Clash.Class.Resize @@ -428,10 +431,12 @@ test-suite unittests Clash.Tests.BlockRam.Blob Clash.Tests.Clocks Clash.Tests.Counter + Clash.Tests.NumConvert Clash.Tests.DerivingDataRepr Clash.Tests.DerivingDataReprTypes Clash.Tests.Fixed Clash.Tests.FixedExhaustive + Clash.Tests.MaybeNumConvert Clash.Tests.MaybeX Clash.Tests.NFDataX Clash.Tests.NumNewtypes diff --git a/clash-prelude/src/Clash/Class/NumConvert.hs b/clash-prelude/src/Clash/Class/NumConvert.hs new file mode 100644 index 0000000000..1569346955 --- /dev/null +++ b/clash-prelude/src/Clash/Class/NumConvert.hs @@ -0,0 +1,33 @@ +{- | +Copyright : (C) 2025 , Martijn Bastiaan +License : BSD2 (see the file LICENSE) +Maintainer : QBayLogic B.V. + +Utilities for converting between Clash number types in a non-erroring way. Its +existence is motivated by the observation that Clash users often need to convert +between different number types (e.g., 'Clash.Sized.Unsigned.Unsigned' to +'Clash.Sized.Signed.Signed') and that it is not always clear how to do so +properly. Two classes are exported: + +* 'NumConvert': for conversions that, based on types, are guaranteed to succeed. +* 'MaybeNumConvert': for conversions that may fail for some values. + +As opposed to 'Prelude.fromIntegral', all conversions are translatable to +synthesizable HDL. + +== __Relation to @convertible@__ +Type classes exported here are similar to the @convertible@ package in that it +aims to facilitate conversions between different types. It is different in three +ways: + + 1. It offers no partial functions. + 2. All its conversions are translatable to synthesizable HDL. + 3. It is focused on (Clash's) number types +-} +module Clash.Class.NumConvert ( + NumConvert (..), + MaybeNumConvert (..), +) where + +import Clash.Class.NumConvert.Internal.MaybeNumConvert +import Clash.Class.NumConvert.Internal.NumConvert diff --git a/clash-prelude/src/Clash/Class/NumConvert/Internal/MaybeNumConvert.hs b/clash-prelude/src/Clash/Class/NumConvert/Internal/MaybeNumConvert.hs new file mode 100644 index 0000000000..dc4f774e0b --- /dev/null +++ b/clash-prelude/src/Clash/Class/NumConvert/Internal/MaybeNumConvert.hs @@ -0,0 +1,181 @@ +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE UndecidableInstances #-} +{-# OPTIONS_GHC -fplugin=GHC.TypeLits.Extra.Solver #-} +{-# OPTIONS_GHC -fplugin=GHC.TypeLits.KnownNat.Solver #-} +{-# OPTIONS_GHC -fplugin=GHC.TypeLits.Normalise #-} +{-# OPTIONS_HADDOCK hide #-} + +{- | +Copyright : (C) 2025 , Martijn Bastiaan +License : BSD2 (see the file LICENSE) +Maintainer : QBayLogic B.V. +-} +module Clash.Class.NumConvert.Internal.MaybeNumConvert where + +import Clash.Class.BitPack +import Clash.Class.Resize +import Clash.Sized.BitVector +import Clash.Sized.Index +import Clash.Sized.Signed +import Clash.Sized.Unsigned + +import GHC.TypeLits (KnownNat, type (+), type (<=), type (^)) +import GHC.TypeLits.Extra (CLog) + +import Data.Int (Int16, Int32, Int64, Int8) +import Data.Word (Word16, Word32, Word64, Word8) + +{- $setup +>>> import Clash.Prelude +>>> import Clash.Class.NumConvert +-} + +{- | Conversions that may fail for some values. A successful conversion retains +the numerical value interpretation of the source type in the target type. A +failure is expressed by returning 'Nothing', never by an 'Clash.XException.XException'. + +== __Laws__ +A conversion is either successful or it fails gracefully. I.e., it does not +produces produce errors (also see "Clash.XException"). I.e., + +> x == fromMaybe x (maybeNumConvert @a @b x >>= maybeNumConvert @b @a) + +for all values @x@ of type @a@. It should also preserve the numerical value +interpretation of the bits. For types that have an "Integral" instance, this +intuition is captured by: + +> toInteger x == fromMaybe (toInteger x) (toInteger (numConvert @a @b x)) + +If a conversion succeeds one way, it should also succeed the other way. I.e., + +> isJust (maybeNumConvert @a @b x) `implies` isJust (maybeNumConvert @a @b x >>= maybeNumConvert @b @a) + +A conversion should succeed if and only if the value is representable in the +target type. For types that have a "Bounded" and "Integral" instance, this +intuition is captured by: + +> isJust (maybeNumConvert @a @b x) == (i x >= i (minBound @b) && i x <= i (maxBound @b)) + +where @i = toInteger@. + +All implementations should be total, i.e., they should not produce \"bottoms\". + +Additionally, any implementation should be translatable to synthesizable HDL. +-} +class MaybeNumConvert a b where + {- | NumConvert a supplied value of type @a@ to a value of type @b@. If the value + cannot be represented in the target type, 'Nothing' is returned. + + >>> maybeNumConvert (1 :: Index 8) :: Maybe (Unsigned 2) + Just 1 + >>> maybeNumConvert (7 :: Index 8) :: Maybe (Unsigned 2) + Nothing + + For the time being, if the input is an 'Clash.XException.XException', then + the output is too. This property might be relaxed in the future. + -} + maybeNumConvert :: a -> Maybe b + +instance (KnownNat n, KnownNat m) => MaybeNumConvert (Index n) (Index m) where + maybeNumConvert !a = maybeResize a + +instance (KnownNat n, KnownNat m, 1 <= n) => MaybeNumConvert (Index n) (Unsigned m) where + maybeNumConvert !a = maybeResize $ bitCoerce @_ @(Unsigned (CLog 2 n)) a + +instance (KnownNat n, KnownNat m, 1 <= n) => MaybeNumConvert (Index n) (Signed m) where + maybeNumConvert !a = maybeNumConvert $ bitCoerce @_ @(Unsigned (CLog 2 n)) a + +instance (KnownNat n, KnownNat m, 1 <= n) => MaybeNumConvert (Index n) (BitVector m) where + maybeNumConvert !a = maybeResize $ pack a + +instance (KnownNat n, KnownNat m) => MaybeNumConvert (Unsigned n) (Index m) where + maybeNumConvert !a = maybeResize $ bitCoerce @_ @(Index (2 ^ n)) a + +instance (KnownNat n, KnownNat m) => MaybeNumConvert (Unsigned n) (Unsigned m) where + maybeNumConvert !a = maybeResize a + +instance (KnownNat n, KnownNat m) => MaybeNumConvert (Unsigned n) (Signed m) where + maybeNumConvert !a = maybeResize $ bitCoerce @(Unsigned (n + 1)) $ extend a + +instance (KnownNat n, KnownNat m) => MaybeNumConvert (Unsigned n) (BitVector m) where + maybeNumConvert !a = maybeResize $ pack a + +instance (KnownNat n, KnownNat m) => MaybeNumConvert (Signed n) (Index m) where + maybeNumConvert n + | n < 0 = Nothing + | otherwise = maybeResize (bitCoerce @_ @(Index (2 ^ n)) (resize n)) + +instance (KnownNat n, KnownNat m) => MaybeNumConvert (Signed n) (Unsigned m) where + maybeNumConvert n + | n < 0 = Nothing + | otherwise = maybeResize (bitCoerce @(Signed (n + 1)) (extend n)) + +instance (KnownNat n, KnownNat m) => MaybeNumConvert (Signed n) (Signed m) where + maybeNumConvert !a = maybeResize a + +instance (KnownNat n, KnownNat m) => MaybeNumConvert (Signed n) (BitVector m) where + maybeNumConvert n + | n < 0 = Nothing + | otherwise = maybeResize (pack @(Signed (n + 1)) (extend n)) + +instance (KnownNat n, KnownNat m) => MaybeNumConvert (BitVector n) (Index m) where + maybeNumConvert !a = maybeResize $ unpack @(Index (2 ^ n)) a + +instance (KnownNat n, KnownNat m) => MaybeNumConvert (BitVector n) (Unsigned m) where + maybeNumConvert !a = maybeResize $ unpack @(Unsigned n) a + +instance (KnownNat n, KnownNat m) => MaybeNumConvert (BitVector n) (Signed m) where + maybeNumConvert !a = maybeResize $ unpack @(Signed (n + 1)) $ extend a + +instance (KnownNat n, KnownNat m) => MaybeNumConvert (BitVector n) (BitVector m) where + maybeNumConvert !a = maybeResize a + +instance (MaybeNumConvert (Unsigned 64) a) => MaybeNumConvert Word a where + maybeNumConvert !a = maybeNumConvert $ bitCoerce @_ @(Unsigned 64) a +instance (MaybeNumConvert (Unsigned 64) a) => MaybeNumConvert Word64 a where + maybeNumConvert !a = maybeNumConvert $ bitCoerce @_ @(Unsigned 64) a +instance (MaybeNumConvert (Unsigned 32) a) => MaybeNumConvert Word32 a where + maybeNumConvert !a = maybeNumConvert $ bitCoerce @_ @(Unsigned 32) a +instance (MaybeNumConvert (Unsigned 16) a) => MaybeNumConvert Word16 a where + maybeNumConvert !a = maybeNumConvert $ bitCoerce @_ @(Unsigned 16) a +instance (MaybeNumConvert (Unsigned 8) a) => MaybeNumConvert Word8 a where + maybeNumConvert !a = maybeNumConvert $ bitCoerce @_ @(Unsigned 8) a + +instance (MaybeNumConvert (Signed 64) a) => MaybeNumConvert Int a where + maybeNumConvert !a = maybeNumConvert $ bitCoerce @_ @(Signed 64) a +instance (MaybeNumConvert (Signed 64) a) => MaybeNumConvert Int64 a where + maybeNumConvert !a = maybeNumConvert $ bitCoerce @_ @(Signed 64) a +instance (MaybeNumConvert (Signed 32) a) => MaybeNumConvert Int32 a where + maybeNumConvert !a = maybeNumConvert $ bitCoerce @_ @(Signed 32) a +instance (MaybeNumConvert (Signed 16) a) => MaybeNumConvert Int16 a where + maybeNumConvert !a = maybeNumConvert $ bitCoerce @_ @(Signed 16) a +instance (MaybeNumConvert (Signed 8) a) => MaybeNumConvert Int8 a where + maybeNumConvert !a = maybeNumConvert $ bitCoerce @_ @(Signed 8) a + +instance (MaybeNumConvert a (Unsigned 64)) => MaybeNumConvert a Word where + maybeNumConvert !a = fmap (bitCoerce @(Unsigned 64)) $ maybeNumConvert a +instance (MaybeNumConvert a (Unsigned 64)) => MaybeNumConvert a Word64 where + maybeNumConvert !a = fmap (bitCoerce @(Unsigned 64)) $ maybeNumConvert a +instance (MaybeNumConvert a (Unsigned 32)) => MaybeNumConvert a Word32 where + maybeNumConvert !a = fmap (bitCoerce @(Unsigned 32)) $ maybeNumConvert a +instance (MaybeNumConvert a (Unsigned 16)) => MaybeNumConvert a Word16 where + maybeNumConvert !a = fmap (bitCoerce @(Unsigned 16)) $ maybeNumConvert a +instance (MaybeNumConvert a (Unsigned 8)) => MaybeNumConvert a Word8 where + maybeNumConvert !a = fmap (bitCoerce @(Unsigned 8)) $ maybeNumConvert a + +instance (MaybeNumConvert a (Signed 64)) => MaybeNumConvert a Int64 where + maybeNumConvert !a = fmap (bitCoerce @(Signed 64)) $ maybeNumConvert a +instance (MaybeNumConvert a (Signed 32)) => MaybeNumConvert a Int32 where + maybeNumConvert !a = fmap (bitCoerce @(Signed 32)) $ maybeNumConvert a +instance (MaybeNumConvert a (Signed 16)) => MaybeNumConvert a Int16 where + maybeNumConvert !a = fmap (bitCoerce @(Signed 16)) $ maybeNumConvert a +instance (MaybeNumConvert a (Signed 8)) => MaybeNumConvert a Int8 where + maybeNumConvert !a = fmap (bitCoerce @(Signed 8)) $ maybeNumConvert a + +instance (MaybeNumConvert a (BitVector 1)) => MaybeNumConvert a Bit where + maybeNumConvert !a = unpack <$> maybeNumConvert a +instance (MaybeNumConvert (BitVector 1) a) => MaybeNumConvert Bit a where + maybeNumConvert !a = maybeNumConvert (pack a) diff --git a/clash-prelude/src/Clash/Class/NumConvert/Internal/NumConvert.hs b/clash-prelude/src/Clash/Class/NumConvert/Internal/NumConvert.hs new file mode 100644 index 0000000000..a52effbacf --- /dev/null +++ b/clash-prelude/src/Clash/Class/NumConvert/Internal/NumConvert.hs @@ -0,0 +1,178 @@ +{-# LANGUAGE CPP #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE UndecidableInstances #-} +{-# OPTIONS_GHC -fplugin=GHC.TypeLits.KnownNat.Solver #-} +{-# OPTIONS_HADDOCK hide #-} + +#include "MachDeps.h" + +{- | +Copyright : (C) 2025 , Martijn Bastiaan +License : BSD2 (see the file LICENSE) +Maintainer : QBayLogic B.V. +-} +module Clash.Class.NumConvert.Internal.NumConvert where + +import Prelude + +import Clash.Class.BitPack +import Clash.Class.Resize +import Clash.Sized.BitVector +import Clash.Sized.Index +import Clash.Sized.Signed +import Clash.Sized.Unsigned + +import GHC.TypeLits (KnownNat, type (+), type (<=), type (^)) +import GHC.TypeLits.Extra (CLog) + +import Data.Int (Int16, Int32, Int64, Int8) +import Data.Word (Word16, Word32, Word64, Word8) + +{- $setup +>>> import Clash.Prelude +>>> import Clash.Class.NumConvert +-} + +{- | Conversions that are, based on their types, guaranteed to succeed. A +successful conversion retains the numerical value interpretation of the source +type in the target type and does not produce errors. + +== __Laws__ +A conversion is successful if a round trip conversion is lossless. I.e., + +> Just x == maybeNumConvert (numConvert @a @b x) + +for all values @x@ of type @a@. It should also preserve the numerical value +interpretation of the bits. For types that have an "Integral" instance, this +intuition is captured by: + +> toInteger x == toInteger (numConvert @a @b x) + +Instances should make sure their constraints are as \"tight\" as possible. I.e., +if an instance's constraints cannot be satisfied, then for the same types +'Clash.Class.NumConvert.maybeConvert' should return 'Nothing' for one or more +values in the domain of the source type @a@: + +> L.any isNothing (L.map (maybeNumConvert @a @b) [minBound ..]) + +All implementations should be total, i.e., they should not produce \"bottoms\". + +Additionally, any implementation should be translatable to synthesizable HDL. +-} +class NumConvert a b where + {- | NumConvert a supplied value of type @a@ to a value of type @b@. The conversion + is guaranteed to succeed. + + >>> numConvert (3 :: Index 8) :: Unsigned 8 + 3 + + The following will fail with a type error, as we cannot prove that all values + of @Index 8@ can be represented by an @Unsigned 2@: + + >>> numConvert (3 :: Index 8) :: Unsigned 2 + ... + + For the time being, if the input is an 'Clash.XException.XException', then + the output is too. This property might be relaxed in the future. + -} + numConvert :: a -> b + +instance (KnownNat n, KnownNat m, n <= m) => NumConvert (Index n) (Index m) where + numConvert = resize + +instance (KnownNat n, KnownNat m, 1 <= n, n <= 2 ^ m) => NumConvert (Index n) (Unsigned m) where + numConvert !a = resize $ bitCoerce a + +{- | Note: Conversion from @Index 1@ to @Signed 0@ is lossless, but not within the +constraints of the instance. +-} +instance (KnownNat n, KnownNat m, 1 <= n, CLog 2 n + 1 <= m) => NumConvert (Index n) (Signed m) where + numConvert !a = numConvert $ bitCoerce @_ @(Unsigned (CLog 2 n)) a + +instance (KnownNat n, KnownNat m, 1 <= n, n <= 2 ^ m) => NumConvert (Index n) (BitVector m) where + numConvert !a = resize $ pack a + +instance (KnownNat n, KnownNat m, 1 <= m, 2 ^ n <= m) => NumConvert (Unsigned n) (Index m) where + numConvert !a = bitCoerce $ resize a + +instance (KnownNat n, KnownNat m, n <= m) => NumConvert (Unsigned n) (Unsigned m) where + numConvert = resize + +{- | Note: Conversion from @Unsigned 0@ to @Signed 0@ is lossless, but not within the +constraints of the instance. +-} +instance (KnownNat n, KnownNat m, n + 1 <= m) => NumConvert (Unsigned n) (Signed m) where + numConvert !a = bitCoerce $ resize a + +instance (KnownNat n, KnownNat m, n <= m) => NumConvert (Unsigned n) (BitVector m) where + numConvert !a = resize $ pack a + +instance (KnownNat n, KnownNat m, n <= m) => NumConvert (Signed n) (Signed m) where + numConvert !a = resize a + +instance (KnownNat n, KnownNat m, 1 <= m, 2 ^ n <= m) => NumConvert (BitVector n) (Index m) where + numConvert = unpack . resize + +instance (KnownNat n, KnownNat m, n <= m) => NumConvert (BitVector n) (Unsigned m) where + numConvert = unpack . resize + +{- | Note: Conversion from @BitVector 0@ to @Signed 0@ is lossless, but not within the +constraints of the instance. +-} +instance (KnownNat n, KnownNat m, n + 1 <= m) => NumConvert (BitVector n) (Signed m) where + numConvert = unpack . resize + +instance (KnownNat n, KnownNat m, n <= m) => NumConvert (BitVector n) (BitVector m) where + numConvert = resize + +instance (NumConvert (Unsigned WORD_SIZE_IN_BITS) a) => NumConvert Word a where + numConvert !a = numConvert $ bitCoerce @_ @(Unsigned 64) a +instance (NumConvert (Unsigned 64) a) => NumConvert Word64 a where + numConvert !a = numConvert $ bitCoerce @_ @(Unsigned 64) a +instance (NumConvert (Unsigned 32) a) => NumConvert Word32 a where + numConvert !a = numConvert $ bitCoerce @_ @(Unsigned 32) a +instance (NumConvert (Unsigned 16) a) => NumConvert Word16 a where + numConvert !a = numConvert $ bitCoerce @_ @(Unsigned 16) a +instance (NumConvert (Unsigned 8) a) => NumConvert Word8 a where + numConvert !a = numConvert $ bitCoerce @_ @(Unsigned 8) a + +instance (NumConvert (Signed WORD_SIZE_IN_BITS) a) => NumConvert Int a where + numConvert !a = numConvert $ bitCoerce @_ @(Signed 64) a +instance (NumConvert (Signed 64) a) => NumConvert Int64 a where + numConvert !a = numConvert $ bitCoerce @_ @(Signed 64) a +instance (NumConvert (Signed 32) a) => NumConvert Int32 a where + numConvert !a = numConvert $ bitCoerce @_ @(Signed 32) a +instance (NumConvert (Signed 16) a) => NumConvert Int16 a where + numConvert !a = numConvert $ bitCoerce @_ @(Signed 16) a +instance (NumConvert (Signed 8) a) => NumConvert Int8 a where + numConvert !a = numConvert $ bitCoerce @_ @(Signed 8) a + +instance (NumConvert a (Unsigned WORD_SIZE_IN_BITS)) => NumConvert a Word where + numConvert = bitCoerce @(Unsigned 64) . numConvert +instance (NumConvert a (Unsigned 64)) => NumConvert a Word64 where + numConvert = bitCoerce @(Unsigned 64) . numConvert +instance (NumConvert a (Unsigned 32)) => NumConvert a Word32 where + numConvert = bitCoerce @(Unsigned 32) . numConvert +instance (NumConvert a (Unsigned 16)) => NumConvert a Word16 where + numConvert = bitCoerce @(Unsigned 16) . numConvert +instance (NumConvert a (Unsigned 8)) => NumConvert a Word8 where + numConvert = bitCoerce @(Unsigned 8) . numConvert + +instance (NumConvert a (Signed WORD_SIZE_IN_BITS)) => NumConvert a Int where + numConvert = bitCoerce @(Signed 64) . numConvert +instance (NumConvert a (Signed 64)) => NumConvert a Int64 where + numConvert = bitCoerce @(Signed 64) . numConvert +instance (NumConvert a (Signed 32)) => NumConvert a Int32 where + numConvert = bitCoerce @(Signed 32) . numConvert +instance (NumConvert a (Signed 16)) => NumConvert a Int16 where + numConvert = bitCoerce @(Signed 16) . numConvert +instance (NumConvert a (Signed 8)) => NumConvert a Int8 where + numConvert = bitCoerce @(Signed 8) . numConvert + +instance (NumConvert a (BitVector 1)) => NumConvert a Bit where + numConvert = unpack . numConvert +instance (NumConvert (BitVector 1) a) => NumConvert Bit a where + numConvert = numConvert . pack diff --git a/clash-prelude/src/Clash/Explicit/Prelude.hs b/clash-prelude/src/Clash/Explicit/Prelude.hs index dd72ec39a7..db853de8e0 100644 --- a/clash-prelude/src/Clash/Explicit/Prelude.hs +++ b/clash-prelude/src/Clash/Explicit/Prelude.hs @@ -130,6 +130,7 @@ module Clash.Explicit.Prelude , module Clash.Class.BitPack , module Clash.Class.Exp , module Clash.Class.Num + , module Clash.Class.NumConvert , module Clash.Class.Resize -- *** Other , module Control.Applicative @@ -163,6 +164,7 @@ import Clash.Class.AutoReg import Clash.Class.BitPack import Clash.Class.Exp import Clash.Class.Num +import Clash.Class.NumConvert import Clash.Class.Resize import Clash.Magic import Clash.NamedTypes diff --git a/clash-prelude/src/Clash/Prelude.hs b/clash-prelude/src/Clash/Prelude.hs index 0b8ab9a0b3..dd61b4ad12 100644 --- a/clash-prelude/src/Clash/Prelude.hs +++ b/clash-prelude/src/Clash/Prelude.hs @@ -146,6 +146,7 @@ module Clash.Prelude , module Clash.Class.BitPack , module Clash.Class.Exp , module Clash.Class.Num + , module Clash.Class.NumConvert , module Clash.Class.Parity , module Clash.Class.Resize -- *** Other @@ -185,6 +186,7 @@ import Clash.Class.AutoReg (AutoReg, deriveAutoReg) import Clash.Class.BitPack import Clash.Class.Exp import Clash.Class.Num +import Clash.Class.NumConvert import Clash.Class.Parity import Clash.Class.Resize import qualified Clash.Explicit.Prelude as E diff --git a/clash-prelude/tests/Clash/Tests/MaybeNumConvert.hs b/clash-prelude/tests/Clash/Tests/MaybeNumConvert.hs new file mode 100644 index 0000000000..6f45c5230b --- /dev/null +++ b/clash-prelude/tests/Clash/Tests/MaybeNumConvert.hs @@ -0,0 +1,326 @@ +{-# LANGUAGE CPP #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE RankNTypes #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE NoImplicitPrelude #-} +{-# OPTIONS_GHC -fplugin=GHC.TypeLits.Extra.Solver #-} +{-# OPTIONS_GHC -fplugin=GHC.TypeLits.KnownNat.Solver #-} +{-# OPTIONS_GHC -fplugin=GHC.TypeLits.Normalise #-} + +{- | +Copyright : (C) 2025 , Martijn Bastiaan +License : BSD2 (see the file LICENSE) +Maintainer : QBayLogic B.V. + +Test generation of 'MaybeNumConvert' instances: + +> for a in ["Index", "Unsigned", "Signed", "BitVector"]: +> for b in ["Index", "Unsigned", "Signed", "BitVector"]: +> ia_max = "indexMax" if a == "Index" else "otherMax" +> ib_max = "indexMax" if b == "Index" else "otherMax" +> n = "(n + 1)" if a == "Index" else "n" +> m = "(m + 1)" if b == "Index" else "m" +> print(f"""case_maybeConvert{a}{b} :: Assertion +> case_maybeConvert{a}{b} = +> forM_ [0 .. {ia_max}] $ \\n -> +> forM_ [0 .. {ib_max}] $ \\m -> +> withSomeSNat n $ \(SNat :: SNat n) -> +> withSomeSNat m $ \(SNat :: SNat m) -> +> forM_ [minBound .. maxBound] $ \(i :: {a} {n}) -> do +> assertBool (show (n, m, i)) (maybeConvertLaw1 (Proxy @({b} {m})) i) +> assertBool (show (n, m, i)) (maybeConvertLaw2 (Proxy @({b} {m})) i) +> assertBool (show (n, m, i)) (maybeConvertLaw3 (Proxy @({b} {m})) i) +> assertBool (show (n, m, i)) (maybeConvertLaw4 (Proxy @({b} {m})) i) +> """) +-} +module Clash.Tests.MaybeNumConvert where + +import Control.Monad (forM_) +import Data.Data (Proxy (..)) +import Data.Either (isLeft) +import Data.Maybe (fromMaybe, isJust) +import GHC.TypeNats (someNatVal) +import Test.Tasty (TestTree, defaultMain) +import Test.Tasty.HUnit (Assertion, assertBool, testCase) +import Test.Tasty.TH (testGroupGenerator) + +#if MIN_VERSION_base(4,18,0) +import Clash.Prelude hiding (someNatVal, withSomeSNat) +#else +import Clash.Prelude hiding (someNatVal) +#endif + +#if !MIN_VERSION_base(4,16,0) +import Numeric.Natural (Natural) +#endif + +main :: IO () +main = defaultMain tests + +tests :: TestTree +tests = $(testGroupGenerator) + +withSomeSNat :: Natural -> (forall (n :: Nat). SNat n -> r) -> r +withSomeSNat n f = case someNatVal n of + SomeNat (_ :: Proxy n) -> f (SNat @n) + +maybeConvertLaw1 :: + forall a b. + (Eq a, MaybeNumConvert a b, MaybeNumConvert b a) => + Proxy b -> + a -> + Bool +maybeConvertLaw1 Proxy x = + x == fromMaybe x (maybeNumConvert @_ @b x >>= maybeNumConvert) + +maybeConvertLaw2 :: + forall a b. + (MaybeNumConvert a b, MaybeNumConvert b a, Integral b, Integral a) => + Proxy b -> + a -> + Bool +maybeConvertLaw2 Proxy x = + toInteger x == fromMaybe (toInteger x) (toInteger <$> maybeNumConvert @_ @b x) + +maybeConvertLaw3 :: + forall a b. + (MaybeNumConvert a b, MaybeNumConvert b a, Integral b, Integral a) => + Proxy b -> + a -> + Bool +maybeConvertLaw3 Proxy x = + isJust (maybeNumConvert @_ @b x) `implies` isJust (maybeNumConvert @_ @a =<< maybeNumConvert @_ @b x) + where + implies :: Bool -> Bool -> Bool + implies True False = False + implies _ _ = True + +maybeConvertLaw4 :: + forall a b. + (MaybeNumConvert a b, MaybeNumConvert b a, Integral b, Integral a, Bounded b, Bounded a) => + Proxy b -> + a -> + Bool +maybeConvertLaw4 Proxy x = + isJust (maybeNumConvert @_ @b x) == (i x >= i (minBound @b) && i x <= i (maxBound @b)) + where + i :: (Integral c) => c -> Integer + i = toInteger + +-- | Checks whether an 'XException' in, means an 'XException' out +convertXException :: forall a b. (MaybeNumConvert a b) => Proxy a -> Proxy b -> Bool +convertXException _ _ = isLeft $ isX $ maybeNumConvert @a @b (errorX "" :: a) + +indexMax :: Natural +indexMax = 128 + +otherMax :: Natural +otherMax = 8 + +case_maybeConvertIndexIndex :: Assertion +case_maybeConvertIndexIndex = + forM_ [0 .. indexMax] $ \n -> + forM_ [0 .. indexMax] $ \m -> + withSomeSNat n $ \(SNat :: SNat n) -> + withSomeSNat m $ \(SNat :: SNat m) -> do + assertBool (show (n, m)) (convertXException (Proxy @(Index (n + 1))) (Proxy @(Index (m + 1)))) + forM_ [minBound .. maxBound] $ \(i :: Index (n + 1)) -> do + assertBool (show (n, m, i)) (maybeConvertLaw1 (Proxy @(Index (m + 1))) i) + assertBool (show (n, m, i)) (maybeConvertLaw2 (Proxy @(Index (m + 1))) i) + assertBool (show (n, m, i)) (maybeConvertLaw3 (Proxy @(Index (m + 1))) i) + assertBool (show (n, m, i)) (maybeConvertLaw4 (Proxy @(Index (m + 1))) i) + +case_maybeConvertIndexUnsigned :: Assertion +case_maybeConvertIndexUnsigned = + forM_ [0 .. indexMax] $ \n -> + forM_ [0 .. otherMax] $ \m -> + withSomeSNat n $ \(SNat :: SNat n) -> + withSomeSNat m $ \(SNat :: SNat m) -> do + assertBool (show (n, m)) (convertXException (Proxy @(Index (n + 1))) (Proxy @(Unsigned m))) + forM_ [minBound .. maxBound] $ \(i :: Index (n + 1)) -> do + assertBool (show (n, m, i)) (maybeConvertLaw1 (Proxy @(Unsigned m)) i) + assertBool (show (n, m, i)) (maybeConvertLaw2 (Proxy @(Unsigned m)) i) + assertBool (show (n, m, i)) (maybeConvertLaw3 (Proxy @(Unsigned m)) i) + assertBool (show (n, m, i)) (maybeConvertLaw4 (Proxy @(Unsigned m)) i) + +case_maybeConvertIndexSigned :: Assertion +case_maybeConvertIndexSigned = + forM_ [0 .. indexMax] $ \n -> + forM_ [0 .. otherMax] $ \m -> + withSomeSNat n $ \(SNat :: SNat n) -> + withSomeSNat m $ \(SNat :: SNat m) -> do + assertBool (show (n, m)) (convertXException (Proxy @(Index (n + 1))) (Proxy @(Signed m))) + forM_ [minBound .. maxBound] $ \(i :: Index (n + 1)) -> do + assertBool (show (n, m, i)) (maybeConvertLaw1 (Proxy @(Signed m)) i) + assertBool (show (n, m, i)) (maybeConvertLaw2 (Proxy @(Signed m)) i) + assertBool (show (n, m, i)) (maybeConvertLaw3 (Proxy @(Signed m)) i) + assertBool (show (n, m, i)) (maybeConvertLaw4 (Proxy @(Signed m)) i) + +case_maybeConvertIndexBitVector :: Assertion +case_maybeConvertIndexBitVector = + forM_ [0 .. indexMax] $ \n -> + forM_ [0 .. otherMax] $ \m -> + withSomeSNat n $ \(SNat :: SNat n) -> + withSomeSNat m $ \(SNat :: SNat m) -> do + assertBool (show (n, m)) (convertXException (Proxy @(Index (n + 1))) (Proxy @(BitVector m))) + forM_ [minBound .. maxBound] $ \(i :: Index (n + 1)) -> do + assertBool (show (n, m, i)) (maybeConvertLaw1 (Proxy @(BitVector m)) i) + assertBool (show (n, m, i)) (maybeConvertLaw2 (Proxy @(BitVector m)) i) + assertBool (show (n, m, i)) (maybeConvertLaw3 (Proxy @(BitVector m)) i) + assertBool (show (n, m, i)) (maybeConvertLaw4 (Proxy @(BitVector m)) i) + +case_maybeConvertUnsignedIndex :: Assertion +case_maybeConvertUnsignedIndex = + forM_ [0 .. otherMax] $ \n -> + forM_ [0 .. indexMax] $ \m -> + withSomeSNat n $ \(SNat :: SNat n) -> + withSomeSNat m $ \(SNat :: SNat m) -> do + assertBool (show (n, m)) (convertXException (Proxy @(Unsigned n)) (Proxy @(Index (m + 1)))) + forM_ [minBound .. maxBound] $ \(i :: Unsigned n) -> do + assertBool (show (n, m, i)) (maybeConvertLaw1 (Proxy @(Index (m + 1))) i) + assertBool (show (n, m, i)) (maybeConvertLaw2 (Proxy @(Index (m + 1))) i) + assertBool (show (n, m, i)) (maybeConvertLaw3 (Proxy @(Index (m + 1))) i) + assertBool (show (n, m, i)) (maybeConvertLaw4 (Proxy @(Index (m + 1))) i) + +case_maybeConvertUnsignedUnsigned :: Assertion +case_maybeConvertUnsignedUnsigned = + forM_ [0 .. otherMax] $ \n -> + forM_ [0 .. otherMax] $ \m -> + withSomeSNat n $ \(SNat :: SNat n) -> + withSomeSNat m $ \(SNat :: SNat m) -> do + assertBool (show (n, m)) (convertXException (Proxy @(Unsigned n)) (Proxy @(Unsigned m))) + forM_ [minBound .. maxBound] $ \(i :: Unsigned n) -> do + assertBool (show (n, m, i)) (maybeConvertLaw1 (Proxy @(Unsigned m)) i) + assertBool (show (n, m, i)) (maybeConvertLaw2 (Proxy @(Unsigned m)) i) + assertBool (show (n, m, i)) (maybeConvertLaw3 (Proxy @(Unsigned m)) i) + assertBool (show (n, m, i)) (maybeConvertLaw4 (Proxy @(Unsigned m)) i) + +case_maybeConvertUnsignedSigned :: Assertion +case_maybeConvertUnsignedSigned = + forM_ [0 .. otherMax] $ \n -> + forM_ [0 .. otherMax] $ \m -> + withSomeSNat n $ \(SNat :: SNat n) -> + withSomeSNat m $ \(SNat :: SNat m) -> do + assertBool (show (n, m)) (convertXException (Proxy @(Unsigned n)) (Proxy @(Signed m))) + forM_ [minBound .. maxBound] $ \(i :: Unsigned n) -> do + assertBool (show (n, m, i)) (maybeConvertLaw1 (Proxy @(Signed m)) i) + assertBool (show (n, m, i)) (maybeConvertLaw2 (Proxy @(Signed m)) i) + assertBool (show (n, m, i)) (maybeConvertLaw3 (Proxy @(Signed m)) i) + assertBool (show (n, m, i)) (maybeConvertLaw4 (Proxy @(Signed m)) i) + +case_maybeConvertUnsignedBitVector :: Assertion +case_maybeConvertUnsignedBitVector = + forM_ [0 .. otherMax] $ \n -> + forM_ [0 .. otherMax] $ \m -> + withSomeSNat n $ \(SNat :: SNat n) -> + withSomeSNat m $ \(SNat :: SNat m) -> do + assertBool (show (n, m)) (convertXException (Proxy @(Unsigned n)) (Proxy @(BitVector m))) + forM_ [minBound .. maxBound] $ \(i :: Unsigned n) -> do + assertBool (show (n, m, i)) (maybeConvertLaw1 (Proxy @(BitVector m)) i) + assertBool (show (n, m, i)) (maybeConvertLaw2 (Proxy @(BitVector m)) i) + assertBool (show (n, m, i)) (maybeConvertLaw3 (Proxy @(BitVector m)) i) + assertBool (show (n, m, i)) (maybeConvertLaw4 (Proxy @(BitVector m)) i) + +case_maybeConvertSignedIndex :: Assertion +case_maybeConvertSignedIndex = + forM_ [0 .. otherMax] $ \n -> + forM_ [0 .. indexMax] $ \m -> + withSomeSNat n $ \(SNat :: SNat n) -> + withSomeSNat m $ \(SNat :: SNat m) -> do + assertBool (show (n, m)) (convertXException (Proxy @(Signed n)) (Proxy @(Index (m + 1)))) + forM_ [minBound .. maxBound] $ \(i :: Signed n) -> do + assertBool (show (n, m, i)) (maybeConvertLaw1 (Proxy @(Index (m + 1))) i) + assertBool (show (n, m, i)) (maybeConvertLaw2 (Proxy @(Index (m + 1))) i) + assertBool (show (n, m, i)) (maybeConvertLaw3 (Proxy @(Index (m + 1))) i) + assertBool (show (n, m, i)) (maybeConvertLaw4 (Proxy @(Index (m + 1))) i) + +case_maybeConvertSignedUnsigned :: Assertion +case_maybeConvertSignedUnsigned = + forM_ [0 .. otherMax] $ \n -> + forM_ [0 .. otherMax] $ \m -> + withSomeSNat n $ \(SNat :: SNat n) -> + withSomeSNat m $ \(SNat :: SNat m) -> do + assertBool (show (n, m)) (convertXException (Proxy @(Signed n)) (Proxy @(Unsigned m))) + forM_ [minBound .. maxBound] $ \(i :: Signed n) -> do + assertBool (show (n, m, i)) (maybeConvertLaw1 (Proxy @(Unsigned m)) i) + assertBool (show (n, m, i)) (maybeConvertLaw2 (Proxy @(Unsigned m)) i) + assertBool (show (n, m, i)) (maybeConvertLaw3 (Proxy @(Unsigned m)) i) + assertBool (show (n, m, i)) (maybeConvertLaw4 (Proxy @(Unsigned m)) i) + +case_maybeConvertSignedSigned :: Assertion +case_maybeConvertSignedSigned = + forM_ [0 .. otherMax] $ \n -> + forM_ [0 .. otherMax] $ \m -> + withSomeSNat n $ \(SNat :: SNat n) -> + withSomeSNat m $ \(SNat :: SNat m) -> do + assertBool (show (n, m)) (convertXException (Proxy @(Signed n)) (Proxy @(Signed m))) + forM_ [minBound .. maxBound] $ \(i :: Signed n) -> do + assertBool (show (n, m, i)) (maybeConvertLaw1 (Proxy @(Signed m)) i) + assertBool (show (n, m, i)) (maybeConvertLaw2 (Proxy @(Signed m)) i) + assertBool (show (n, m, i)) (maybeConvertLaw3 (Proxy @(Signed m)) i) + assertBool (show (n, m, i)) (maybeConvertLaw4 (Proxy @(Signed m)) i) + +case_maybeConvertSignedBitVector :: Assertion +case_maybeConvertSignedBitVector = + forM_ [0 .. otherMax] $ \n -> + forM_ [0 .. otherMax] $ \m -> + withSomeSNat n $ \(SNat :: SNat n) -> + withSomeSNat m $ \(SNat :: SNat m) -> do + assertBool (show (n, m)) (convertXException (Proxy @(Signed n)) (Proxy @(BitVector m))) + forM_ [minBound .. maxBound] $ \(i :: Signed n) -> do + assertBool (show (n, m, i)) (maybeConvertLaw1 (Proxy @(BitVector m)) i) + assertBool (show (n, m, i)) (maybeConvertLaw2 (Proxy @(BitVector m)) i) + assertBool (show (n, m, i)) (maybeConvertLaw3 (Proxy @(BitVector m)) i) + assertBool (show (n, m, i)) (maybeConvertLaw4 (Proxy @(BitVector m)) i) + +case_maybeConvertBitVectorIndex :: Assertion +case_maybeConvertBitVectorIndex = + forM_ [0 .. otherMax] $ \n -> + forM_ [0 .. indexMax] $ \m -> + withSomeSNat n $ \(SNat :: SNat n) -> + withSomeSNat m $ \(SNat :: SNat m) -> do + assertBool (show (n, m)) (convertXException (Proxy @(BitVector n)) (Proxy @(Index (m + 1)))) + forM_ [minBound .. maxBound] $ \(i :: BitVector n) -> do + assertBool (show (n, m, i)) (maybeConvertLaw1 (Proxy @(Index (m + 1))) i) + assertBool (show (n, m, i)) (maybeConvertLaw2 (Proxy @(Index (m + 1))) i) + assertBool (show (n, m, i)) (maybeConvertLaw3 (Proxy @(Index (m + 1))) i) + assertBool (show (n, m, i)) (maybeConvertLaw4 (Proxy @(Index (m + 1))) i) + +case_maybeConvertBitVectorUnsigned :: Assertion +case_maybeConvertBitVectorUnsigned = + forM_ [0 .. otherMax] $ \n -> + forM_ [0 .. otherMax] $ \m -> + withSomeSNat n $ \(SNat :: SNat n) -> + withSomeSNat m $ \(SNat :: SNat m) -> do + assertBool (show (n, m)) (convertXException (Proxy @(BitVector n)) (Proxy @(Unsigned m))) + forM_ [minBound .. maxBound] $ \(i :: BitVector n) -> do + assertBool (show (n, m, i)) (maybeConvertLaw1 (Proxy @(Unsigned m)) i) + assertBool (show (n, m, i)) (maybeConvertLaw2 (Proxy @(Unsigned m)) i) + assertBool (show (n, m, i)) (maybeConvertLaw3 (Proxy @(Unsigned m)) i) + assertBool (show (n, m, i)) (maybeConvertLaw4 (Proxy @(Unsigned m)) i) + +case_maybeConvertBitVectorSigned :: Assertion +case_maybeConvertBitVectorSigned = + forM_ [0 .. otherMax] $ \n -> + forM_ [0 .. otherMax] $ \m -> + withSomeSNat n $ \(SNat :: SNat n) -> + withSomeSNat m $ \(SNat :: SNat m) -> do + assertBool (show (n, m)) (convertXException (Proxy @(BitVector n)) (Proxy @(Signed m))) + forM_ [minBound .. maxBound] $ \(i :: BitVector n) -> do + assertBool (show (n, m, i)) (maybeConvertLaw1 (Proxy @(Signed m)) i) + assertBool (show (n, m, i)) (maybeConvertLaw2 (Proxy @(Signed m)) i) + assertBool (show (n, m, i)) (maybeConvertLaw3 (Proxy @(Signed m)) i) + assertBool (show (n, m, i)) (maybeConvertLaw4 (Proxy @(Signed m)) i) + +case_maybeConvertBitVectorBitVector :: Assertion +case_maybeConvertBitVectorBitVector = + forM_ [0 .. otherMax] $ \n -> + forM_ [0 .. otherMax] $ \m -> + withSomeSNat n $ \(SNat :: SNat n) -> + withSomeSNat m $ \(SNat :: SNat m) -> do + assertBool (show (n, m)) (convertXException (Proxy @(BitVector n)) (Proxy @(BitVector m))) + forM_ [minBound .. maxBound] $ \(i :: BitVector n) -> do + assertBool (show (n, m, i)) (maybeConvertLaw1 (Proxy @(BitVector m)) i) + assertBool (show (n, m, i)) (maybeConvertLaw2 (Proxy @(BitVector m)) i) + assertBool (show (n, m, i)) (maybeConvertLaw3 (Proxy @(BitVector m)) i) + assertBool (show (n, m, i)) (maybeConvertLaw4 (Proxy @(BitVector m)) i) diff --git a/clash-prelude/tests/Clash/Tests/NumConvert.hs b/clash-prelude/tests/Clash/Tests/NumConvert.hs new file mode 100644 index 0000000000..2b93124d53 --- /dev/null +++ b/clash-prelude/tests/Clash/Tests/NumConvert.hs @@ -0,0 +1,339 @@ +{-# LANGUAGE CPP #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE RankNTypes #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE NoImplicitPrelude #-} +{-# OPTIONS_GHC -fplugin=GHC.TypeLits.Extra.Solver #-} +{-# OPTIONS_GHC -fplugin=GHC.TypeLits.KnownNat.Solver #-} +{-# OPTIONS_GHC -fplugin=GHC.TypeLits.Normalise #-} + +{- | +Copyright : (C) 2025 , Martijn Bastiaan +License : BSD2 (see the file LICENSE) +Maintainer : QBayLogic B.V. + +Test generation of 'NumConvert' instances: + +> constraints = { +> ("Index", "Index") : (False, "SNat @{n} `compareSNat` SNat @{m}"), +> ("Index", "Unsigned") : (False, "SNat @{n} `compareSNat` SNat @(2 ^ {m})"), +> ("Index", "Signed") : (True, "SNat @(CLog 2 {n} + 1) `compareSNat` SNat @{m}"), +> ("Index", "BitVector") : (False, "SNat @{n} `compareSNat` SNat @(2 ^ {m})"), +> ("Unsigned", "Index") : (False, "SNat @(2^{n}) `compareSNat` SNat @{m}"), +> ("Unsigned", "Unsigned") : (False, "SNat @{n} `compareSNat` SNat @{m}"), +> ("Unsigned", "Signed") : (True, "SNat @({n} + 1) `compareSNat` SNat @{m}"), +> ("Unsigned", "BitVector") : (False, "SNat @{n} `compareSNat` SNat @{m}"), +> ("Signed", "Signed") : (False, "SNat @{n} `compareSNat` SNat @{m}"), +> ("BitVector", "Index") : (False, "SNat @(2^{n}) `compareSNat` SNat @{m}"), +> ("BitVector", "Unsigned") : (False, "SNat @{n} `compareSNat` SNat @{m}"), +> ("BitVector", "Signed") : (True, "SNat @({n} + 1) `compareSNat` SNat @{m}"), +> ("BitVector", "BitVector") : (False, "SNat @{n} `compareSNat` SNat @{m}"), +> } +> +> for a in ["Index", "Unsigned", "Signed", "BitVector"]: +> for b in ["Index", "Unsigned", "Signed", "BitVector"]: +> ia_max = "indexMax" if a == "Index" else "otherMax" +> ib_max = "indexMax" if b == "Index" else "otherMax" +> n = "(n + 1)" if a == "Index" else "n" +> m = "(m + 1)" if b == "Index" else "m" +> if (a, b) not in constraints: +> continue +> print(f"""case_convert{a}{b} :: Assertion +> case_convert{a}{b} = +> forM_ [0 .. {ia_max}] $ \\n -> +> forM_ [0 .. {ib_max}] $ \\m -> +> withSomeSNat n $ \\(SNat :: SNat n) -> +> withSomeSNat m $ \\(SNat :: SNat m) -> +> case {constraints[(a, b)][1].format(n=n, m=m)} of +> SNatLE -> do +> assertBool (show (n, m)) (convertXException (Proxy @({a} {n})) (Proxy @({b} {m}))) +> forM_ [minBound .. maxBound] $ \\(i :: {a} {n}) -> do +> assertBool (show (n, m, i)) (convertLaw1 (Proxy @({b} {m})) i) +> assertBool (show (n, m, i)) (convertLaw2 (Proxy @({b} {m})) i) +> _ | (n == 0 && m == 0 && zeroWidthSkip) -> pure () +> _ -> assertBool (show (n, m)) (convertLaw3 (Proxy @({a} {n})) (Proxy @({b} {m}))) +> where +> zeroWidthSkip = {constraints[(a, b)][0]} +> """) +-} +module Clash.Tests.NumConvert where + +import Control.Monad (forM_) +import Data.Data (Proxy (..)) +import Data.Maybe (isNothing) +import GHC.TypeNats (someNatVal) +import Test.Tasty (TestTree, defaultMain) +import Test.Tasty.HUnit (Assertion, assertBool, testCase) +import Test.Tasty.TH (testGroupGenerator) + +import qualified Data.List as L + +#if MIN_VERSION_base(4,18,0) +import Clash.Prelude hiding (someNatVal, withSomeSNat) +#else +import Clash.Prelude hiding (someNatVal) +#endif + +#if !MIN_VERSION_base(4,16,0) +import Numeric.Natural (Natural) +#endif + +convertLaw1 :: forall a b. (NumConvert a b, MaybeNumConvert b a, Eq a) => Proxy b -> a -> Bool +convertLaw1 _ x = Just x == maybeNumConvert (numConvert @a @b x) + +convertLaw2 :: forall a b. (NumConvert a b, Eq a, Integral b, Integral a) => Proxy b -> a -> Bool +convertLaw2 _ x = toInteger x == toInteger (numConvert @a @b x) + +{- | Tightness law: this law is tested for if there is _no_ instance of +'NumConvert'. If this is the case, 'MaybeNumConvert' should at least return a 'Nothing' +once when converting the domain of @a@ to @b@. If all conversions are possible, +the constraints of the instances should be relaxed. If the domain of @a@ is +empty, this law is considered satisfied too. +-} +convertLaw3 :: forall a b. (MaybeNumConvert a b, Bounded a, Enum a) => Proxy a -> Proxy b -> Bool +convertLaw3 _ _ = L.any isNothing results + where + results = L.map (maybeNumConvert @a @b) [minBound ..] + +-- | Checks whether an 'XException' in, means an 'XException' out +convertXException :: forall a b. (NumConvert a b) => Proxy a -> Proxy b -> Bool +convertXException _ _ = case isX $ numConvert @a @b (errorX "BOO" :: a) of + Left s -> "BOO" `L.isInfixOf` s + Right _ -> False + +main :: IO () +main = defaultMain tests + +tests :: TestTree +tests = $(testGroupGenerator) + +withSomeSNat :: Natural -> (forall (n :: Nat). SNat n -> r) -> r +withSomeSNat n f = case someNatVal n of + SomeNat (_ :: Proxy n) -> f (SNat @n) + +indexMax :: Natural +indexMax = 128 + +otherMax :: Natural +otherMax = 8 + +case_convertIndexIndex :: Assertion +case_convertIndexIndex = + forM_ [0 .. indexMax] $ \n -> + forM_ [0 .. indexMax] $ \m -> + withSomeSNat n $ \(SNat :: SNat n) -> + withSomeSNat m $ \(SNat :: SNat m) -> + case SNat @(n + 1) `compareSNat` SNat @(m + 1) of + SNatLE -> do + assertBool (show (n, m)) (convertXException (Proxy @(Index (n + 1))) (Proxy @(Index (m + 1)))) + forM_ [minBound .. maxBound] $ \(i :: Index (n + 1)) -> do + assertBool (show (n, m, i)) (convertLaw1 (Proxy @(Index (m + 1))) i) + assertBool (show (n, m, i)) (convertLaw2 (Proxy @(Index (m + 1))) i) + _ | (n == 0 && m == 0 && zeroWidthSkip) -> pure () + _ -> assertBool (show (n, m)) (convertLaw3 (Proxy @(Index (n + 1))) (Proxy @(Index (m + 1)))) + where + zeroWidthSkip = False + +case_convertIndexUnsigned :: Assertion +case_convertIndexUnsigned = + forM_ [0 .. indexMax] $ \n -> + forM_ [0 .. otherMax] $ \m -> + withSomeSNat n $ \(SNat :: SNat n) -> + withSomeSNat m $ \(SNat :: SNat m) -> + case SNat @(n + 1) `compareSNat` SNat @(2 ^ m) of + SNatLE -> do + assertBool (show (n, m)) (convertXException (Proxy @(Index (n + 1))) (Proxy @(Unsigned m))) + forM_ [minBound .. maxBound] $ \(i :: Index (n + 1)) -> do + assertBool (show (n, m, i)) (convertLaw1 (Proxy @(Unsigned m)) i) + assertBool (show (n, m, i)) (convertLaw2 (Proxy @(Unsigned m)) i) + _ | (n == 0 && m == 0 && zeroWidthSkip) -> pure () + _ -> assertBool (show (n, m)) (convertLaw3 (Proxy @(Index (n + 1))) (Proxy @(Unsigned m))) + where + zeroWidthSkip = False + +case_convertIndexSigned :: Assertion +case_convertIndexSigned = + forM_ [0 .. indexMax] $ \n -> + forM_ [0 .. otherMax] $ \m -> + withSomeSNat n $ \(SNat :: SNat n) -> + withSomeSNat m $ \(SNat :: SNat m) -> + case SNat @(CLog 2 (n + 1) + 1) `compareSNat` SNat @m of + SNatLE -> do + assertBool (show (n, m)) (convertXException (Proxy @(Index (n + 1))) (Proxy @(Signed m))) + forM_ [minBound .. maxBound] $ \(i :: Index (n + 1)) -> do + assertBool (show (n, m, i)) (convertLaw1 (Proxy @(Signed m)) i) + assertBool (show (n, m, i)) (convertLaw2 (Proxy @(Signed m)) i) + _ | (n == 0 && m == 0 && zeroWidthSkip) -> pure () + _ -> assertBool (show (n, m)) (convertLaw3 (Proxy @(Index (n + 1))) (Proxy @(Signed m))) + where + zeroWidthSkip = True + +case_convertIndexBitVector :: Assertion +case_convertIndexBitVector = + forM_ [0 .. indexMax] $ \n -> + forM_ [0 .. otherMax] $ \m -> + withSomeSNat n $ \(SNat :: SNat n) -> + withSomeSNat m $ \(SNat :: SNat m) -> + case SNat @(n + 1) `compareSNat` SNat @(2 ^ m) of + SNatLE -> do + assertBool (show (n, m)) (convertXException (Proxy @(Index (n + 1))) (Proxy @(BitVector m))) + forM_ [minBound .. maxBound] $ \(i :: Index (n + 1)) -> do + assertBool (show (n, m, i)) (convertLaw1 (Proxy @(BitVector m)) i) + assertBool (show (n, m, i)) (convertLaw2 (Proxy @(BitVector m)) i) + _ | (n == 0 && m == 0 && zeroWidthSkip) -> pure () + _ -> assertBool (show (n, m)) (convertLaw3 (Proxy @(Index (n + 1))) (Proxy @(BitVector m))) + where + zeroWidthSkip = False + +case_convertUnsignedIndex :: Assertion +case_convertUnsignedIndex = + forM_ [0 .. otherMax] $ \n -> + forM_ [0 .. indexMax] $ \m -> + withSomeSNat n $ \(SNat :: SNat n) -> + withSomeSNat m $ \(SNat :: SNat m) -> + case SNat @(2 ^ n) `compareSNat` SNat @(m + 1) of + SNatLE -> do + assertBool (show (n, m)) (convertXException (Proxy @(Unsigned n)) (Proxy @(Index (m + 1)))) + forM_ [minBound .. maxBound] $ \(i :: Unsigned n) -> do + assertBool (show (n, m, i)) (convertLaw1 (Proxy @(Index (m + 1))) i) + assertBool (show (n, m, i)) (convertLaw2 (Proxy @(Index (m + 1))) i) + _ | (n == 0 && m == 0 && zeroWidthSkip) -> pure () + _ -> assertBool (show (n, m)) (convertLaw3 (Proxy @(Unsigned n)) (Proxy @(Index (m + 1)))) + where + zeroWidthSkip = False + +case_convertUnsignedUnsigned :: Assertion +case_convertUnsignedUnsigned = + forM_ [0 .. otherMax] $ \n -> + forM_ [0 .. otherMax] $ \m -> + withSomeSNat n $ \(SNat :: SNat n) -> + withSomeSNat m $ \(SNat :: SNat m) -> + case SNat @n `compareSNat` SNat @m of + SNatLE -> do + assertBool (show (n, m)) (convertXException (Proxy @(Unsigned n)) (Proxy @(Unsigned m))) + forM_ [minBound .. maxBound] $ \(i :: Unsigned n) -> do + assertBool (show (n, m, i)) (convertLaw1 (Proxy @(Unsigned m)) i) + assertBool (show (n, m, i)) (convertLaw2 (Proxy @(Unsigned m)) i) + _ | (n == 0 && m == 0 && zeroWidthSkip) -> pure () + _ -> assertBool (show (n, m)) (convertLaw3 (Proxy @(Unsigned n)) (Proxy @(Unsigned m))) + where + zeroWidthSkip = False + +case_convertUnsignedSigned :: Assertion +case_convertUnsignedSigned = + forM_ [0 .. otherMax] $ \n -> + forM_ [0 .. otherMax] $ \m -> + withSomeSNat n $ \(SNat :: SNat n) -> + withSomeSNat m $ \(SNat :: SNat m) -> + case SNat @(n + 1) `compareSNat` SNat @m of + SNatLE -> do + assertBool (show (n, m)) (convertXException (Proxy @(Unsigned n)) (Proxy @(Signed m))) + forM_ [minBound .. maxBound] $ \(i :: Unsigned n) -> do + assertBool (show (n, m, i)) (convertLaw1 (Proxy @(Signed m)) i) + assertBool (show (n, m, i)) (convertLaw2 (Proxy @(Signed m)) i) + _ | (n == 0 && m == 0 && zeroWidthSkip) -> pure () + _ -> assertBool (show (n, m)) (convertLaw3 (Proxy @(Unsigned n)) (Proxy @(Signed m))) + where + zeroWidthSkip = True + +case_convertUnsignedBitVector :: Assertion +case_convertUnsignedBitVector = + forM_ [0 .. otherMax] $ \n -> + forM_ [0 .. otherMax] $ \m -> + withSomeSNat n $ \(SNat :: SNat n) -> + withSomeSNat m $ \(SNat :: SNat m) -> + case SNat @n `compareSNat` SNat @m of + SNatLE -> do + assertBool (show (n, m)) (convertXException (Proxy @(Unsigned n)) (Proxy @(BitVector m))) + forM_ [minBound .. maxBound] $ \(i :: Unsigned n) -> do + assertBool (show (n, m, i)) (convertLaw1 (Proxy @(BitVector m)) i) + assertBool (show (n, m, i)) (convertLaw2 (Proxy @(BitVector m)) i) + _ | (n == 0 && m == 0 && zeroWidthSkip) -> pure () + _ -> assertBool (show (n, m)) (convertLaw3 (Proxy @(Unsigned n)) (Proxy @(BitVector m))) + where + zeroWidthSkip = False + +case_convertSignedSigned :: Assertion +case_convertSignedSigned = + forM_ [0 .. otherMax] $ \n -> + forM_ [0 .. otherMax] $ \m -> + withSomeSNat n $ \(SNat :: SNat n) -> + withSomeSNat m $ \(SNat :: SNat m) -> + case SNat @n `compareSNat` SNat @m of + SNatLE -> do + assertBool (show (n, m)) (convertXException (Proxy @(Signed n)) (Proxy @(Signed m))) + forM_ [minBound .. maxBound] $ \(i :: Signed n) -> do + assertBool (show (n, m, i)) (convertLaw1 (Proxy @(Signed m)) i) + assertBool (show (n, m, i)) (convertLaw2 (Proxy @(Signed m)) i) + _ | (n == 0 && m == 0 && zeroWidthSkip) -> pure () + _ -> assertBool (show (n, m)) (convertLaw3 (Proxy @(Signed n)) (Proxy @(Signed m))) + where + zeroWidthSkip = False + +case_convertBitVectorIndex :: Assertion +case_convertBitVectorIndex = + forM_ [0 .. otherMax] $ \n -> + forM_ [0 .. indexMax] $ \m -> + withSomeSNat n $ \(SNat :: SNat n) -> + withSomeSNat m $ \(SNat :: SNat m) -> + case SNat @(2 ^ n) `compareSNat` SNat @(m + 1) of + SNatLE -> do + assertBool (show (n, m)) (convertXException (Proxy @(BitVector n)) (Proxy @(Index (m + 1)))) + forM_ [minBound .. maxBound] $ \(i :: BitVector n) -> do + assertBool (show (n, m, i)) (convertLaw1 (Proxy @(Index (m + 1))) i) + assertBool (show (n, m, i)) (convertLaw2 (Proxy @(Index (m + 1))) i) + _ | (n == 0 && m == 0 && zeroWidthSkip) -> pure () + _ -> assertBool (show (n, m)) (convertLaw3 (Proxy @(BitVector n)) (Proxy @(Index (m + 1)))) + where + zeroWidthSkip = False + +case_convertBitVectorUnsigned :: Assertion +case_convertBitVectorUnsigned = + forM_ [0 .. otherMax] $ \n -> + forM_ [0 .. otherMax] $ \m -> + withSomeSNat n $ \(SNat :: SNat n) -> + withSomeSNat m $ \(SNat :: SNat m) -> + case SNat @n `compareSNat` SNat @m of + SNatLE -> do + assertBool (show (n, m)) (convertXException (Proxy @(BitVector n)) (Proxy @(Unsigned m))) + forM_ [minBound .. maxBound] $ \(i :: BitVector n) -> do + assertBool (show (n, m, i)) (convertLaw1 (Proxy @(Unsigned m)) i) + assertBool (show (n, m, i)) (convertLaw2 (Proxy @(Unsigned m)) i) + _ | (n == 0 && m == 0 && zeroWidthSkip) -> pure () + _ -> assertBool (show (n, m)) (convertLaw3 (Proxy @(BitVector n)) (Proxy @(Unsigned m))) + where + zeroWidthSkip = False + +case_convertBitVectorSigned :: Assertion +case_convertBitVectorSigned = + forM_ [0 .. otherMax] $ \n -> + forM_ [0 .. otherMax] $ \m -> + withSomeSNat n $ \(SNat :: SNat n) -> + withSomeSNat m $ \(SNat :: SNat m) -> + case SNat @(n + 1) `compareSNat` SNat @m of + SNatLE -> do + assertBool (show (n, m)) (convertXException (Proxy @(BitVector n)) (Proxy @(Signed m))) + forM_ [minBound .. maxBound] $ \(i :: BitVector n) -> do + assertBool (show (n, m, i)) (convertLaw1 (Proxy @(Signed m)) i) + assertBool (show (n, m, i)) (convertLaw2 (Proxy @(Signed m)) i) + _ | (n == 0 && m == 0 && zeroWidthSkip) -> pure () + _ -> assertBool (show (n, m)) (convertLaw3 (Proxy @(BitVector n)) (Proxy @(Signed m))) + where + zeroWidthSkip = True + +case_convertBitVectorBitVector :: Assertion +case_convertBitVectorBitVector = + forM_ [0 .. otherMax] $ \n -> + forM_ [0 .. otherMax] $ \m -> + withSomeSNat n $ \(SNat :: SNat n) -> + withSomeSNat m $ \(SNat :: SNat m) -> + case SNat @n `compareSNat` SNat @m of + SNatLE -> do + assertBool (show (n, m)) (convertXException (Proxy @(BitVector n)) (Proxy @(BitVector m))) + forM_ [minBound .. maxBound] $ \(i :: BitVector n) -> do + assertBool (show (n, m, i)) (convertLaw1 (Proxy @(BitVector m)) i) + assertBool (show (n, m, i)) (convertLaw2 (Proxy @(BitVector m)) i) + _ | (n == 0 && m == 0 && zeroWidthSkip) -> pure () + _ -> assertBool (show (n, m)) (convertLaw3 (Proxy @(BitVector n)) (Proxy @(BitVector m))) + where + zeroWidthSkip = False diff --git a/clash-prelude/tests/unittests.hs b/clash-prelude/tests/unittests.hs index 4c4d6cde68..db304a38d9 100644 --- a/clash-prelude/tests/unittests.hs +++ b/clash-prelude/tests/unittests.hs @@ -13,8 +13,10 @@ import qualified Clash.Tests.Counter import qualified Clash.Tests.DerivingDataRepr import qualified Clash.Tests.Fixed import qualified Clash.Tests.FixedExhaustive +import qualified Clash.Tests.MaybeNumConvert import qualified Clash.Tests.MaybeX import qualified Clash.Tests.NFDataX +import qualified Clash.Tests.NumConvert import qualified Clash.Tests.NumNewtypes import qualified Clash.Tests.Ram import qualified Clash.Tests.Reset @@ -35,15 +37,17 @@ tests = testGroup "Unittests" , Clash.Tests.AutoReg.tests , Clash.Tests.BitPack.tests , Clash.Tests.BitVector.tests - , Clash.Tests.BlockRam.tests , Clash.Tests.BlockRam.Blob.tests + , Clash.Tests.BlockRam.tests , Clash.Tests.Clocks.tests , Clash.Tests.Counter.tests , Clash.Tests.DerivingDataRepr.tests , Clash.Tests.Fixed.tests , Clash.Tests.FixedExhaustive.tests + , Clash.Tests.MaybeNumConvert.tests , Clash.Tests.MaybeX.tests , Clash.Tests.NFDataX.tests + , Clash.Tests.NumConvert.tests , Clash.Tests.NumNewtypes.tests , Clash.Tests.Ram.tests , Clash.Tests.Reset.tests From 6ae31a6fbce8a781b8a719bd9304406f2485cafd Mon Sep 17 00:00:00 2001 From: Peter Lebbing Date: Thu, 5 Jun 2025 15:15:19 +0200 Subject: [PATCH 2/2] My review comments --- clash-prelude/src/Clash/Class/NumConvert.hs | 9 ++++----- .../Clash/Class/NumConvert/Internal/MaybeNumConvert.hs | 6 +++--- .../src/Clash/Class/NumConvert/Internal/NumConvert.hs | 6 +++--- clash-prelude/tests/Clash/Tests/MaybeNumConvert.hs | 7 +++++-- 4 files changed, 15 insertions(+), 13 deletions(-) diff --git a/clash-prelude/src/Clash/Class/NumConvert.hs b/clash-prelude/src/Clash/Class/NumConvert.hs index 1569346955..be4003a61b 100644 --- a/clash-prelude/src/Clash/Class/NumConvert.hs +++ b/clash-prelude/src/Clash/Class/NumConvert.hs @@ -5,9 +5,8 @@ Maintainer : QBayLogic B.V. Utilities for converting between Clash number types in a non-erroring way. Its existence is motivated by the observation that Clash users often need to convert -between different number types (e.g., 'Clash.Sized.Unsigned.Unsigned' to -'Clash.Sized.Signed.Signed') and that it is not always clear how to do so -properly. Two classes are exported: +between different number types (e.g., @Unsigned@ to @Signed@) and that it is not +always clear how to do so properly. Two classes are exported: * 'NumConvert': for conversions that, based on types, are guaranteed to succeed. * 'MaybeNumConvert': for conversions that may fail for some values. @@ -20,8 +19,8 @@ Type classes exported here are similar to the @convertible@ package in that it aims to facilitate conversions between different types. It is different in three ways: - 1. It offers no partial functions. - 2. All its conversions are translatable to synthesizable HDL. + 1. It offers no partial functions + 2. All its conversions are translatable to synthesizable HDL 3. It is focused on (Clash's) number types -} module Clash.Class.NumConvert ( diff --git a/clash-prelude/src/Clash/Class/NumConvert/Internal/MaybeNumConvert.hs b/clash-prelude/src/Clash/Class/NumConvert/Internal/MaybeNumConvert.hs index dc4f774e0b..026f7010c0 100644 --- a/clash-prelude/src/Clash/Class/NumConvert/Internal/MaybeNumConvert.hs +++ b/clash-prelude/src/Clash/Class/NumConvert/Internal/MaybeNumConvert.hs @@ -44,7 +44,7 @@ produces produce errors (also see "Clash.XException"). I.e., > x == fromMaybe x (maybeNumConvert @a @b x >>= maybeNumConvert @b @a) for all values @x@ of type @a@. It should also preserve the numerical value -interpretation of the bits. For types that have an "Integral" instance, this +interpretation of the bits. For types that have an @Integral@ instance, this intuition is captured by: > toInteger x == fromMaybe (toInteger x) (toInteger (numConvert @a @b x)) @@ -54,7 +54,7 @@ If a conversion succeeds one way, it should also succeed the other way. I.e., > isJust (maybeNumConvert @a @b x) `implies` isJust (maybeNumConvert @a @b x >>= maybeNumConvert @b @a) A conversion should succeed if and only if the value is representable in the -target type. For types that have a "Bounded" and "Integral" instance, this +target type. For types that have a @Bounded@ and @Integral@ instance, this intuition is captured by: > isJust (maybeNumConvert @a @b x) == (i x >= i (minBound @b) && i x <= i (maxBound @b)) @@ -66,7 +66,7 @@ All implementations should be total, i.e., they should not produce \"bottoms\". Additionally, any implementation should be translatable to synthesizable HDL. -} class MaybeNumConvert a b where - {- | NumConvert a supplied value of type @a@ to a value of type @b@. If the value + {- | Convert a supplied value of type @a@ to a value of type @b@. If the value cannot be represented in the target type, 'Nothing' is returned. >>> maybeNumConvert (1 :: Index 8) :: Maybe (Unsigned 2) diff --git a/clash-prelude/src/Clash/Class/NumConvert/Internal/NumConvert.hs b/clash-prelude/src/Clash/Class/NumConvert/Internal/NumConvert.hs index a52effbacf..6015f38d45 100644 --- a/clash-prelude/src/Clash/Class/NumConvert/Internal/NumConvert.hs +++ b/clash-prelude/src/Clash/Class/NumConvert/Internal/NumConvert.hs @@ -46,14 +46,14 @@ A conversion is successful if a round trip conversion is lossless. I.e., > Just x == maybeNumConvert (numConvert @a @b x) for all values @x@ of type @a@. It should also preserve the numerical value -interpretation of the bits. For types that have an "Integral" instance, this +interpretation of the bits. For types that have an @Integral@ instance, this intuition is captured by: > toInteger x == toInteger (numConvert @a @b x) Instances should make sure their constraints are as \"tight\" as possible. I.e., if an instance's constraints cannot be satisfied, then for the same types -'Clash.Class.NumConvert.maybeConvert' should return 'Nothing' for one or more +'Clash.Class.NumConvert.maybeNumConvert' should return 'Nothing' for one or more values in the domain of the source type @a@: > L.any isNothing (L.map (maybeNumConvert @a @b) [minBound ..]) @@ -63,7 +63,7 @@ All implementations should be total, i.e., they should not produce \"bottoms\". Additionally, any implementation should be translatable to synthesizable HDL. -} class NumConvert a b where - {- | NumConvert a supplied value of type @a@ to a value of type @b@. The conversion + {- | Convert a supplied value of type @a@ to a value of type @b@. The conversion is guaranteed to succeed. >>> numConvert (3 :: Index 8) :: Unsigned 8 diff --git a/clash-prelude/tests/Clash/Tests/MaybeNumConvert.hs b/clash-prelude/tests/Clash/Tests/MaybeNumConvert.hs index 6f45c5230b..7d9e3bc8b8 100644 --- a/clash-prelude/tests/Clash/Tests/MaybeNumConvert.hs +++ b/clash-prelude/tests/Clash/Tests/MaybeNumConvert.hs @@ -37,7 +37,6 @@ module Clash.Tests.MaybeNumConvert where import Control.Monad (forM_) import Data.Data (Proxy (..)) -import Data.Either (isLeft) import Data.Maybe (fromMaybe, isJust) import GHC.TypeNats (someNatVal) import Test.Tasty (TestTree, defaultMain) @@ -54,6 +53,8 @@ import Clash.Prelude hiding (someNatVal) import Numeric.Natural (Natural) #endif +import qualified Data.List as L + main :: IO () main = defaultMain tests @@ -109,7 +110,9 @@ maybeConvertLaw4 Proxy x = -- | Checks whether an 'XException' in, means an 'XException' out convertXException :: forall a b. (MaybeNumConvert a b) => Proxy a -> Proxy b -> Bool -convertXException _ _ = isLeft $ isX $ maybeNumConvert @a @b (errorX "" :: a) +convertXException _ _ = case isX $ maybeNumConvert @a @b (errorX "BOO" :: a) of + Left s -> "BOO" `L.isInfixOf` s + Right _ -> False indexMax :: Natural indexMax = 128