packages feed

checked-literals-0.1.2: src-nums/CheckedLiterals/Nums/Fixed.hs

{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}

module CheckedLiterals.Nums.Fixed (
  Fixed (..),
  SFixed,
  UFixed,
) where

import CheckedLiterals (
  CheckedNegativeIntegerLiteral,
  CheckedNegativeRationalLiteral,
  CheckedPositiveIntegerLiteral,
  CheckedPositiveRationalLiteral,
  uncheckedLiteral,
 )
import CheckedLiterals.Class.Rational.TypeNats (IsPowerOfTwo)
import CheckedLiterals.Nums.Signed (Signed (..))
import CheckedLiterals.Nums.Unsigned (Unsigned (..))
import Data.Bits (Bits (..), shiftL, shiftR, (.&.))
import Data.Kind (Constraint, Type)
import Data.Proxy (Proxy (..))
import Data.Ratio (denominator, numerator, (%))
import Data.Type.Bool (If)
import GHC.TypeError (Assert, ErrorMessage (ShowType, Text, (:$$:), (:<>:)), TypeError)
import GHC.TypeLits (KnownNat, Nat, natVal, type Div, type (+), type (-), type (<=?), type (^))
import GHC.TypeLits.Extra (CLog)
import Prelude

{- | Fixed-point number

Where:

* @rep@ is the underlying representation (Signed or Unsigned)
* @int@ is the number of bits used to represent the integer part
* @frac@ is the number of bits used to represent the fractional part
-}
newtype Fixed (rep :: Nat -> Type) (int :: Nat) (frac :: Nat)
  = Fixed {unFixed :: rep (int + frac)}

{- | Signed fixed-point number with @int@ integer bits (including sign bit)
and @frac@ fractional bits
-}
type SFixed = Fixed Signed

-- | Unsigned fixed-point number with @int@ integer bits and @frac@ fractional bits
type UFixed = Fixed Unsigned

type PositiveSignedRationalRequiredIntBits (num :: Nat) (den :: Nat) =
  CLog 2 (num + 1) + 1 - CLog 2 den

type NegativeSignedRationalRequiredIntBits (num :: Nat) (den :: Nat) =
  CLog 2 num + 1 - CLog 2 den

type family FitsPositiveSignedRational (num :: Nat) (den :: Nat) (int :: Nat) :: Bool where
  FitsPositiveSignedRational 0 den int = 'True
  FitsPositiveSignedRational num den int =
    PositiveSignedRationalRequiredIntBits num den <=? int

type family FitsNegativeSignedRational (num :: Nat) (den :: Nat) (int :: Nat) :: Bool where
  FitsNegativeSignedRational num den int =
    NegativeSignedRationalRequiredIntBits num den <=? int

