packages feed

wireform-core-0.2.0.0: src/Wireform/Parser/Internal.hs

{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE ExplicitNamespaces #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE MagicHash #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE UnboxedSums #-}
{-# LANGUAGE UnboxedTuples #-}

{- | Core parser types, mirroring flatparse's representation for
identical inner-loop performance.

The parser threads @Addr#@ pointers and @State# RealWorld@ directly
(no IO wrapper, no boxed Ptr).  The result is an unboxed sum
matching flatparse's @Res#@.

Streaming suspension via @control0#@ is confined to 'ensureNSlow',
which is @NOINLINE@ and never fires on the hot path for whole-input
parsing.
-}
module Wireform.Parser.Internal (
  -- * Parser type
  Parser (..),

  -- * Mode types and class
  Pure,
  Stream,
  ParserMode (..),

  -- * Result types
  type Res#,
  type StRes#,
  pattern OK#,
  pattern Fail#,
  pattern Err#,

  -- * Step / Resume (driver protocol)
  Step (..),
  Resume (..),

  -- * Parser environment
  ParserEnv (..),
  curToPos,
  curToPosIO,

  -- * Suspension / checkpoint primitives
  ensureN#,
  ensureNSlow,
  checkpoint,

  -- * Tag storage
  tagToAny,
  pePromptTag,

  -- * End-pointer access
  readEnd#,
  writeEnd#,
  readEnd,
  writeEnd,

  -- * Anchor access
  writeAnchor#,
  writeAnchor,
) where

import Control.Applicative (Alternative (..))
import Control.Monad (MonadPlus)
import Data.Kind (Type)
import Data.Word (Word64, Word8)
import GHC.Exts
import GHC.ForeignPtr (ForeignPtrContents (..))
import GHC.IO (IO (..))
import GHC.Word (Word64 (..))


------------------------------------------------------------------------
-- Result types — unboxed, identical to flatparse
------------------------------------------------------------------------

{- | Parser result. We drop the State# token from the result type
since it's threaded implicitly by IO.  The parser function signature
still takes State# and must thread it, but Res# is just the
unboxed sum of outcomes.
-}
type Res# e a =
  (#
    (# a, Addr# #) |
    (# #) |
    (# e #)
  #)


pattern OK# :: a -> Addr# -> Res# e a
pattern OK# a s = (# (# a, s #) | | #)


pattern Fail# :: Res# e a
pattern Fail# = (# | (# #) | #)


pattern Err# :: e -> Res# e a
pattern Err# e = (# | | (# e #) #)


{-# COMPLETE OK#, Fail#, Err# #-}


------------------------------------------------------------------------
-- Parser mode (compile-time dispatch for ensure slow path)
------------------------------------------------------------------------

{- | Whole-input mode.  When bounds check fails, just return 'Fail#'.
Identical codegen to flatparse — zero suspension overhead.
-}
data Pure


{- | Streaming mode.  When bounds check fails, suspend via @control0#@
to wait for more data from the transport.
-}
data Stream


{- | Type class dispatching the ensure slow path.
Resolved at compile time via specialization.
-}
class ParserMode (m :: Type) where
  onEnsureFail
    :: ParserEnv
    -> Addr#
    -> Addr#
    -> Int#
    -> State# RealWorld
    -> StRes# e ()


  {- | Mode-polymorphic checkpoint.  In streaming mode, advances the
  transport's tail to the parser's current position (freeing ring
  space behind it).  In whole-input mode, a no-op — there is no
  ring to refill.

  Used by 'Wireform.Parser.takeBs' / 'takeBsCopy' to drain reads
  larger than the ring without deadlocking.
  -}
  modeCheckpoint :: Parser m e ()


instance ParserMode Pure where
  onEnsureFail _env _eob _s _n st = (# st, Fail# #)
  {-# INLINE onEnsureFail #-}
  modeCheckpoint = Parser \_env _eob s st -> (# st, OK# () s #)
  {-# INLINE modeCheckpoint #-}


instance ParserMode Stream where
  onEnsureFail = ensureNSlow
  {-# INLINE onEnsureFail #-}
  modeCheckpoint = checkpoint
  {-# INLINE modeCheckpoint #-}


------------------------------------------------------------------------
-- Parser environment (carries mutable end pointer for streaming)
------------------------------------------------------------------------

{- | Shared context for a parse run.

Three mutable cells (pointer-to-Ptr fields) carry state that the
driver updates between parser resumptions:

  * 'peEndPtr'    — the producer's head pointer, refreshed after
                    a 'StepSuspend' / 'StepCheckpoint' round-trip.
  * 'peAnchorPos' — the absolute byte offset of the parser's most
                    recent /anchor/.  Initially the parse run's
                    @startPos@; updated whenever the driver wraps
                    'cur' back into the first mapping (currently
                    on every 'StepCheckpoint' / 'StepSuspend' resume).
  * 'peAnchorCur' — the cur pointer that pairs with 'peAnchorPos'.

Logical position is computed as @anchorPos + (cur - anchorCur)@
(see 'curToPos').  Re-anchoring on cur-resets keeps that identity
correct even when a single parse run drains more bytes than the
ring buffer can hold (the parser's logical 'cur' advances past
@anchorCur + ringSize@, the driver wraps it back, and the anchor
shifts forward by the wrap amount).

== Concurrency model

These cells are mutable but require neither locks nor memory
barriers, because the writer (the driver) and the reader (the
parser) never run at the same time:

  * Parser writes are confined to 'resumeContinue' closures, which
    run /while the parser is suspended/ (between 'control0#'
    reifying the continuation and 'prompt#' re-entering it).
  * The parser cannot observe a cell mid-update because it is not
    executing at all during the update.
  * The update of all three cells happens-before the @prompt#@
    that resumes the parser, by virtue of being sequenced in the
    same @IO@ action.

So the three writes look atomic to the parser even though they are
three separate stores: suspension is the synchronization
primitive.  No GHC fence is needed (single-threaded use; the
transport contract requires that producer and consumer share a
thread for a given parse run).
-}
data ParserEnv = ParserEnv
  { peEndPtr :: {-# UNPACK #-} !(Ptr (Ptr Word8))
  -- ^ Mutable end pointer (single deref to read, updated on resume).
  , peBaseAddr :: {-# UNPACK #-} !(Ptr Word8)
  -- ^ Ring base address.
  , peMask :: {-# UNPACK #-} !Int
  -- ^ @ringSize - 1@ (or 'maxBound' in whole-input mode).
  , peAnchorPos :: {-# UNPACK #-} !(Ptr Word64)
  {- ^ Mutable anchor: absolute byte position corresponding to
  'peAnchorCur'.
  -}
  , peAnchorCur :: {-# UNPACK #-} !(Ptr (Ptr Word8))
  -- ^ Mutable anchor: cur address corresponding to 'peAnchorPos'.
  , peBackingFp :: !ForeignPtrContents
  -- ^ Keeps the backing memory alive for zero-copy slices.
  , peTag :: Any
  {- ^ PromptTag# stored as Any via unsafeCoerce#.
  Only meaningful for streaming mode.
  -}
  }


-- | Recover the typed PromptTag# from the env. Only call from ensureNSlow.
{-# INLINE pePromptTag #-}
pePromptTag :: forall e r. ParserEnv -> PromptTag# (Step e r)
pePromptTag env = unsafeCoerce# (peTag env)


-- | Store a PromptTag# into an Any-typed field.
{-# INLINE tagToAny #-}
tagToAny :: PromptTag# a -> Any
tagToAny t = unsafeCoerce# t


{- | Convert a cur address to an absolute logical position.  Reads
the mutable anchor cells, so it must be threaded through 'State#'.
-}
{-# INLINE curToPos #-}
curToPos
  :: ParserEnv
  -> Addr#
  -> State# RealWorld
  -> (# State# RealWorld, Word64 #)
curToPos env cur s0 =
  let !(Ptr posCell) = peAnchorPos env
      !(Ptr curCell#) = peAnchorCur env
  in case readWord64OffAddr# posCell 0# s0 of
       (# s1, ap# #) ->
         case readAddrOffAddr# curCell# 0# s1 of
           (# s2, ac# #) ->
             let !off = I# (minusAddr# cur ac#)
                 !pos = W64# ap# + fromIntegral off
             in (# s2, pos #)


-- | Boxed wrapper for IO contexts.
curToPosIO :: ParserEnv -> Ptr Word8 -> IO Word64
curToPosIO env (Ptr cur#) = IO \s -> curToPos env cur# s
{-# INLINE curToPosIO #-}


{- | Write a fresh @(anchorPos, anchorCur)@ pair.  Called by the driver
whenever it wraps the parser's cur back into the first mapping
(StepCheckpoint resume, StepSuspend resume).
-}
writeAnchor#
  :: ParserEnv
  -> Word64
  -> Addr#
  -> State# RealWorld
  -> State# RealWorld
writeAnchor# env (W64# pos#) cur# s0 =
  let !(Ptr posCell) = peAnchorPos env
      !(Ptr curCell#) = peAnchorCur env
      !s1 = writeWord64OffAddr# posCell 0# pos# s0
      !s2 = writeAddrOffAddr# curCell# 0# cur# s1
  in s2
{-# INLINE writeAnchor# #-}


writeAnchor :: ParserEnv -> Word64 -> Ptr Word8 -> IO ()
writeAnchor env pos (Ptr cur#) = IO \s -> (# writeAnchor# env pos cur# s, () #)
{-# INLINE writeAnchor #-}


{-# INLINE readEnd# #-}
readEnd# :: ParserEnv -> State# RealWorld -> (# State# RealWorld, Addr# #)
readEnd# env s =
  let !(Ptr p) = peEndPtr env
  in case readAddrOffAddr# p 0# s of
       (# s', a #) -> (# s', a #)


{-# INLINE writeEnd# #-}
writeEnd# :: ParserEnv -> Addr# -> State# RealWorld -> State# RealWorld
writeEnd# env val s =
  let !(Ptr p) = peEndPtr env
  in writeAddrOffAddr# p 0# val s


-- Boxed wrappers for driver code
readEnd :: ParserEnv -> IO (Ptr Word8)
readEnd env = IO \s -> case readEnd# env s of
  (# s', a #) -> (# s', Ptr a #)
{-# INLINE readEnd #-}


writeEnd :: ParserEnv -> Ptr Word8 -> IO ()
writeEnd env (Ptr a) = IO \s -> (# writeEnd# env a s, () #)
{-# INLINE writeEnd #-}


------------------------------------------------------------------------
-- Step / Resume (driver protocol)
------------------------------------------------------------------------

data Step e r
  = StepDone {-# UNPACK #-} !Word64 r
  | StepFail {-# UNPACK #-} !Word64
  | StepErr {-# UNPACK #-} !Word64 e
  | StepSuspend
      {-# UNPACK #-} !Word64
      {-# UNPACK #-} !Int
      !(Resume e r)
  | StepCheckpoint {-# UNPACK #-} !Word64 !(Resume e r)


data Resume e r = Resume
  { resumeContinue :: !(Ptr Word8 -> Ptr Word8 -> IO (Step e r))
  , resumeEof :: !(IO (Step e r))
  }


------------------------------------------------------------------------
-- The Parser newtype — flatparse-shaped
------------------------------------------------------------------------

{- | A parser consuming bytes from @(cur, eob)@ pointers.

The representation mirrors flatparse exactly:
@ForeignPtrContents -> Addr# (eob) -> Addr# (cur) -> State# -> Res#@

The @ParserEnv@ and @PromptTag#@ are additional parameters for
streaming support.  For whole-input parsing ('parseByteString'),
the PromptTag# is allocated but @ensureNSlow@ is never reached.
| Parser result with state token.
-}
type StRes# e a = (# State# RealWorld, Res# e a #)


{- | The parser type.  Parameterized by mode @m@ ('Pure' or 'Stream'),
error type @e@, and result type @a@.

Four args on the hot path: env, eob, cur, state.  The mode @m@ is
phantom — it controls which 'ParserMode' instance is used for
bounds-check failures, resolved at compile time.
-}
newtype Parser (m :: Type) e a = Parser
  { runParser#
      :: ParserEnv
      -> Addr# -- eob (end of buffer / current end)
      -> Addr# -- cur (current position)
      -> State# RealWorld
      -> StRes# e a
  }


instance Functor (Parser m e) where
  fmap f (Parser g) = Parser \env eob s st ->
    case g env eob s st of
      (# st', OK# a s' #) -> let !b = f a in (# st', OK# b s' #)
      (# st', x #) -> (# st', unsafeCoerce# x #)
  {-# INLINE fmap #-}


  a' <$ Parser g = Parser \env eob s st ->
    case g env eob s st of
      (# st', OK# _ s' #) -> (# st', OK# a' s' #)
      (# st', x #) -> (# st', unsafeCoerce# x #)
  {-# INLINE (<$) #-}


instance Applicative (Parser m e) where
  pure !a = Parser \_ _ s st -> (# st, OK# a s #)
  {-# INLINE pure #-}


  Parser ff <*> Parser fa = Parser \env eob s st ->
    case ff env eob s st of
      (# st', OK# f s' #) -> case fa env eob s' st' of
        (# st'', OK# a s'' #) -> let !b = f a in (# st'', OK# b s'' #)
        (# st'', x #) -> (# st'', unsafeCoerce# x #)
      (# st', x #) -> (# st', unsafeCoerce# x #)
  {-# INLINE (<*>) #-}


  Parser fa *> Parser fb = Parser \env eob s st ->
    case fa env eob s st of
      (# st', OK# _ s' #) -> fb env eob s' st'
      (# st', x #) -> (# st', unsafeCoerce# x #)
  {-# INLINE (*>) #-}


  Parser fa <* Parser fb = Parser \env eob s st ->
    case fa env eob s st of
      (# st', OK# a s' #) -> case fb env eob s' st' of
        (# st'', OK# _ s'' #) -> (# st'', OK# a s'' #)
        (# st'', x #) -> (# st'', unsafeCoerce# x #)
      (# st', x #) -> (# st', unsafeCoerce# x #)
  {-# INLINE (<*) #-}


instance Monad (Parser m e) where
  Parser fa >>= f = Parser \env eob s st ->
    case fa env eob s st of
      (# st', OK# a s' #) -> runParser# (f a) env eob s' st'
      (# st', x #) -> (# st', unsafeCoerce# x #)
  {-# INLINE (>>=) #-}


  (>>) = (*>)
  {-# INLINE (>>) #-}


instance MonadFail (Parser m e) where
  fail _ = Parser \_ _ _ st -> (# st, Fail# #)
  {-# INLINE fail #-}


instance ParserMode m => Alternative (Parser m e) where
  empty = Parser \_ _ _ st -> (# st, Fail# #)
  {-# INLINE empty #-}
  (Parser f) <|> (Parser g) = Parser \env eob s st ->
    case f env eob s st of
      (# st', Fail# #) -> g env eob s st'
      x -> x
  {-# INLINE (<|>) #-}


instance ParserMode m => MonadPlus (Parser m e)


------------------------------------------------------------------------
-- ensureN#: bounds check + suspension
------------------------------------------------------------------------

{- | Require @n#@ bytes available from @cur@.

Fast path: single register comparison (identical to flatparse).
Semi-fast: re-read 'peEndPtr' to catch stale @eob@ after a
prior streaming resume; no function-call overhead.
Slow path: suspends to the driver via @control0#@.
-}
ensureN# :: forall m e. ParserMode m => Int# -> Parser m e ()
ensureN# n# = Parser \env eob s st ->
  case n# <=# minusAddr# eob s of
    1# -> (# st, OK# () s #)
    _ -> case readEnd# env st of
      (# st', eob' #) -> case n# <=# minusAddr# eob' s of
        1# -> (# st', OK# () s #)
        _ -> onEnsureFail @m env eob' s n# st'
{-# INLINE ensureN# #-}


{- | Streaming slow path.

Before suspending via @control0#@, re-read the mutable end pointer.
After a prior resume, @>>=@ threads the stale @eob@ that was captured
in its closure, so the fast-path comparison in 'ensureN#' fails even
though new data is already available in the ring.  The re-read here
catches that case and avoids a redundant suspension round-trip.
-}
ensureNSlow
  :: ParserEnv
  -> Addr#
  -> Addr#
  -> Int#
  -> State# RealWorld
  -> StRes# e ()
ensureNSlow env _eob s n# st =
  case readEnd# env st of
    (# st0, eob' #) ->
      case n# <=# minusAddr# eob' s of
        1# -> (# st0, OK# () s #)
        _ ->
          let tag :: PromptTag# (Step Any Any)
              tag = unsafeCoerce# (peTag env)
          in case curToPos env s st0 of
               (# st0', pos #) ->
                 let !needed = I# n#
                 in control0#
                      tag
                      ( \k st1 ->
                          let resume =
                                Resume
                                  { resumeContinue = \(Ptr newCur) (Ptr newEnd) ->
                                      IO \st2 ->
                                        -- The driver always wraps newCur back
                                        -- into the first mapping; re-anchor
                                        -- so 'curToPos' stays correct after
                                        -- the wrap.  Both stores are safe
                                        -- without a fence: the parser is
                                        -- suspended (between this 'control0#'
                                        -- and the 'prompt#' below) while
                                        -- they execute.
                                        let st3 = writeEnd# env newEnd st2
                                            st4 = writeAnchor# env pos newCur st3
                                        in prompt# tag (\st5 -> k (\st6 -> (# st6, OK# () newCur #)) st5) st4
                                  , resumeEof =
                                      IO \st2 -> prompt# tag (\st3 -> k (\st4 -> (# st4, Fail# #)) st3) st2
                                  }
                          in (# st1, StepSuspend pos needed resume #)
                      )
                      st0'
{-# NOINLINE ensureNSlow #-}


------------------------------------------------------------------------
-- checkpoint
------------------------------------------------------------------------

-- | Checkpoint is only meaningful in streaming mode.
checkpoint :: Parser Stream e ()
checkpoint = Parser \env _ s st0 ->
  let tag :: PromptTag# (Step Any Any)
      tag = unsafeCoerce# (peTag env)
  in case curToPos env s st0 of
       (# st0', pos #) ->
         control0#
           tag
           ( \k st1 ->
               let resume =
                     Resume
                       { resumeContinue = \(Ptr newCur) (Ptr newEnd) ->
                           IO \st2 ->
                             -- Re-anchor: the driver has wrapped newCur
                             -- back to @base + (pos .&. mask)@ which
                             -- decouples the cur pointer from the original
                             -- anchor.  Without re-anchoring, 'curToPos'
                             -- would compute the wrong absolute position
                             -- on the next call.
                             --
                             -- These two stores (end pointer + anchor
                             -- pair) are not behind any lock or fence:
                             -- they run inside the resume body, which is
                             -- only invoked while the parser is suspended
                             -- between 'control0#' and the matching
                             -- 'prompt#' below.  The parser cannot observe
                             -- a partial update because it is not running
                             -- at all during the update — suspension is
                             -- the synchronization mechanism.
                             let st3 = writeEnd# env newEnd st2
                                 st4 = writeAnchor# env pos newCur st3
                             in prompt# tag (\st5 -> k (\st6 -> (# st6, OK# () newCur #)) st5) st4
                       , resumeEof =
                           IO \st2 -> prompt# tag (\st3 -> k (\st4 -> (# st4, Fail# #)) st3) st2
                       }
               in (# st1, StepCheckpoint pos resume #)
           )
           st0'