packages feed

safe-numeric-0.1: src/Safe/Numeric.hs

{-# LANGUAGE AllowAmbiguousTypes   #-}
{-# LANGUAGE DefaultSignatures     #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE ScopedTypeVariables   #-}
{-# LANGUAGE TypeSynonymInstances  #-}

{- | Safe numeric operations.

We take a fairly conservative approach here - this module is an extension not
a replacement for "Prelude". Our suffix naming conventions are as follows:

- @\@@ or @W@ -- wrap - same as "Prelude", but confirms explicitly to the
  reader that you thought about the issue
- @%@ or @E@ -- explicit error (@'Either' 'ArithException'@)
- @:@ or @S@ -- saturate at 'minBound' or 'maxBound'
- @!@ or @X@ -- runtime exception. This is suitable only for trusted inputs,
  since otherwise you allow the attacker to crash your code arbitrarily.

Currently we provide replacements for:

- conversion functions 'fromIntegral' and 'fromInteger'; instead use

    - 'ex' for cost-free conversions to a bigger or same-sized type, or
    - 'ctW', 'ctE', 'ctS', or 'ctX' for checked conversions to a smaller type

- arithmetic functions '+', '-', '*', '^'; instead use

    - '+@', '+%', '+:', '+!', etc

- division functions 'div', 'mod', 'divMod', 'quot', 'rem', 'quotRem'; instead use

    - 'divE', 'divX', etc

When using this module, you might also like to ban the unsafe functions from
your codebase, e.g. via hlint:

@
- functions:
  - {name: [fromIntegral, fromInteger, +, \'-\', \'*\', ^], within: [], message: "Use safe versions from Safe.Numeric"}
  - {name: [div, mod, divMod, quot, rem, quotRem], within: [], message: "Use safe versions from Safe.Numeric"}
@

-}
module Safe.Numeric
  (
  -- * Conversions
    NumExpand(..)
  , NumConvert(..)
  -- ** Summary of instances
  -- $instances
  , Word29_
  , Int29_
  -- * Arithmetic
  , (+@)
  , (+%)
  , (+:)
  , (+!)
  , (-@)
  , (-%)
  , (-:)
  , (-!)
  , (*@)
  , (*%)
  , (*:)
  , (*!)
  , (^@)
  , (^%)
  , (^:)
  , (^!)
  -- * Division
  , DivResult
  , divE
  , divX
  , modE
  , modX
  , divModE
  , divModX
  , quotE
  , quotX
  , remE
  , remX
  , quotRemE
  , quotRemX
  ) where

-- external
import           Control.Exception (ArithException (..))
import           Data.Int
import           Data.WideWord
import           Data.Word
import           Safe.Partial      (Partial)


class NumExpand b a where
  {- | Safely expand type @b@ into type @a@, with no runtime bounds checking.

  a.k.a. "'fromIntegral' hurts my fingers and my eyes"

  The value is statically guaranteed to remain the same relative to 0 in both
  directions, i.e. not overflow or underflow, without any runtime checks.
  -}
  ex :: b -> a
  default ex :: (Num a, Integral b) => b -> a
  ex = fromIntegral
  {-# INLINE ex #-}

checkE
  :: forall a . (Integral a, Bounded a) => Integer -> Either ArithException a
checkE v | v < toInteger (minBound :: a) = Left Underflow
         | v > toInteger (maxBound :: a) = Left Overflow
         | otherwise                     = Right (fromInteger v)
{-# INLINE checkE #-}

checkS :: forall a . (Integral a, Bounded a) => Integer -> a
checkS v | v < toInteger (minBound :: a) = minBound
         | v > toInteger (maxBound :: a) = maxBound
         | otherwise                     = fromInteger v
{-# INLINE checkS #-}

checkX :: forall a . (Integral a, Bounded a) => Partial => Integer -> a
checkX v | v < toInteger (minBound :: a) = error "Underflow"
         | v > toInteger (maxBound :: a) = error "Overflow"
         | otherwise                     = fromInteger v
{-# INLINE checkX #-}

-- | Convert from a type into a smaller type
class NumConvert b a where

  -- | Wrap around if the input is out-of-bounds.
  ctW :: b -> a
  default ctW :: (Num a, Integral b) => b -> a
  ctW b = fromIntegral b
  {-# INLINE ctW #-}

  -- | Explicit error if the input is out-of-bounds.
  ctE :: b -> Either ArithException a
  default ctE :: (Integral a, Bounded a, Integral b) => b -> Either ArithException a
  ctE = checkE . toInteger
  {-# INLINE ctE #-}

  -- | Output 'minBound' if the input is too small, or 'maxBound' if too large.
  ctS :: b -> a
  default ctS :: (Integral a, Bounded a, Integral b) => b -> a
  ctS = checkS . toInteger
  {-# INLINE ctS #-}

  -- | Runtime (async) exception if the input is out-of-bounds.
  ctX :: Partial => b -> a
  default ctX :: (Integral a, Bounded a, Integral b) => Partial => b -> a
  ctX = checkX . toInteger
  {-# INLINE ctX #-}

{- | Type alias for 'Word' that explicitly states its lower-bound size. -}
type Word29_ = Word

{- | Type alias for 'Int' that explicitly states its lower-bound size. -}
type Int29_ = Int

{- $instances

@
Fr\\To W8    W16   W29_  W32   W64   W128  W256  I8    I16   I29_  I32   I64   I128  I256  Itgr
W8    X     X     X     X     X     X     X           X     X     X     X     X     X     X
W16         X     X     X     X     X     X                 X     X     X     X     X     X
W29_              X                                                                       X
W32                     X     X     X     X                             X     X     X     X
W64                           X     X     X                                   X     X     X
W128                                X     X                                         X     X
W256                                      X                                               X
I8                                              X     X     X     X     X     X     X     X
I16                                                   X     X     X     X     X     X     X
I29_                                                        X                             X
I32                                                               X     X     X     X     X
I64                                                                     X     X     X     X
I128                                                                          X     X     X
I256                                                                                X     X
Itgr                                                                                      X
@

In the above table, X means 'NumExpand', empty means 'NumConvert'.

Safe, free 'NumExpand' from @W29_@ and @I29_@ to other bounded types are not
guaranteed since there is no specified upper bound on the sizes of the former.

Generally, we cannot have @NumExpand a b@ /and/ @NumExpand b a@ unless @b = a@.
-}

-- note: wide-word does not yet define Int256, so references to it below are
-- commented out

{- | -}
instance NumExpand Word8 Word8
instance NumExpand Word8 Word16
instance NumExpand Word8 Word29_
instance NumExpand Word8 Word32
instance NumExpand Word8 Word64
instance NumExpand Word8 Word128
instance NumExpand Word8 Word256
instance NumConvert Word8 Int8
instance NumExpand Word8 Int16
instance NumExpand Word8 Int29_
instance NumExpand Word8 Int32
instance NumExpand Word8 Int64
instance NumExpand Word8 Int128
--instance NumExpand Word8 Int256
instance NumExpand Word8 Integer

instance NumConvert Word16 Word8
instance NumExpand Word16 Word16
instance NumExpand Word16 Word29_
instance NumExpand Word16 Word32
instance NumExpand Word16 Word64
instance NumExpand Word16 Word128
instance NumExpand Word16 Word256
instance NumConvert Word16 Int8
instance NumConvert Word16 Int16
instance NumExpand Word16 Int29_
instance NumExpand Word16 Int32
instance NumExpand Word16 Int64
instance NumExpand Word16 Int128
--instance NumExpand Word16 Int256
instance NumExpand Word16 Integer

instance NumConvert Word29_ Word8
instance NumConvert Word29_ Word16
instance NumExpand Word29_ Word29_
instance NumConvert Word29_ Word32
instance NumConvert Word29_ Word64
instance NumConvert Word29_ Word128
instance NumConvert Word29_ Word256
instance NumConvert Word29_ Int8
instance NumConvert Word29_ Int16
instance NumConvert Word29_ Int29_
instance NumConvert Word29_ Int32
instance NumConvert Word29_ Int64
instance NumConvert Word29_ Int128
--instance NumConvert Word29_ Int256
instance NumExpand Word29_ Integer

instance NumConvert Word32 Word8
instance NumConvert Word32 Word16
instance NumConvert Word32 Word29_
instance NumExpand Word32 Word32
instance NumExpand Word32 Word64
instance NumExpand Word32 Word128
instance NumExpand Word32 Word256
instance NumConvert Word32 Int8
instance NumConvert Word32 Int16
instance NumConvert Word32 Int29_
instance NumConvert Word32 Int32
instance NumExpand Word32 Int64
instance NumExpand Word32 Int128
--instance NumExpand Word32 Int256
instance NumExpand Word32 Integer

instance NumConvert Word64 Word8
instance NumConvert Word64 Word16
instance NumConvert Word64 Word29_
instance NumConvert Word64 Word32
instance NumExpand Word64 Word64
instance NumExpand Word64 Word128
instance NumExpand Word64 Word256
instance NumConvert Word64 Int8
instance NumConvert Word64 Int16
instance NumConvert Word64 Int29_
instance NumConvert Word64 Int32
instance NumConvert Word64 Int64
instance NumExpand Word64 Int128
--instance NumExpand Word64 Int256
instance NumExpand Word64 Integer

instance NumConvert Word128 Word8
instance NumConvert Word128 Word16
instance NumConvert Word128 Word29_
instance NumConvert Word128 Word32
instance NumConvert Word128 Word64
instance NumExpand Word128 Word128
instance NumExpand Word128 Word256
instance NumConvert Word128 Int8
instance NumConvert Word128 Int16
instance NumConvert Word128 Int29_
instance NumConvert Word128 Int32
instance NumConvert Word128 Int64
instance NumConvert Word128 Int128
--instance NumExpand Word128 Int256
instance NumExpand Word128 Integer

instance NumConvert Word256 Word8
instance NumConvert Word256 Word16
instance NumConvert Word256 Word29_
instance NumConvert Word256 Word32
instance NumConvert Word256 Word64
instance NumConvert Word256 Word128
instance NumExpand Word256 Word256
instance NumConvert Word256 Int8
instance NumConvert Word256 Int16
instance NumConvert Word256 Int29_
instance NumConvert Word256 Int32
instance NumConvert Word256 Int64
instance NumConvert Word256 Int128
--instance NumConvert Word256 Int256
instance NumExpand Word256 Integer

instance NumConvert Int8 Word8
instance NumConvert Int8 Word16
instance NumConvert Int8 Word29_
instance NumConvert Int8 Word32
instance NumConvert Int8 Word64
instance NumConvert Int8 Word128
instance NumConvert Int8 Word256
instance NumExpand Int8 Int8
instance NumExpand Int8 Int16
instance NumExpand Int8 Int29_
instance NumExpand Int8 Int32
instance NumExpand Int8 Int64
instance NumExpand Int8 Int128
--instance NumExpand Int8 Int256
instance NumExpand Int8 Integer

instance NumConvert Int16 Word8
instance NumConvert Int16 Word16
instance NumConvert Int16 Word29_
instance NumConvert Int16 Word32
instance NumConvert Int16 Word64
instance NumConvert Int16 Word128
instance NumConvert Int16 Word256
instance NumConvert Int16 Int8
instance NumExpand Int16 Int16
instance NumExpand Int16 Int29_
instance NumExpand Int16 Int32
instance NumExpand Int16 Int64
instance NumExpand Int16 Int128
--instance NumExpand Int16 Int256
instance NumExpand Int16 Integer

instance NumConvert Int29_ Word8
instance NumConvert Int29_ Word16
instance NumConvert Int29_ Word29_
instance NumConvert Int29_ Word32
instance NumConvert Int29_ Word64
instance NumConvert Int29_ Word128
instance NumConvert Int29_ Word256
instance NumConvert Int29_ Int8
instance NumConvert Int29_ Int16
instance NumExpand Int29_ Int29_
instance NumConvert Int29_ Int32
instance NumConvert Int29_ Int64
instance NumConvert Int29_ Int128
--instance NumConvert Int29_ Int256
instance NumExpand Int29_ Integer

instance NumConvert Int32 Word8
instance NumConvert Int32 Word16
instance NumConvert Int32 Word29_
instance NumConvert Int32 Word32
instance NumConvert Int32 Word64
instance NumConvert Int32 Word128
instance NumConvert Int32 Word256
instance NumConvert Int32 Int8
instance NumConvert Int32 Int16
instance NumConvert Int32 Int29_
instance NumExpand Int32 Int32
instance NumExpand Int32 Int64
instance NumExpand Int32 Int128
--instance NumExpand Int32 Int256
instance NumExpand Int32 Integer

instance NumConvert Int64 Word8
instance NumConvert Int64 Word16
instance NumConvert Int64 Word29_
instance NumConvert Int64 Word32
instance NumConvert Int64 Word64
instance NumConvert Int64 Word128
instance NumConvert Int64 Word256
instance NumConvert Int64 Int8
instance NumConvert Int64 Int16
instance NumConvert Int64 Int29_
instance NumConvert Int64 Int32
instance NumExpand Int64 Int64
instance NumExpand Int64 Int128
--instance NumExpand Int64 Int256
instance NumExpand Int64 Integer

instance NumConvert Int128 Word8
instance NumConvert Int128 Word16
instance NumConvert Int128 Word29_
instance NumConvert Int128 Word32
instance NumConvert Int128 Word64
instance NumConvert Int128 Word128
instance NumConvert Int128 Word256
instance NumConvert Int128 Int8
instance NumConvert Int128 Int16
instance NumConvert Int128 Int29_
instance NumConvert Int128 Int32
instance NumConvert Int128 Int64
instance NumExpand Int128 Int128
--instance NumExpand Int128 Int256
instance NumExpand Int128 Integer

{-instance NumConvert Int256 Word8
instance NumConvert Int256 Word16
instance NumConvert Int256 Word29_
instance NumConvert Int256 Word32
instance NumConvert Int256 Word64
instance NumConvert Int256 Word128
instance NumConvert Int256 Word256
instance NumConvert Int256 Int8
instance NumConvert Int256 Int16
instance NumConvert Int256 Int29_
instance NumConvert Int256 Int32
instance NumConvert Int256 Int64
instance NumConvert Int256 Int128
instance NumExpand Int256 Int256
instance NumExpand Int256 Integer-}

instance NumConvert Integer Word8
instance NumConvert Integer Word16
instance NumConvert Integer Word29_
instance NumConvert Integer Word32
instance NumConvert Integer Word64
instance NumConvert Integer Word128
instance NumConvert Integer Word256
instance NumConvert Integer Int8
instance NumConvert Integer Int16
instance NumConvert Integer Int29_
instance NumConvert Integer Int32
instance NumConvert Integer Int64
instance NumConvert Integer Int128
--instance NumConvert Integer Int256
instance NumExpand Integer Integer

{- | Add with wrap-around.

Same as 'Prelude.+' but indicates to the reader that you explicitly thought
about this issue and decided that wrap-around is the correct behaviour.
-}
(+@) :: Num a => a -> a -> a
(+@) = (+)
infixl 6 +@
{-# INLINE (+@) #-}

-- | Add with explicit error on overflow or underflow.
(+%) :: (Integral a, Bounded a) => a -> a -> Either ArithException a
(+%) a b = checkE $ toInteger a + toInteger b
infixl 6 +%
{-# INLINE (+%) #-}

-- | Add with output 'maxBound' on overflow or 'minBound' on underflow.
(+:) :: (Integral a, Bounded a) => a -> a -> a
(+:) a b = checkS $ toInteger a + toInteger b
infixl 6 +:
{-# INLINE (+:) #-}

-- | Add with runtime (async) exception on overflow or underflow.
(+!) :: (Integral a, Bounded a) => Partial => a -> a -> a
(+!) a b = checkX $ toInteger a + toInteger b
infixl 6 +!
{-# INLINE (+!) #-}

{- | Subtract with wrap-around.

Same as 'Prelude.-' but indicates to the reader that you explicitly thought
about this issue and decided that wrap-around is the correct behaviour.
-}
(-@) :: Num a => a -> a -> a
(-@) = (-)
infixl 6 -@
{-# INLINE (-@) #-}

-- | Subtract with explicit error on overflow or underflow.
(-%) :: (Integral a, Bounded a) => a -> a -> Either ArithException a
(-%) a b = checkE $ toInteger a - toInteger b
infixl 6 -%
{-# INLINE (-%) #-}

-- | Subtract with output 'maxBound' on overflow or 'minBound' on underflow.
(-:) :: (Integral a, Bounded a) => a -> a -> a
(-:) a b = checkS $ toInteger a - toInteger b
infixl 6 -:
{-# INLINE (-:) #-}

-- | Subtract with runtime (async) exception on overflow or underflow.
(-!) :: (Integral a, Bounded a) => Partial => a -> a -> a
(-!) a b = checkX $ toInteger a - toInteger b
infixl 6 -!
{-# INLINE (-!) #-}

{- | Multiply with wrap-around.

Same as 'Prelude.*' but indicates to the reader that you explicitly thought
about this issue and decided that wrap-around is the correct behaviour.
-}
(*@) :: Num a => a -> a -> a
(*@) = (*)
infixl 7 *@
{-# INLINE (*@) #-}

-- | Multiply with explicit error on overflow or underflow.
(*%) :: (Integral a, Bounded a) => a -> a -> Either ArithException a
(*%) a b = checkE $ toInteger a * toInteger b
infixl 7 *%
{-# INLINE (*%) #-}

-- | Multiply with output 'maxBound' on overflow or 'minBound' on underflow.
(*:) :: (Integral a, Bounded a) => a -> a -> a
(*:) a b = checkS $ toInteger a * toInteger b
infixl 7 *:
{-# INLINE (*:) #-}

-- | Multiply with runtime (async) exception on overflow or underflow.
(*!) :: (Integral a, Bounded a) => Partial => a -> a -> a
(*!) a b = checkX $ toInteger a * toInteger b
infixl 7 *!
{-# INLINE (*!) #-}

{- | Power with wrap-around.

Same as 'Prelude.^' but indicates to the reader that you explicitly thought
about this issue and decided that wrap-around is the correct behaviour.
-}
(^@) :: Integral a => a -> a -> a
(^@) = (^)
infixr 8 ^@
{-# INLINE (^@) #-}

-- | Power with explicit error on overflow or underflow.
(^%) :: (Integral a, Bounded a) => a -> a -> Either ArithException a
(^%) a b = checkE $ toInteger a ^ toInteger b
infixr 8 ^%
{-# INLINE (^%) #-}

-- | Power with output 'maxBound' on overflow or 'minBound' on underflow.
(^:) :: (Integral a, Bounded a) => a -> a -> a
(^:) a b = checkS $ toInteger a ^ toInteger b
infixr 8 ^:
{-# INLINE (^:) #-}

-- | Power with runtime (async) exception on overflow or underflow.
(^!) :: (Integral a, Bounded a) => Partial => a -> a -> a
(^!) a b = checkX $ toInteger a ^ toInteger b
infixr 8 ^!
{-# INLINE (^!) #-}

{- | Type alias for a division-operation result with explicit error.

The @Left@ case means /division by zero/, and its parameter represents the
sign of the nominator operand.
-}
type DivResult a = Either Ordering a

explicitDiv :: (Eq a, Ord a, Num a) => (a -> a -> b) -> a -> a -> DivResult b
explicitDiv op x y = if y == 0 then Left $ compare x 0 else Right $ x `op` y
{-# INLINE explicitDiv #-}

-- | Division (truncated towards -Inf) with explicit error on division-by-zero.
divE :: Integral a => a -> a -> DivResult a
divE = explicitDiv div
{-# INLINE divE #-}

{- | Division (truncated towards -Inf) with runtime (async) exception on division-by-zero.

Same as 'Prelude.div' but indicates to the reader that you explicitly thought
about this issue and decided that runtime exception is the correct behaviour.
-}
divX :: Integral a => a -> a -> a
divX = div
{-# INLINE divX #-}

-- | Modulus (truncated towards -Inf) with explicit error on division-by-zero.
modE :: Integral a => a -> a -> DivResult a
modE = explicitDiv mod
{-# INLINE modE #-}

{- | Modulus (truncated towards -Inf) with runtime (async) exception on division-by-zero.

Same as 'Prelude.mod' but indicates to the reader that you explicitly thought
about this issue and decided that runtime exception is the correct behaviour.
-}
modX :: Integral a => a -> a -> a
modX = mod
{-# INLINE modX #-}

-- | Division-and-modulus (truncated towards -Inf) with explicit error on division-by-zero.
divModE :: Integral a => a -> a -> DivResult (a, a)
divModE = explicitDiv divMod
{-# INLINE divModE #-}

{- | Division-and-modulus (truncated towards -Inf) with runtime (async) exception on division-by-zero.

Same as 'Prelude.divMod' but indicates to the reader that you explicitly thought
about this issue and decided that runtime exception is the correct behaviour.
-}
divModX :: Integral a => a -> a -> (a, a)
divModX = divMod
{-# INLINE divModX #-}

-- | Division (truncated towards 0) with explicit error on division-by-zero.
quotE :: Integral a => a -> a -> DivResult a
quotE = explicitDiv quot
{-# INLINE quotE #-}

{- | Division (truncated towards 0) with runtime (async) exception on division-by-zero.

Same as 'Prelude.quot' but indicates to the reader that you explicitly thought
about this issue and decided that runtime exception is the correct behaviour.
-}
quotX :: Integral a => a -> a -> a
quotX = quot
{-# INLINE quotX #-}

-- | Modulus (truncated towards 0) with explicit error on division-by-zero.
remE :: Integral a => a -> a -> DivResult a
remE = explicitDiv rem
{-# INLINE remE #-}

{- | Modulus (truncated towards 0) with runtime (async) exception on division-by-zero.

Same as 'Prelude.rem' but indicates to the reader that you explicitly thought
about this issue and decided that runtime exception is the correct behaviour.
-}
remX :: Integral a => a -> a -> a
remX = rem
{-# INLINE remX #-}

-- | Division-and-modulus (truncated towards 0) with explicit error on division-by-zero.
quotRemE :: Integral a => a -> a -> DivResult (a, a)
quotRemE = explicitDiv quotRem
{-# INLINE quotRemE #-}

{- | Division-and-modulus (truncated towards 0) with runtime (async) exception on division-by-zero.

Same as 'Prelude.quotRem' but indicates to the reader that you explicitly thought
about this issue and decided that runtime exception is the correct behaviour.
-}
quotRemX :: Integral a => a -> a -> (a, a)
quotRemX = quotRem
{-# INLINE quotRemX #-}