packages feed

persist-1.0.0.0: src/Data/Persist/Internal.hs

{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedRecordDot #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE NoFieldSelectors #-}

module Data.Persist.Internal
  ( (:!:) (..)

    -- * The Get type
  , Get (..)
  , GetEnv (..)
  , GetException (..)
  , getOffset
  , failGet
  , runGet
  , runGetIO
  , unsafeGetPrefix

    -- * The Put type
  , Put (..)
  , PutEnv (..)
  , PutException (..)
  , Chunk (..)
  , evalPut
  , evalPutStrictIO
  , evalPutLazy
  , evalPutLazyIO
  , grow

    -- * Size reservations
  , PutSize (..)
  ) where

import Control.Exception
import Control.Monad
import qualified Control.Monad.Fail as Fail
import Data.ByteString (ByteString)
import qualified Data.ByteString as B
import qualified Data.ByteString.Internal as B
import qualified Data.ByteString.Lazy.Internal as BL
#if MIN_VERSION_base(4,20,0)
import Data.Foldable (foldlM)
#else
import Data.Foldable (foldl', foldlM)
#endif
import Data.IORef
import Data.List.NonEmpty (NonEmpty (..))
import Data.Word
import Foreign
  ( ForeignPtr
  , Ptr
  , allocaBytes
  , finalizerFree
  , free
  , mallocBytes
  , minusPtr
  , newForeignPtr
  , plusPtr
  , reallocBytes
  , withForeignPtr
  )
import Foreign.Marshal.Utils (copyBytes)
import System.IO.Unsafe

#include "MachDeps.h"

data a :!: b = !a :!: !b
infixl 2 :!:

data GetEnv = GetEnv
  { buf :: !(ForeignPtr Word8)
  , begin :: {-# UNPACK #-} !(Ptr Word8)
  , end :: {-# UNPACK #-} !(Ptr Word8)
  , tmp :: {-# UNPACK #-} !(Ptr Word8)
  }

newtype Get a = Get
  { unGet :: GetEnv -> Ptr Word8 -> IO (Ptr Word8 :!: a)
  }

instance Functor Get where
  fmap f m = Get $ \e p -> do
    p' :!: x <- m.unGet e p
    pure $! p' :!: f x
  {-# INLINE fmap #-}

instance Applicative Get where
  pure a = Get $ \_ p -> pure $! p :!: a
  {-# INLINE pure #-}

  f <*> a = Get $ \e p -> do
    p' :!: f' <- f.unGet e p
    p'' :!: a' <- a.unGet e p'
    pure $! p'' :!: f' a'
  {-# INLINE (<*>) #-}

  m1 *> m2 = do
    void m1
    m2
  {-# INLINE (*>) #-}

instance Monad Get where
  m >>= f = Get $ \e p -> do
    p' :!: x <- m.unGet e p
    (f x).unGet e p'
  {-# INLINE (>>=) #-}

#if !MIN_VERSION_base(4,11,0)
  fail = Fail.fail
  {-# INLINE fail #-}
#endif

data GetException
  = LengthException Int String
  | CharException Int String
  | EOFException Int String
  | GenericGetException Int String
  deriving (Eq, Show)

instance Exception GetException

data PutException
  = PutSizeMissingStartChunk
  deriving (Eq, Show)

instance Exception PutException

instance Fail.MonadFail Get where
  fail msg = failGet GenericGetException ("Failed reading: " <> msg)
  {-# INLINE fail #-}

getOffset :: Get Int
getOffset = Get $ \e p -> pure $! p :!: (p `minusPtr` e.begin)
{-# INLINE getOffset #-}

failGet :: (Int -> String -> GetException) -> String -> Get a
failGet ctor msg = do
  offset <- getOffset
  Get $ \_ _ -> throwIO (ctor offset msg)

runGetIO :: Get a -> ByteString -> IO a
runGetIO m s = run
 where
  run = withForeignPtr buf $ \p -> allocaBytes 8 $ \t -> do
    let env = GetEnv {buf, begin = p, end = p `plusPtr` (pos + len), tmp = t}
    _ :!: r <- m.unGet env (p `plusPtr` pos)
    pure r
  (B.PS buf pos len) = s

-- | Run the Get monad applies a 'get'-based parser on the input ByteString
runGet :: Get a -> ByteString -> Either String a
runGet m s = unsafePerformIO $ catch (Right <$!> runGetIO m s) handler
 where
  handler (e :: GetException) = pure $ Left $ displayException e
{-# NOINLINE runGet #-}

unsafeGetPrefix :: Int -> Get a -> Get a
unsafeGetPrefix prefixLength baseGet = Get $ \env p -> do
  let p' = p `plusPtr` prefixLength
      env' = (\GetEnv {..} -> GetEnv {end = p', ..}) env
  _ :!: r <- baseGet.unGet env' p
  pure $ p' :!: r
{-# INLINE unsafeGetPrefix #-}

data Chunk = Chunk
  { begin :: {-# UNPACK #-} !(Ptr Word8)
  , end :: {-# UNPACK #-} !(Ptr Word8)
  }

data PutEnv = PutEnv
  { chunks :: !(IORef (NonEmpty Chunk))
  , end :: !(IORef (Ptr Word8))
  , tmp :: {-# UNPACK #-} !(Ptr Word8)
  }

newtype Put a = Put
  {unPut :: PutEnv -> Ptr Word8 -> IO (Ptr Word8 :!: a)}

instance Functor Put where
  fmap f m = Put $ \e p -> do
    p' :!: x <- m.unPut e p
    pure $! p' :!: f x
  {-# INLINE fmap #-}

instance Applicative Put where
  pure a = Put $ \_ p -> pure $! p :!: a
  {-# INLINE pure #-}

  f <*> a = Put $ \e p -> do
    p' :!: f' <- f.unPut e p
    p'' :!: a' <- a.unPut e p'
    pure $! p'' :!: f' a'
  {-# INLINE (<*>) #-}

  m1 *> m2 = do
    void m1
    m2
  {-# INLINE (*>) #-}

instance Monad Put where
  m >>= f = Put $ \e p -> do
    p' :!: x <- m.unPut e p
    (f x).unPut e p'
  {-# INLINE (>>=) #-}

data PutSize a = PutSize
  { sizePtr :: !(Ptr Word8)
  , sizeStart :: !(Ptr Word8)
  , chunkStart :: !(Ptr Word8)
  }

minChunkSize :: Int
minChunkSize = 0x10000
{-# INLINE minChunkSize #-}

newChunk :: Int -> IO Chunk
newChunk size = do
  let n = max size minChunkSize
  p <- mallocBytes n
  pure $! Chunk p $ p `plusPtr` n
{-# INLINE newChunk #-}

-- | Ensure that @n@ bytes can be written.
grow :: Int -> Put ()
grow n
  | n < 0 = error "grow: negative length"
  | otherwise = Put $ \e p -> do
      end <- readIORef e.end
      if end `minusPtr` p >= n
        then
          pure $! p :!: ()
        else
          doGrow e p n
{-# INLINE grow #-}

doGrow :: PutEnv -> Ptr Word8 -> Int -> IO (Ptr Word8 :!: ())
doGrow e p n = do
  k <- newChunk n
  modifyIORef' e.chunks $ \case
    (c :| cs) ->
      let !c' = (\Chunk {..} -> Chunk {end = p, ..}) c
       in k :| c' : cs
  writeIORef e.end $! k.end
  pure $! k.begin :!: ()
{-# NOINLINE doGrow #-}

chunksLength :: [Chunk] -> Int
chunksLength = foldl' (\s c -> s + c.end `minusPtr` c.begin) 0
{-# INLINE chunksLength #-}

catChunks :: [Chunk] -> IO ByteString
catChunks chks = B.create (chunksLength chks) $ \p ->
  void
    $ foldlM
      ( \q c -> do
          let n = c.end `minusPtr` c.begin
          copyBytes q c.begin n
          free c.begin
          pure (q `plusPtr` n)
      )
      p
    $ reverse chks
{-# INLINE catChunks #-}

evalPutIO :: Put a -> (a -> NonEmpty Chunk -> IO (a, b)) -> IO (a, b)
evalPutIO p chunkConsumer = do
  k <- newChunk 0
  chunks <- newIORef (k :| [])
  curEnd <- newIORef k.end
  p' :!: r <- allocaBytes 8 $ \tmp ->
    p.unPut PutEnv {chunks, end = curEnd, tmp} k.begin
  cs <- readIORef chunks
  case cs of
    (x :| xs) -> do
      let !x' = (\Chunk {..} -> Chunk {end = p', ..}) x
      chunkConsumer r (x' :| xs)
{-# INLINE evalPutIO #-}

evalPutStrictIO :: Put a -> IO (a, ByteString)
evalPutStrictIO p = evalPutIO p chunkHandler
 where
  chunkHandler r cs = do
    s <- case cs of
      (x :| []) -> singleChunk x
      (x :| xs) -> catChunks (x : xs)
    pure (r, s)
  singleChunk Chunk {..} = do
    case end `minusPtr` begin of
      0 -> do
        free begin
        pure B.empty
      newSize -> do
        newPtr <- reallocBytes begin newSize
        foreignNewPtr <- newForeignPtr finalizerFree newPtr
        pure $ B.BS foreignNewPtr newSize
{-# INLINE evalPutStrictIO #-}

evalPutLazyIO :: Put a -> IO (a, BL.ByteString)
evalPutLazyIO p = evalPutIO p chunkHandler
 where
  chunkHandler r cs = do
    s <- case cs of
      (x :| xs) -> foldlM makeLBSChunk BL.Empty (x : xs)
    pure (r, s)
  makeLBSChunk :: BL.ByteString -> Chunk -> IO BL.ByteString
  makeLBSChunk lbsTail Chunk {..} = do
    case end `minusPtr` begin of
      0 -> do
        free begin
        pure lbsTail
      newSize -> do
        newPtr <- reallocBytes begin newSize
        foreignNewPtr <- newForeignPtr finalizerFree newPtr
        let strictChunk = B.BS foreignNewPtr newSize
        pure $ BL.Chunk strictChunk lbsTail
{-# INLINE evalPutLazyIO #-}

evalPut :: Put a -> (a, ByteString)
evalPut p = unsafePerformIO $ evalPutStrictIO p
{-# NOINLINE evalPut #-}

evalPutLazy :: Put a -> (a, BL.ByteString)
evalPutLazy p = unsafePerformIO $ evalPutLazyIO p
{-# NOINLINE evalPutLazy #-}