packages feed

jsonlogic-hs-0.1.0.0: src/JsonLogic/Core/Ryu.hs

{-# LANGUAGE BangPatterns #-}

-- |
-- Module      : JsonLogic.Core.Ryu
--
-- The shortest decimal that reads back as a double: Ryū, by Ulf Adams
-- (\"Ryū: fast float-to-string conversion\", PLDI 2018), following the
-- reference implementation's @d2d@ (github.com/ulfjack/ryu, @d2s.c@).
--
-- Ryū's answer is the one ECMAScript's @Number::toString@ asks for: the
-- fewest significant digits whose decimal reads back as the double under
-- round-half-even (so an endpoint of the rounding interval counts when the
-- double's significand is even), and among those the closest to the double,
-- an exact tie going to the even digit.
--
-- The 128-bit multipliers of the reference implementation's tables are
-- computed here once, by the formulas its table generator uses, and the
-- products are taken in 64-bit words, as the reference does. The test suite checks this module against
-- an independent exact algorithm (@JsonLogic.Core.Number.shortestDigitsExact@)
-- on hundreds of thousands of doubles.
module JsonLogic.Core.Ryu
  ( shortestDecimalRyu
  ) where

import Data.Bits (shiftL, shiftR, (.&.), (.|.))
import qualified Data.Vector.Unboxed as Unboxed
import Data.Word (Word64)
import GHC.Float (castDoubleToWord64)

-- | For a positive, finite, non-zero double, @(digits, exponent)@ with the
-- double's shortest decimal equal to @digits * 10^exponent@. The digits may
-- end in zeros; the caller strips them.
shortestDecimalRyu :: Double -> (Word64, Int)
shortestDecimalRyu d = removeDigits acceptBounds (interval acceptBounds e2 m2 mmShift)
  where
    bits = castDoubleToWord64 d
    ieeeMantissa = bits .&. ((1 `shiftL` mantissaBits) - 1)
    ieeeExponent = fromIntegral ((bits `shiftR` mantissaBits) .&. 0x7FF) :: Int
    -- Step 1: the double as m2 * 2^e2, with two bits of room below.
    e2
      | ieeeExponent == 0 = 1 - bias - mantissaBits - 2
      | otherwise = ieeeExponent - bias - mantissaBits - 2
    m2
      | ieeeExponent == 0 = ieeeMantissa
      | otherwise = (1 `shiftL` mantissaBits) + ieeeMantissa
    -- An endpoint of the rounding interval reads back to the double under
    -- round-half-even exactly when the double's significand is even.
    acceptBounds = even m2
    -- The lower end of the interval is closer when the significand is a power
    -- of two (the gap below it is half the gap above).
    mmShift = if ieeeMantissa /= 0 || ieeeExponent <= 1 then 1 else 0

-- | Step 2 and 3: the decimals that read back, @[vm, vp]@ around the double's
-- own @vr@, scaled by @10^e10@, and whether the digits dropped by the scaling
-- were all zeros (for @vm@ and for @vr@).
data Interval = Interval
  { _vr :: {-# UNPACK #-} !Word64
  , _vp :: {-# UNPACK #-} !Word64
  , _vm :: {-# UNPACK #-} !Word64
  , _e10 :: {-# UNPACK #-} !Int
  , _vmZeros :: !Bool
  , _vrZeros :: !Bool
  }

interval :: Bool -> Int -> Word64 -> Word64 -> Interval
interval acceptBounds e2 m2 mmShift
  | e2 >= 0 =
      let q = log10Pow2 e2 - (if e2 > 3 then 1 else 0)
          k = pow5InvBitCount + pow5bits q - 1
          i = negate e2 + q + k
          (vr, vp, vm) = mulShiftAll m2 (pow5InvSplit q) i mmShift
       in if q > 21
            then Interval vr vp vm q False False
            else
              if mv `rem` 5 == 0
                then Interval vr vp vm q False (multipleOfPowerOf5 mv q)
                else
                  if acceptBounds
                    then Interval vr vp vm q (multipleOfPowerOf5 (mv - 1 - mmShift) q) False
                    else Interval vr (vp - (if multipleOfPowerOf5 (mv + 2) q then 1 else 0)) vm q False False
  | otherwise =
      let q = log10Pow5 (negate e2) - (if negate e2 > 1 then 1 else 0)
          i = negate e2 - q
          k = pow5bits i - pow5BitCount
          j = q - k
          (vr, vp, vm) = mulShiftAll m2 (pow5Split i) j mmShift
       in if q <= 1
            then
              -- mv has at least q trailing zero bits, so vr is exact
              if acceptBounds
                then Interval vr vp vm (q + e2) (mmShift == 1) True
                else Interval vr (vp - 1) vm (q + e2) False True
            else Interval vr vp vm (q + e2) False (q < 63 && multipleOfPowerOf2 mv q)
  where
    mv = 4 * m2

-- | Step 4: remove digits while the interval still holds a shorter decimal,
-- then round.
removeDigits :: Bool -> Interval -> (Word64, Int)
removeDigits acceptBounds (Interval vr0 vp0 vm0 e10 vmZeros0 vrZeros0)
  | vmZeros0 || vrZeros0 =
      -- The general case, which keeps track of whether the removed digits of
      -- vm and vr were all zeros, so that bounds and exact ties are decided
      -- right.
      case dropWhileWide vr0 vp0 vm0 vmZeros0 vrZeros0 0 0 of
        Dropped vr1 vp1 vm1 vmZeros1 vrZeros1 last1 removed1 ->
          let Dropped vr2 _ vm2 _ vrZeros2 last2 removed2
                | vmZeros1 = dropZerosOfVm vr1 vp1 vm1 vrZeros1 last1 removed1
                | otherwise = Dropped vr1 vp1 vm1 vmZeros1 vrZeros1 last1 removed1
              -- round half to even when the exact value is ...50...0
              last3 = if vrZeros2 && last2 == 5 && even vr2 then 4 else last2
              up = (vr2 == vm2 && (not acceptBounds || not vmZeros1)) || last3 >= 5
           in (vr2 + (if up then 1 else 0), e10 + removed2)
  | otherwise =
      -- The common case: no exact ties or bounds to worry about. Two digits
      -- at a time first, while that is possible.
      let wide = vp0 `quot` 100 > vm0 `quot` 100
          vr1 = if wide then vr0 `quot` 100 else vr0
          vp1 = if wide then vp0 `quot` 100 else vp0
          vm1 = if wide then vm0 `quot` 100 else vm0
          roundUp1 = wide && vr0 `rem` 100 >= 50
       in case dropCommon vr1 vp1 vm1 roundUp1 (if wide then 2 else 0) of
            Dropped vr2 _ vm2 _ roundUp2 _ removed2 ->
              (vr2 + (if vr2 == vm2 || roundUp2 then 1 else 0), e10 + removed2)

-- | The state of digit removal: vr, vp, vm, whether vm's removed digits
-- were zeros, whether vr's were (or, in the common case, whether to round
-- up), the last digit removed from vr, and how many were removed.
data Dropped
  = Dropped
      {-# UNPACK #-} !Word64
      {-# UNPACK #-} !Word64
      {-# UNPACK #-} !Word64
      !Bool
      !Bool
      {-# UNPACK #-} !Word64
      {-# UNPACK #-} !Int

-- | Remove digits while the shortened interval is still non-empty, tracking
-- trailing zeros.
dropWhileWide :: Word64 -> Word64 -> Word64 -> Bool -> Bool -> Word64 -> Int -> Dropped
dropWhileWide !vr !vp !vm !vmZeros !vrZeros !lastDigit !removed
  | vp `quot` 10 <= vm `quot` 10 = Dropped vr vp vm vmZeros vrZeros lastDigit removed
  | otherwise =
      dropWhileWide
        (vr `quot` 10)
        (vp `quot` 10)
        (vm `quot` 10)
        (vmZeros && vm `rem` 10 == 0)
        (vrZeros && lastDigit == 0)
        (vr `rem` 10)
        (removed + 1)

-- | When vm ends in zeros, remove them too: the shorter vm is still in range.
dropZerosOfVm :: Word64 -> Word64 -> Word64 -> Bool -> Word64 -> Int -> Dropped
dropZerosOfVm !vr !vp !vm !vrZeros !lastDigit !removed
  | vm == 0 || vm `rem` 10 /= 0 = Dropped vr vp vm True vrZeros lastDigit removed
  | otherwise =
      dropZerosOfVm
        (vr `quot` 10)
        (vp `quot` 10)
        (vm `quot` 10)
        (vrZeros && lastDigit == 0)
        (vr `rem` 10)
        (removed + 1)

-- | The common case's digit removal: remember only whether to round up.
dropCommon :: Word64 -> Word64 -> Word64 -> Bool -> Int -> Dropped
dropCommon !vr !vp !vm !roundUp !removed
  | vp `quot` 10 <= vm `quot` 10 = Dropped vr vp vm False roundUp 0 removed
  | otherwise = dropCommon (vr `quot` 10) (vp `quot` 10) (vm `quot` 10) (vr `rem` 10 >= 5) (removed + 1)

-- | @floor (m * multiplier / 2^shift)@ for the centre and both ends of the
-- interval: @4m@, @4m + 2@ and @4m - 1 - mmShift@. The multiplier is 128
-- bits, as a (low, high) pair.
mulShiftAll :: Word64 -> (Word64, Word64) -> Int -> Word64 -> (Word64, Word64, Word64)
{-# INLINE mulShiftAll #-}
mulShiftAll m multiplier shift mmShift =
  ( mulShift64 (4 * m) multiplier shift
  , mulShift64 (4 * m + 2) multiplier shift
  , mulShift64 (4 * m - 1 - mmShift) multiplier shift
  )

-- | @floor (m * multiplier / 2^shift)@ for a 64-bit @m@ and a 128-bit
-- multiplier, with @64 < shift < 128@: the reference implementation's
-- @mulShift64@.
mulShift64 :: Word64 -> (Word64, Word64) -> Int -> Word64
{-# INLINE mulShift64 #-}
mulShift64 m (mulLow, mulHigh) shift = shiftRight128 sumLow sumHigh (shift - 64)
  where
    (high0, _) = umul128 m mulLow
    (high2, low2) = umul128 m mulHigh
    sumLow = high0 + low2
    sumHigh = high2 + (if sumLow < high0 then 1 else 0)

-- | The low 64 bits of a 128-bit value shifted right by @0 < n < 64@.
shiftRight128 :: Word64 -> Word64 -> Int -> Word64
{-# INLINE shiftRight128 #-}
shiftRight128 low high n = (high `shiftL` (64 - n)) .|. (low `shiftR` n)

-- | The full 128-bit product of two 64-bit words, as (high, low), from
-- 32-bit halves (the reference implementation's portable @umul128@).
umul128 :: Word64 -> Word64 -> (Word64, Word64)
{-# INLINE umul128 #-}
umul128 a b = (high, low)
  where
    lowMask = 0xFFFFFFFF
    aLow = a .&. lowMask
    aHigh = a `shiftR` 32
    bLow = b .&. lowMask
    bHigh = b `shiftR` 32
    b00 = aLow * bLow
    b01 = aLow * bHigh
    b10 = aHigh * bLow
    b11 = aHigh * bHigh
    mid1 = b10 + (b00 `shiftR` 32)
    mid2 = b01 + (mid1 .&. lowMask)
    high = b11 + (mid1 `shiftR` 32) + (mid2 `shiftR` 32)
    low = ((mid2 .&. lowMask) `shiftL` 32) .|. (b00 .&. lowMask)

mantissaBits :: Int
mantissaBits = 52

bias :: Int
bias = 1023

pow5InvBitCount :: Int
pow5InvBitCount = 125

pow5BitCount :: Int
pow5BitCount = 125

-- | The bit length of @5^e@, for @0 <= e <= 3528@.
pow5bits :: Int -> Int
pow5bits e = ((e * 1217359) `shiftR` 19) + 1

-- | @floor (log10 (2^e))@, for @0 <= e <= 1650@.
log10Pow2 :: Int -> Int
log10Pow2 e = (e * 78913) `shiftR` 18

-- | @floor (log10 (5^e))@, for @0 <= e <= 2620@.
log10Pow5 :: Int -> Int
log10Pow5 e = (e * 732923) `shiftR` 20

multipleOfPowerOf5 :: Word64 -> Int -> Bool
multipleOfPowerOf5 value p = pow5Factor value 0 >= p
  where
    pow5Factor !v !count
      | v /= 0 && v `rem` 5 == 0 = pow5Factor (v `quot` 5) (count + 1)
      | otherwise = count :: Int

multipleOfPowerOf2 :: Word64 -> Int -> Bool
multipleOfPowerOf2 value p = value .&. ((1 `shiftL` p) - 1) == 0

-- | @floor (2^(bitLength (5^q) - 1 + 125) / 5^q) + 1@, the reference
-- implementation's @DOUBLE_POW5_INV_SPLIT[q]@, as (low, high) words.
pow5InvSplit :: Int -> (Word64, Word64)
pow5InvSplit q = (pow5InvLow Unboxed.! q, pow5InvHigh Unboxed.! q)

-- | @5^i@ normalised to 125 bits, the reference implementation's
-- @DOUBLE_POW5_SPLIT[i]@, as (low, high) words.
pow5Split :: Int -> (Word64, Word64)
pow5Split i = (pow5Low Unboxed.! i, pow5High Unboxed.! i)

-- | Both tables are computed once, on first use, from their definitions.
pow5InvLow, pow5InvHigh, pow5Low, pow5High :: Unboxed.Vector Word64
pow5InvLow = Unboxed.generate pow5InvTableSize (lowWord . pow5InvEntry)
pow5InvHigh = Unboxed.generate pow5InvTableSize (highWord . pow5InvEntry)
pow5Low = Unboxed.generate pow5TableSize (lowWord . pow5Entry)
pow5High = Unboxed.generate pow5TableSize (highWord . pow5Entry)
{-# NOINLINE pow5InvLow #-}
{-# NOINLINE pow5InvHigh #-}
{-# NOINLINE pow5Low #-}
{-# NOINLINE pow5High #-}

-- | Large enough for every double: @q@ reaches 291 and @i@ reaches 325.
pow5InvTableSize, pow5TableSize :: Int
pow5InvTableSize = 342
pow5TableSize = 326

pow5InvEntry :: Int -> Integer
pow5InvEntry q = ((1 :: Integer) `shiftL` (bitLength power - 1 + pow5InvBitCount)) `quot` power + 1
  where
    power = 5 ^ q

pow5Entry :: Int -> Integer
pow5Entry i
  | excess >= 0 = power `shiftR` excess
  | otherwise = power `shiftL` negate excess
  where
    power = 5 ^ i :: Integer
    excess = bitLength power - pow5BitCount

lowWord :: Integer -> Word64
lowWord = fromInteger . (.&. 0xFFFFFFFFFFFFFFFF)

highWord :: Integer -> Word64
highWord = fromInteger . (`shiftR` 64)

bitLength :: Integer -> Int
bitLength = go 0
  where
    go n v
      | v == 0 = n
      | otherwise = go (n + 1) (v `shiftR` 1)