dataframe-core-2.5.0.0: src-internal/DataFrame/Internal/Column/Bitmap.hs
{-# LANGUAGE BangPatterns #-}
module DataFrame.Internal.Column.Bitmap where
import Control.Monad (foldM_, forM_, when)
import Control.Monad.ST (ST, runST)
import Data.Bits (
complement,
popCount,
setBit,
shiftL,
shiftR,
testBit,
(.&.),
(.|.),
)
import Data.List (foldl')
import Data.Maybe (fromMaybe, isNothing)
import qualified Data.Vector.Unboxed as VU
import qualified Data.Vector.Unboxed.Mutable as VUM
import Data.Word (Word8)
-- | A bit-packed validity bitmap. Bit @i@ = 1 means row @i@ is valid (not null).
type Bitmap = VU.Vector Word8
-- | A bitmap attached to its row counts so we can splice it.
data Validity = Validity !(Maybe Bitmap) {-# UNPACK #-} !Int
vBitmap :: Validity -> Maybe Bitmap
vBitmap (Validity bm _) = bm
vRowCount :: Validity -> Int
vRowCount (Validity _ n) = n
{- | Test whether row @i@ is valid (not null) in a bitmap.
The bit-level arithmetric is dense but read this as:
`shiftR` 3 is equivalent to `div` 8, and `.&. 7` is equivalent to `mod` 8.
-}
bitmapTestBit :: Bitmap -> Int -> Bool
bitmapTestBit bm i = testBit (VU.unsafeIndex bm (i `shiftR` 3)) (i .&. 7)
{-# INLINE bitmapTestBit #-}
-- | Build a fully-valid bitmap for @n@ rows (all bits set).
allValidBitmap :: Int -> Bitmap
allValidBitmap n = runST (allValidBitmap' n >>= VU.unsafeFreeze)
{-# INLINE allValidBitmap #-}
allValidBitmap' :: Int -> ST s (VUM.MVector s Word8)
allValidBitmap' n =
let
bytes = (n + 7) `shiftR` 3
lastBits = n .&. 7
lastByte = if lastBits == 0 then 0xFF else (1 `shiftL` lastBits) - 1
in
if bytes == 0
then VUM.new 0
else do
mv <- VUM.replicate bytes (0xFF :: Word8) :: ST s (VUM.MVector s Word8)
when (lastBits /= 0) $ VUM.unsafeWrite mv (bytes - 1) lastByte
pure mv
{-# INLINE allValidBitmap' #-}
{- | Build a bitmap from a @VU.Vector Word8@ validity vector
(1 = valid, 0 = null), as produced by Arrow / Parquet decoders.
-}
buildBitmapFromValid :: VU.Vector Word8 -> Bitmap
buildBitmapFromValid valid =
let n = VU.length valid
bytes = (n + 7) `shiftR` 3
in VU.generate bytes $ \b ->
let base = b `shiftL` 3
setBitIf acc bit =
let idx = base + bit
in if idx < n && VU.unsafeIndex valid idx /= 0
then setBit acc bit
else acc
in foldl' setBitIf (0 :: Word8) [0 .. 7]
{- | Build a bitmap from a list of null-row indices.
@nullIdxs@ are the positions that are NULL.
-}
buildBitmapFromNulls :: Int -> [Int] -> Bitmap
buildBitmapFromNulls n idxs = buildBitmapFromNulls' n (VU.fromList idxs)
buildBitmapFromNulls' :: Int -> VU.Vector Int -> VU.Vector Word8
buildBitmapFromNulls' n nullIdxs = runST $ do
bm' <- allValidBitmap' n
VU.forM_ nullIdxs $ \i -> do
let byteIdx = i `shiftR` 3
bitIdx = i .&. 7
v <- VUM.unsafeRead bm' byteIdx
VUM.unsafeWrite bm' byteIdx (clearBit8 v bitIdx)
VU.unsafeFreeze bm'
where
clearBit8 :: Word8 -> Int -> Word8
clearBit8 b bit = b .&. complement (1 `shiftL` bit)
-- | Slice a bitmap for rows @[start .. start+len-1]@.
bitmapSlice :: Int -> Int -> Bitmap -> Bitmap
bitmapSlice start len bm
| start .&. 7 == 0 =
let startByte = start `shiftR` 3
bytes = min ((len + 7) `shiftR` 3) (VU.length bm - startByte)
in VU.slice startByte bytes bm
| otherwise =
let n = min len (VU.length bm `shiftL` 3 - start)
in buildBitmapFromValid $
VU.generate n $
\i -> if bitmapTestBit bm (start + i) then 1 else 0
{- | Count the set bits among the first @n@ bits of a bitmap. A bitmap does
not know the length of the column it describes, and 'bitmapSlice' keeps whole
bytes on its aligned path, so the bits past @n@ may still describe rows
outside the slice.
-}
popCountUpTo :: Int -> Bitmap -> Int
popCountUpTo n bm = whole + partial
where
!fullBytes = min (n `shiftR` 3) (VU.length bm)
!rest = n .&. 7
whole = VU.foldl' (\acc b -> acc + popCount b) 0 (VU.take fullBytes bm)
partial
| rest == 0 || fullBytes >= VU.length bm = 0
| otherwise =
popCount (VU.unsafeIndex bm fullBytes .&. ((1 `shiftL` rest) - 1))
{-# INLINE popCountUpTo #-}
-- | Concatenate two bitmaps covering @n1@ and @n2@ rows respectively.
bitmapConcat :: Int -> Bitmap -> Int -> Bitmap -> Bitmap
bitmapConcat n1 bm1 n2 bm2 =
buildBitmapFromValid $
VU.generate (n1 + n2) $ \i ->
if i < n1
then if bitmapTestBit bm1 i then 1 else 0
else if bitmapTestBit bm2 (i - n1) then 1 else 0
-- | Combine two bitmaps with AND (both must be valid for result to be valid).
andBitmaps :: Bitmap -> Bitmap -> Bitmap
andBitmaps = VU.zipWith (.&.)
{- | Splice chunk bitmaps end to end at the bit level. 'Nothing' if no chunk
carries a bitmap; chunks without one count as all-valid otherwise.
-}
concatValidity :: [Validity] -> Maybe Bitmap
concatValidity parts
| all (isNothing . vBitmap) parts = Nothing
| otherwise = Just $ VU.create $ do
let total = sum (map vRowCount parts)
outBytes = (total + 7) `shiftR` 3
mv <- VUM.replicate outBytes 0
let orInto i w =
when (i < outBytes && w /= 0) $ do
old <- VUM.unsafeRead mv i
VUM.unsafeWrite mv i (old .|. w)
splice !bitPos (Validity !mb !len) = do
let bm = fromMaybe (allValidBitmap len) mb
sh = bitPos .&. 7
byte0 = bitPos `shiftR` 3
lastIdx = ((len + 7) `shiftR` 3) - 1
tailBits = len .&. 7
lastMask =
if tailBits == 0 then 0xFF else (1 `shiftL` tailBits) - 1
forM_ [0 .. lastIdx] $ \k -> do
let raw = VU.unsafeIndex bm k
masked = if k == lastIdx then raw .&. lastMask else raw
w = fromIntegral masked :: Word
orInto (byte0 + k) (fromIntegral (w `shiftL` sh))
when (sh /= 0) $
orInto (byte0 + k + 1) (fromIntegral (w `shiftR` (8 - sh)))
pure (bitPos + len)
foldM_ splice 0 parts
pure mv
-- | Pack a 0\/1 byte-per-row validity prefix into a bit-packed 'Bitmap'.
packValidity :: Int -> VUM.MVector s Word8 -> ST s Bitmap
packValidity n val = do
bytes <- VU.unsafeFreeze (VUM.slice 0 n val)
let assemble b =
let base = b `shiftL` 3
m = min 8 (n - base)
go !acc !k
| k >= m = acc
| VU.unsafeIndex bytes (base + k) /= 0 =
go (acc .|. (1 `shiftL` k)) (k + 1)
| otherwise = go acc (k + 1)
in go (0 :: Word8) 0
pure $! VU.generate ((n + 7) `shiftR` 3) assemble