Skip to content

Add Clash.Class.NumConvert #2915

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jun 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ADD: `Clash.Class.NumConvert`: Utilities for safely converting between various Clash number types
5 changes: 5 additions & 0 deletions clash-prelude/clash-prelude.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
32 changes: 32 additions & 0 deletions clash-prelude/src/Clash/Class/NumConvert.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{- |
Copyright : (C) 2025 , Martijn Bastiaan
License : BSD2 (see the file LICENSE)
Maintainer : QBayLogic B.V. <[email protected]>

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., @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.

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
181 changes: 181 additions & 0 deletions clash-prelude/src/Clash/Class/NumConvert/Internal/MaybeNumConvert.hs
Original file line number Diff line number Diff line change
@@ -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. <[email protected]>
-}
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
{- | 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)
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)
Loading