wireform-core-0.2.0.0: src/Wireform/Parser/Adapter.hs
{-# LANGUAGE BlockArguments #-}
module Wireform.Parser.Adapter (
-- * ChunkParser type
ChunkParser (..),
ChunkStep (..),
ChunkFinal (..),
ChunkParseError (..),
-- * Running
runChunked,
runChunkedLoop,
-- * Chunk mode
ChunkMode (..),
) where
import Data.Bits ((.&.))
import Data.ByteString (ByteString)
import Data.ByteString.Internal qualified as BSI
import Data.Word (Word64, Word8)
import Foreign.ForeignPtr (newForeignPtr_)
import Foreign.Marshal.Utils (copyBytes)
import Foreign.Ptr (Ptr, plusPtr)
import Wireform.Parser.Driver (LoopControl (..))
import Wireform.Ring.Internal (ringBase, ringMask, ringSize)
import Wireform.Transport
------------------------------------------------------------------------
-- Types
------------------------------------------------------------------------
{- | A chunked-feeding parser. The lingua franca for incremental
parser libraries (attoparsec, binary, cereal, etc.).
-}
data ChunkParser a = ChunkParser
{ stepChunk :: !(ByteString -> ChunkStep a)
-- ^ Feed non-empty input.
, stepEof :: !(ChunkFinal a)
-- ^ Signal end of input.
}
data ChunkStep a
= -- | Consumed N bytes; needs more.
ChunkConsumed {-# UNPACK #-} !Int !(ChunkParser a)
| -- | Done; consumed N bytes of the fed chunk (rest is leftover).
ChunkDone !a {-# UNPACK #-} !Int
| ChunkFailed !ChunkParseError
data ChunkFinal a
= FinalDone !a
| FinalFailed !ChunkParseError
data ChunkParseError = ChunkParseError
{ chunkErrorMessage :: !String
, chunkErrorContext :: ![String]
, chunkErrorPosition :: {-# UNPACK #-} !Word64
}
deriving stock (Show)
------------------------------------------------------------------------
-- Chunk mode
------------------------------------------------------------------------
data ChunkMode
= -- | Allocate a fresh 'ByteString' per chunk (safe, default).
ChunkCopy
| {- | Reference ring memory directly. Caller must not retain the
'ByteString' past the @stepChunk@ return.
-}
ChunkZeroCopy
deriving stock (Eq, Show)
------------------------------------------------------------------------
-- Running
------------------------------------------------------------------------
-- | Run a 'ChunkParser' against a transport.
runChunked
:: ReceiveTransport
-> ChunkMode
-> ChunkParser a
-> IO (Either ChunkParseError a)
runChunked t mode cp0 = do
startPos <- receiveLoadHead t
loop cp0 startPos startPos startPos
where
ring = receiveRing t
base = ringBase ring
msk = ringMask ring
sz = ringSize ring
advanceThreshold = sz `div` 4
loop cp parserStart parserPos lastTailAdvance = do
h <- receiveLoadHead t
if h > parserPos
then do
let !chunkLen = fromIntegral (min (h - parserPos) (fromIntegral sz))
!off = fromIntegral parserPos .&. msk
!ptr = base `plusPtr` off
chunk <- makeChunk mode ptr chunkLen
case stepChunk cp chunk of
ChunkDone a consumed -> do
let !newPos = parserPos + fromIntegral consumed
receiveAdvanceTail t newPos
pure (Right a)
ChunkConsumed consumed cp' -> do
let !newPos = parserPos + fromIntegral consumed
if newPos - lastTailAdvance >= fromIntegral advanceThreshold
then do
receiveAdvanceTail t newPos
loop cp' parserStart newPos newPos
else loop cp' parserStart newPos lastTailAdvance
ChunkFailed e -> pure (Left e)
else do
r <- receiveWaitData t parserPos
case r of
ReceiveMoreData _ ->
loop cp parserStart parserPos lastTailAdvance
ReceiveEndOfInput ->
case stepEof cp of
FinalDone a
| parserPos == parserStart -> pure (Right a)
| otherwise -> pure (Right a)
FinalFailed e -> pure (Left e)
ReceiveFailed exc ->
pure (Left (ChunkParseError (show exc) [] parserPos))
-- | Loop variant for repeated parsing.
runChunkedLoop
:: ReceiveTransport
-> ChunkMode
-> ChunkParser a
-> (a -> IO LoopControl)
-> IO (Either ChunkParseError ())
runChunkedLoop t mode mkParser k = loop
where
loop = do
r <- runChunked t mode mkParser
case r of
Right a -> do
ctl <- k a
case ctl of
Continue -> loop
Stop -> pure (Right ())
Left e -> pure (Left e)
------------------------------------------------------------------------
-- Helpers
------------------------------------------------------------------------
makeChunk :: ChunkMode -> Ptr Word8 -> Int -> IO ByteString
makeChunk ChunkCopy ptr len =
BSI.create len \dst -> copyBytes dst ptr len
makeChunk ChunkZeroCopy ptr len = do
fptr <- newForeignPtr_ ptr
pure (BSI.fromForeignPtr fptr 0 len)