packages feed

exchangealgebra-0.6.0.0: src/ExchangeAlgebra/Algebra/Value/Accum.hs

{-# LANGUAGE MagicHash #-}
{-# LANGUAGE UnboxedTuples #-}

{-|
Module      : ExchangeAlgebra.Algebra.Value.Accum
Description : Internal exact accumulator for IEEE binary64 values.

Internal module of the Algebra layer. It holds the exact signed-sum state
behind the 'ExchangeAlgebra.Algebra.Value.Class.Accum' instances of
'Double' and @MoneyDouble@. Users never name
'FloatingState'; they use the t'ExchangeAlgebra.Algebra.Value.HatVal'
methods. The module is not exposed: the boundary tests in
@test/Algebra/AccumSpec.hs@ observe the state only through the public
t'ExchangeAlgebra.Algebra.Value.HatVal' methods ('ExchangeAlgebra.Algebra.Value.exactAccum',
'ExchangeAlgebra.Algebra.Value.signAccum', 'ExchangeAlgebra.Algebra.Value.splitAccum',
'ExchangeAlgebra.Algebra.Value.roundAccum').

The module deliberately does /not/ enable @{-\# LANGUAGE Strict \#-}@: the
cold paths (residual capture, overflow into the integer-coefficient state)
are bound in @where@ clauses and must stay lazy so that the common in-range
addition does not allocate an 'Integer'. Its users,
"ExchangeAlgebra.Algebra.Value.Class" and
"ExchangeAlgebra.Algebra.Core.Representation", are compiled with @Strict@ and
only delegate here.

Reading order: 'FloatingState', then the operations in the order of the
t'ExchangeAlgebra.Algebra.Value.HatVal' methods.
-}
module ExchangeAlgebra.Algebra.Value.Accum
    ( -- * State
      FloatingState
      -- * Operations (one per accumulator method)
    , emptyFloating
    , addFloating
    , mergeFloating
    , negateFloating
    , signFloating
    , roundFloating
    , splitFloating
    , exactFloating
    , roundRationalDouble
    ) where

import Data.Bits (shiftL, shiftR)
import Data.Ratio ((%))
import GHC.Float (castDoubleToWord64, castWord64ToDouble)
import GHC.Exts (Int(I#), addIntC#)
import GHC.Num.Integer (integerLog2)

-- * State

-- | Exact signed sum of finite binary64 inputs, or an IEEE special value.
--
-- Invariant: a finite state represents the exact rational sum of its inputs,
-- including sums outside the binary64 range (S0 contract R5). The
-- representation (scalar, two-term expansion, integer coefficient with a
-- variable exponent, special value) is chosen by the implementation.
data FloatingState
    = Integral {-# UNPACK #-} !Int
    | Scalar {-# UNPACK #-} !Double
    | Expansion {-# UNPACK #-} !Double {-# UNPACK #-} !Double
    | Dyadic !Integer !Int
    | Special {-# UNPACK #-} !Double


-- * Operations

-- | The state of the empty sum (value 0).
emptyFloating :: FloatingState
emptyFloating = Integral 0

-- | Normalize both signs of zero without changing any other value.
positiveZero :: Double -> Double
positiveZero value
    | value == 0 = 0
    | otherwise = value

-- | Form an exact dyadic sum, aligning integer coefficients at the smaller exponent.
addDyadic :: (Integer, Int) -> (Integer, Int) -> FloatingState
addDyadic (left, leftPower) (right, rightPower)
    | left == 0 = Dyadic right rightPower
    | right == 0 = Dyadic left leftPower
    | otherwise = Dyadic
        ((left `shiftL` (leftPower - power)) + (right `shiftL` (rightPower - power))) power
  where
    power = min leftPower rightPower

-- | Error-free two-sum with magnitude ordering. The caller handles overflow before
-- using the residual; gradual underflow preserves the identity for subnormal inputs.
twoSum :: Double -> Double -> (# Double, Double #)
{-# INLINE twoSum #-}
twoSum left right
    | abs left >= abs right = (# high, right - (high - left) #)
    | otherwise            = (# high, left - (high - right) #)
  where
    high = left + right

-- | Store two exact components, collapsing an exactly zero low component.
expansion :: Double -> Double -> FloatingState
{-# INLINE expansion #-}
expansion high low
    | low == 0  = Scalar high
    | otherwise = Expansion high low

-- | Error-free scalar addition, retaining the low part until final observation.
-- Invariant: the left input is finite. A finite sum also proves the right input finite.
addScalars :: Double -> Double -> FloatingState
{-# INLINE addScalars #-}
addScalars left right = case twoSum left right of
    (# high, low #)
        | isNaN high || isInfinite high -> exceptional
        | otherwise -> expansion high low
  where
    exceptional
        | isNaN right || isInfinite right = Special right
        | otherwise = addDyadic (decodeFloat left) (decodeFloat right)

-- | Recover all three original components at one common exponent on the cold path.
-- Keeping this conversion separate leaves the floating update free of recursive calls.
addWideExpansion :: Double -> Double -> Double -> FloatingState
addWideExpansion value high low = Dyadic coefficient power
  where
    (valueCoefficient, valuePower) = decodeFloat value
    (highCoefficient, highPower)   = decodeFloat high
    (lowCoefficient, lowPower)     = decodeFloat low
    power = min valuePower (min highPower lowPower)
    coefficient = (valueCoefficient `shiftL` (valuePower - power))
                + (highCoefficient `shiftL` (highPower - power))
                + (lowCoefficient `shiftL` (lowPower - power))

-- | Add machine integers exactly, widening before signed overflow can lose a bit.
addIntegral :: Int -> Int -> FloatingState
{-# INLINE addIntegral #-}
addIntegral left@(I# first) right@(I# second) = case addIntC# first second of
    (# result, 0# #) -> Integral (I# result)
    _               -> Dyadic (toInteger left + toInteger right) 0

-- | Leave the integral fast path without rounding a large machine integer.
fromIntegralState :: Int -> FloatingState
fromIntegralState value
    | abs exact <= 9007199254740992 = Scalar (fromIntegral value)
    | otherwise                    = Dyadic exact 0
  where
    exact = toInteger value

-- | Classify an integer input by a safe machine conversion and an exact round trip.
-- The upper bound is exclusive because maxBound rounds upward on 64-bit machines.
-- A fractional or larger value leaves this fast path, preserving the existing total.
addIntegralValue :: Double -> Int -> FloatingState
{-# INLINE addIntegralValue #-}
addIntegralValue value total
    | value >= lower && value < upper && fromIntegral integral == value =
        addIntegral total integral
    | isNaN value || isInfinite value = Special value
    | abs (toInteger total) <= 9007199254740992 = addScalars (fromIntegral total) value
    | otherwise = addDyadic (toInteger total, 0) (decodeFloat value)
  where
    lower    = fromIntegral (minBound :: Int)
    upper    = negate lower
    integral = truncate value

-- | Add to two exact components, deferring their normalization until observation.
-- For finite inputs, a third residual or either overflow retains all values in Dyadic.
addExpansion :: Double -> Double -> Double -> FloatingState
{-# INLINE addExpansion #-}
addExpansion value high low = case twoSum high value of
    (# upper, errorValue #)
        | isNaN upper || isInfinite upper -> exceptional
        | otherwise        -> case twoSum low errorValue of
            (# lower, remainder #)
                | isInfinite lower || remainder /= 0 -> fallback
                | otherwise -> expansion upper lower
  where
    exceptional
        | isNaN value || isInfinite value = Special value
        | otherwise = fallback
    fallback = addWideExpansion value high low

-- | Keep finite overflow exact and distinguish it from nonfinite inputs.
addFloating :: Double -> FloatingState -> FloatingState
{-# INLINE addFloating #-}
addFloating value (Integral total) = addIntegralValue value total
addFloating value (Scalar total) = addScalars total value
addFloating value (Expansion high low) = addExpansion value high low
addFloating value (Dyadic total power)
    | isNaN value || isInfinite value = Special value
    | otherwise = addDyadic (total, power) (decodeFloat value)
addFloating value state@(Special previous)
    | isNaN value || isInfinite value = Special (value + previous)
    | otherwise = state

-- | Merge finite dyadics exactly; special values follow IEEE addition.
mergeFloating :: FloatingState -> FloatingState -> FloatingState
mergeFloating (Special left) (Special right) = Special (left + right)
mergeFloating special@(Special _) _ = special
mergeFloating _ special@(Special _) = special
mergeFloating (Integral left) (Integral right) = addIntegral left right
mergeFloating (Integral left) right = mergeFloating (fromIntegralState left) right
mergeFloating left (Integral right) = mergeFloating left (fromIntegralState right)
mergeFloating (Expansion high low) right = addFloating low (addFloating high right)
mergeFloating left (Expansion high low) = addFloating low (addFloating high left)
mergeFloating (Scalar left) (Scalar right) = addScalars left right
mergeFloating (Scalar left) (Dyadic right power) = addDyadic (decodeFloat left) (right, power)
mergeFloating (Dyadic left power) (Scalar right) = addDyadic (left, power) (decodeFloat right)
mergeFloating (Dyadic left first) (Dyadic right second) = addDyadic (left, first) (right, second)

-- | Negation never narrows a finite state.
negateFloating :: FloatingState -> FloatingState
negateFloating (Integral value)
    | value == minBound = Dyadic (negate (toInteger value)) 0
    | otherwise         = Integral (negate value)
negateFloating (Scalar value) = Scalar (negate value)
negateFloating (Expansion high low) = Expansion (negate high) (negate low)
negateFloating (Dyadic value power) = Dyadic (negate value) power
negateFloating (Special value) = Special (negate value)

-- | Inspect the exact sign, including beyond the Double range. The sum of two finite
-- binary64 components is a multiple of the smallest subnormal, so rounding their sum
-- cannot turn a nonzero value into zero; overflow also preserves its sign.
signFloating :: FloatingState -> Maybe Ordering
signFloating (Integral value) = Just (compare value 0)
signFloating (Scalar value) = Just (compare value 0)
signFloating (Expansion high low) = Just (compare (high + low) 0)
signFloating (Dyadic value _) = Just (compare value 0)
signFloating (Special value)
    | isNaN value = Nothing
    | otherwise = Just (compare value 0)

-- | Round a dyadic integer explicitly to 53 significant bits (or subnormal units).
-- encodeFloat truncates large Integer significands, so it receives only an already
-- rounded, exactly representable significand here. The final exponent may overflow.
roundDyadic :: Integer -> Int -> Double
roundDyadic 0 _ = 0
roundDyadic coefficient power
    | coefficient < 0 = negate (roundDyadic (negate coefficient) power)
    | otherwise = encodeFloat rounded (power + places)
  where
    highest = fromIntegral (integerLog2 coefficient)
    places = max 0 (max (highest - 52) (-1074 - power))
    whole = coefficient `shiftR` places
    remainder = coefficient - (whole `shiftL` places)
    halfwayComparison = compare (remainder * 2) (1 `shiftL` places)
    rounded
        | halfwayComparison == GT = whole + 1
        | halfwayComparison == EQ && odd whole = whole + 1
        | otherwise = whole

-- | Final nearest-even observation, normalizing either sign of zero.
roundFloating :: FloatingState -> Double
roundFloating (Integral value) = fromIntegral value
roundFloating (Scalar value) = positiveZero value
roundFloating (Expansion high low) = positiveZero (high + low)
roundFloating (Dyadic value power) = positiveZero (roundDyadic value power)
roundFloating (Special value) = value

-- | Greedily split a positive exact state. The predecessor correction prevents
-- a nearest-even result above the exact residual from making the next residual negative.
splitFloating :: FloatingState -> Maybe [Double]
splitFloating (Special _) = Nothing
splitFloating state
    | signFloating state /= Just GT = Nothing
    | isInfinite (roundFloating state) = Nothing
    | otherwise = Just (go state)
  where
    go remaining
        | signFloating remaining == Just EQ = []
        | otherwise = component : go (addFloating (negate component) remaining)
      where
        rounded = roundFloating remaining
        excess = addFloating (negate rounded) remaining
        component
            | signFloating excess == Just LT =
                castWord64ToDouble (castDoubleToWord64 rounded - 1)
            | otherwise = rounded

-- | The exact value of a finite state; 'Nothing' for a special value.
exactFloating :: FloatingState -> Maybe Rational
exactFloating (Integral value) = Just (fromIntegral value)
exactFloating (Scalar value) = Just (toRational value)
exactFloating (Expansion high low) = Just (toRational high + toRational low)
exactFloating (Dyadic value power)
    | power >= 0 = Just ((value `shiftL` power) % 1)
    | otherwise  = Just (value % (1 `shiftL` negate power))
exactFloating (Special _) = Nothing

-- | Round a rational once to binary64 (to nearest, ties to even);
-- 'Nothing' when the result would be infinite.
roundRationalDouble :: Rational -> Maybe Double
roundRationalDouble value
    | isInfinite rounded = Nothing
    | otherwise          = Just rounded
  where
    rounded = fromRational value