dataframe-core-2.6.0.0: src-internal/DataFrame/Internal/Data/PackedText.hs
{-# LANGUAGE BangPatterns #-}
-- | Packed-text payload and byte-slice primitives over one shared buffer.
module DataFrame.Internal.Data.PackedText (
PackedTextData (..),
PackedOffsets (..),
PackedSel (..),
offAt,
offCount,
selAt,
selLength,
mkPackedContiguous,
mkPackedContiguous32,
mkOffsets,
mkSel,
packedGather,
packedTake,
packedRowOffsets,
packedLength,
packedSlice,
packedIndexText,
sliceEqBytes,
sliceCmpBytes,
) where
import qualified Data.Text as T
import qualified Data.Text.Array as A
import qualified Data.Vector.Unboxed as VU
import qualified Data.Vector.Unboxed.Mutable as VUM
import Data.Int (Int32)
import Data.Ord (comparing)
import Data.Text.Internal (Text (Text))
import DataFrame.Internal.Control.Concurrent (
parThreshold,
parallelChunks_,
shouldParallelize,
)
import DataFrame.Internal.Data.PackedText.Utf8 (
isValidUtf8Slice,
lenientDecodeSlice,
)
import System.IO.Unsafe (unsafePerformIO)
-- | Row byte-offsets, 'Int32' when every value fits and 'Int' otherwise.
data PackedOffsets
= Offs32 {-# UNPACK #-} !(VU.Vector Int32)
| Offs64 {-# UNPACK #-} !(VU.Vector Int)
-- | Offset at index @i@, widened to 'Int'.
offAt :: PackedOffsets -> Int -> Int
offAt (Offs32 v) i = fromIntegral (VU.unsafeIndex v i)
offAt (Offs64 v) i = VU.unsafeIndex v i
{-# INLINE offAt #-}
-- | Number of offset entries (row count + 1).
offCount :: PackedOffsets -> Int
offCount (Offs32 v) = VU.length v
offCount (Offs64 v) = VU.length v
{-# INLINE offCount #-}
-- | A selection layer mapping logical rows to base rows; @-1@ marks null.
data PackedSel
= Sel32 {-# UNPACK #-} !(VU.Vector Int32)
| Sel64 {-# UNPACK #-} !(VU.Vector Int)
-- | Base row for logical row @i@ (may be @-1@).
selAt :: PackedSel -> Int -> Int
selAt (Sel32 v) i = fromIntegral (VU.unsafeIndex v i)
selAt (Sel64 v) i = VU.unsafeIndex v i
{-# INLINE selAt #-}
selLength :: PackedSel -> Int
selLength (Sel32 v) = VU.length v
selLength (Sel64 v) = VU.length v
{-# INLINE selLength #-}
-- | A shared UTF-8 byte buffer plus @n+1@ row offsets and optional selection.
data PackedTextData = PackedTextData
{ ptBytes :: {-# UNPACK #-} !A.Array
, ptOffsets :: !PackedOffsets
, ptSel :: !(Maybe PackedSel)
, ptCanonicalSel :: !Bool
}
int32Max :: Int
int32Max = fromIntegral (maxBound :: Int32)
-- | Narrow an 'Int' offset vector when the final offset (total bytes) fits.
mkOffsets :: VU.Vector Int -> PackedOffsets
mkOffsets offs
| not (VU.null offs) && VU.last offs <= int32Max =
Offs32 (VU.map fromIntegral offs)
| otherwise = Offs64 offs
{-# INLINE mkOffsets #-}
-- | Narrow an 'Int' base-row vector to 'Int32' when the row count fits.
mkSel :: Int -> VU.Vector Int -> PackedSel
mkSel base rows
| base <= int32Max = Sel32 (VU.map fromIntegral rows)
| otherwise = Sel64 rows
{-# INLINE mkSel #-}
-- | Build a contiguous packed payload (no selection): the freeze-path shape.
mkPackedContiguous :: A.Array -> VU.Vector Int -> PackedTextData
mkPackedContiguous arr offs = PackedTextData arr (mkOffsets offs) Nothing False
{-# INLINE mkPackedContiguous #-}
-- | 'mkPackedContiguous' from offsets already produced at 'Int32' width.
mkPackedContiguous32 :: A.Array -> VU.Vector Int32 -> PackedTextData
mkPackedContiguous32 arr offs = PackedTextData arr (Offs32 offs) Nothing False
{-# INLINE mkPackedContiguous32 #-}
-- | Reindex a packed payload by a selection vector, sharing the byte buffer.
packedGather :: VU.Vector Int -> PackedTextData -> PackedTextData
packedGather indices (PackedTextData arr offs msel canon) =
let !base = offCount offs - 1
canon' = case msel of
Nothing -> False
Just _ -> canon
-- Only the selector is rebuilt; bytes and offsets stay shared.
sel' = case msel of
Nothing
| base <= int32Max -> Sel32 (gatherBaseTo32 indices base)
| otherwise -> Sel64 (gatherBaseTo64 indices base)
Just (Sel32 s)
| base <= int32Max -> Sel32 (gatherSel32To32 indices s base)
| otherwise -> Sel64 (gatherSel32To64 indices s base)
Just (Sel64 s)
| base <= int32Max -> Sel32 (gatherSel64To32 indices s base)
| otherwise -> Sel64 (gatherSel64To64 indices s base)
in PackedTextData arr offs (Just sel') canon'
-- Monomorphic 'packedGather' selector kernels; out-of-range picks give -1.
gatherBaseTo32 :: VU.Vector Int -> Int -> VU.Vector Int32
gatherBaseTo32 indices !base =
parGenSelInline (VU.length indices) $ \i ->
let !r = VU.unsafeIndex indices i
in if r >= 0 && r < base then fromIntegral r else -1
{-# NOINLINE gatherBaseTo32 #-}
gatherBaseTo64 :: VU.Vector Int -> Int -> VU.Vector Int
gatherBaseTo64 indices !base =
parGenSelInline (VU.length indices) $ \i ->
let !r = VU.unsafeIndex indices i
in if r >= 0 && r < base then r else -1
{-# NOINLINE gatherBaseTo64 #-}
gatherSel32To32 :: VU.Vector Int -> VU.Vector Int32 -> Int -> VU.Vector Int32
gatherSel32To32 indices s !base =
let !sn = VU.length s
in parGenSelInline (VU.length indices) $ \i ->
let !j = VU.unsafeIndex indices i
in if j >= 0 && j < sn
then
let !r = fromIntegral (VU.unsafeIndex s j) :: Int
in if r >= 0 && r < base then fromIntegral r else -1
else -1
{-# NOINLINE gatherSel32To32 #-}
gatherSel32To64 :: VU.Vector Int -> VU.Vector Int32 -> Int -> VU.Vector Int
gatherSel32To64 indices s !base =
let !sn = VU.length s
in parGenSelInline (VU.length indices) $ \i ->
let !j = VU.unsafeIndex indices i
in if j >= 0 && j < sn
then
let !r = fromIntegral (VU.unsafeIndex s j) :: Int
in if r >= 0 && r < base then r else -1
else -1
{-# NOINLINE gatherSel32To64 #-}
gatherSel64To32 :: VU.Vector Int -> VU.Vector Int -> Int -> VU.Vector Int32
gatherSel64To32 indices s !base =
let !sn = VU.length s
in parGenSelInline (VU.length indices) $ \i ->
let !j = VU.unsafeIndex indices i
in if j >= 0 && j < sn
then
let !r = VU.unsafeIndex s j
in if r >= 0 && r < base then fromIntegral r else -1
else -1
{-# NOINLINE gatherSel64To32 #-}
gatherSel64To64 :: VU.Vector Int -> VU.Vector Int -> Int -> VU.Vector Int
gatherSel64To64 indices s !base =
let !sn = VU.length s
in parGenSelInline (VU.length indices) $ \i ->
let !j = VU.unsafeIndex indices i
in if j >= 0 && j < sn
then
let !r = VU.unsafeIndex s j
in if r >= 0 && r < base then r else -1
else -1
{-# NOINLINE gatherSel64To64 #-}
-- elector generate over 'parallelChunks_', one chunk per capability.
-- | 'parGenSel' with an INLINE body so each kernel inlines its pick function.
parGenSelInline :: (VU.Unbox c) => Int -> (Int -> c) -> VU.Vector c
parGenSelInline n f
| not (shouldParallelize parThreshold n) = VU.generate n f
| otherwise = unsafePerformIO $ do
mv <- VUM.unsafeNew n
parallelChunks_ parThreshold n $ \ !lo !hi ->
let fill !i
| i >= hi = pure ()
| otherwise = VUM.unsafeWrite mv i (f i) >> fill (i + 1)
in fill lo
VU.unsafeFreeze mv
{-# INLINE parGenSelInline #-}
parGenSel :: (VU.Unbox c) => Int -> (Int -> c) -> VU.Vector c
parGenSel n f
| not (shouldParallelize parThreshold n) = VU.generate n f
| otherwise = unsafePerformIO $ do
mv <- VUM.unsafeNew n
parallelChunks_ parThreshold n $ \ !lo !hi ->
let fill !i
| i >= hi = pure ()
| otherwise = VUM.unsafeWrite mv i (f i) >> fill (i + 1)
in fill lo
VU.unsafeFreeze mv
{-# NOINLINE parGenSel #-}
-- | Take the first @k@ logical rows, sharing the byte buffer, in O(k).
packedTake :: Int -> PackedTextData -> PackedTextData
packedTake k (PackedTextData arr offs msel canon) =
let !base = offCount offs - 1
!k' = max 0 k
(sel', canon') = case msel of
Just (Sel32 s) -> (Sel32 (VU.take k' s), canon)
Just (Sel64 s) -> (Sel64 (VU.take k' s), canon)
Nothing -> (mkSel base (VU.enumFromN 0 (min k' base)), False)
in PackedTextData arr offs (Just sel') canon'
{-# INLINE packedTake #-}
-- | Map a logical row index to its base row, honoring any selection layer.
baseRow :: PackedTextData -> Int -> Int
baseRow (PackedTextData _ _ Nothing _) i = i
baseRow (PackedTextData _ _ (Just sel) _) i = selAt sel i
{-# INLINE baseRow #-}
-- | Row count: @length sel@ when selected, else @length offsets - 1@.
packedLength :: PackedTextData -> Int
packedLength (PackedTextData _ offs Nothing _) = offCount offs - 1
packedLength (PackedTextData _ _ (Just sel) _) = selLength sel
{-# INLINE packedLength #-}
-- | Raw byte slice for logical row @i@: @(buffer, offset, length)@.
packedSlice :: PackedTextData -> Int -> (A.Array, Int, Int)
packedSlice p@(PackedTextData arr offs _ _) i =
let !r = baseRow p i
in if r < 0
then (arr, 0, 0)
else
let o = offAt offs r in (arr, o, offAt offs (r + 1) - o)
{-# INLINE packedSlice #-}
-- | The shared buffer and contiguous offsets when the payload is unselected.
packedRowOffsets :: PackedTextData -> Maybe (A.Array, PackedOffsets)
packedRowOffsets (PackedTextData arr offs Nothing _) = Just (arr, offs)
packedRowOffsets _ = Nothing
{-# INLINE packedRowOffsets #-}
-- | On-demand single 'Data.Text.Text' for row @i@, similar to freeze.
packedIndexText :: PackedTextData -> Int -> T.Text
packedIndexText p i =
let (arr, o, l) = packedSlice p i
in decodeField arr o l
{-# INLINE packedIndexText #-}
-- Decode one field exactly as the boxed freeze path does per row.
decodeField :: A.Array -> Int -> Int -> T.Text
decodeField arr o l
| l == 0 = T.empty
| isValidUtf8Slice arr o l = Text arr o l
| otherwise = lenientDecodeSlice arr o l
{-# INLINE decodeField #-}
-- | Byte-wise equality of two slices, agreeing with 'Text' equality.
sliceEqBytes :: A.Array -> Int -> Int -> A.Array -> Int -> Int -> Bool
sliceEqBytes a ao al b bo bl
| al /= bl = False
| otherwise = go 0
where
go !k
| k >= al = True
| A.unsafeIndex a (ao + k) == A.unsafeIndex b (bo + k) = go (k + 1)
| otherwise = False
{-# INLINE sliceEqBytes #-}
-- | Unsigned byte-lexicographic comparison of two slices (memcmp semantics).
sliceCmpBytes :: A.Array -> Int -> Int -> A.Array -> Int -> Int -> Ordering
sliceCmpBytes a ao al b bo bl = go 0
where
!m = min al bl
go !k
| k >= m = compare al bl
| otherwise = case comparing id (A.unsafeIndex a (ao + k)) (A.unsafeIndex b (bo + k)) of
EQ -> go (k + 1)
r -> r
{-# INLINE sliceCmpBytes #-}