packages feed

dataframe-core-2.6.0.0: src-internal/DataFrame/Internal/Interpreter/Kernels.hs

{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeOperators #-}

module DataFrame.Internal.Interpreter.Kernels (
    Operand (..),
    Branch (..),
    CmpOp (..),
    cmpOpByName,
    ArithOp (..),
    arithOpByName,
    compareOperands,
    arithOperands,
    zipOperands,
    mergeBranches,
    partitionRows,
    intToDouble,
    packedEqLit,
    packedContainsLit,
    packedCharCount,
    parSumBy,
) where

import Control.Monad.ST (runST)
import Data.Bits ((.&.))
import qualified Data.Text as T
import qualified Data.Text.Array as A
import Data.Text.Internal (Text (Text))
import Data.Type.Equality (testEquality, (:~:) (Refl))
import qualified Data.Vector.Unboxed as VU
import qualified Data.Vector.Unboxed.Mutable as VUM
import Type.Reflection (Typeable, typeRep)

import DataFrame.Internal.Column.Operations (parGenerateUnboxedInline)
import DataFrame.Internal.Control.Concurrent (parThreshold, parallelChunks)
import DataFrame.Internal.Data.PackedText (
    PackedOffsets (..),
    PackedTextData (..),
    packedLength,
    selAt,
    sliceEqBytes,
 )
import System.IO.Unsafe (unsafePerformIO)

-- | One side of an operation: the same value in every row, or one per row.
data Operand a = Literal !a | Column !(VU.Vector a)

data CmpOp = CmpLt | CmpLeq | CmpGt | CmpGeq | CmpEq | CmpNeq
    deriving (Eq, Show)

cmpOpByName :: T.Text -> Maybe CmpOp
cmpOpByName name = case name of
    "lt" -> Just CmpLt
    "leq" -> Just CmpLeq
    "gt" -> Just CmpGt
    "geq" -> Just CmpGeq
    "eq" -> Just CmpEq
    "neq" -> Just CmpNeq
    _ -> Nothing

data ArithOp = ArithAdd | ArithSub | ArithMult
    deriving (Eq, Show)

arithOpByName :: T.Text -> Maybe ArithOp
arithOpByName name = case name of
    "add" -> Just ArithAdd
    "sub" -> Just ArithSub
    "mult" -> Just ArithMult
    _ -> Nothing

-- | Runs @k@ with the element type known, only for 'Double' or 'Int'.
withNumeric ::
    forall a r.
    (Typeable a) =>
    (forall x. (VU.Unbox x, Ord x, Num x) => a :~: x -> r) -> Maybe r
withNumeric k
    | Just Refl <- testEquality (typeRep @a) (typeRep @Double) =
        Just (k @Double Refl)
    | Just Refl <- testEquality (typeRep @a) (typeRep @Int) = Just (k @Int Refl)
    | otherwise = Nothing
{-# INLINE withNumeric #-}

compareOperands ::
    forall a.
    (Typeable a) => Maybe (CmpOp -> Operand a -> Operand a -> Operand Bool)
compareOperands = withNumeric @a (\Refl -> compareLoop)

arithOperands ::
    forall a. (Typeable a) => Maybe (ArithOp -> Operand a -> Operand a -> Operand a)
arithOperands = withNumeric @a (\Refl -> arithLoop)

-- | One branch of an if-then-else: constant, every-row, or chosen-rows values.
data Branch a = Constant !a | EveryRow !(VU.Vector a) | ChosenRows !(VU.Vector a)

-- | Each row's value from the branch its condition chooses.
mergeBranches ::
    forall a.
    (Typeable a) => Maybe (VU.Vector Bool -> Branch a -> Branch a -> VU.Vector a)
mergeBranches = withNumeric @a (\Refl -> mergeLoop)

-- The operator must be fully applied or it is not inlined into the loop.
compareLoop ::
    (VU.Unbox a, Ord a) => CmpOp -> Operand a -> Operand a -> Operand Bool
compareLoop op l r = case op of
    CmpLt -> zipOperands (<) l r
    CmpLeq -> zipOperands (<=) l r
    CmpGt -> zipOperands (>) l r
    CmpGeq -> zipOperands (>=) l r
    CmpEq -> zipOperands (==) l r
    CmpNeq -> zipOperands (/=) l r
{-# INLINE compareLoop #-}

arithLoop ::
    (VU.Unbox a, Num a) => ArithOp -> Operand a -> Operand a -> Operand a
arithLoop op l r = case op of
    ArithAdd -> zipOperands (+) l r
    ArithSub -> zipOperands (-) l r
    ArithMult -> zipOperands (*) l r
{-# INLINE arithLoop #-}

zipOperands ::
    (VU.Unbox a, VU.Unbox b, VU.Unbox c) =>
    (a -> b -> c) -> Operand a -> Operand b -> Operand c
zipOperands f (Literal x) (Literal y) = Literal (f x y)
zipOperands f (Column xs) (Literal y) =
    Column
        (parGenerateUnboxedInline (VU.length xs) (\i -> f (VU.unsafeIndex xs i) y))
zipOperands f (Literal x) (Column ys) =
    Column (parGenerateUnboxedInline (VU.length ys) (f x . VU.unsafeIndex ys))
zipOperands f (Column xs) (Column ys) =
    Column
        ( parGenerateUnboxedInline
            (min (VU.length xs) (VU.length ys))
            (\i -> f (VU.unsafeIndex xs i) (VU.unsafeIndex ys i))
        )
{-# INLINE zipOperands #-}

mergeLoop ::
    (VU.Unbox a) => VU.Vector Bool -> Branch a -> Branch a -> VU.Vector a
mergeLoop cs l r = case (l, r) of
    -- With a value per chosen row, the output is filled in row order.
    (ChosenRows _, _) -> inOrder
    (_, ChosenRows _) -> inOrder
    _ ->
        parGenerateUnboxedInline
            n
            (\i -> if VU.unsafeIndex cs i then at l i 0 else at r i 0)
  where
    n = VU.length cs
    -- Row i's value, given how many values this branch has given out.
    at (Constant x) _ _ = x
    at (EveryRow xs) i _ = VU.unsafeIndex xs i
    at (ChosenRows xs) _ used = VU.unsafeIndex xs used
    {-# INLINE at #-}
    inOrder = runST $ do
        out <- VUM.unsafeNew n
        let go !i !lUsed !rUsed
                | i >= n = pure ()
                | VU.unsafeIndex cs i =
                    VUM.unsafeWrite out i (at l i lUsed) >> go (i + 1) (lUsed + 1) rUsed
                | otherwise =
                    VUM.unsafeWrite out i (at r i rUsed) >> go (i + 1) lUsed (rUsed + 1)
        go 0 0 0
        VU.unsafeFreeze out
{-# INLINE mergeLoop #-}

-- | The positions where the condition holds and where it does not.
partitionRows :: VU.Vector Bool -> (VU.Vector Int, VU.Vector Int)
partitionRows cs = runST $ do
    let n = VU.length cs
    yes <- VUM.unsafeNew n
    no <- VUM.unsafeNew n
    -- Write to both and advance one side, so there is no data-dependent branch.
    let go !i !ny !nn
            | i >= n = pure (ny, nn)
            | otherwise = do
                let c = fromEnum (VU.unsafeIndex cs i)
                VUM.unsafeWrite yes ny i
                VUM.unsafeWrite no nn i
                go (i + 1) (ny + c) (nn + 1 - c)
    (ny, nn) <- go 0 0 0
    (,) <$> VU.unsafeFreeze (VUM.take ny yes) <*> VU.unsafeFreeze (VUM.take nn no)

intToDouble :: VU.Vector Int -> VU.Vector Double
intToDouble v = parGenerateUnboxedInline (VU.length v) (fromIntegral . VU.unsafeIndex v)
{-# NOINLINE intToDouble #-}

-- | Whether each row of a packed Text column equals a literal.
packedEqLit :: Bool -> PackedTextData -> T.Text -> VU.Vector Bool
packedEqLit wantEq p@(PackedTextData arr offs sel _) (Text la lo ll) =
    -- Match the layout once so each row costs two offset reads and a compare.
    case (sel, offs) of
        (Nothing, Offs32 o) -> gen id (off32 o)
        (Nothing, Offs64 o) -> gen id (VU.unsafeIndex o)
        (Just s, Offs32 o) -> gen (selAt s) (off32 o)
        (Just s, Offs64 o) -> gen (selAt s) (VU.unsafeIndex o)
  where
    off32 o i = fromIntegral (VU.unsafeIndex o i)
    -- A negative base row is the empty string.
    gen base off =
        parGenerateUnboxedInline (packedLength p) $ \i ->
            let !r = base i
                !o = if r < 0 then 0 else off r
                !l = if r < 0 then 0 else off (r + 1) - o
             in (l == ll && sliceEqBytes arr o l la lo ll) == wantEq
    {-# INLINE gen #-}
{-# NOINLINE packedEqLit #-}

-- | Whether each row of a packed Text column contains a literal substring.
packedContainsLit :: PackedTextData -> T.Text -> VU.Vector Bool
packedContainsLit p@(PackedTextData arr offs sel _) (Text la lo ll)
    | ll == 0 = VU.replicate (packedLength p) True
    | otherwise = case (sel, offs) of
        (Nothing, Offs32 o) -> gen id (off32 o)
        (Nothing, Offs64 o) -> gen id (VU.unsafeIndex o)
        (Just s, Offs32 o) -> gen (selAt s) (off32 o)
        (Just s, Offs64 o) -> gen (selAt s) (VU.unsafeIndex o)
  where
    off32 o i = fromIntegral (VU.unsafeIndex o i)
    !first = A.unsafeIndex la lo
    gen base off =
        parGenerateUnboxedInline (packedLength p) $ \i ->
            let !r = base i
             in r >= 0 && search (off r) (off (r + 1))
    {-# INLINE gen #-}
    -- Scan for the needle's first byte, then compare the rest.
    search !o !end = go o
      where
        !lastStart = end - ll
        go !k
            | k > lastStart = False
            | A.unsafeIndex arr k == first && sliceEqBytes arr k ll la lo ll = True
            | otherwise = go (k + 1)
{-# NOINLINE packedContainsLit #-}

-- | Character count of each row of a packed Text column (UTF-8 lead bytes).
packedCharCount :: PackedTextData -> VU.Vector Int
packedCharCount p@(PackedTextData arr offs sel _) = case (sel, offs) of
    (Nothing, Offs32 o) -> gen id (off32 o)
    (Nothing, Offs64 o) -> gen id (VU.unsafeIndex o)
    (Just s, Offs32 o) -> gen (selAt s) (off32 o)
    (Just s, Offs64 o) -> gen (selAt s) (VU.unsafeIndex o)
  where
    off32 o i = fromIntegral (VU.unsafeIndex o i)
    gen base off =
        parGenerateUnboxedInline (packedLength p) $ \i ->
            let !r = base i
             in if r < 0 then 0 else count (off r) (off (r + 1)) 0
    {-# INLINE gen #-}
    count !k !end !n
        | k >= end = n
        | A.unsafeIndex arr k .&. 0xC0 == 0x80 = count (k + 1) end n
        | otherwise = count (k + 1) end (n + 1)
{-# NOINLINE packedCharCount #-}

-- | @sum (map f v)@ as one parallel loop with no intermediate column.
parSumBy :: (VU.Unbox a, Num b) => (a -> b) -> VU.Vector a -> b
parSumBy f v = unsafePerformIO $ do
    parts <-
        parallelChunks parThreshold (VU.length v) (\lo hi -> pure $! go lo hi 0)
    pure $! sum parts
  where
    go !i !hi !acc
        | i >= hi = acc
        | otherwise = go (i + 1) hi (acc + f (VU.unsafeIndex v i))
{-# INLINE parSumBy #-}