packages feed

lsm-tree-1.0.0.0: src-core/Database/LSMTree/Internal/RawBytes.hs

{-# LANGUAGE CPP                #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE MagicHash          #-}
{-# LANGUAGE TypeFamilies       #-}
{-# LANGUAGE UnboxedTuples      #-}
{-# OPTIONS_HADDOCK not-home #-}

#ifndef __HLINT__
-- HLint would fail to find this file and emit a warning
#include <MachDeps.h>
#endif

-- |
--
-- This module is intended to be imported qualified, to avoid name clashes with
-- "Prelude" functions:
--
-- @
--   import Database.LSMTree.Internal.RawBytes (RawBytes (..))
--   import qualified Database.LSMTree.Internal.RawBytes as RB
-- @
module Database.LSMTree.Internal.RawBytes (
    -- See Note: [Export structure]
    -- * Raw bytes
    RawBytes (..)
    -- * Accessors
    -- ** Length information
  , size
    -- ** Extracting subvectors (slicing)
  , take
  , drop
  , topBits64
    -- * Construction
    -- | Use 'Semigroup' and 'Monoid' operations
    -- ** Restricting memory usage
  , copy
  , force
    -- * Conversions
  , fromVector
  , fromByteArray
    -- ** Lists
  , pack
  , unpack
    -- * @bytestring@ utils
  , fromByteString
  , unsafeFromByteString
  , toByteString
  , unsafePinnedToByteString
  , fromShortByteString
  , builder
  ) where

import           Control.DeepSeq (NFData)
import           Data.Bits (Bits (..))
import           Data.BloomFilter.Hash (Hashable (..), hashByteArray)
import qualified Data.ByteString as BS
import qualified Data.ByteString.Builder as BB
import           Data.ByteString.Short (ShortByteString (SBS))
import qualified Data.ByteString.Short as SBS
import           Data.Primitive.ByteArray (ByteArray (..), compareByteArrays)
import qualified Data.Primitive.ByteArray as BA
import qualified Data.Vector.Primitive as VP
import           Database.LSMTree.Internal.ByteString (byteArrayToByteString,
                     shortByteStringFromTo, tryGetByteArray,
                     unsafePinnedByteArrayToByteString)
import           Database.LSMTree.Internal.Vector
import           Prelude hiding (drop, take)

import           GHC.Exts
import           GHC.Stack
import           GHC.Word
import           Text.Printf (printf)

-- $setup
-- >>> import Numeric

{- Note: [Export structure]
   ~~~~~~~~~~~~~~~~~~~~~~~
   Since RawBytes are very similar to Primitive Vectors, the code is sectioned
   and structured much like the "Data.Vector.Primitive" module.
-}

{-------------------------------------------------------------------------------
  Raw bytes
-------------------------------------------------------------------------------}

{- |
Raw bytes.

This type imposes no alignment constraint and provides no guarantee of whether the memory is pinned or unpinned.
-}
newtype RawBytes = RawBytes (VP.Vector Word8)
  deriving newtype (NFData)

-- TODO: Should we have a more well-behaved instance for 'Show'?
--       For instance, an instance that prints the bytes as a hexadecimal string?
deriving newtype instance Show RawBytes

_showBytesAsHex :: RawBytes -> ShowS
_showBytesAsHex (RawBytes bytes) = VP.foldr ((.) . showByte) id bytes
  where
    showByte :: Word8 -> ShowS
    showByte = showString . printf "%02x"

instance Eq RawBytes where
  bs1 == bs2 = compareBytes bs1 bs2 == EQ

{- |
This instance uses lexicographic ordering.
-}
instance Ord RawBytes where
  compare = compareBytes

-- | Based on @Ord 'ShortByteString'@.
compareBytes :: RawBytes -> RawBytes -> Ordering
compareBytes rb1@(RawBytes vec1) rb2@(RawBytes vec2) =
    let !len1 = size rb1
        !len2 = size rb2
        !len  = min len1 len2
     in case compareByteArrays ba1 off1 ba2 off2 len of
          EQ | len1 < len2 -> LT
             | len1 > len2 -> GT
          o  -> o
  where
    VP.Vector off1 _size1 ba1 = vec1
    VP.Vector off2 _size2 ba2 = vec2

instance Hashable RawBytes where
  hashSalt64 :: Word64 -> RawBytes -> Word64
  hashSalt64 = hash

hash :: Word64 -> RawBytes -> Word64
hash salt (RawBytes (VP.Vector off len ba)) = hashByteArray ba off len salt

{- |
@'fromList'@: \(O(n)\).

@'toList'@: \(O(n)\).
-}
instance IsList RawBytes where
  type Item RawBytes = Word8

  fromList :: [Item RawBytes] -> RawBytes
  fromList = pack

  toList :: RawBytes -> [Item RawBytes]
  toList = unpack

{- |
@'fromString'@: \(O(n)\).

__Warning:__ 'fromString' truncates multi-byte characters to octets. e.g. \"枯朶に烏のとまりけり秋の暮\" becomes \"�6k�nh~�Q��n�\".
-}
instance IsString RawBytes where
    fromString = fromByteString . fromString

{-------------------------------------------------------------------------------
  Accessors
-------------------------------------------------------------------------------}

-- | \( O(1) \)
size :: RawBytes -> Int
size = coerce VP.length

-- | \( O(1) \)
take :: Int -> RawBytes -> RawBytes
take = coerce VP.take

-- | \( O(1) \)
drop :: Int -> RawBytes -> RawBytes
drop = coerce VP.drop

-- | @'topBits64' rb@ slices the first @64@ bits from the /top/ of the raw bytes
-- @rb@. Returns the string of bits as a 'Word64'.
--
-- The /top/ corresponds to the most significant bit (big-endian).
--
-- If the number of bits is smaller than @64@, then any missing bits default to
-- @0@s.
--
-- >>> showHex (topBits64 (pack [1,0,0,0,0,0,0,0])) ""
-- "100000000000000"
--
-- >>> showHex (topBits64 (pack [1,0,0])) ""
-- "100000000000000"
--
-- TODO: optimisation ideas: use unsafe shift/byteswap primops, look at GHC
-- core, find other opportunities for using primops.
--
topBits64 :: RawBytes -> Word64
topBits64 rb@(RawBytes v@(VP.Vector (I# off#) _size (ByteArray k#)))
  | n >= 8
  = toWord64 (indexWord8ArrayAsWord64# k# off#)
  | otherwise
  = VP.foldl' f 0 v `unsafeShiftL` ((8 - n) * 8)
  where
    !n = size rb

    f :: Word64 -> Word8 -> Word64
    f acc w = acc `unsafeShiftL` 8 + fromIntegral w

#if (MIN_VERSION_GLASGOW_HASKELL(9, 4, 0, 0))
toWord64 :: Word64# -> Word64
#else
toWord64 :: Word# -> Word64
#endif
#if WORDS_BIGENDIAN
toWord64 = W64#
#else
toWord64 x# = byteSwap64 (W64# x#)
#endif

{-------------------------------------------------------------------------------
  Construction
-------------------------------------------------------------------------------}

{- |
@('<>')@: \(O(n)\).

@'Data.Semigroup.sconcat'@: \(O(n)\).
-}
instance Semigroup RawBytes where
    (<>) = coerce (VP.++)

{- |
@'mempty'@: \(O(1)\).

@'mconcat'@: \(O(n)\).
-}
instance Monoid RawBytes where
    mempty = coerce VP.empty
    mconcat = coerce VP.concat

-- | O(n) Yield the argument, but force it not to retain any extra memory by
-- copying it.
--
-- Useful when dealing with slices. Also, see
-- "Database.LSMTree.Internal.Unsliced"
copy :: RawBytes -> RawBytes
copy (RawBytes pvec) = RawBytes (VP.force pvec)

-- | Force 'RawBytes' to not retain any extra memory. This may copy the contents.
force :: RawBytes -> ByteArray
force (RawBytes (VP.Vector off len ba))
    | off == 0
    , BA.sizeofByteArray ba == len
    = ba

    | otherwise
    = BA.runByteArray $ do
        mba <- BA.newByteArray len
        BA.copyByteArray mba 0 ba off len
        pure mba

{-------------------------------------------------------------------------------
  Conversions
-------------------------------------------------------------------------------}

-- | \( O(1) \)
fromVector :: VP.Vector Word8 -> RawBytes
fromVector v = RawBytes v

-- | \( O(1) \)
fromByteArray :: Int -> Int -> ByteArray -> RawBytes
fromByteArray off len ba = RawBytes (mkPrimVector off len ba)

pack :: [Word8] -> RawBytes
pack = coerce VP.fromList

unpack :: RawBytes -> [Word8]
unpack = coerce VP.toList

{-------------------------------------------------------------------------------
  @bytestring@ utils
-------------------------------------------------------------------------------}

-- | \( O(n) \) conversion from a strict bytestring to raw bytes.
fromByteString :: BS.ByteString -> RawBytes
fromByteString = fromShortByteString . SBS.toShort

-- | \( O(1) \) conversion from a strict bytestring to raw bytes.
--
-- Strict bytestrings are allocated using 'mallocPlainForeignPtrBytes', so we
-- are expecting a 'PlainPtr' (or 'FinalPtr' with length 0).
-- For other variants, this function will fail.
unsafeFromByteString :: HasCallStack => BS.ByteString -> RawBytes
unsafeFromByteString bs =
    case tryGetByteArray bs of
      Right (ba, n) -> RawBytes (mkPrimVector 0 n ba)
      Left err      -> error $ "unsafeFromByteString: " <> err

-- | \( O(1) \) conversion from raw bytes to a bytestring if pinned,
-- \( O(n) \) if unpinned.
toByteString :: RawBytes -> BS.ByteString
toByteString (RawBytes (VP.Vector off len ba)) =
    byteArrayToByteString off len ba

-- | \( O(1) \) conversion from raw bytes to a bytestring.
-- Fails if the underlying byte array is not pinned.
unsafePinnedToByteString :: HasCallStack => RawBytes -> BS.ByteString
unsafePinnedToByteString (RawBytes (VP.Vector off len ba)) =
    unsafePinnedByteArrayToByteString off len ba

-- | \( O(1) \) conversion from a short bytestring to raw bytes.
fromShortByteString :: ShortByteString -> RawBytes
fromShortByteString sbs@(SBS ba#) =
    RawBytes (mkPrimVector 0 (SBS.length sbs) (ByteArray ba#))

{-# INLINE builder #-}
builder :: RawBytes -> BB.Builder
builder (RawBytes (VP.Vector off sz (ByteArray ba#))) =
    shortByteStringFromTo off (off + sz) (SBS ba#)