instance (KnownNat frac, Integral (rep (int + frac))) => Show (Fixed rep int frac) where
  show (Fixed fRep) = i ++ "." ++ fracStr
   where
    nF = fromInteger (natVal (Proxy @frac)) :: Int
    fRepI = toInteger fRep
    fRepI_abs = abs fRepI
    i =
      if fRepI < 0
        then '-' : show (fRepI_abs `shiftR` nF)
        else show (fRepI `shiftR` nF)
    nom =
      if fRepI < 0
        then fRepI_abs .&. ((2 ^ nF) - 1)
        else fRepI .&. ((2 ^ nF) - 1)
    denom = 2 ^ nF
    fracStr = padZeros nF (show (numerator r'))
     where
      r = nom % denom
      -- Multiply by 10^nF to get decimal representation
      r' =
        iterate
          ( \x ->
              let n = numerator x * 10
                  d = denominator x
               in n % d
          )
          r
          !! nF
    padZeros n str = replicate (n - length str) '0' ++ str

instance (KnownNat frac, Integral (rep (int + frac)), Eq (rep (int + frac))) => Eq (Fixed rep int frac) where
  Fixed a == Fixed b = a == b

instance (KnownNat frac, Integral (rep (int + frac)), Ord (rep (int + frac))) => Ord (Fixed rep int frac) where
  Fixed a `compare` Fixed b = a `compare` b

-- | Num instance for Fixed - operations saturate on overflow
instance
  ( KnownNat frac
  , KnownNat int
  , Integral (rep (int + frac))
  , Bounded (rep (int + frac))
  , Bits (rep (int + frac))
  ) =>
  Num (Fixed rep int frac)
  where
  Fixed a + Fixed b = Fixed (fromInteger sat)
   where
    res = toInteger a + toInteger b
    maxB = toInteger (maxBound :: rep (int + frac))
    minB = toInteger (minBound :: rep (int + frac))
    sat = max minB (min maxB res)

  Fixed a * Fixed b = Fixed (fromInteger sat)
   where
    nF = fromInteger (natVal (Proxy @frac)) :: Int
    res = (toInteger a * toInteger b) `shiftR` nF
    maxB = toInteger (maxBound :: rep (int + frac))
    minB = toInteger (minBound :: rep (int + frac))
    sat = max minB (min maxB res)

  Fixed a - Fixed b = Fixed (fromInteger sat)
   where
    res = toInteger a - toInteger b
    maxB = toInteger (maxBound :: rep (int + frac))
    minB = toInteger (minBound :: rep (int + frac))
    sat = max minB (min maxB res)

  negate (Fixed a) = Fixed (negate a)

  abs (Fixed a) = Fixed (abs a)

  signum (Fixed a)
    | a == uncheckedLiteral 0 = uncheckedLiteral 0
    | a < uncheckedLiteral 0 = uncheckedLiteral (-1)
    | otherwise = uncheckedLiteral 1

  fromInteger i = Fixed (fromInteger sat)
   where
    nF = fromInteger (natVal (Proxy @frac)) :: Int
    res = i `shiftL` nF
    maxB = toInteger (maxBound :: rep (int + frac))
    minB = toInteger (minBound :: rep (int + frac))
    sat = max minB (min maxB res)

-- | Fractional instance for Fixed - division and rational conversion
instance
  ( KnownNat frac
  , KnownNat int
  , Integral (rep (int + frac))
  , Bounded (rep (int + frac))
  , Bits (rep (int + frac))
  ) =>
  Fractional (Fixed rep int frac)
  where
  Fixed a / Fixed b = Fixed (fromInteger sat)
   where
    nF = fromInteger (natVal (Proxy @frac)) :: Int
    -- Shift numerator left by frac bits before division for precision
    num = toInteger a `shiftL` nF
    res = num `quot` toInteger b
    maxB = toInteger (maxBound :: rep (int + frac))
    minB = toInteger (minBound :: rep (int + frac))
    sat = max minB (min maxB res)

  recip (Fixed a) = Fixed (fromInteger sat)
   where
    nF = fromInteger (natVal (Proxy @frac)) :: Int
    -- 1.0 in fixed point is 1 << frac
    one = uncheckedLiteral 1 `shiftL` nF :: Integer
    -- (1 << frac) / a in fixed point needs another shift
    num = one `shiftL` nF
    res = num `quot` toInteger a
    maxB = toInteger (maxBound :: rep (int + frac))
    minB = toInteger (minBound :: rep (int + frac))
    sat = max minB (min maxB res)

  fromRational r = Fixed (fromInteger sat)
   where
    nF = fromInteger (natVal (Proxy @frac)) :: Int
    n = numerator r `shiftL` (2 * nF)
    d = denominator r `shiftL` nF
    res = n `quot` d
    maxB = toInteger (maxBound :: rep (int + frac))
    minB = toInteger (minBound :: rep (int + frac))
    sat = max minB (min maxB res)

type PositiveUnsignedError strLit lit int typ maxVal =
  TypeError
    ( 'Text "Literal "
        ':<>: strLit
        ':<>: 'Text " is (potentially) out of bounds."
        ':$$: 'Text "Note: integer part needs at least "
          ':<>: 'ShowType (CLog 2 (lit + 1))
          ':<>: 'Text " bit(s)."
        ':$$: 'Text "Possible fix: add a constraint: "
          ':<>: 'ShowType (CLog 2 (lit + 1))
          ':<>: 'Text " <= "
          ':<>: 'ShowType int
          ':<>: 'Text "."
        ':$$: 'Text "Possible fix: use 'uncheckedLiteral' from 'CheckedLiterals' to bypass this check."
    )

instance
  ( Assert
      (If (lit <=? 0) (lit <=? 0) (CLog 2 (lit + 1) <=? int))
      (PositiveUnsignedError (ShowType lit) lit int (UFixed int frac) ((2 ^ int) - 1))
  ) =>
  CheckedPositiveIntegerLiteral lit (UFixed int frac)

type NegativeUnsignedError strLit typ maxVal =
  TypeError
    ( 'Text "Literal "
        ':<>: strLit
        ':<>: 'Text " is out of bounds, because UFixed cannot represent negative numbers."
        ':$$: 'Text "Possible fix: use 'uncheckedLiteral' from 'CheckedLiterals' to bypass this check."
    )

instance
  (NegativeUnsignedError ('Text "-" ':<>: 'ShowType lit) (UFixed int frac) ((2 ^ int) - 1)) =>
  CheckedNegativeIntegerLiteral lit (UFixed int frac)

type FixedPointNotPow2Error strLit den typ =
  TypeError
    ( 'Text "Literal "
        ':<>: strLit
        ':<>: 'Text " cannot be represented exactly by "
        ':<>: 'ShowType typ
        ':<>: 'Text "."
        ':$$: 'Text "The reduced denominator "
          ':<>: 'ShowType den
          ':<>: 'Text " is not a power of 2."
        ':$$: 'Text "Possible fix: use 'uncheckedLiteral' from 'CheckedLiterals' to bypass this check."
    )

type FixedPointNotEnoughFracError strLit den frac typ =
  TypeError
    ( 'Text "Literal "
        ':<>: strLit
        ':<>: 'Text " cannot be represented exactly by "
        ':<>: 'ShowType typ
        ':<>: 'Text "."
        ':$$: 'Text "The fractional part needs at least "
          ':<>: 'ShowType (CLog 2 den)
          ':<>: 'Text " bit(s)."
        ':$$: 'Text "Possible fix: add a constraint: "
          ':<>: 'ShowType (CLog 2 den)
          ':<>: 'Text " <= "
          ':<>: 'ShowType frac
          ':<>: 'Text "."
        ':$$: 'Text "Possible fix: use 'uncheckedLiteral' from 'CheckedLiterals' to bypass this check."
    )

type family
  CheckFrac (isPow2 :: Bool) (strLit :: ErrorMessage) (den :: Nat) (frac :: Nat) (typ :: Type) ::
    Constraint
  where
  CheckFrac 'False strLit den frac typ = FixedPointNotPow2Error strLit den typ
  CheckFrac 'True strLit den frac typ =
    Assert
      (CLog 2 den <=? frac)
      (FixedPointNotEnoughFracError strLit den frac typ)

instance
  ( CheckFrac (IsPowerOfTwo den) ('Text str) den frac (UFixed int frac)
  , Assert
      (If (Div num den <=? 0) (Div num den <=? 0) (CLog 2 (Div num den + 1) <=? int))
      (PositiveUnsignedError ('Text str) (Div num den) int (UFixed int frac) ((2 ^ int) - 1))
  ) =>
  CheckedPositiveRationalLiteral str num den (UFixed int frac)

instance
  (NegativeUnsignedError ('Text str) (UFixed int frac) ((2 ^ int) - 1)) =>
  CheckedNegativeRationalLiteral str num den (UFixed int frac)

type PositiveSignedError strLit lit int typ =
  TypeError
    ( 'Text "Literal "
        ':<>: strLit
        ':<>: 'Text " is (potentially) out of bounds."
        ':$$: 'Text "Note: integer part needs at least "
          ':<>: 'ShowType (CLog 2 (lit + 1) + 1)
          ':<>: 'Text " bit(s), including sign bit."
        ':$$: 'Text "Possible fix: add a constraint: "
          ':<>: 'ShowType (CLog 2 (lit + 1) + 1)
          ':<>: 'Text " <= "
          ':<>: 'ShowType int
          ':<>: 'Text "."
        ':$$: 'Text "Possible fix: use 'uncheckedLiteral' from 'CheckedLiterals' to bypass this check."
    )

type PositiveSignedRationalRangeError strLit num den int =
  TypeError
    ( 'Text "Literal "
        ':<>: strLit
        ':<>: 'Text " is (potentially) out of bounds."
        ':$$: 'Text "Note: integer part needs at least "
          ':<>: 'ShowType (PositiveSignedRationalRequiredIntBits num den)
          ':<>: 'Text " bit(s), including sign bit."
        ':$$: 'Text "Possible fix: add a constraint: "
          ':<>: 'ShowType (PositiveSignedRationalRequiredIntBits num den)
          ':<>: 'Text " <= "
          ':<>: 'ShowType int
          ':<>: 'Text "."
        ':$$: 'Text "Possible fix: use 'uncheckedLiteral' from 'CheckedLiterals' to bypass this check."
    )

instance
  ( Assert
      (If (lit <=? 0) (lit <=? 0) (CLog 2 (lit + 1) + 1 <=? int))
      (PositiveSignedError (ShowType lit) lit int (SFixed int frac))
  ) =>
  CheckedPositiveIntegerLiteral lit (SFixed int frac)

type NegativeSignedError strLit lit int typ =
  TypeError
    ( 'Text "Literal "
        ':<>: strLit
        ':<>: 'Text " is (potentially) out of bounds."
        ':$$: 'Text "Note: integer part needs at least "
          ':<>: 'ShowType (CLog 2 lit + 1)
          ':<>: 'Text " bit(s), including sign bit."
        ':$$: 'Text "Possible fix: add a constraint: "
          ':<>: 'ShowType (CLog 2 lit + 1)
          ':<>: 'Text " <= "
          ':<>: 'ShowType int
          ':<>: 'Text "."
        ':$$: 'Text "Possible fix: use 'uncheckedLiteral' from 'CheckedLiterals' to bypass this check."
    )

type NegativeSignedRationalRangeError strLit num den int =
  TypeError
    ( 'Text "Literal "
        ':<>: strLit
        ':<>: 'Text " is (potentially) out of bounds."
        ':$$: 'Text "Note: integer part needs at least "
          ':<>: 'ShowType (NegativeSignedRationalRequiredIntBits num den)
          ':<>: 'Text " bit(s), including sign bit."
        ':$$: 'Text "Possible fix: add a constraint: "
          ':<>: 'ShowType (NegativeSignedRationalRequiredIntBits num den)
          ':<>: 'Text " <= "
          ':<>: 'ShowType int
          ':<>: 'Text "."
        ':$$: 'Text "Possible fix: use 'uncheckedLiteral' from 'CheckedLiterals' to bypass this check."
    )

instance
  ( Assert
      (If (lit <=? 0) (lit <=? 0) (CLog 2 lit + 1 <=? int))
      (NegativeSignedError ('Text "-" ':<>: 'ShowType lit) lit int (SFixed int frac))
  ) =>
  CheckedNegativeIntegerLiteral lit (SFixed int frac)

instance
  ( CheckFrac (IsPowerOfTwo den) ('Text str) den frac (SFixed int frac)
  , Assert
      (FitsPositiveSignedRational num den int)
      (PositiveSignedRationalRangeError ('Text str) num den int)
  ) =>
  CheckedPositiveRationalLiteral str num den (SFixed int frac)

instance
  ( CheckFrac (IsPowerOfTwo den) ('Text str) den frac (SFixed int frac)
  , Assert
      (FitsNegativeSignedRational num den int)
      (NegativeSignedRationalRangeError ('Text str) num den int)
  ) =>
  CheckedNegativeRationalLiteral str num den (SFixed int frac)