packages feed

dataframe-parsing-1.0.2.0: src/DataFrame/Internal/Parsing/Fast/Common.hs

{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE MagicHash #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}

{- | Shared byte-level helpers for the fast slice parsers.

All functions operate on @(buf, start, end)@ slices with the hsthrift
one-bounds-check discipline: the caller guarantees
@0 <= start <= end <= length buf@ and everything below uses 'unsafeIndex'.
-}
module DataFrame.Internal.Parsing.Fast.Common (
    isStripByte,
    isDigitByte,
    skipStrip,
    skipStripEnd,
    skipZeroes,
    takeDigits64,
) where

import qualified Data.ByteString as BS
import qualified Data.ByteString.Unsafe as BSU

import Data.Word (Word64, Word8)
import GHC.Exts (
    Int (..),
    Int#,
    RuntimeRep,
    TYPE,
    Word64#,
    isTrue#,
    plusWord64#,
    timesWord64#,
    wordToWord64#,
    (+#),
    (>=#),
 )
import GHC.Word (Word64 (..))

{- | Bytes removed by 'Data.ByteString.Char8.strip' (Latin-1 'isSpace'):
HT LF VT FF CR SP and NBSP 0xA0. Probed over all 256 bytes; parity
with the strip-based reference parsers depends on this exact set.
-}
isStripByte :: Word8 -> Bool
isStripByte w = w == 0x20 || (w - 0x09) <= 4 || w == 0xA0
{-# INLINE isStripByte #-}

isDigitByte :: Word8 -> Bool
isDigitByte w = (w - 0x30) <= 9
{-# INLINE isDigitByte #-}

-- | Index of the first non-strip byte in @[i, end)@.
skipStrip :: BS.ByteString -> Int -> Int -> Int
skipStrip bs = go
  where
    go !i !end
        | i < end && isStripByte (BSU.unsafeIndex bs i) = go (i + 1) end
        | otherwise = i
{-# INLINE skipStrip #-}

-- | New exclusive end after dropping trailing strip bytes in @[i, end)@.
skipStripEnd :: BS.ByteString -> Int -> Int -> Int
skipStripEnd bs = go
  where
    go !i !end
        | end > i && isStripByte (BSU.unsafeIndex bs (end - 1)) = go i (end - 1)
        | otherwise = end
{-# INLINE skipStripEnd #-}

-- | Index of the first non-@\'0\'@ byte in @[i, end)@.
skipZeroes :: BS.ByteString -> Int -> Int -> Int
skipZeroes bs = go
  where
    go !i !end
        | i < end && BSU.unsafeIndex bs i == 0x30 = go (i + 1) end
        | otherwise = i
{-# INLINE skipZeroes #-}

{- | Consume ASCII digits from @i@, passing the stop index and the
wrapping 'Word64' accumulation to the (possibly unboxed-result)
continuation. Callers must bound the significant digit count before
trusting the value. The loop carries @Int#@/@Word64#@ explicitly:
GHC does not worker\/wrapper join points, so boxed loop arguments
would otherwise allocate 16 bytes per digit (ticky-verified).
-}
takeDigits64 ::
    forall (rep :: RuntimeRep) (r :: TYPE rep).
    BS.ByteString ->
    Int ->
    Int ->
    (Int -> Word64 -> r) ->
    r
takeDigits64 bs (I# i0) (I# end) k = go i0 (wordToWord64# 0##)
  where
    go :: Int# -> Word64# -> r
    go i acc
        | isTrue# (i >=# end) = k (I# i) (W64# acc)
        | isDigitByte w =
            case fromIntegral (w - 0x30) :: Word64 of
                W64# d ->
                    go
                        (i +# 1#)
                        ((acc `timesWord64#` wordToWord64# 10##) `plusWord64#` d)
        | otherwise = k (I# i) (W64# acc)
      where
        w = BSU.unsafeIndex bs (I# i)
{-# INLINE takeDigits64 #-}