wireform-core 0.1.0.0 → 0.2.0.0
raw patch · 44 files changed
+8136/−205 lines, 44 filesdep +QuickCheckdep +containersdep +criteriondep ~basedep ~bytestringdep ~ghc-primPVP ok
version bump matches the API change (PVP)
Dependencies added: QuickCheck, containers, criterion, flatparse, sydtest, template-haskell, wireform-core
Dependency ranges changed: base, bytestring, ghc-prim
API changes (from Hackage documentation)
- Wireform.Builder: byteString :: ByteString -> Builder
- Wireform.Builder: byteStringCopy :: ByteString -> Builder
- Wireform.Builder: byteStringInsert :: ByteString -> Builder
- Wireform.Builder: byteStringThreshold :: Int -> ByteString -> Builder
- Wireform.Builder: data Builder
- Wireform.Builder: hPutBuilder :: Handle -> Builder -> IO ()
- Wireform.Builder: hPutBuilderLen :: Handle -> Builder -> IO Int
- Wireform.Builder: hPutBuilderWith :: Handle -> Int -> Int -> Builder -> IO Int
- Wireform.Builder: primBounded :: BoundedPrim a -> a -> Builder
- Wireform.Builder: primFixed :: FixedPrim a -> a -> Builder
- Wireform.Builder: rebuild :: Builder -> Builder
- Wireform.Builder: toLazyByteString :: Builder -> ByteString
- Wireform.Builder: toStrictByteString :: Builder -> ByteString
+ Wireform.Base64: decodeBase64 :: ByteString -> Maybe ByteString
+ Wireform.Base64: decodeBase64MaxLength :: Int -> Int
+ Wireform.Base64: encodeBase64 :: ByteString -> ByteString
+ Wireform.Base64: encodeBase64Length :: Int -> Int
+ Wireform.Builder.FastBuilder: RingSink :: !IORef RingSinkState -> DataSink
+ Wireform.Builder.FastBuilder: RingSinkState :: {-# UNPACK #-} !Word64 -> {-# UNPACK #-} !Word64 -> {-# UNPACK #-} !Ptr Word8 -> {-# UNPACK #-} !Int -> {-# UNPACK #-} !Int -> {-# UNPACK #-} !Int -> !Word64 -> IO () -> !Word64 -> IO () -> !IO Word64 -> RingSinkState
+ Wireform.Builder.FastBuilder: [rsBase] :: RingSinkState -> {-# UNPACK #-} !Ptr Word8
+ Wireform.Builder.FastBuilder: [rsChunkSize] :: RingSinkState -> {-# UNPACK #-} !Int
+ Wireform.Builder.FastBuilder: [rsEnsureRoom] :: RingSinkState -> !Word64 -> IO ()
+ Wireform.Builder.FastBuilder: [rsHead] :: RingSinkState -> {-# UNPACK #-} !Word64
+ Wireform.Builder.FastBuilder: [rsLoadTail] :: RingSinkState -> !IO Word64
+ Wireform.Builder.FastBuilder: [rsMask] :: RingSinkState -> {-# UNPACK #-} !Int
+ Wireform.Builder.FastBuilder: [rsPublishHead] :: RingSinkState -> !Word64 -> IO ()
+ Wireform.Builder.FastBuilder: [rsPublished] :: RingSinkState -> {-# UNPACK #-} !Word64
+ Wireform.Builder.FastBuilder: [rsRingSize] :: RingSinkState -> {-# UNPACK #-} !Int
+ Wireform.Builder.FastBuilder: data RingSinkState
+ Wireform.Builder.FastBuilder: finalizeRingSink :: IORef RingSinkState -> Ptr Word8 -> IO Word64
+ Wireform.FFI: fastRandomWord64 :: IO Word64
+ Wireform.FFI: xorRepeatingKey :: Ptr Word8 -> Int -> Word32 -> IO ()
+ Wireform.FFI: xorRepeatingKeyBS :: ByteString -> Word32 -> IO ()
+ Wireform.Parser: (<|>) :: Parser m e a -> Parser m e a -> Parser m e a
+ Wireform.Parser: Pos :: Word64 -> Pos
+ Wireform.Parser: Span :: !Pos -> !Pos -> Span
+ Wireform.Parser: [unPos] :: Pos -> Word64
+ Wireform.Parser: anyAsciiChar :: ParserMode m => Parser m e Char
+ Wireform.Parser: anyAsciiDecimalInt :: ParserMode m => Parser m e Int
+ Wireform.Parser: anyAsciiDecimalInteger :: ParserMode m => Parser m e Integer
+ Wireform.Parser: anyAsciiDecimalWord :: ParserMode m => Parser m e Word
+ Wireform.Parser: anyAsciiHexInt :: ParserMode m => Parser m e Int
+ Wireform.Parser: anyAsciiHexWord :: ParserMode m => Parser m e Word
+ Wireform.Parser: anyChar :: ParserMode m => Parser m e Char
+ Wireform.Parser: anyChar_ :: ParserMode m => Parser m e ()
+ Wireform.Parser: anyDoublebe :: ParserMode m => Parser m e Double
+ Wireform.Parser: anyDoublele :: ParserMode m => Parser m e Double
+ Wireform.Parser: anyFloatbe :: ParserMode m => Parser m e Float
+ Wireform.Parser: anyFloatle :: ParserMode m => Parser m e Float
+ Wireform.Parser: anyInt16 :: ParserMode m => Parser m e Int16
+ Wireform.Parser: anyInt16be :: ParserMode m => Parser m e Int16
+ Wireform.Parser: anyInt16le :: ParserMode m => Parser m e Int16
+ Wireform.Parser: anyInt32 :: ParserMode m => Parser m e Int32
+ Wireform.Parser: anyInt32be :: ParserMode m => Parser m e Int32
+ Wireform.Parser: anyInt32le :: ParserMode m => Parser m e Int32
+ Wireform.Parser: anyInt64 :: ParserMode m => Parser m e Int64
+ Wireform.Parser: anyInt64be :: ParserMode m => Parser m e Int64
+ Wireform.Parser: anyInt64le :: ParserMode m => Parser m e Int64
+ Wireform.Parser: anyInt8 :: ParserMode m => Parser m e Int8
+ Wireform.Parser: anyWord16 :: ParserMode m => Parser m e Word16
+ Wireform.Parser: anyWord16_ :: ParserMode m => Parser m e ()
+ Wireform.Parser: anyWord16be :: ParserMode m => Parser m e Word16
+ Wireform.Parser: anyWord16le :: ParserMode m => Parser m e Word16
+ Wireform.Parser: anyWord32 :: ParserMode m => Parser m e Word32
+ Wireform.Parser: anyWord32_ :: ParserMode m => Parser m e ()
+ Wireform.Parser: anyWord32be :: ParserMode m => Parser m e Word32
+ Wireform.Parser: anyWord32le :: ParserMode m => Parser m e Word32
+ Wireform.Parser: anyWord64 :: ParserMode m => Parser m e Word64
+ Wireform.Parser: anyWord64_ :: ParserMode m => Parser m e ()
+ Wireform.Parser: anyWord64be :: ParserMode m => Parser m e Word64
+ Wireform.Parser: anyWord64le :: ParserMode m => Parser m e Word64
+ Wireform.Parser: anyWord8 :: ParserMode m => Parser m e Word8
+ Wireform.Parser: anyWord8_ :: ParserMode m => Parser m e ()
+ Wireform.Parser: atEnd :: ParserMode m => Parser m e Bool
+ Wireform.Parser: branch :: Parser m e a -> Parser m e b -> Parser m e b -> Parser m e b
+ Wireform.Parser: byteString :: ByteString -> Parser m e ()
+ Wireform.Parser: byteStringOf :: Parser m e a -> Parser m e ByteString
+ Wireform.Parser: chainl :: (b -> a -> b) -> Parser m e b -> Parser m e a -> Parser m e b
+ Wireform.Parser: chainr :: (a -> b -> b) -> Parser m e a -> Parser m e b -> Parser m e b
+ Wireform.Parser: checkpoint :: Parser Stream e ()
+ Wireform.Parser: cut :: Parser m e a -> e -> Parser m e a
+ Wireform.Parser: cutting :: Parser m e a -> e -> (e -> e -> e) -> Parser m e a
+ Wireform.Parser: data Mark
+ Wireform.Parser: data Parser m e a
+ Wireform.Parser: data Span
+ Wireform.Parser: empty :: Parser m e a
+ Wireform.Parser: ensureN# :: ParserMode m => Int# -> Parser m e ()
+ Wireform.Parser: ensureNOrEof :: ParserMode m => Int -> e -> Parser m e ()
+ Wireform.Parser: eof :: ParserMode m => Parser m e ()
+ Wireform.Parser: err :: e -> Parser m e a
+ Wireform.Parser: fails :: Parser m e a -> Parser m e ()
+ Wireform.Parser: fusedSatisfy :: ParserMode m => (Char -> Bool) -> (Word8 -> Bool) -> Parser m e Char
+ Wireform.Parser: getPos :: Parser m e Pos
+ Wireform.Parser: inSpan :: Span -> Parser m e a -> Parser m e a
+ Wireform.Parser: infixr 6 <|>
+ Wireform.Parser: isDigit :: Char -> Bool
+ Wireform.Parser: isGreekLetter :: Char -> Bool
+ Wireform.Parser: isLatinLetter :: Char -> Bool
+ Wireform.Parser: isolate :: ParserMode m => Int -> Parser m e a -> Parser m e a
+ Wireform.Parser: lookahead :: Parser m e a -> Parser m e a
+ Wireform.Parser: many :: Parser m e a -> Parser m e [a]
+ Wireform.Parser: many_ :: Parser m e a -> Parser m e ()
+ Wireform.Parser: mark :: Parser m e Mark
+ Wireform.Parser: newtype Pos
+ Wireform.Parser: notFollowedBy :: Parser m e a -> Parser m e ()
+ Wireform.Parser: optional :: Parser m e a -> Parser m e (Maybe a)
+ Wireform.Parser: optional_ :: Parser m e a -> Parser m e ()
+ Wireform.Parser: release :: Mark -> Parser m e ()
+ Wireform.Parser: remaining :: Parser m e Int
+ Wireform.Parser: restore :: Mark -> Parser m e ()
+ Wireform.Parser: satisfy :: ParserMode m => (Char -> Bool) -> Parser m e Char
+ Wireform.Parser: satisfyAscii :: ParserMode m => (Char -> Bool) -> Parser m e Char
+ Wireform.Parser: satisfyAscii_ :: ParserMode m => (Char -> Bool) -> Parser m e ()
+ Wireform.Parser: satisfy_ :: ParserMode m => (Char -> Bool) -> Parser m e ()
+ Wireform.Parser: skip :: ParserMode m => Int -> Parser m e ()
+ Wireform.Parser: skipAnyAsciiChar :: ParserMode m => Parser m e ()
+ Wireform.Parser: skipBack :: Int -> Parser m e ()
+ Wireform.Parser: skipMany :: Parser m e a -> Parser m e ()
+ Wireform.Parser: skipSatisfyAscii :: ParserMode m => (Char -> Bool) -> Parser m e ()
+ Wireform.Parser: skipSome :: Parser m e a -> Parser m e ()
+ Wireform.Parser: some :: Parser m e a -> Parser m e [a]
+ Wireform.Parser: some_ :: Parser m e a -> Parser m e ()
+ Wireform.Parser: spanToByteString :: Span -> Parser m e ByteString
+ Wireform.Parser: subPos :: Pos -> Pos -> Int
+ Wireform.Parser: takeBs :: ParserMode m => Int -> Parser m e ByteString
+ Wireform.Parser: takeBsCopy :: ParserMode m => Int -> Parser m e ByteString
+ Wireform.Parser: takeRest :: Parser m e ByteString
+ Wireform.Parser: try :: Parser m e a -> Parser m e a
+ Wireform.Parser: withAnyWord16 :: ParserMode m => (Word16 -> Parser m e r) -> Parser m e r
+ Wireform.Parser: withAnyWord32 :: ParserMode m => (Word32 -> Parser m e r) -> Parser m e r
+ Wireform.Parser: withAnyWord64 :: ParserMode m => (Word64 -> Parser m e r) -> Parser m e r
+ Wireform.Parser: withAnyWord8 :: ParserMode m => (Word8 -> Parser m e r) -> Parser m e r
+ Wireform.Parser: withByteString :: Parser m e a -> (a -> ByteString -> Parser m e b) -> Parser m e b
+ Wireform.Parser: withError :: (e -> Parser m e a) -> Parser m e a -> Parser m e a
+ Wireform.Parser: withOption :: Parser m e a -> (a -> Parser m e b) -> Parser m e b -> Parser m e b
+ Wireform.Parser: withSatisfyAscii :: ParserMode m => (Char -> Bool) -> (Char -> Parser m e r) -> Parser m e r
+ Wireform.Parser: withSpan :: Parser m e a -> (a -> Span -> Parser m e b) -> Parser m e b
+ Wireform.Parser: word8 :: ParserMode m => Word8 -> Parser m e ()
+ Wireform.Parser.Adapter: ChunkConsumed :: {-# UNPACK #-} !Int -> !ChunkParser a -> ChunkStep a
+ Wireform.Parser.Adapter: ChunkCopy :: ChunkMode
+ Wireform.Parser.Adapter: ChunkDone :: !a -> {-# UNPACK #-} !Int -> ChunkStep a
+ Wireform.Parser.Adapter: ChunkFailed :: !ChunkParseError -> ChunkStep a
+ Wireform.Parser.Adapter: ChunkParseError :: !String -> ![String] -> {-# UNPACK #-} !Word64 -> ChunkParseError
+ Wireform.Parser.Adapter: ChunkParser :: !ByteString -> ChunkStep a -> !ChunkFinal a -> ChunkParser a
+ Wireform.Parser.Adapter: ChunkZeroCopy :: ChunkMode
+ Wireform.Parser.Adapter: FinalDone :: !a -> ChunkFinal a
+ Wireform.Parser.Adapter: FinalFailed :: !ChunkParseError -> ChunkFinal a
+ Wireform.Parser.Adapter: [chunkErrorContext] :: ChunkParseError -> ![String]
+ Wireform.Parser.Adapter: [chunkErrorMessage] :: ChunkParseError -> !String
+ Wireform.Parser.Adapter: [chunkErrorPosition] :: ChunkParseError -> {-# UNPACK #-} !Word64
+ Wireform.Parser.Adapter: [stepChunk] :: ChunkParser a -> !ByteString -> ChunkStep a
+ Wireform.Parser.Adapter: [stepEof] :: ChunkParser a -> !ChunkFinal a
+ Wireform.Parser.Adapter: data ChunkFinal a
+ Wireform.Parser.Adapter: data ChunkMode
+ Wireform.Parser.Adapter: data ChunkParseError
+ Wireform.Parser.Adapter: data ChunkParser a
+ Wireform.Parser.Adapter: data ChunkStep a
+ Wireform.Parser.Adapter: instance GHC.Classes.Eq Wireform.Parser.Adapter.ChunkMode
+ Wireform.Parser.Adapter: instance GHC.Show.Show Wireform.Parser.Adapter.ChunkMode
+ Wireform.Parser.Adapter: instance GHC.Show.Show Wireform.Parser.Adapter.ChunkParseError
+ Wireform.Parser.Adapter: runChunked :: ReceiveTransport -> ChunkMode -> ChunkParser a -> IO (Either ChunkParseError a)
+ Wireform.Parser.Adapter: runChunkedLoop :: ReceiveTransport -> ChunkMode -> ChunkParser a -> (a -> IO LoopControl) -> IO (Either ChunkParseError ())
+ Wireform.Parser.Driver: Continue :: LoopControl
+ Wireform.Parser.Driver: IRCleanEof :: InternalResult e a
+ Wireform.Parser.Driver: IRDone :: {-# UNPACK #-} !Word64 -> !a -> InternalResult e a
+ Wireform.Parser.Driver: IRErr :: !Word64 -> !e -> InternalResult e a
+ Wireform.Parser.Driver: IRFail :: !Word64 -> InternalResult e a
+ Wireform.Parser.Driver: IRRingOverflow :: {-# UNPACK #-} !Word64 -> {-# UNPACK #-} !Int -> {-# UNPACK #-} !Int -> InternalResult e a
+ Wireform.Parser.Driver: IRTransportError :: !SomeException -> InternalResult e a
+ Wireform.Parser.Driver: IRUnexpectedEof :: !Word64 -> !Int -> InternalResult e a
+ Wireform.Parser.Driver: Stop :: LoopControl
+ Wireform.Parser.Driver: data InternalResult e a
+ Wireform.Parser.Driver: data LoopControl
+ Wireform.Parser.Driver: instance GHC.Classes.Eq Wireform.Parser.Driver.LoopControl
+ Wireform.Parser.Driver: instance GHC.Show.Show Wireform.Parser.Driver.LoopControl
+ Wireform.Parser.Driver: parseByteString :: Parser Pure e a -> ByteString -> Either (ParseError e) a
+ Wireform.Parser.Driver: runParser :: ReceiveTransport -> Parser Stream e a -> IO (Either (ParseError e) a)
+ Wireform.Parser.Driver: runParserInternal :: ReceiveTransport -> Parser Stream e a -> Word64 -> IO (InternalResult e a)
+ Wireform.Parser.Driver: runParserLoop :: ReceiveTransport -> Parser Stream e a -> (a -> IO LoopControl) -> IO (Either (ParseError e) ())
+ Wireform.Parser.Error: CleanEof :: CleanEof
+ Wireform.Parser.Error: ParseErr :: !Word64 -> !e -> ParseError e
+ Wireform.Parser.Error: ParseFail :: !Word64 -> ParseError e
+ Wireform.Parser.Error: ParseRingOverflow :: !Word64 -> !Int -> !Int -> ParseError e
+ Wireform.Parser.Error: ParseTransportError :: !SomeException -> ParseError e
+ Wireform.Parser.Error: ParseUnexpectedEof :: !Word64 -> !Int -> ParseError e
+ Wireform.Parser.Error: data CleanEof
+ Wireform.Parser.Error: data ParseError e
+ Wireform.Parser.Error: instance GHC.Base.Functor Wireform.Parser.Error.ParseError
+ Wireform.Parser.Error: instance GHC.Exception.Type.Exception Wireform.Parser.Error.CleanEof
+ Wireform.Parser.Error: instance GHC.Show.Show Wireform.Parser.Error.CleanEof
+ Wireform.Parser.Error: instance GHC.Show.Show e => GHC.Show.Show (Wireform.Parser.Error.ParseError e)
+ Wireform.Parser.Internal: Parser :: (ParserEnv -> Addr# -> Addr# -> State# RealWorld -> StRes# e a) -> Parser m e a
+ Wireform.Parser.Internal: ParserEnv :: {-# UNPACK #-} !Ptr (Ptr Word8) -> {-# UNPACK #-} !Ptr Word8 -> {-# UNPACK #-} !Int -> {-# UNPACK #-} !Ptr Word64 -> {-# UNPACK #-} !Ptr (Ptr Word8) -> !ForeignPtrContents -> (Any :: Type) -> ParserEnv
+ Wireform.Parser.Internal: Resume :: !Ptr Word8 -> Ptr Word8 -> IO (Step e r) -> !IO (Step e r) -> Resume e r
+ Wireform.Parser.Internal: StepCheckpoint :: {-# UNPACK #-} !Word64 -> !Resume e r -> Step e r
+ Wireform.Parser.Internal: StepDone :: {-# UNPACK #-} !Word64 -> r -> Step e r
+ Wireform.Parser.Internal: StepErr :: {-# UNPACK #-} !Word64 -> e -> Step e r
+ Wireform.Parser.Internal: StepFail :: {-# UNPACK #-} !Word64 -> Step e r
+ Wireform.Parser.Internal: StepSuspend :: {-# UNPACK #-} !Word64 -> {-# UNPACK #-} !Int -> !Resume e r -> Step e r
+ Wireform.Parser.Internal: [peAnchorCur] :: ParserEnv -> {-# UNPACK #-} !Ptr (Ptr Word8)
+ Wireform.Parser.Internal: [peAnchorPos] :: ParserEnv -> {-# UNPACK #-} !Ptr Word64
+ Wireform.Parser.Internal: [peBackingFp] :: ParserEnv -> !ForeignPtrContents
+ Wireform.Parser.Internal: [peBaseAddr] :: ParserEnv -> {-# UNPACK #-} !Ptr Word8
+ Wireform.Parser.Internal: [peEndPtr] :: ParserEnv -> {-# UNPACK #-} !Ptr (Ptr Word8)
+ Wireform.Parser.Internal: [peMask] :: ParserEnv -> {-# UNPACK #-} !Int
+ Wireform.Parser.Internal: [peTag] :: ParserEnv -> Any :: Type
+ Wireform.Parser.Internal: [resumeContinue] :: Resume e r -> !Ptr Word8 -> Ptr Word8 -> IO (Step e r)
+ Wireform.Parser.Internal: [resumeEof] :: Resume e r -> !IO (Step e r)
+ Wireform.Parser.Internal: [runParser#] :: Parser m e a -> ParserEnv -> Addr# -> Addr# -> State# RealWorld -> StRes# e a
+ Wireform.Parser.Internal: checkpoint :: Parser Stream e ()
+ Wireform.Parser.Internal: class ParserMode m
+ Wireform.Parser.Internal: curToPos :: ParserEnv -> Addr# -> State# RealWorld -> (# State# RealWorld, Word64 #)
+ Wireform.Parser.Internal: curToPosIO :: ParserEnv -> Ptr Word8 -> IO Word64
+ Wireform.Parser.Internal: data ParserEnv
+ Wireform.Parser.Internal: data Pure
+ Wireform.Parser.Internal: data Resume e r
+ Wireform.Parser.Internal: data Step e r
+ Wireform.Parser.Internal: data Stream
+ Wireform.Parser.Internal: ensureN# :: ParserMode m => Int# -> Parser m e ()
+ Wireform.Parser.Internal: ensureNSlow :: ParserEnv -> Addr# -> Addr# -> Int# -> State# RealWorld -> StRes# e ()
+ Wireform.Parser.Internal: instance Control.Monad.Fail.MonadFail (Wireform.Parser.Internal.Parser m e)
+ Wireform.Parser.Internal: instance GHC.Base.Applicative (Wireform.Parser.Internal.Parser m e)
+ Wireform.Parser.Internal: instance GHC.Base.Functor (Wireform.Parser.Internal.Parser m e)
+ Wireform.Parser.Internal: instance GHC.Base.Monad (Wireform.Parser.Internal.Parser m e)
+ Wireform.Parser.Internal: instance Wireform.Parser.Internal.ParserMode Wireform.Parser.Internal.Pure
+ Wireform.Parser.Internal: instance Wireform.Parser.Internal.ParserMode Wireform.Parser.Internal.Stream
+ Wireform.Parser.Internal: instance Wireform.Parser.Internal.ParserMode m => GHC.Base.Alternative (Wireform.Parser.Internal.Parser m e)
+ Wireform.Parser.Internal: instance Wireform.Parser.Internal.ParserMode m => GHC.Base.MonadPlus (Wireform.Parser.Internal.Parser m e)
+ Wireform.Parser.Internal: modeCheckpoint :: ParserMode m => Parser m e ()
+ Wireform.Parser.Internal: newtype Parser m e a
+ Wireform.Parser.Internal: onEnsureFail :: ParserMode m => ParserEnv -> Addr# -> Addr# -> Int# -> State# RealWorld -> StRes# e ()
+ Wireform.Parser.Internal: pattern Err# :: e -> Res# e a
+ Wireform.Parser.Internal: pattern Fail# :: Res# e a
+ Wireform.Parser.Internal: pattern OK# :: a -> Addr# -> Res# e a
+ Wireform.Parser.Internal: pePromptTag :: ParserEnv -> PromptTag# (Step e r)
+ Wireform.Parser.Internal: readEnd :: ParserEnv -> IO (Ptr Word8)
+ Wireform.Parser.Internal: readEnd# :: ParserEnv -> State# RealWorld -> (# State# RealWorld, Addr# #)
+ Wireform.Parser.Internal: tagToAny :: PromptTag# a -> Any :: Type
+ Wireform.Parser.Internal: type Res# e a = (# (# a, Addr# #) | (# #) | (# e #) #)
+ Wireform.Parser.Internal: type StRes# e a = (# State# RealWorld, Res# e a #)
+ Wireform.Parser.Internal: writeAnchor :: ParserEnv -> Word64 -> Ptr Word8 -> IO ()
+ Wireform.Parser.Internal: writeAnchor# :: ParserEnv -> Word64 -> Addr# -> State# RealWorld -> State# RealWorld
+ Wireform.Parser.Internal: writeEnd :: ParserEnv -> Ptr Word8 -> IO ()
+ Wireform.Parser.Internal: writeEnd# :: ParserEnv -> Addr# -> State# RealWorld -> State# RealWorld
+ Wireform.Parser.Mark: Mark :: Word64 -> Mark
+ Wireform.Parser.Mark: [unMark] :: Mark -> Word64
+ Wireform.Parser.Mark: instance GHC.Classes.Eq Wireform.Parser.Mark.Mark
+ Wireform.Parser.Mark: instance GHC.Classes.Ord Wireform.Parser.Mark.Mark
+ Wireform.Parser.Mark: instance GHC.Show.Show Wireform.Parser.Mark.Mark
+ Wireform.Parser.Mark: mark :: Parser m e Mark
+ Wireform.Parser.Mark: newtype Mark
+ Wireform.Parser.Mark: release :: Mark -> Parser m e ()
+ Wireform.Parser.Mark: restore :: Mark -> Parser m e ()
+ Wireform.Parser.Position: Pos :: Word64 -> Pos
+ Wireform.Parser.Position: Span :: !Pos -> !Pos -> Span
+ Wireform.Parser.Position: [unPos] :: Pos -> Word64
+ Wireform.Parser.Position: byteStringOf :: Parser m e a -> Parser m e ByteString
+ Wireform.Parser.Position: data Span
+ Wireform.Parser.Position: getPos :: Parser m e Pos
+ Wireform.Parser.Position: inSpan :: Span -> Parser m e a -> Parser m e a
+ Wireform.Parser.Position: instance GHC.Classes.Eq Wireform.Parser.Position.Pos
+ Wireform.Parser.Position: instance GHC.Classes.Eq Wireform.Parser.Position.Span
+ Wireform.Parser.Position: instance GHC.Classes.Ord Wireform.Parser.Position.Pos
+ Wireform.Parser.Position: instance GHC.Num.Num Wireform.Parser.Position.Pos
+ Wireform.Parser.Position: instance GHC.Show.Show Wireform.Parser.Position.Pos
+ Wireform.Parser.Position: instance GHC.Show.Show Wireform.Parser.Position.Span
+ Wireform.Parser.Position: newtype Pos
+ Wireform.Parser.Position: spanToByteString :: Span -> Parser m e ByteString
+ Wireform.Parser.Position: subPos :: Pos -> Pos -> Int
+ Wireform.Parser.Position: withSpan :: Parser m e a -> (a -> Span -> Parser m e b) -> Parser m e b
+ Wireform.Parser.Stateful: ParserS :: (forall result. () => PromptTag# (Step e result) -> ParserEnv -> r -> MutVar# RealWorld s -> Addr# -> Addr# -> State# RealWorld -> StRes# e a) -> ParserS r s e a
+ Wireform.Parser.Stateful: [runParserS#] :: ParserS r s e a -> forall result. () => PromptTag# (Step e result) -> ParserEnv -> r -> MutVar# RealWorld s -> Addr# -> Addr# -> State# RealWorld -> StRes# e a
+ Wireform.Parser.Stateful: ask :: ParserS r s e r
+ Wireform.Parser.Stateful: asks :: (r -> a) -> ParserS r s e a
+ Wireform.Parser.Stateful: get :: ParserS r s e s
+ Wireform.Parser.Stateful: instance GHC.Base.Applicative (Wireform.Parser.Stateful.ParserS r s e)
+ Wireform.Parser.Stateful: instance GHC.Base.Functor (Wireform.Parser.Stateful.ParserS r s e)
+ Wireform.Parser.Stateful: instance GHC.Base.Monad (Wireform.Parser.Stateful.ParserS r s e)
+ Wireform.Parser.Stateful: liftParser :: Parser m e a -> ParserS r s e a
+ Wireform.Parser.Stateful: local :: (r -> r) -> ParserS r s e a -> ParserS r s e a
+ Wireform.Parser.Stateful: modify :: (s -> s) -> ParserS r s e ()
+ Wireform.Parser.Stateful: modify' :: (s -> s) -> ParserS r s e ()
+ Wireform.Parser.Stateful: newtype ParserS r s e a
+ Wireform.Parser.Stateful: parseByteStringS :: ParserS r s e a -> r -> s -> ByteString -> Either (ParseError e) (a, s)
+ Wireform.Parser.Stateful: put :: s -> ParserS r s e ()
+ Wireform.Parser.Stateful: runParserS :: ParserS r s e a -> r -> s -> ByteString -> Either (ParseError e) (a, s)
+ Wireform.Parser.Switch: char :: Char -> Q Exp
+ Wireform.Parser.Switch: string :: String -> Q Exp
+ Wireform.Parser.Switch: switch :: Q Exp -> Q Exp
+ Wireform.Parser.Switch: switchAnyWord8Unsafe :: Parser m e Word8
+ Wireform.Parser.Switch: switchBranch :: Int -> Parser m e a -> Parser m e a -> Parser m e a
+ Wireform.Parser.Switch: switchFailed :: Parser m e a
+ Wireform.Parser.Switch: switchPeekWord8Unsafe :: Parser m e Word8
+ Wireform.Parser.Switch: switchSkip1 :: Parser m e ()
+ Wireform.Parser.Switch: switchWithPost :: Maybe (Q Exp) -> Q Exp -> Q Exp
+ Wireform.Ring: MagicRingUnavailable :: !String -> MagicRingException
+ Wireform.Ring: copyRingSlice :: forall {k} (s :: k). RingSlice s -> IO ByteString
+ Wireform.Ring: data MagicRing (s :: k)
+ Wireform.Ring: data MagicRingException
+ Wireform.Ring: data RingSlice (s :: k)
+ Wireform.Ring: peekRingSliceByte :: forall {k} (s :: k). RingSlice s -> Int -> IO Word8
+ Wireform.Ring: ringBase :: forall {k} (s :: k). MagicRing s -> Ptr Word8
+ Wireform.Ring: ringMask :: forall {k} (s :: k). MagicRing s -> Int
+ Wireform.Ring: ringSize :: forall {k} (s :: k). MagicRing s -> Int
+ Wireform.Ring: ringSlice :: forall {k} (s :: k). MagicRing s -> Int -> Int -> RingSlice s
+ Wireform.Ring: ringSliceAtPos :: forall {k} (s :: k). MagicRing s -> Word64 -> Int -> RingSlice s
+ Wireform.Ring: ringSliceLength :: forall {k} (s :: k). RingSlice s -> Int
+ Wireform.Ring: withMagicRing :: Int -> (forall (s :: k). () => MagicRing s -> IO a) -> IO a
+ Wireform.Ring: withRingSlice :: forall {k} (s :: k) a. RingSlice s -> (Ptr Word8 -> Int -> IO a) -> IO a
+ Wireform.Ring.Internal: MagicRing :: {-# UNPACK #-} !Ptr Word8 -> {-# UNPACK #-} !Int -> MagicRing (s :: k)
+ Wireform.Ring.Internal: MagicRingUnavailable :: !String -> MagicRingException
+ Wireform.Ring.Internal: RingSlice :: {-# UNPACK #-} !Ptr Word8 -> {-# UNPACK #-} !Int -> RingSlice (s :: k)
+ Wireform.Ring.Internal: [_rsLen] :: RingSlice (s :: k) -> {-# UNPACK #-} !Int
+ Wireform.Ring.Internal: [_rsPtr] :: RingSlice (s :: k) -> {-# UNPACK #-} !Ptr Word8
+ Wireform.Ring.Internal: [mrBase] :: MagicRing (s :: k) -> {-# UNPACK #-} !Ptr Word8
+ Wireform.Ring.Internal: [mrSize] :: MagicRing (s :: k) -> {-# UNPACK #-} !Int
+ Wireform.Ring.Internal: copyRingSlice :: forall {k} (s :: k). RingSlice s -> IO ByteString
+ Wireform.Ring.Internal: data MagicRing (s :: k)
+ Wireform.Ring.Internal: data MagicRingException
+ Wireform.Ring.Internal: data RingSlice (s :: k)
+ Wireform.Ring.Internal: destroyMagicRing :: forall {k} (s :: k). MagicRing s -> IO ()
+ Wireform.Ring.Internal: instance Foreign.Storable.Storable Wireform.Ring.Internal.CRing
+ Wireform.Ring.Internal: instance GHC.Exception.Type.Exception Wireform.Ring.Internal.MagicRingException
+ Wireform.Ring.Internal: instance GHC.Show.Show Wireform.Ring.Internal.MagicRingException
+ Wireform.Ring.Internal: newMagicRing :: forall {k} (s :: k). Int -> IO (MagicRing s)
+ Wireform.Ring.Internal: peekRingSliceByte :: forall {k} (s :: k). RingSlice s -> Int -> IO Word8
+ Wireform.Ring.Internal: ringBase :: forall {k} (s :: k). MagicRing s -> Ptr Word8
+ Wireform.Ring.Internal: ringMask :: forall {k} (s :: k). MagicRing s -> Int
+ Wireform.Ring.Internal: ringSize :: forall {k} (s :: k). MagicRing s -> Int
+ Wireform.Ring.Internal: ringSlice :: forall {k} (s :: k). MagicRing s -> Int -> Int -> RingSlice s
+ Wireform.Ring.Internal: ringSliceAtPos :: forall {k} (s :: k). MagicRing s -> Word64 -> Int -> RingSlice s
+ Wireform.Ring.Internal: ringSliceBase :: forall {k} (s :: k). RingSlice s -> Ptr Word8
+ Wireform.Ring.Internal: ringSliceLength :: forall {k} (s :: k). RingSlice s -> Int
+ Wireform.Ring.Internal: unsafeRingScope :: forall {k1} {k2} (s :: k1) (s' :: k2). MagicRing s -> MagicRing s'
+ Wireform.Ring.Internal: unsafeRingSliceFromPtr :: forall {k} (s :: k). Ptr Word8 -> Int -> RingSlice s
+ Wireform.Ring.Internal: withMagicRing :: Int -> (forall (s :: k). () => MagicRing s -> IO a) -> IO a
+ Wireform.Ring.Internal: withRingSlice :: forall {k} (s :: k) a. RingSlice s -> (Ptr Word8 -> Int -> IO a) -> IO a
+ Wireform.Ring.Pool: RingPoolConfig :: !Int -> RingPoolConfig
+ Wireform.Ring.Pool: [ringPoolMaxIdle] :: RingPoolConfig -> !Int
+ Wireform.Ring.Pool: acquireRing :: forall {k} (s :: k). RingPool -> Int -> IO (MagicRing s)
+ Wireform.Ring.Pool: closeRingPool :: RingPool -> IO ()
+ Wireform.Ring.Pool: data RingPool
+ Wireform.Ring.Pool: data RingPoolConfig
+ Wireform.Ring.Pool: defaultRingPoolConfig :: RingPoolConfig
+ Wireform.Ring.Pool: newRingPool :: RingPoolConfig -> IO RingPool
+ Wireform.Ring.Pool: releaseRing :: forall {k} (s :: k). RingPool -> MagicRing s -> IO ()
+ Wireform.Transport.Capabilities: CoreTopology :: !IntMap Int -> !IntMap Int -> !IntMap [Int] -> CoreTopology
+ Wireform.Transport.Capabilities: IOUringFeatures :: !Bool -> !Bool -> !Bool -> IOUringFeatures
+ Wireform.Transport.Capabilities: NumaNodeInfo :: !Int -> ![Int] -> !Int -> NumaNodeInfo
+ Wireform.Transport.Capabilities: Placement :: !Maybe Int -> !Maybe Int -> Placement
+ Wireform.Transport.Capabilities: SystemCapabilities :: !Int -> ![Int] -> !Bool -> !IOUringFeatures -> ![NumaNodeInfo] -> ![Int] -> !Int -> !CoreTopology -> SystemCapabilities
+ Wireform.Transport.Capabilities: [capCoreCount] :: SystemCapabilities -> !Int
+ Wireform.Transport.Capabilities: [capCoreTopology] :: SystemCapabilities -> !CoreTopology
+ Wireform.Transport.Capabilities: [capHasIOUring] :: SystemCapabilities -> !Bool
+ Wireform.Transport.Capabilities: [capHugePageSizes] :: SystemCapabilities -> ![Int]
+ Wireform.Transport.Capabilities: [capIOUringFeatures] :: SystemCapabilities -> !IOUringFeatures
+ Wireform.Transport.Capabilities: [capIsolatedCores] :: SystemCapabilities -> ![Int]
+ Wireform.Transport.Capabilities: [capNumaNodes] :: SystemCapabilities -> ![NumaNodeInfo]
+ Wireform.Transport.Capabilities: [capPageSize] :: SystemCapabilities -> !Int
+ Wireform.Transport.Capabilities: [ioUringFeatureMultishotRecv] :: IOUringFeatures -> !Bool
+ Wireform.Transport.Capabilities: [ioUringFeatureProvidedBuffers] :: IOUringFeatures -> !Bool
+ Wireform.Transport.Capabilities: [ioUringFeatureSQPoll] :: IOUringFeatures -> !Bool
+ Wireform.Transport.Capabilities: [numaNodeCores] :: NumaNodeInfo -> ![Int]
+ Wireform.Transport.Capabilities: [numaNodeId] :: NumaNodeInfo -> !Int
+ Wireform.Transport.Capabilities: [numaNodeMemoryMB] :: NumaNodeInfo -> !Int
+ Wireform.Transport.Capabilities: [placementCore] :: Placement -> !Maybe Int
+ Wireform.Transport.Capabilities: [placementNumaNode] :: Placement -> !Maybe Int
+ Wireform.Transport.Capabilities: [topoCoreToL3] :: CoreTopology -> !IntMap Int
+ Wireform.Transport.Capabilities: [topoCoreToPackage] :: CoreTopology -> !IntMap Int
+ Wireform.Transport.Capabilities: [topoSiblings] :: CoreTopology -> !IntMap [Int]
+ Wireform.Transport.Capabilities: data CoreTopology
+ Wireform.Transport.Capabilities: data IOUringFeatures
+ Wireform.Transport.Capabilities: data NumaNodeInfo
+ Wireform.Transport.Capabilities: data Placement
+ Wireform.Transport.Capabilities: data SystemCapabilities
+ Wireform.Transport.Capabilities: detectCapabilities :: IO SystemCapabilities
+ Wireform.Transport.Capabilities: instance GHC.Show.Show Wireform.Transport.Capabilities.CoreTopology
+ Wireform.Transport.Capabilities: instance GHC.Show.Show Wireform.Transport.Capabilities.IOUringFeatures
+ Wireform.Transport.Capabilities: instance GHC.Show.Show Wireform.Transport.Capabilities.NumaNodeInfo
+ Wireform.Transport.Capabilities: instance GHC.Show.Show Wireform.Transport.Capabilities.Placement
+ Wireform.Transport.Capabilities: instance GHC.Show.Show Wireform.Transport.Capabilities.SystemCapabilities
+ Wireform.Transport.Capabilities: recommendPlacement :: Maybe Int -> IO Placement
+ Wireform.Transport.Config: FailHard :: CapabilityAction
+ Wireform.Transport.Config: IOUringConfig :: !Int -> !SQPollPolicy -> !Bool -> !CompletionWaitMode -> IOUringConfig
+ Wireform.Transport.Config: LogAndContinue :: CapabilityAction
+ Wireform.Transport.Config: LowLatency :: Profile
+ Wireform.Transport.Config: NoNumaPreference :: NumaPolicy
+ Wireform.Transport.Config: NoPinning :: PinningPolicy
+ Wireform.Transport.Config: NoSQPoll :: SQPollPolicy
+ Wireform.Transport.Config: NumaAutoFromCurrentCore :: NumaPolicy
+ Wireform.Transport.Config: NumaAutoFromFd :: NumaPolicy
+ Wireform.Transport.Config: NumaNode :: !Int -> NumaPolicy
+ Wireform.Transport.Config: PinIsolated :: PinningPolicy
+ Wireform.Transport.Config: PinNearFd :: PinningPolicy
+ Wireform.Transport.Config: PinToCore :: !Int -> PinningPolicy
+ Wireform.Transport.Config: PinToCoreSet :: ![Int] -> PinningPolicy
+ Wireform.Transport.Config: PreferHugePages :: PagePolicy
+ Wireform.Transport.Config: RequireHugePages :: PagePolicy
+ Wireform.Transport.Config: SQPollWithIdle :: !Int -> SQPollPolicy
+ Wireform.Transport.Config: SilentlyIgnore :: CapabilityAction
+ Wireform.Transport.Config: SpinIterations :: !Int -> SpinBudget
+ Wireform.Transport.Config: SpinNanos :: !Int -> SpinBudget
+ Wireform.Transport.Config: SpinUntilPaused :: SpinBudget
+ Wireform.Transport.Config: StandardPages :: PagePolicy
+ Wireform.Transport.Config: Throughput :: Profile
+ Wireform.Transport.Config: TransportConfig :: !Int -> !WaitPolicy -> !PinningPolicy -> !PagePolicy -> !Bool -> !NumaPolicy -> !IOUringConfig -> !CapabilityAction -> TransportConfig
+ Wireform.Transport.Config: UltraLowLatency :: Profile
+ Wireform.Transport.Config: WaitBusyPoll :: WaitPolicy
+ Wireform.Transport.Config: WaitParkImmediately :: WaitPolicy
+ Wireform.Transport.Config: WaitSpinThenPark :: !SpinBudget -> WaitPolicy
+ Wireform.Transport.Config: WaitViaBusyPoll :: CompletionWaitMode
+ Wireform.Transport.Config: WaitViaEventFd :: CompletionWaitMode
+ Wireform.Transport.Config: [ioUringCompletionWait] :: IOUringConfig -> !CompletionWaitMode
+ Wireform.Transport.Config: [ioUringProvidedBuffers] :: IOUringConfig -> !Bool
+ Wireform.Transport.Config: [ioUringQueueDepth] :: IOUringConfig -> !Int
+ Wireform.Transport.Config: [ioUringSQPoll] :: IOUringConfig -> !SQPollPolicy
+ Wireform.Transport.Config: [ioUring] :: TransportConfig -> !IOUringConfig
+ Wireform.Transport.Config: [memoryLocking] :: TransportConfig -> !Bool
+ Wireform.Transport.Config: [numaPlacement] :: TransportConfig -> !NumaPolicy
+ Wireform.Transport.Config: [onCapabilityLimit] :: TransportConfig -> !CapabilityAction
+ Wireform.Transport.Config: [pages] :: TransportConfig -> !PagePolicy
+ Wireform.Transport.Config: [pinning] :: TransportConfig -> !PinningPolicy
+ Wireform.Transport.Config: [ringSizeHint] :: TransportConfig -> !Int
+ Wireform.Transport.Config: [waitPolicy] :: TransportConfig -> !WaitPolicy
+ Wireform.Transport.Config: data CapabilityAction
+ Wireform.Transport.Config: data CompletionWaitMode
+ Wireform.Transport.Config: data IOUringConfig
+ Wireform.Transport.Config: data NumaPolicy
+ Wireform.Transport.Config: data PagePolicy
+ Wireform.Transport.Config: data PinningPolicy
+ Wireform.Transport.Config: data Profile
+ Wireform.Transport.Config: data SQPollPolicy
+ Wireform.Transport.Config: data SpinBudget
+ Wireform.Transport.Config: data TransportConfig
+ Wireform.Transport.Config: data WaitPolicy
+ Wireform.Transport.Config: defaultIOUringConfig :: IOUringConfig
+ Wireform.Transport.Config: defaultTransportConfig :: TransportConfig
+ Wireform.Transport.Config: instance GHC.Classes.Eq Wireform.Transport.Config.CapabilityAction
+ Wireform.Transport.Config: instance GHC.Classes.Eq Wireform.Transport.Config.Profile
+ Wireform.Transport.Config: instance GHC.Classes.Ord Wireform.Transport.Config.CapabilityAction
+ Wireform.Transport.Config: instance GHC.Classes.Ord Wireform.Transport.Config.Profile
+ Wireform.Transport.Config: instance GHC.Enum.Bounded Wireform.Transport.Config.CapabilityAction
+ Wireform.Transport.Config: instance GHC.Enum.Bounded Wireform.Transport.Config.Profile
+ Wireform.Transport.Config: instance GHC.Enum.Enum Wireform.Transport.Config.CapabilityAction
+ Wireform.Transport.Config: instance GHC.Enum.Enum Wireform.Transport.Config.Profile
+ Wireform.Transport.Config: instance GHC.Read.Read Wireform.Transport.Config.CapabilityAction
+ Wireform.Transport.Config: instance GHC.Read.Read Wireform.Transport.Config.Profile
+ Wireform.Transport.Config: instance GHC.Show.Show Wireform.Transport.Config.CapabilityAction
+ Wireform.Transport.Config: instance GHC.Show.Show Wireform.Transport.Config.CompletionWaitMode
+ Wireform.Transport.Config: instance GHC.Show.Show Wireform.Transport.Config.IOUringConfig
+ Wireform.Transport.Config: instance GHC.Show.Show Wireform.Transport.Config.NumaPolicy
+ Wireform.Transport.Config: instance GHC.Show.Show Wireform.Transport.Config.PagePolicy
+ Wireform.Transport.Config: instance GHC.Show.Show Wireform.Transport.Config.PinningPolicy
+ Wireform.Transport.Config: instance GHC.Show.Show Wireform.Transport.Config.Profile
+ Wireform.Transport.Config: instance GHC.Show.Show Wireform.Transport.Config.SQPollPolicy
+ Wireform.Transport.Config: instance GHC.Show.Show Wireform.Transport.Config.SpinBudget
+ Wireform.Transport.Config: instance GHC.Show.Show Wireform.Transport.Config.TransportConfig
+ Wireform.Transport.Config: instance GHC.Show.Show Wireform.Transport.Config.WaitPolicy
+ Wireform.Transport.Config: profileConfig :: Profile -> TransportConfig
+ Wireform.Transport.Receive: ReceiveEndOfInput :: ReceiveWait
+ Wireform.Transport.Receive: ReceiveFailed :: !SomeException -> ReceiveWait
+ Wireform.Transport.Receive: ReceiveMoreData :: {-# UNPACK #-} !Word64 -> ReceiveWait
+ Wireform.Transport.Receive: ReceiveTransport :: {-# UNPACK #-} !Ptr Word8 -> {-# UNPACK #-} !Int -> {-# UNPACK #-} !Int -> !IO Word64 -> !Word64 -> IO () -> !Word64 -> IO ReceiveWait -> !IO () -> ReceiveTransport
+ Wireform.Transport.Receive: [receiveAdvanceTail] :: ReceiveTransport -> !Word64 -> IO ()
+ Wireform.Transport.Receive: [receiveClose] :: ReceiveTransport -> !IO ()
+ Wireform.Transport.Receive: [receiveLoadHead] :: ReceiveTransport -> !IO Word64
+ Wireform.Transport.Receive: [receiveRingBase] :: ReceiveTransport -> {-# UNPACK #-} !Ptr Word8
+ Wireform.Transport.Receive: [receiveRingMask] :: ReceiveTransport -> {-# UNPACK #-} !Int
+ Wireform.Transport.Receive: [receiveRingSize] :: ReceiveTransport -> {-# UNPACK #-} !Int
+ Wireform.Transport.Receive: [receiveWaitData] :: ReceiveTransport -> !Word64 -> IO ReceiveWait
+ Wireform.Transport.Receive: data ReceiveTransport
+ Wireform.Transport.Receive: data ReceiveWait
+ Wireform.Transport.Receive: instance GHC.Show.Show Wireform.Transport.Receive.ReceiveWait
+ Wireform.Transport.Receive: receiveRing :: forall {k} (s :: k). ReceiveTransport -> MagicRing s
+ Wireform.Transport.Send: SendFailed :: !SomeException -> SendWait
+ Wireform.Transport.Send: SendPeerClosed :: SendWait
+ Wireform.Transport.Send: SendReservationTooLarge :: !Int -> !Int -> SendReservationTooLarge
+ Wireform.Transport.Send: SendRingFull :: !Int -> !Word64 -> !Word64 -> SendRingFull
+ Wireform.Transport.Send: SendSpaceAvailable :: {-# UNPACK #-} !Word64 -> SendWait
+ Wireform.Transport.Send: SendTransport :: {-# UNPACK #-} !Ptr Word8 -> {-# UNPACK #-} !Int -> {-# UNPACK #-} !Int -> !IO Word64 -> !IO Word64 -> !Word64 -> IO () -> !Word64 -> IO SendWait -> !IO () -> !IO () -> !IO () -> SendTransport
+ Wireform.Transport.Send: [sendClose] :: SendTransport -> !IO ()
+ Wireform.Transport.Send: [sendFlush] :: SendTransport -> !IO ()
+ Wireform.Transport.Send: [sendLoadHead] :: SendTransport -> !IO Word64
+ Wireform.Transport.Send: [sendLoadTail] :: SendTransport -> !IO Word64
+ Wireform.Transport.Send: [sendPublishHead] :: SendTransport -> !Word64 -> IO ()
+ Wireform.Transport.Send: [sendReservationRequested] :: SendReservationTooLarge -> !Int
+ Wireform.Transport.Send: [sendReservationRingSize] :: SendReservationTooLarge -> !Int
+ Wireform.Transport.Send: [sendRingBase] :: SendTransport -> {-# UNPACK #-} !Ptr Word8
+ Wireform.Transport.Send: [sendRingFullHead] :: SendRingFull -> !Word64
+ Wireform.Transport.Send: [sendRingFullSize] :: SendRingFull -> !Int
+ Wireform.Transport.Send: [sendRingFullTail] :: SendRingFull -> !Word64
+ Wireform.Transport.Send: [sendRingMask] :: SendTransport -> {-# UNPACK #-} !Int
+ Wireform.Transport.Send: [sendRingSize] :: SendTransport -> {-# UNPACK #-} !Int
+ Wireform.Transport.Send: [sendShutdownWrite] :: SendTransport -> !IO ()
+ Wireform.Transport.Send: [sendWaitSpace] :: SendTransport -> !Word64 -> IO SendWait
+ Wireform.Transport.Send: data SendReservationTooLarge
+ Wireform.Transport.Send: data SendRingFull
+ Wireform.Transport.Send: data SendTransport
+ Wireform.Transport.Send: data SendWait
+ Wireform.Transport.Send: instance GHC.Exception.Type.Exception Wireform.Transport.Send.SendReservationTooLarge
+ Wireform.Transport.Send: instance GHC.Exception.Type.Exception Wireform.Transport.Send.SendRingFull
+ Wireform.Transport.Send: instance GHC.Show.Show Wireform.Transport.Send.SendReservationTooLarge
+ Wireform.Transport.Send: instance GHC.Show.Show Wireform.Transport.Send.SendRingFull
+ Wireform.Transport.Send: instance GHC.Show.Show Wireform.Transport.Send.SendWait
+ Wireform.Transport.Send: reserveSend :: SendTransport -> Int -> IO (Ptr Word8, Word64)
+ Wireform.Transport.Send: sendBuilder :: SendTransport -> Builder -> IO ()
+ Wireform.Transport.Send: sendBuilderDirect :: SendTransport -> Builder -> IO ()
+ Wireform.Transport.Send: sendBuilderViaByteString :: SendTransport -> Builder -> IO ()
+ Wireform.Transport.Send: sendByteString :: SendTransport -> ByteString -> IO ()
+ Wireform.Transport.Send: sendByteStringMany :: SendTransport -> [ByteString] -> IO ()
+ Wireform.Transport.Send: sendRing :: forall {k} (s :: k). SendTransport -> MagicRing s
+ Wireform.Transport.Send: withSendCork :: SendTransport -> (SendTransport -> IO a) -> IO a
+ Wireform.Transport.Send: withSendReservation :: SendTransport -> Int -> (Ptr Word8 -> Int -> IO Int) -> IO Int
Files
- CHANGELOG.md +116/−1
- README.md +0/−13
- bench/Common.hs +19/−0
- bench/FPBasic.hs +94/−0
- bench/ParserBench.hs +328/−0
- bench/SendBuilderBench.hs +149/−0
- bench/WFBasic.hs +155/−0
- cbits/base64.c +293/−0
- cbits/fast_rng.c +154/−0
- cbits/fast_scan.c +58/−0
- cbits/magic_ring_linux.c +101/−0
- cbits/magic_ring_posix.c +115/−0
- cbits/magic_ring_windows.c +103/−0
- include/magic_ring.h +18/−0
- include/simde/simde/arm/neon/st1_lane.h +1/−0
- src/Wireform/Base64.hs +141/−0
- src/Wireform/Builder.hs +6/−32
- src/Wireform/Builder/FastBuilder.hs +219/−37
- src/Wireform/Encode/Direct.hs +69/−49
- src/Wireform/FFI.hs +85/−7
- src/Wireform/Hash.hs +98/−63
- src/Wireform/Parser.hs +1221/−0
- src/Wireform/Parser/Adapter.hs +166/−0
- src/Wireform/Parser/Driver.hs +354/−0
- src/Wireform/Parser/Error.hs +46/−0
- src/Wireform/Parser/Internal.hs +572/−0
- src/Wireform/Parser/Mark.hs +42/−0
- src/Wireform/Parser/Position.hs +101/−0
- src/Wireform/Parser/Stateful.hs +252/−0
- src/Wireform/Parser/Switch.hs +288/−0
- src/Wireform/Ring.hs +48/−0
- src/Wireform/Ring/Internal.hs +315/−0
- src/Wireform/Ring/Pool.hs +102/−0
- src/Wireform/Transport.hs +17/−0
- src/Wireform/Transport/Capabilities.hs +306/−0
- src/Wireform/Transport/Config.hs +221/−0
- src/Wireform/Transport/Receive.hs +94/−0
- src/Wireform/Transport/Send.hs +451/−0
- test/Spec.hs +15/−0
- test/Wireform/Base64/Test.hs +72/−0
- test/Wireform/Parser/Test.hs +275/−0
- test/Wireform/Ring/Test.hs +214/−0
- test/Wireform/Transport/SendTest.hs +534/−0
- wireform-core.cabal +108/−3
CHANGELOG.md view
@@ -1,5 +1,120 @@ # Changelog for wireform-core -## 0.1.0.0 -- 2026-05-16+## 0.2.0.0 -- 2026-07-16 +* `Wireform.Base64` -- RFC 4648 §4 base64 encode / decode. SSSE3+ (via simde) inner loop for the encoder (12 input bytes -> 16+ output chars per iteration) plus an SSE2 pre-scan for the+ decoder that rejects any 16-byte window containing a high-bit+ byte before scalar sextet extraction. Replaces ad-hoc+ `base64-bytestring` usage in downstream packages — the+ WebSocket handshake (`wireform-websocket`) is the first+ consumer.++* `Wireform.FFI.fastRandomWord64` -- thread-local xoshiro256+++ PRNG implemented in `cbits/fast_rng.c`. Per-OS-thread 256-bit+ state stored in `__thread` storage, seeded on first use from+ `getrandom(2)` (`arc4random_buf` on BSDs, `/dev/urandom`+ elsewhere). Each call is a single FFI trip + a handful of+ register-only XOR / rotate ops — typically ~1 ns including the+ FFI boundary, versus ~50 ns for the global `splitmix` `MVar`+ generator. Caveat: because Haskell threads are multiplexed+ across OS threads, the per-Haskell-thread sequence is not+ reproducible; use `System.Random.Stateful` for that. The+ intended uses are non-deterministic-randomness needs on hot+ paths — WebSocket frame masks, retry jitter, etc.++* `Wireform.Ring` -- gave `MagicRing` a phantom type parameter `s`+ modelled after `Control.Monad.ST.ST`. `withMagicRing` is now+ rank-2 (`Int -> (forall s. MagicRing s -> IO a) -> IO a`), which+ seals `s` inside the ring's scope. New `RingSlice s` type (a+ pointer + length tagged with the ring's `s`) replaces ad-hoc+ `ByteString` slicing for callers that want type-system-enforced+ safety against dangling references after a refill. `copyRingSlice+ :: RingSlice s -> IO ByteString` is the explicit escape hatch.++* `Wireform.Transport` -- replaced the typed `transportRing ::+ MagicRing` field with three raw fields (`transportRingBaseField`,+ `transportRingSizeField`, `transportRingMaskField`). This keeps+ `Transport` un-parameterised so the new `s` does not have to+ cascade through every downstream `Transport`-using package. The+ existing `transportRing :: Transport -> MagicRing s` getter+ remains for backwards compatibility but is polymorphic in `s`, i.e.+ un-scoped from a safety standpoint. Transport constructors must+ populate the three raw fields explicitly.++* `Wireform.Parser` -- `takeBs` and `takeBsCopy` now drain any byte+ count, including counts larger than the magic ring's capacity.+ Reads that fit in the ring keep their existing fast path+ (zero-copy slice or single memcpy). Reads that exceed the ring+ allocate a fresh pinned 'ByteString' and chunk-copy through the+ ring, calling 'modeCheckpoint' between chunks so the producer has+ room to keep refilling. No deadlock, no `ParseRingOverflow` for+ these primitives — the caller gets the full byte string they+ asked for. Primitives that cannot be drained safely (literal+ `byteString` matches, `isolate`, raw `ensureN#`) still surface+ `ParseRingOverflow` when their size exceeds the ring.++* `Wireform.Parser.Driver` / `Wireform.Parser.Error` -- detect the+ case where the streaming parser asks (via `ensureN#`, `byteString`,+ or `isolate`) for more bytes than the ring can ever hold and+ short-circuit with a new `ParseRingOverflow` error variant+ (position, requested bytes, ring size). Without this guard the+ wait loop spins forever on a no-progress @MoreData@ from the+ transport because the producer cannot make room and the consumer+ is suspended waiting for it.++* `Wireform.Parser.Driver` -- fix a latent wrap bug in the+ StepSuspend / StepCheckpoint resume paths. The driver used to+ compute the resumed eob as @base + (head .&. mask)@, which+ collapses onto cur whenever the producer has filled exactly one+ ring-worth of bytes since the suspension (because the masked+ offsets coincide). The parser would then see zero bytes available+ even though `head - cur == ringSize`. Replaced with+ @newCur + (head - pos)@, which correctly places eob in the second+ mapping when wrap happens. Same fix applied to the initial eob+ in `runParserInternal`.++* `Wireform.Parser.Internal.ParserEnv` -- replaced the immutable+ `peStartPos` / `peInitCur` fields with mutable `peAnchorPos` /+ `peAnchorCur` cells, plus matching `writeAnchor` / `writeAnchor#`+ helpers. Whenever the driver wraps the parser's cur back into the+ first mapping (StepCheckpoint or StepSuspend resume), it now also+ re-anchors the env so that `curToPos` keeps producing the correct+ absolute position even after the wrap. `curToPos` is now+ State#-threaded (no behavioural change for ordinary callers — the+ existing call sites all already have a State# in scope). Callers+ that constructed `ParserEnv` directly (driver, parseByteString,+ Stateful) now stack-allocate a 24-byte cells buffer (via+ `allocaBytes`) instead of `bracket (mallocBytes 8) free`. Removes+ a C `malloc` + `free` round-trip per `runParser` call on the+ streaming path.++## 0.1.0.0 -- 2026+ Initial release.++* `Wireform.FFI` -- C FFI surface for the per-format encoders /+ decoders:+ * Packed-varint pre-scan (`countPackedVarints`,+ `packedAllSingleByte`).+ * SWAR UTF-8 validation (`validateUtf8SWAR`).+ * Branchless 8-byte varint decode (`decodeVarintSWAR`).+ * Page-boundary relocation helper (`relocatePageBoundary`).+ * C-native scalar field encoders (`encodeLengthDelimitedC`,+ `encodeVarintFieldC`, `encodeBoolFieldC`).+ * SIMD NUL / byte / ASCII / JSON-escape / EDN-whitespace+ scanners (`findNul`, `findByte`, `isAscii`,+ `findJsonEscape`, `skipWhitespace`).+ * SIMD Iceberg partition-bounds comparison (`compareBounds`).+ * SIMD Arrow IPC buffer validation+ (`validateArrowBuffers`).+ * BE / LE endianness helpers (`readBE16H` … `writeLE64H`).+ * Text decoding via the `text` library's bundled simdutf+ (`decodeTextFast`).+* `Wireform.Encode.Direct` -- shared direct-write encode buffer+ primitive (`Buf`) for the per-format encoders.+* `Wireform.Hash` -- SIMD-accelerated hashing helpers.++C sources live in `cbits/`; the vendored `simde` headers ship in+`include/simde/`.
README.md view
@@ -41,19 +41,6 @@ duplicating the `__attribute__((target(...)))` / `simde-features.h` plumbing. -## Performance tip--Compiling with `-fllvm` alongside `-O2` typically yields **20–30% throughput-gains** on the encode/decode hot paths. LLVM produces better instruction-scheduling and loop vectorisation for the unboxed arithmetic in `Wireform.FFI`-and `Wireform.Encode.Direct`.--```--- in your package's cabal file, or in cabal.project.local:-package wireform-core- ghc-options: -fllvm-```- ## License BSD-3-Clause. Vendored `simde` headers carry their own MIT license
+ bench/Common.hs view
@@ -0,0 +1,19 @@+{-# LANGUAGE StrictData #-}++module Common where++import Data.ByteString qualified as B+++type Name = B.ByteString+++data Tm+ = Var Name+ | App Tm Tm+ | Lam Name Tm+ | Let Name Tm Tm+ | Int Int+ | Add Tm Tm+ | Mul Tm Tm+ deriving (Show)
+ bench/FPBasic.hs view
@@ -0,0 +1,94 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TemplateHaskell #-}++module FPBasic (+ runSexp,+ runLongws,+ runNumcsv,+ runTm,+) where++import Common+import Data.ByteString qualified as B+import FlatParse.Basic+++ws, open, close, ident, sexp, src :: Parser () ()+ws = skipMany $(switch [|case _ of " " -> pure (); "\n" -> pure ()|])+open = $(char '(') >> ws+close = $(char ')') >> ws+ident = skipSome (skipSatisfyAscii isLatinLetter) >> ws+sexp = branch open (skipSome sexp >> close) ident+src = sexp >> eof+++runSexp :: B.ByteString -> Result () ()+runSexp = runParser src+++longw, longws :: Parser () ()+longw = $(string "thisisalongkeyword")+longws = skipSome (longw >> ws) >> eof+++runLongws :: B.ByteString -> Result () ()+runLongws = runParser longws+++numeral, comma, numcsv :: Parser () ()+numeral = skipSome (skipSatisfyAscii isDigit) >> ws+comma = $(char ',') >> ws+numcsv = numeral >> skipMany (comma >> numeral) >> eof+++runNumcsv :: B.ByteString -> Result () ()+runNumcsv = runParser numcsv+++ident' :: Parser () B.ByteString+ident' = byteStringOf (skipSome (skipSatisfyAscii \c -> isLatinLetter c || isDigit c)) <* ws+++equal, semi, dot, addOp, mulOp, parl, parr :: Parser () ()+equal = $(string "=") <* ws+semi = $(string ";") <* ws+dot = $(string ".") <* ws+addOp = $(string "+") <* ws+mulOp = $(string "*") <* ws+parl = $(string "(") <* ws+parr = $(string ")") <* ws+++add :: Parser () Tm+add = chainl Add mul (addOp *> mul)+++mul :: Parser () Tm+mul = chainl Mul spine (mulOp *> spine)+++spine :: Parser () Tm+spine = chainl App atom atom+++atom :: Parser () Tm+atom =+ (Int <$> (anyAsciiDecimalInt <* ws))+ <|> (Var <$> ident')+ <|> (parl *> tm <* parr)+++tm :: Parser () Tm+tm =+ $( switch+ [|+ case _ of+ "fun" -> do ws; x <- ident'; dot; t <- tm; pure (Lam x t)+ "let" -> do ws; x <- ident'; equal; t <- tm; semi; u <- tm; pure (Let x t u)+ _ -> add+ |]+ )+++runTm :: B.ByteString -> Result () Tm+runTm = runParser (ws *> tm <* eof)
+ bench/ParserBench.hs view
@@ -0,0 +1,328 @@+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE BlockArguments #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}++module Main where++import Criterion.Main+import Data.ByteString (ByteString)+import Data.ByteString qualified as BS+import Data.ByteString.Builder qualified as BSB+import Data.ByteString.Char8 qualified as BSC+import Data.ByteString.Lazy qualified as LBS+import Data.Int+import Data.Word+import FPBasic qualified+import FlatParse.Basic qualified as FP+import WFBasic qualified+import Wireform.Parser qualified as W+import Wireform.Parser.Driver qualified as W+import Wireform.Parser.Internal (Pure)+++------------------------------------------------------------------------+-- Input generation+------------------------------------------------------------------------++-- | N copies of a 4-byte big-endian word+mkWord32Input :: Int -> ByteString+mkWord32Input n =+ LBS.toStrict . BSB.toLazyByteString $+ mconcat [BSB.word32BE (fromIntegral i) | i <- [0 .. n - 1]]+++-- | N copies of a single byte+mkByteInput :: Int -> ByteString+mkByteInput n = BS.replicate n 0x42+++{- | Length-prefixed messages: 1-byte length + payload+Each message is 8 bytes of payload (length byte = 0x08)+-}+mkLengthPrefixedInput :: Int -> ByteString+mkLengthPrefixedInput n =+ LBS.toStrict . BSB.toLazyByteString $+ mconcat+ [ BSB.word8 8 <> BSB.byteString (BS.replicate 8 (fromIntegral i))+ | i <- [0 .. n - 1]+ ]+++-- | ASCII decimal numbers separated by newlines+mkAsciiDecimalInput :: Int -> ByteString+mkAsciiDecimalInput n =+ LBS.toStrict . BSB.toLazyByteString $+ mconcat [BSB.stringUtf8 (show i) <> BSB.word8 0x0A | i <- [0 .. n - 1]]+++-- | Alternating tag bytes: 0x01 or 0x02, each followed by a word32be+mkTaggedInput :: Int -> ByteString+mkTaggedInput n =+ LBS.toStrict . BSB.toLazyByteString $+ mconcat+ [ BSB.word8 (if even i then 0x01 else 0x02)+ <> BSB.word32BE (fromIntegral i)+ | i <- [0 .. n - 1]+ ]+++-- | UTF-8 text: ASCII (1-byte) characters+mkAsciiTextInput :: Int -> ByteString+mkAsciiTextInput n = BS.replicate n 0x61 -- 'a'+++-- | UTF-8 text: 2-byte characters (Latin-1 supplement, e.g. é = C3 A9)+mkUtf8_2byteInput :: Int -> ByteString+mkUtf8_2byteInput n =+ LBS.toStrict . BSB.toLazyByteString $+ mconcat [BSB.word8 0xC3 <> BSB.word8 0xA9 | _ <- [1 .. n]]+++------------------------------------------------------------------------+-- Wireform parsers+------------------------------------------------------------------------++type WP = W.Parser Pure ()+++wfWord32s :: Int -> WP ()+wfWord32s 0 = pure ()+wfWord32s n = do+ !_ <- W.anyWord32be+ wfWord32s (n - 1)+{-# INLINE wfWord32s #-}+++wfBytes :: Int -> WP ()+wfBytes 0 = pure ()+wfBytes n = do+ !_ <- W.anyWord8+ wfBytes (n - 1)+{-# INLINE wfBytes #-}+++wfLengthPrefixed :: Int -> WP ()+wfLengthPrefixed 0 = pure ()+wfLengthPrefixed n = do+ !len <- W.anyWord8+ !_ <- W.takeBs (fromIntegral len)+ wfLengthPrefixed (n - 1)+{-# INLINE wfLengthPrefixed #-}+++wfAsciiDecimals :: Int -> WP ()+wfAsciiDecimals 0 = pure ()+wfAsciiDecimals n = do+ !_ <- W.anyAsciiDecimalWord+ W.word8 0x0A+ wfAsciiDecimals (n - 1)+{-# INLINE wfAsciiDecimals #-}+++wfTagged :: Int -> WP ()+wfTagged 0 = pure ()+wfTagged n = do+ (W.word8 0x01 >> W.anyWord32be >> pure ())+ W.<|> (W.word8 0x02 >> W.anyWord32be >> pure ())+ wfTagged (n - 1)+{-# INLINE wfTagged #-}+++wfAsciiChars :: Int -> WP ()+wfAsciiChars 0 = pure ()+wfAsciiChars n = do+ W.skipSatisfyAscii (\_ -> True)+ wfAsciiChars (n - 1)+{-# INLINE wfAsciiChars #-}+++wfUtf8Chars :: Int -> WP ()+wfUtf8Chars 0 = pure ()+wfUtf8Chars n = do+ !_ <- W.anyChar+ wfUtf8Chars (n - 1)+{-# INLINE wfUtf8Chars #-}+++------------------------------------------------------------------------+-- FlatParse parsers+------------------------------------------------------------------------++type FPP = FP.Parser ()+++fpWord32s :: Int -> FPP ()+fpWord32s 0 = pure ()+fpWord32s n = do+ !_ <- FP.anyWord32be+ fpWord32s (n - 1)+{-# INLINE fpWord32s #-}+++fpBytes :: Int -> FPP ()+fpBytes 0 = pure ()+fpBytes n = do+ !_ <- FP.anyWord8+ fpBytes (n - 1)+{-# INLINE fpBytes #-}+++fpLengthPrefixed :: Int -> FPP ()+fpLengthPrefixed 0 = pure ()+fpLengthPrefixed n = do+ !len <- FP.anyWord8+ !_ <- FP.take (fromIntegral len)+ fpLengthPrefixed (n - 1)+{-# INLINE fpLengthPrefixed #-}+++fpAsciiDecimals :: Int -> FPP ()+fpAsciiDecimals 0 = pure ()+fpAsciiDecimals n = do+ !_ <- FP.anyAsciiDecimalWord+ FP.word8 0x0A+ fpAsciiDecimals (n - 1)+{-# INLINE fpAsciiDecimals #-}+++fpTagged :: Int -> FPP ()+fpTagged 0 = pure ()+fpTagged n = do+ (FP.word8 0x01 >> FP.anyWord32be >> pure ())+ FP.<|> (FP.word8 0x02 >> FP.anyWord32be >> pure ())+ fpTagged (n - 1)+{-# INLINE fpTagged #-}+++fpAsciiChars :: Int -> FPP ()+fpAsciiChars 0 = pure ()+fpAsciiChars n = do+ FP.skipSatisfyAscii (\_ -> True)+ fpAsciiChars (n - 1)+{-# INLINE fpAsciiChars #-}+++fpUtf8Chars :: Int -> FPP ()+fpUtf8Chars 0 = pure ()+fpUtf8Chars n = do+ !_ <- FP.anyChar+ fpUtf8Chars (n - 1)+{-# INLINE fpUtf8Chars #-}+++------------------------------------------------------------------------+-- Runners+------------------------------------------------------------------------++runWF :: WP a -> ByteString -> a+runWF p bs = case W.parseByteString p bs of+ Right a -> a+ Left _ -> error "wireform parse failed"+{-# INLINE runWF #-}+++runFP :: FPP a -> ByteString -> a+runFP p bs = case FP.runParser p bs of+ FP.OK a _ -> a+ _ -> error "flatparse parse failed"+{-# INLINE runFP #-}+++------------------------------------------------------------------------+-- Benchmark harness+------------------------------------------------------------------------++main :: IO ()+main = do+ let !n = 100000++ -- Pre-generate inputs+ let !byteInput = mkByteInput n+ !word32Input = mkWord32Input n+ !lpInput = mkLengthPrefixedInput n+ !decInput = mkAsciiDecimalInput n+ !taggedInput = mkTaggedInput n+ !asciiInput = mkAsciiTextInput n+ !utf8Input = mkUtf8_2byteInput n++ putStrLn $ "Input sizes:"+ putStrLn $ " byte: " <> show (BS.length byteInput) <> " bytes"+ putStrLn $ " word32: " <> show (BS.length word32Input) <> " bytes"+ putStrLn $ " len-pfx: " <> show (BS.length lpInput) <> " bytes"+ putStrLn $ " decimal: " <> show (BS.length decInput) <> " bytes"+ putStrLn $ " tagged: " <> show (BS.length taggedInput) <> " bytes"+ putStrLn $ " ascii: " <> show (BS.length asciiInput) <> " bytes"+ putStrLn $ " utf8-2b: " <> show (BS.length utf8Input) <> " bytes"++ defaultMain+ [ bgroup+ "anyWord8 x100k"+ [ bench "wireform" $ nf (runWF (wfBytes n)) byteInput+ , bench "flatparse" $ nf (runFP (fpBytes n)) byteInput+ ]+ , bgroup+ "anyWord32be x100k"+ [ bench "wireform" $ nf (runWF (wfWord32s n)) word32Input+ , bench "flatparse" $ nf (runFP (fpWord32s n)) word32Input+ ]+ , bgroup+ "length-prefixed messages x100k"+ [ bench "wireform" $ nf (runWF (wfLengthPrefixed n)) lpInput+ , bench "flatparse" $ nf (runFP (fpLengthPrefixed n)) lpInput+ ]+ , bgroup+ "ASCII decimal + newline x100k"+ [ bench "wireform" $ nf (runWF (wfAsciiDecimals n)) decInput+ , bench "flatparse" $ nf (runFP (fpAsciiDecimals n)) decInput+ ]+ , bgroup+ "tagged alternatives x100k"+ [ bench "wireform" $ nf (runWF (wfTagged n)) taggedInput+ , bench "flatparse" $ nf (runFP (fpTagged n)) taggedInput+ ]+ , bgroup+ "anyCharASCII x100k"+ [ bench "wireform" $ nf (runWF (wfAsciiChars n)) asciiInput+ , bench "flatparse" $ nf (runFP (fpAsciiChars n)) asciiInput+ ]+ , bgroup+ "anyChar (2-byte UTF-8) x100k"+ [ bench "wireform" $ nf (runWF (wfUtf8Chars n)) utf8Input+ , bench "flatparse" $ nf (runFP (fpUtf8Chars n)) utf8Input+ ]+ , -- Flatparse-equivalent real-world benchmarks+ bgroup+ "sexp"+ [ bench "wireform" $ whnf WFBasic.runSexp sexpInp+ , bench "flatparse" $ whnf FPBasic.runSexp sexpInp+ ]+ , bgroup+ "long keyword"+ [ bench "wireform" $ whnf WFBasic.runLongws longwsInp+ , bench "flatparse" $ whnf FPBasic.runLongws longwsInp+ ]+ , bgroup+ "numeral csv"+ [ bench "wireform" $ whnf WFBasic.runNumcsv numcsvInp+ , bench "flatparse" $ whnf FPBasic.runNumcsv numcsvInp+ ]+ , bgroup+ "lambda term"+ [ bench "wireform" $ whnf WFBasic.runTm tmInp+ , bench "flatparse" $ whnf FPBasic.runTm tmInp+ ]+ ]+ where+ sexpInp = BS.concat $ "(" : replicate 33333 "(foo (foo (foo ((bar baza)))))" <> [")"]+ longwsInp = BS.concat $ replicate 55555 "thisisalongkeyword "+ numcsvInp = BSC.pack (concat ("0" : [", " <> show i | i <- [1 .. 100000 :: Int]]))+ tmInp =+ BSC.pack+ ( unlines+ ( [ "let x" <> show x <> " = fun f. fun g. fun x. fun y. f (f (f ((g x y + g x y) * g x y * g x y * 13500)));"+ | x <- [0 .. 3000 :: Int]+ ]+ <> [("x1000" :: String)]+ )+ )
+ bench/SendBuilderBench.hs view
@@ -0,0 +1,149 @@+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}++{- | Benchmark: sendBuilder (via ByteString) vs sendBuilderDirect (ring sink).++Uses an in-memory send transport backed by a magic ring so the+benchmark isolates builder-to-ring staging cost without any I/O.+-}+module Main where++import Criterion.Main+import Data.ByteString qualified as BS+import Data.IORef+import Data.Word+import Foreign.Ptr (Ptr)+import Wireform.Builder+import Wireform.Ring.Internal (ringBase, ringMask, ringSize, withMagicRing)+import Wireform.Transport.Send+++------------------------------------------------------------------------+-- In-memory send transport (no I/O, immediate drain)+------------------------------------------------------------------------++{- | Build a 'SendTransport' over a magic ring that acts as an+instant drain: every 'sendPublishHead' immediately advances the+tail to match, so the ring never fills. This isolates the+builder staging cost.+-}+mkMemorySendTransport+ :: Ptr Word8+ -> Int+ -> Int+ -> IO SendTransport+mkMemorySendTransport base sz msk = do+ headRef <- newIORef (0 :: Word64)+ tailRef <- newIORef (0 :: Word64)+ let publish h = do+ writeIORef headRef h+ writeIORef tailRef h+ pure+ SendTransport+ { sendRingBase = base+ , sendRingSize = sz+ , sendRingMask = msk+ , sendLoadTail = readIORef tailRef+ , sendLoadHead = readIORef headRef+ , sendPublishHead = publish+ , sendWaitSpace = \_ -> do+ tl <- readIORef tailRef+ pure (SendSpaceAvailable tl)+ , sendFlush = pure ()+ , sendShutdownWrite = pure ()+ , sendClose = pure ()+ }+++------------------------------------------------------------------------+-- Builders of various sizes+------------------------------------------------------------------------++-- 64-byte payload: small protocol header / Kafka produce ack+smallBuilder :: Builder+smallBuilder =+ mconcat+ [ word32BE 0x00000001+ , word32BE 0x00000040+ , byteStringCopy (BS.replicate 56 0xAA)+ ]+++-- 256-byte payload: typical Kafka produce record+mediumBuilder :: Builder+mediumBuilder =+ mconcat+ [ word32BE 0x00000002+ , word32BE 0x00000100+ , byteStringCopy (BS.replicate 248 0xBB)+ ]+++-- 1 KiB payload: HTTP response header block+kiloBuilder :: Builder+kiloBuilder =+ mconcat+ [ word32BE 0x00000003+ , word32BE 0x00000400+ , byteStringCopy (BS.replicate 1016 0xCC)+ ]+++-- 4 KiB payload: typical HTTP response body chunk+fourKBuilder :: Builder+fourKBuilder =+ mconcat+ [ word32BE 0x00000004+ , word32BE 0x00001000+ , byteStringCopy (BS.replicate 4088 0xDD)+ ]+++-- 16 KiB payload: large message, exercises ring overflow+sixteenKBuilder :: Builder+sixteenKBuilder =+ mconcat+ [ word32BE 0x00000005+ , word32BE 0x00004000+ , byteStringCopy (BS.replicate 16376 0xEE)+ ]+++-- 64 KiB payload: stresses ring chunking+sixtyFourKBuilder :: Builder+sixtyFourKBuilder =+ mconcat+ [ word32BE 0x00000006+ , word32BE 0x00010000+ , byteStringCopy (BS.replicate 65528 0xFF)+ ]+++------------------------------------------------------------------------+-- Main+------------------------------------------------------------------------++main :: IO ()+main =+ withMagicRing (256 * 1024) $ \ring -> do+ let !base = ringBase ring+ !sz = ringSize ring+ !msk = ringMask ring+ t <- mkMemorySendTransport base sz msk++ let benchPair name builder =+ bgroup+ name+ [ bench "direct (RingSink)" $ nfIO (sendBuilderDirect t builder)+ , bench "via ByteString (old)" $ nfIO (sendBuilderViaByteString t builder)+ ]++ defaultMain+ [ benchPair "64 B" smallBuilder+ , benchPair "256 B" mediumBuilder+ , benchPair "1 KiB" kiloBuilder+ , benchPair "4 KiB" fourKBuilder+ , benchPair "16 KiB" sixteenKBuilder+ , benchPair "64 KiB" sixtyFourKBuilder+ ]
+ bench/WFBasic.hs view
@@ -0,0 +1,155 @@+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE BlockArguments #-}+{-# LANGUAGE OverloadedStrings #-}++{- | Wireform equivalents of flatparse's benchmark parsers.+These test the same parse patterns (sexp, long keyword, numeric csv,+lambda term) using wireform's combinator API.+-}+module WFBasic (+ runSexp,+ runLongws,+ runNumcsv,+ runTm,+) where++import Common (Tm (..))+import Data.ByteString (ByteString)+import Data.ByteString qualified as B+import Wireform.Parser+import Wireform.Parser.Driver (parseByteString)+import Wireform.Parser.Internal (Pure)+++type P = Parser Pure ()+++------------------------------------------------------------------------+-- sexp+------------------------------------------------------------------------++ws :: P ()+ws = skipMany (satisfyAscii (\c -> c == ' ' || c == '\n') *> pure ())+{-# INLINE ws #-}+++open :: P ()+open = word8 0x28 >> ws -- '('+{-# INLINE open #-}+++close :: P ()+close = word8 0x29 >> ws -- ')'+{-# INLINE close #-}+++ident :: P ()+ident = skipSome (skipSatisfyAscii isLatinLetter) >> ws+{-# INLINE ident #-}+++sexp :: P ()+sexp = branch open (skipSome sexp >> close) ident+++src :: P ()+src = sexp >> eof+++runSexp :: ByteString -> Either (ParseError ()) ()+runSexp = parseByteString src+++------------------------------------------------------------------------+-- long keyword+------------------------------------------------------------------------++longw :: P ()+longw = byteString "thisisalongkeyword"+{-# INLINE longw #-}+++longws :: P ()+longws = skipSome (longw >> ws) >> eof+++runLongws :: ByteString -> Either (ParseError ()) ()+runLongws = parseByteString longws+++------------------------------------------------------------------------+-- numeral csv+------------------------------------------------------------------------++numeral :: P ()+numeral = skipSome (skipSatisfyAscii isDigit) >> ws+{-# INLINE numeral #-}+++comma :: P ()+comma = word8 0x2C >> ws -- ','+{-# INLINE comma #-}+++numcsv :: P ()+numcsv = numeral >> skipMany (comma >> numeral) >> eof+++runNumcsv :: ByteString -> Either (ParseError ()) ()+runNumcsv = parseByteString numcsv+++------------------------------------------------------------------------+-- lambda term+------------------------------------------------------------------------++ident' :: P ByteString+ident' = byteStringOf (skipSome (skipSatisfyAscii \c -> isLatinLetter c || isDigit c)) <* ws+{-# INLINE ident' #-}+++equal, semi, dot, addOp, mulOp, parl, parr :: P ()+equal = byteString "=" >> ws+{-# INLINE equal #-}+semi = byteString ";" >> ws+{-# INLINE semi #-}+dot = byteString "." >> ws+{-# INLINE dot #-}+addOp = byteString "+" >> ws+{-# INLINE addOp #-}+mulOp = byteString "*" >> ws+{-# INLINE mulOp #-}+parl = byteString "(" >> ws+{-# INLINE parl #-}+parr = byteString ")" >> ws+{-# INLINE parr #-}+++add :: P Tm+add = chainl Add mul (addOp *> mul)+++mul :: P Tm+mul = chainl Mul spine (mulOp *> spine)+++spine :: P Tm+spine = chainl App atom atom+++atom :: P Tm+atom =+ (Int <$> (anyAsciiDecimalInt <* ws))+ <|> (Var <$> ident')+ <|> (parl *> tm <* parr)+++tm :: P Tm+tm =+ (byteString "fun" *> ws *> do x <- ident'; dot; t <- tm; pure (Lam x t))+ <|> (byteString "let" *> ws *> do x <- ident'; equal; t <- tm; semi; u <- tm; pure (Let x t u))+ <|> add+++runTm :: ByteString -> Either (ParseError ()) Tm+runTm = parseByteString (ws *> tm <* eof)
+ cbits/base64.c view
@@ -0,0 +1,293 @@+/*+ * RFC 4648 base64 encode / decode.+ *+ * Two SIMD-accelerated primitives, in line with the rest of+ * wireform-core:+ *+ * * 'hs_base64_encode': SSSE3 (via simde) inner loop, 12 input+ * bytes -> 16 output chars per iteration, scalar prologue /+ * epilogue for the tail.+ * * 'hs_base64_decode': SSE2 (via simde) pre-scan, 16 chars at a+ * time, rejects any window containing a high-bit byte (which+ * by construction is outside the base64 alphabet); the actual+ * sextet extraction then runs through a tight scalar loop on+ * a 256-entry decode table. This is the same shape every+ * wireform format takes for "validate-then-decode" SIMD: see+ * 'hs_proto_validate_utf8_fast' / 'hs_find_byte'.+ *+ * The encode SIMD path uses the well-known Mula / alfredklomp+ * formula (no LUT in memory; ASCII offset is derived from the+ * 6-bit value via a single PSHUFB on a 16-byte lookup).+ *+ * The decoder is kept scalar in the sextet-extraction step+ * because the typical wireform call site (SHA-1 = 28 base64+ * chars; proto3 JSON bytes fields = O(100s)) does not benefit+ * enough from a Mula-style PSHUFB classifier to be worth the+ * table-debugging cost. Encoding -- which is on the hot path+ * for big payloads via the proto3 JSON mapping -- stays SSSE3.+ */++#include <stdint.h>+#include <string.h>+#include <simde/x86/sse2.h>+#include <simde/x86/ssse3.h>++static const char b64_alphabet[64] =+ "ABCDEFGHIJKLMNOPQRSTUVWXYZ"+ "abcdefghijklmnopqrstuvwxyz"+ "0123456789+/";++/* Reverse-lookup table: ASCII -> 6-bit value, or 0xFF for any byte+ * outside the standard base64 alphabet. '=' maps to 0xFF too;+ * the tail handler checks for it explicitly. */+static const uint8_t b64_dec_table[256] = {+ /* 0x00..0x2A (43 entries) */+ 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,+ 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,+ 255,255,255,255,255,255,255,255,255,255,255,+ /* 0x2B '+' = 62, 0x2C ',' invalid, 0x2D '-' invalid,+ * 0x2E '.' invalid, 0x2F '/' = 63 */+ 62, 255, 255, 255, 63,+ /* 0x30..0x39 '0'..'9' = 52..61 */+ 52, 53, 54, 55, 56, 57, 58, 59, 60, 61,+ /* 0x3A..0x40 (7 entries) */+ 255, 255, 255, 255, 255, 255, 255,+ /* 0x41..0x5A 'A'..'Z' = 0..25 */+ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,+ 10, 11, 12, 13, 14, 15, 16, 17, 18, 19,+ 20, 21, 22, 23, 24, 25,+ /* 0x5B..0x60 (6 entries) */+ 255, 255, 255, 255, 255, 255,+ /* 0x61..0x7A 'a'..'z' = 26..51 */+ 26, 27, 28, 29, 30, 31, 32, 33, 34, 35,+ 36, 37, 38, 39, 40, 41, 42, 43, 44, 45,+ 46, 47, 48, 49, 50, 51,+ /* 0x7B..0xFF (133 entries) */+ 255, 255, 255, 255, 255,+ 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,+ 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,+ 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,+ 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,+ 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,+ 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,+ 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,+ 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255+};++/* ------------------------------------------------------------------+ * Length helpers (RFC 4648 sec 4).+ *+ * Encode: 3-byte triplets pad up to the next multiple of 4 chars.+ * Decode (upper bound): every 4 chars produce up to 3 bytes;+ * trailing '=' chars subtract one byte each. The exact decoded+ * length is reported by hs_base64_decode itself; this helper just+ * sizes the output buffer.+ * ------------------------------------------------------------------ */++int hs_base64_encoded_length(int in_len)+{+ if (in_len < 0) return 0;+ return ((in_len + 2) / 3) * 4;+}++int hs_base64_decoded_max_length(int in_len)+{+ if (in_len < 0) return 0;+ return (in_len / 4) * 3;+}++/* ------------------------------------------------------------------+ * Encode+ *+ * Returns the number of bytes written to @out. Always equals+ * hs_base64_encoded_length(in_len) (padded form).+ * ------------------------------------------------------------------ */++static inline simde__m128i enc_reshuffle(simde__m128i in)+{+ /* Spread each 3-byte triplet into a 4-byte slot so the 6-bit+ * fields land in adjacent bytes. */+ in = simde_mm_shuffle_epi8(in, simde_mm_setr_epi8(+ 1, 0, 2, 1,+ 4, 3, 5, 4,+ 7, 6, 8, 7,+ 10, 9, 11, 10));++ simde__m128i t0 = simde_mm_and_si128(in, simde_mm_set1_epi32(0x0fc0fc00));+ simde__m128i t1 = simde_mm_mulhi_epu16(t0, simde_mm_set1_epi32(0x04000040));+ simde__m128i t2 = simde_mm_and_si128(in, simde_mm_set1_epi32(0x003f03f0));+ simde__m128i t3 = simde_mm_mullo_epi16(t2, simde_mm_set1_epi32(0x01000010));+ return simde_mm_or_si128(t1, t3);+}++static inline simde__m128i enc_translate(simde__m128i in)+{+ /* 6-bit value v -> ASCII char via a 16-entry PSHUFB lookup of+ * the offset to add to v. Branchless. */+ simde__m128i lut = simde_mm_setr_epi8(+ 65, 71, -4, -4, -4, -4, -4, -4,+ -4, -4, -4, -4, -19, -16, 0, 0);+ simde__m128i indices = simde_mm_subs_epu8(in, simde_mm_set1_epi8(51));+ simde__m128i mask = simde_mm_cmpgt_epi8(in, simde_mm_set1_epi8(25));+ indices = simde_mm_sub_epi8(indices, mask);+ simde__m128i offsets = simde_mm_shuffle_epi8(lut, indices);+ return simde_mm_add_epi8(in, offsets);+}++int hs_base64_encode(const uint8_t *in, int in_len, uint8_t *out)+{+ int i = 0, j = 0;++ /* SIMD: 12 in -> 16 out per iter. Need 16 readable input bytes+ * (we load 16 then mask out the upper 4) so guard accordingly. */+ while (in_len - i >= 16) {+ simde__m128i chunk = simde_mm_loadu_si128((const simde__m128i *)(in + i));+ simde__m128i v = enc_reshuffle(chunk);+ simde__m128i c = enc_translate(v);+ simde_mm_storeu_si128((simde__m128i *)(out + j), c);+ i += 12;+ j += 16;+ }++ /* Scalar tail: 3 in -> 4 out. */+ while (in_len - i >= 3) {+ uint32_t v = ((uint32_t)in[i] << 16)+ | ((uint32_t)in[i+1] << 8)+ | (uint32_t)in[i+2];+ out[j+0] = (uint8_t)b64_alphabet[(v >> 18) & 0x3F];+ out[j+1] = (uint8_t)b64_alphabet[(v >> 12) & 0x3F];+ out[j+2] = (uint8_t)b64_alphabet[(v >> 6) & 0x3F];+ out[j+3] = (uint8_t)b64_alphabet[ v & 0x3F];+ i += 3;+ j += 4;+ }++ /* RFC 4648 sec 4 padding. */+ int rem = in_len - i;+ if (rem == 1) {+ uint32_t v = (uint32_t)in[i] << 16;+ out[j+0] = (uint8_t)b64_alphabet[(v >> 18) & 0x3F];+ out[j+1] = (uint8_t)b64_alphabet[(v >> 12) & 0x3F];+ out[j+2] = (uint8_t)'=';+ out[j+3] = (uint8_t)'=';+ j += 4;+ } else if (rem == 2) {+ uint32_t v = ((uint32_t)in[i] << 16) | ((uint32_t)in[i+1] << 8);+ out[j+0] = (uint8_t)b64_alphabet[(v >> 18) & 0x3F];+ out[j+1] = (uint8_t)b64_alphabet[(v >> 12) & 0x3F];+ out[j+2] = (uint8_t)b64_alphabet[(v >> 6) & 0x3F];+ out[j+3] = (uint8_t)'=';+ j += 4;+ }+ return j;+}++/* ------------------------------------------------------------------+ * Decode+ *+ * Strict RFC 4648 decoder. Input length must be a multiple of 4;+ * any out-of-alphabet byte (other than '=' in the trailing+ * position) produces -1. Returns the number of output bytes+ * written on success.+ * ------------------------------------------------------------------ */++/* SSE2 fast-path probe: does this 16-byte window contain any byte+ * with the high bit set? Any such byte is by construction outside+ * the base64 alphabet, so we can short-circuit. */+static inline int dec_window_high_bits(const uint8_t *in)+{+ simde__m128i chunk = simde_mm_loadu_si128((const simde__m128i *)in);+ return simde_mm_movemask_epi8(chunk);+}++int hs_base64_decode(const uint8_t *in, int in_len, uint8_t *out)+{+ if (in_len < 0 || (in_len & 3) != 0) return -1;+ if (in_len == 0) return 0;++ int i = 0, j = 0;+ int main_len = in_len - 4; /* tail quartet handled separately */++ /* SIMD pre-scan + scalar decode of the main body.+ *+ * The SIMD step rejects any 16-byte window containing a byte+ * with the high bit set (cheap PMOVMSKB + branch); the scalar+ * step then decodes 4 quartets = 12 bytes from each clean+ * window. This gives us the bulk of the SIMD win (early+ * rejection of garbage input) without the table-debugging+ * overhead of a full PSHUFB classifier. */+ while (main_len - i >= 16) {+ if (dec_window_high_bits(in + i) != 0) return -1;+ for (int k = 0; k < 16; k += 4) {+ uint8_t a = b64_dec_table[in[i+k+0]];+ uint8_t b = b64_dec_table[in[i+k+1]];+ uint8_t c = b64_dec_table[in[i+k+2]];+ uint8_t d = b64_dec_table[in[i+k+3]];+ if ((a | b | c | d) >= 64) return -1;+ uint32_t v = ((uint32_t)a << 18)+ | ((uint32_t)b << 12)+ | ((uint32_t)c << 6)+ | (uint32_t)d;+ out[j+0] = (uint8_t)(v >> 16);+ out[j+1] = (uint8_t)(v >> 8);+ out[j+2] = (uint8_t) v;+ j += 3;+ }+ i += 16;+ }++ /* Scalar middle: remaining full quartets before the tail. */+ while (i < main_len) {+ uint8_t a = b64_dec_table[in[i+0]];+ uint8_t b = b64_dec_table[in[i+1]];+ uint8_t c = b64_dec_table[in[i+2]];+ uint8_t d = b64_dec_table[in[i+3]];+ if ((a | b | c | d) >= 64) return -1;+ uint32_t v = ((uint32_t)a << 18)+ | ((uint32_t)b << 12)+ | ((uint32_t)c << 6)+ | (uint32_t)d;+ out[j+0] = (uint8_t)(v >> 16);+ out[j+1] = (uint8_t)(v >> 8);+ out[j+2] = (uint8_t) v;+ i += 4;+ j += 3;+ }++ /* Tail quartet: may contain '=' padding. */+ {+ uint8_t a = b64_dec_table[in[i+0]];+ uint8_t b = b64_dec_table[in[i+1]];+ if (a >= 64 || b >= 64) return -1;+ if (in[i+2] == (uint8_t)'=') {+ if (in[i+3] != (uint8_t)'=') return -1;+ uint32_t v = ((uint32_t)a << 18) | ((uint32_t)b << 12);+ out[j+0] = (uint8_t)(v >> 16);+ j += 1;+ } else if (in[i+3] == (uint8_t)'=') {+ uint8_t c = b64_dec_table[in[i+2]];+ if (c >= 64) return -1;+ uint32_t v = ((uint32_t)a << 18)+ | ((uint32_t)b << 12)+ | ((uint32_t)c << 6);+ out[j+0] = (uint8_t)(v >> 16);+ out[j+1] = (uint8_t)(v >> 8);+ j += 2;+ } else {+ uint8_t c = b64_dec_table[in[i+2]];+ uint8_t d = b64_dec_table[in[i+3]];+ if (c >= 64 || d >= 64) return -1;+ uint32_t v = ((uint32_t)a << 18)+ | ((uint32_t)b << 12)+ | ((uint32_t)c << 6)+ | (uint32_t)d;+ out[j+0] = (uint8_t)(v >> 16);+ out[j+1] = (uint8_t)(v >> 8);+ out[j+2] = (uint8_t) v;+ j += 3;+ }+ i += 4;+ }+ (void)i;+ return j;+}
+ cbits/fast_rng.c view
@@ -0,0 +1,154 @@+/*+ * xoshiro256++ — a fast, non-cryptographic 64-bit PRNG.+ *+ * Reference:+ * D. Blackman and S. Vigna, "Scrambled Linear Pseudorandom+ * Number Generators", ACM Trans. on Math. Software 47:4+ * (2021). https://vigna.di.unimi.it/ftp/papers/ScrambledLinear.pdf+ *+ * One 256-bit state per pthread, stored in '__thread' storage so+ * concurrent generation across many Haskell capabilities never+ * touches a shared cache line. Seeded on first use from+ * @getrandom(2)@.+ *+ * Used by 'wireform-websocket' to roll per-frame masking keys+ * without going through the global 'splitmix' MVar. Exposed+ * generically because the same primitive is what any+ * non-cryptographic random need on the hot path should reach+ * for.+ */++#include <stdint.h>+#include <stddef.h>+#include <string.h>+#include <unistd.h>++#if defined(__linux__)+#include <sys/syscall.h>+#include <linux/random.h>+static int kernel_random(void *buf, size_t len)+{+ long r = syscall(SYS_getrandom, buf, len, 0);+ return r == (long)len ? 0 : -1;+}+#elif defined(__APPLE__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__)+#include <stdlib.h>+static int kernel_random(void *buf, size_t len)+{+ arc4random_buf(buf, len);+ return 0;+}+#else+/* Fall back to /dev/urandom for portability. */+#include <fcntl.h>+static int kernel_random(void *buf, size_t len)+{+ int fd = open("/dev/urandom", O_RDONLY);+ if (fd < 0) return -1;+ size_t off = 0;+ while (off < len) {+ ssize_t n = read(fd, (char *)buf + off, len - off);+ if (n <= 0) { close(fd); return -1; }+ off += (size_t)n;+ }+ close(fd);+ return 0;+}+#endif++/* TLS access model for the per-thread state below.+ *+ * A file-local 'static __thread' defaults to the local-exec model: a+ * single FS/GS-relative MOV, the fastest TLS access (what the comment+ * below describes). That is only valid when the object lands in the+ * main executable's static TLS block. When the object is built+ * position-independent for a shared library — GHC's dynamic+ * ('.dyn_o' -> '.so') way, used whenever wireform-core sits in a+ * Template-Haskell dependency closure — local-exec / local-dynamic+ * emit 'R_X86_64_TPOFF32' / 'R_X86_64_DTPOFF32' relocations that ld+ * cannot link into the '.so' on x86_64-linux ("relocation truncated+ * to fit").+ *+ * '__PIC__' is defined by GCC / Clang exactly when compiling+ * position-independent (the '.dyn_o' way) and undefined for the plain+ * static '.o'. Keep the fast local-exec model for the static object;+ * only step down to initial-exec ('R_X86_64_GOTTPOFF', a single GOT+ * indirection that links into a '.so') when actually building PIC.+ * initial-exec keeps the symbol file-local (unlike global-dynamic,+ * which would require an exported symbol) and its few words of TLS sit+ * within glibc's static-TLS surplus when the library is dlopen'd. */+#if defined(__PIC__)+#define WF_TLS_MODEL __attribute__((tls_model("initial-exec")))+#else+#define WF_TLS_MODEL+#endif++/* Per-thread state. 256-bit (4 × uint64_t). '__thread' is the+ * GCC / Clang extension; on every modern Unix it lands in the+ * TLS block one offset away from FS / GS register, so the+ * dispatch is a single MOV (local-exec; see WF_TLS_MODEL above for+ * the position-independent / shared-object case). */+static __thread uint64_t xoshiro_state[4] WF_TLS_MODEL;+static __thread int xoshiro_seeded WF_TLS_MODEL = 0;++static inline uint64_t rotl64(uint64_t x, int k)+{+ return (x << k) | (x >> (64 - k));+}++/* Recover from a degenerate all-zero seed (xoshiro256++ has a+ * fixed point at 0). Vigna's reference splitmix64 is the+ * recommended way to expand a single 64-bit input into a 256-bit+ * xoshiro seed. We only invoke this when 'kernel_random'+ * returns all-zero, which on a working kernel never happens. */+static inline uint64_t splitmix64_step(uint64_t *x)+{+ *x += 0x9E3779B97F4A7C15ULL;+ uint64_t z = *x;+ z = (z ^ (z >> 30)) * 0xBF58476D1CE4E5B9ULL;+ z = (z ^ (z >> 27)) * 0x94D049BB133111EBULL;+ return z ^ (z >> 31);+}++static void xoshiro_seed(void)+{+ if (kernel_random(xoshiro_state, sizeof(xoshiro_state)) != 0+ || (xoshiro_state[0] | xoshiro_state[1]+ | xoshiro_state[2] | xoshiro_state[3]) == 0)+ {+ /* Last-resort fallback: tid + nanotime through splitmix. */+ uint64_t fallback = (uint64_t)(uintptr_t)&xoshiro_state;+ for (int i = 0; i < 4; i++) {+ xoshiro_state[i] = splitmix64_step(&fallback);+ }+ }+ xoshiro_seeded = 1;+}++/* The xoshiro256++ next() generator. Pure register work after+ * the (cached) TLS load. */+uint64_t hs_xoshiro256pp_next(void)+{+ if (!xoshiro_seeded) xoshiro_seed();++ const uint64_t *s = xoshiro_state;+ const uint64_t result = rotl64(s[0] + s[3], 23) + s[0];+ const uint64_t t = xoshiro_state[1] << 17;++ xoshiro_state[2] ^= xoshiro_state[0];+ xoshiro_state[3] ^= xoshiro_state[1];+ xoshiro_state[1] ^= xoshiro_state[2];+ xoshiro_state[0] ^= xoshiro_state[3];+ xoshiro_state[2] ^= t;+ xoshiro_state[3] = rotl64(xoshiro_state[3], 45);++ return result;+}++/* Force reseed. Useful after fork() and for tests that want+ * deterministic state — they can call this then immediately+ * overwrite the seed via hs_xoshiro256pp_reseed. */+void hs_xoshiro256pp_reseed(void)+{+ xoshiro_seeded = 0;+}
cbits/fast_scan.c view
@@ -49,3 +49,61 @@ return len; }++/*+ * In-place XOR of a 4-byte repeating mask over @buf[0..len).+ * Used by wireform-websocket for RFC 6455 sec 5.3 frame masking+ * (both directions: the parser un-masks an inbound payload; the+ * builder masks an outbound client payload).+ *+ * The mask repeats in 4-byte network-order chunks; for the SSE2+ * inner loop we broadcast that 32-bit pattern into a 16-byte+ * vector once and XOR 16 bytes per iteration. Scalar prologue ++ * epilogue handle any sub-16-byte head / tail.+ *+ * @mask is a uint32_t with the four mask bytes in NETWORK ORDER,+ * i.e. byte 0 of the mask in the high byte. The same byte+ * ordering the wire format uses.+ */+void hs_ws_mask(uint8_t *buf, int len, uint32_t mask)+{+ /* Build the byte ordering the loop needs: mask[0] at offset 0,+ * mask[1] at offset 1, … Easiest done by re-laying the bytes. */+ uint8_t mb[4];+ mb[0] = (uint8_t)(mask >> 24);+ mb[1] = (uint8_t)(mask >> 16);+ mb[2] = (uint8_t)(mask >> 8);+ mb[3] = (uint8_t) mask;++ int i = 0;++ /* Align the scalar prologue so the main loop can use 'movdqa'+ * once we hit a 16-byte boundary. The mask byte at offset i+ * is mb[i & 3] because the mask repeats every 4 bytes. */+ while (i < len && ((uintptr_t)(buf + i) & 15) != 0) {+ buf[i] ^= mb[i & 3];+ i++;+ }++ /* SSE2 main loop: 16 bytes per iteration. The 4-byte mask+ * tiled into a 16-byte vector once. If i hit alignment at an+ * offset where i & 3 != 0, the tile rotates accordingly. */+ if (i + 16 <= len) {+ int phase = i & 3;+ uint8_t tile[16];+ for (int k = 0; k < 16; k++) {+ tile[k] = mb[(phase + k) & 3];+ }+ simde__m128i v = simde_mm_loadu_si128((const simde__m128i *)tile);+ for (; i + 16 <= len; i += 16) {+ simde__m128i chunk = simde_mm_load_si128((const simde__m128i *)(buf + i));+ chunk = simde_mm_xor_si128(chunk, v);+ simde_mm_store_si128((simde__m128i *)(buf + i), chunk);+ }+ }++ /* Scalar epilogue for the trailing <16 bytes. */+ for (; i < len; i++) {+ buf[i] ^= mb[i & 3];+ }+}
+ cbits/magic_ring_linux.c view
@@ -0,0 +1,101 @@+#define _GNU_SOURCE+#include <sys/mman.h>+#include <sys/syscall.h>+#include <unistd.h>+#include <string.h>+#include <errno.h>+#include <stdint.h>+#include <stdlib.h>+#include "magic_ring.h"++static long cached_page_size = 0;++long hs_page_size(void) {+ if (cached_page_size == 0) {+ cached_page_size = sysconf(_SC_PAGESIZE);+ }+ return cached_page_size;+}++/* Round up to the next power of two >= val, with val >= page_size. */+static size_t next_pow2(size_t val) {+ val--;+ val |= val >> 1;+ val |= val >> 2;+ val |= val >> 4;+ val |= val >> 8;+ val |= val >> 16;+ val |= val >> 32;+ val++;+ return val;+}++/*+ * Allocate a double-mapped ring of at least `requested` bytes.+ * Returns 0 on success, -1 on failure (sets errno).+ * On success, out->base and out->size are populated.+ */+int hs_ring_create(size_t requested, struct hs_ring *out) {+ long ps = hs_page_size();+ size_t size;+ int fd;+ void *base, *m1, *m2;++ if (requested < (size_t)ps)+ requested = (size_t)ps;++ /* Round up to page-size multiple, then to power of two */+ size = (requested + (size_t)ps - 1) & ~((size_t)ps - 1);+ size = next_pow2(size);++ fd = (int)syscall(SYS_memfd_create, "wireform_ring", 1 /* MFD_CLOEXEC */);+ if (fd < 0)+ return -1;++ if (ftruncate(fd, (off_t)size) != 0) {+ close(fd);+ return -1;+ }++ /* Reserve 2N contiguous virtual addresses with no permissions */+ base = mmap(NULL, size * 2, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);+ if (base == MAP_FAILED) {+ close(fd);+ return -1;+ }++ /* Map the first copy */+ m1 = mmap(base, size, PROT_READ | PROT_WRITE,+ MAP_SHARED | MAP_FIXED, fd, 0);+ if (m1 == MAP_FAILED) {+ munmap(base, size * 2);+ close(fd);+ return -1;+ }++ /* Map the second copy immediately after */+ m2 = mmap((char *)base + size, size, PROT_READ | PROT_WRITE,+ MAP_SHARED | MAP_FIXED, fd, 0);+ if (m2 == MAP_FAILED) {+ munmap(base, size * 2);+ close(fd);+ return -1;+ }++ close(fd); /* mappings keep the memfd alive */++ /* Pre-fault every page to avoid latency surprises on first touch */+ memset(base, 0, size);++ out->base = base;+ out->size = size;+ return 0;+}++void hs_ring_destroy(struct hs_ring *ring) {+ if (ring && ring->base) {+ munmap(ring->base, ring->size * 2);+ ring->base = NULL;+ ring->size = 0;+ }+}
+ cbits/magic_ring_posix.c view
@@ -0,0 +1,115 @@+#include <sys/mman.h>+#include <fcntl.h>+#include <unistd.h>+#include <string.h>+#include <errno.h>+#include <stdint.h>+#include <stdlib.h>+#include <stdio.h>+#include <stdatomic.h>+#include "magic_ring.h"++static long cached_page_size = 0;++long hs_page_size(void) {+ if (cached_page_size == 0) {+ cached_page_size = sysconf(_SC_PAGESIZE);+ }+ return cached_page_size;+}++static size_t next_pow2(size_t val) {+ val--;+ val |= val >> 1;+ val |= val >> 2;+ val |= val >> 4;+ val |= val >> 8;+ val |= val >> 16;+ val |= val >> 32;+ val++;+ return val;+}++#if !defined(__FreeBSD__) || !defined(SHM_ANON)+static _Atomic long shm_counter = 0;+#endif++int hs_ring_create(size_t requested, struct hs_ring *out) {+ long ps = hs_page_size();+ size_t size;+ int fd;+ void *base, *m1, *m2;+ int saved_errno;++ if (requested < (size_t)ps)+ requested = (size_t)ps;++ size = (requested + (size_t)ps - 1) & ~((size_t)ps - 1);+ size = next_pow2(size);++#if defined(__FreeBSD__) && defined(SHM_ANON)+ fd = shm_open(SHM_ANON, O_RDWR, 0600);+#else+ {+ char name[64];+ snprintf(name, sizeof(name), "/wfring.%d.%ld",+ getpid(), atomic_fetch_add(&shm_counter, 1));+ fd = shm_open(name, O_RDWR | O_CREAT | O_EXCL, 0600);+ if (fd >= 0)+ shm_unlink(name);+ }+#endif++ if (fd < 0)+ return -1;++ if (ftruncate(fd, (off_t)size) != 0) {+ saved_errno = errno;+ close(fd);+ errno = saved_errno;+ return -1;+ }++ base = mmap(NULL, size * 2, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);+ if (base == MAP_FAILED) {+ saved_errno = errno;+ close(fd);+ errno = saved_errno;+ return -1;+ }++ m1 = mmap(base, size, PROT_READ | PROT_WRITE,+ MAP_SHARED | MAP_FIXED, fd, 0);+ if (m1 == MAP_FAILED) {+ saved_errno = errno;+ munmap(base, size * 2);+ close(fd);+ errno = saved_errno;+ return -1;+ }++ m2 = mmap((char *)base + size, size, PROT_READ | PROT_WRITE,+ MAP_SHARED | MAP_FIXED, fd, 0);+ if (m2 == MAP_FAILED) {+ saved_errno = errno;+ munmap(base, size * 2);+ close(fd);+ errno = saved_errno;+ return -1;+ }++ close(fd);+ memset(base, 0, size);++ out->base = base;+ out->size = size;+ return 0;+}++void hs_ring_destroy(struct hs_ring *ring) {+ if (ring && ring->base) {+ munmap(ring->base, ring->size * 2);+ ring->base = NULL;+ ring->size = 0;+ }+}
+ cbits/magic_ring_windows.c view
@@ -0,0 +1,103 @@+#ifdef _WIN32++#include <windows.h>+#include <memoryapi.h>+#include <stdint.h>+#include "magic_ring.h"++static long cached_page_size = 0;++long hs_page_size(void) {+ if (cached_page_size == 0) {+ SYSTEM_INFO si;+ GetSystemInfo(&si);+ cached_page_size = (long)si.dwAllocationGranularity;+ }+ return cached_page_size;+}++static size_t next_pow2(size_t val) {+ val--;+ val |= val >> 1;+ val |= val >> 2;+ val |= val >> 4;+ val |= val >> 8;+ val |= val >> 16;+#if defined(_WIN64)+ val |= val >> 32;+#endif+ val++;+ return val;+}++int hs_ring_create(size_t requested, struct hs_ring *out) {+ long ps = hs_page_size();+ size_t size;+ PVOID placeholder = NULL;+ HANDLE section = NULL;+ PVOID view1 = NULL, view2 = NULL;++ if (requested < (size_t)ps)+ requested = (size_t)ps;++ size = (requested + (size_t)ps - 1) & ~((size_t)ps - 1);+ size = next_pow2(size);++ placeholder = VirtualAlloc2(NULL, NULL, size * 2,+ MEM_RESERVE | MEM_RESERVE_PLACEHOLDER, PAGE_NOACCESS, NULL, 0);+ if (!placeholder)+ return -1;++ /* Split the placeholder into two halves */+ if (!VirtualFree(placeholder, size, MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER)) {+ VirtualFree(placeholder, 0, MEM_RELEASE);+ return -1;+ }++ section = CreateFileMappingW(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE,+ (DWORD)(size >> 32), (DWORD)(size & 0xFFFFFFFF), NULL);+ if (!section) {+ VirtualFree(placeholder, 0, MEM_RELEASE);+ VirtualFree((char *)placeholder + size, 0, MEM_RELEASE);+ return -1;+ }++ view1 = MapViewOfFile3(section, NULL, placeholder, 0, size,+ MEM_REPLACE_PLACEHOLDER, PAGE_READWRITE, NULL, 0);+ if (!view1)+ goto fail;++ view2 = MapViewOfFile3(section, NULL, (char *)placeholder + size, 0, size,+ MEM_REPLACE_PLACEHOLDER, PAGE_READWRITE, NULL, 0);+ if (!view2) {+ UnmapViewOfFile(view1);+ goto fail;+ }++ /* Pre-fault */+ memset(view1, 0, size);++ out->base = view1;+ out->size = size;+ out->section = section;+ return 0;++fail:+ CloseHandle(section);+ VirtualFree(placeholder, 0, MEM_RELEASE);+ VirtualFree((char *)placeholder + size, 0, MEM_RELEASE);+ return -1;+}++void hs_ring_destroy(struct hs_ring *ring) {+ if (ring && ring->base) {+ UnmapViewOfFile(ring->base);+ UnmapViewOfFile((char *)ring->base + ring->size);+ CloseHandle(ring->section);+ ring->base = NULL;+ ring->size = 0;+ ring->section = NULL;+ }+}++#endif /* _WIN32 */
+ include/magic_ring.h view
@@ -0,0 +1,18 @@+#ifndef WIREFORM_MAGIC_RING_H+#define WIREFORM_MAGIC_RING_H++#include <stddef.h>++struct hs_ring {+ void *base;+ size_t size;+#ifdef _WIN32+ void *section; /* HANDLE */+#endif+};++long hs_page_size(void);+int hs_ring_create(size_t requested, struct hs_ring *out);+void hs_ring_destroy(struct hs_ring *ring);++#endif
include/simde/simde/arm/neon/st1_lane.h view
@@ -522,3 +522,4 @@ HEDLEY_DIAGNOSTIC_POP #endif /* !defined(SIMDE_ARM_NEON_ST1_LANE_H) */+
+ src/Wireform/Base64.hs view
@@ -0,0 +1,141 @@+{-# LANGUAGE BangPatterns #-}++{- | RFC 4648 \u00a74 base64 encode / decode.++The implementation lives in @cbits\/base64.c@: an SSSE3 (via+simde) inner loop encoding 12 input bytes per iteration with a+scalar prologue \/ epilogue for the tail. The decoder takes the+same shape and additionally validates the input alphabet+character by character; non-alphabet bytes outside the trailing+@=@ padding produce 'Nothing'.++These primitives are kept here (rather than in a downstream+package) so every wireform format that needs base64 \u2014+WebSocket handshakes, BSON binary subtype 0x00, the proto3 JSON+mapping for @bytes@, the Avro JSON encoding, the Iceberg+@avro-data-files@ glue \u2014 shares one SIMD implementation+rather than each pulling its own @base64-bytestring@.++The encoder produces /padded/ standard base64; there is no+URL-safe variant. Add one in a separate module if needed.+-}+module Wireform.Base64 (+ -- * Encode+ encodeBase64,+ encodeBase64Length,++ -- * Decode+ decodeBase64,+ decodeBase64MaxLength,+) where++import Data.Bits ((.&.))+import Data.ByteString (ByteString)+import Data.ByteString qualified as BS+import Data.ByteString.Internal qualified as BSI+import Data.ByteString.Unsafe qualified as BSU+import Data.Word (Word8)+import Foreign.C.Types (CInt (..))+import Foreign.ForeignPtr (withForeignPtr)+import Foreign.Ptr (Ptr, castPtr, plusPtr)+import System.IO.Unsafe (unsafePerformIO)+++------------------------------------------------------------------------+-- FFI+------------------------------------------------------------------------++foreign import ccall unsafe "hs_base64_encoded_length"+ c_base64_encoded_length :: CInt -> CInt+++foreign import ccall unsafe "hs_base64_decoded_max_length"+ c_base64_decoded_max_length :: CInt -> CInt+++{- | Encoder \/ decoder run as 'IO' so the buffer-mutating side+effect is sequenced; declaring them as pure @CInt@-returning+functions confuses GHC's strictness analysis and the call can+be silently elided.+-}+foreign import ccall unsafe "hs_base64_encode"+ c_base64_encode :: Ptr Word8 -> CInt -> Ptr Word8 -> IO CInt+++foreign import ccall unsafe "hs_base64_decode"+ c_base64_decode :: Ptr Word8 -> CInt -> Ptr Word8 -> IO CInt+++------------------------------------------------------------------------+-- Length helpers+------------------------------------------------------------------------++{- | Number of base64 characters produced for an input of @n@ bytes+(including @=@ padding).+-}+encodeBase64Length :: Int -> Int+encodeBase64Length = fromIntegral . c_base64_encoded_length . fromIntegral+{-# INLINE encodeBase64Length #-}+++{- | Upper bound on the number of bytes produced when decoding @n@+input characters. The actual decoded length is shorter by the+number of trailing @=@ characters.+-}+decodeBase64MaxLength :: Int -> Int+decodeBase64MaxLength = fromIntegral . c_base64_decoded_max_length . fromIntegral+{-# INLINE decodeBase64MaxLength #-}+++------------------------------------------------------------------------+-- Encode+------------------------------------------------------------------------++{- | Encode a 'ByteString' to RFC 4648 \u00a74 base64 (with @=@+padding). One allocation of the exact output length, one+SIMD pass.+-}+encodeBase64 :: ByteString -> ByteString+encodeBase64 bs = unsafePerformIO $ do+ let !inLen = BS.length bs+ !outLen = encodeBase64Length inLen+ BSI.create outLen $ \outPtr ->+ BSU.unsafeUseAsCStringLen bs $ \(inPtr, _) -> do+ _ <- c_base64_encode (castPtr inPtr) (fromIntegral inLen) outPtr+ pure ()+{-# NOINLINE encodeBase64 #-}+++------------------------------------------------------------------------+-- Decode+------------------------------------------------------------------------++{- | Decode an RFC 4648 \u00a74 base64 'ByteString'. Returns+'Nothing' if the input is malformed: not a multiple of four+characters, contains an out-of-alphabet byte, or uses padding+in an invalid position.++Whitespace is /not/ tolerated; strip it before calling if your+input is line-wrapped (PEM, MIME, etc.).+-}+decodeBase64 :: ByteString -> Maybe ByteString+decodeBase64 bs+ | BS.null bs = Just BS.empty+ | (BS.length bs .&. 3) /= 0 = Nothing+ | otherwise = unsafePerformIO $ do+ let !inLen = BS.length bs+ !maxOut = decodeBase64MaxLength inLen+ (inFp, inOff, _) = BSI.toForeignPtr bs+ outFp <- BSI.mallocByteString maxOut+ actual <- withForeignPtr inFp $ \inBase ->+ withForeignPtr outFp $ \outPtr -> do+ n <-+ c_base64_decode+ (inBase `plusPtr` inOff)+ (fromIntegral inLen)+ outPtr+ pure (fromIntegral n :: Int)+ if actual < 0+ then pure Nothing+ else pure (Just (BSI.fromForeignPtr outFp 0 actual))+{-# NOINLINE decodeBase64 #-}
src/Wireform/Builder.hs view
@@ -55,34 +55,6 @@ @ -} module Wireform.Builder (- -- * Builder type- -- | A chunk-based byte builder. Combine fragments with @('<>')@.- -- Run with 'toStrictByteString', 'toLazyByteString', or 'hPutBuilder'.- Builder,-- -- * Running builders- -- | Convert a 'Builder' into output bytes or write directly to a handle.- toStrictByteString,- toLazyByteString,- hPutBuilder,- hPutBuilderLen,- hPutBuilderWith,-- -- * Performance tuning- -- | 'rebuild' resets GHC's inlining budget for a builder, which can- -- help when combining many small builders causes excessive code bloat.- rebuild,-- -- * Bounded / fixed primitives- primBounded,- primFixed,-- -- * ByteString to Builder- byteString,- byteStringInsert,- byteStringCopy,- byteStringThreshold,- -- * Single byte word8, int8,@@ -124,10 +96,12 @@ string7, -- * Builder internals (advanced)- -- | 'StreamSink' and 'withStreamTransform' allow interposing a- -- streaming transformation (e.g. compression) between the builder- -- and its output destination. See "Wireform.Builder.FastBuilder" for- -- the full internal API.++ {- | 'StreamSink' and 'withStreamTransform' allow interposing a+ streaming transformation (e.g. compression) between the builder+ and its output destination. See "Wireform.Builder.FastBuilder" for+ the full internal API.+ -} module Wireform.Builder.FastBuilder, ) where
src/Wireform/Builder/FastBuilder.hs view
@@ -46,6 +46,10 @@ withStreamTransform, runBuilderStreaming, + -- * Ring-direct sink (zero-copy builder → send ring)+ RingSinkState (..),+ finalizeRingSink,+ -- * Basic builders primBounded, primFixed,@@ -69,6 +73,7 @@ import Control.Concurrent.MVar import Control.Exception qualified as E import Control.Monad+import Data.Bits ((.&.)) import Data.ByteString qualified as S import Data.ByteString.Builder.Extra qualified as X import Data.ByteString.Builder.Prim qualified as P@@ -141,15 +146,18 @@ {-# INLINE mempty #-} mappend = (<>) {-# INLINE mappend #-}++ -- Don't be tempted to write `mconcat = fold` here — the @Foldable []@ -- instance defines `fold = mconcat` as an optimisation, so going -- through `fold` would bottom out in a value-level cycle and trip -- GHC's blackhole detection at runtime (<<loop>>). mconcat = foldr mappend mempty {-# INLINE mconcat #-}- {-# HLINT ignore "Use fold" #-} +{- HLINT ignore "Use fold" -}+ -- | 'fromString' = 'stringUtf8' instance IsString Builder where fromString = builderFromString@@ -162,26 +170,38 @@ DynamicSink !(IORef DynamicSink) | -- | Bytes are accumulated in a contiguous buffer. GrowingBuffer !(IORef (ForeignPtr Word8))- | -- | Bytes are first accumulated in the 'Queue', then flushed to the- -- 'IO.Handle'.+ | {- | Bytes are first accumulated in the 'Queue', then flushed to the+ 'IO.Handle'.+ -} HandleSink !IO.Handle !Int {-next buffer size-} !(IORef Queue)- | -- | The buffer has a known-exact size. No 'IORef', no growth.- -- Used by 'toStrictByteStringExact' when the output size is- -- known in advance. If the builder overflows, behaviour is- -- undefined (but in practice 'getBytes_' will 'error').+ | {- | The buffer has a known-exact size. No 'IORef', no growth.+ Used by 'toStrictByteStringExact' when the output size is+ known in advance. If the builder overflows, behaviour is+ undefined (but in practice 'getBytes_' will 'error').+ -} FixedBuffer- | -- | Bytes are accumulated in a buffer, then fed to a streaming- -- transform (compression, encryption, etc.) when the buffer fills.+ | {- | Bytes are accumulated in a buffer, then fed to a streaming+ transform (compression, encryption, etc.) when the buffer fills.+ -} StreamingSink !Int {-next buffer size-} !(IORef StreamQueue)+ | {- | Bytes are written directly into a double-mapped magic ring+ buffer (used by 'Wireform.Transport.Send.sendBuilderDirect').+ When the builder overflows, the already-written bytes are+ published to the ring consumer and the builder continues+ writing into newly-available ring space — no intermediate+ 'S.ByteString' allocation or memcpy.+ -}+ RingSink !(IORef RingSinkState) -- | Variable-destination cases. data DynamicSink = -- | Bytes are sent to another thread. ThreadedSink !(MVar Request) !(MVar Response)- | -- | Bytes are accumulated in a contiguous buffer until the- -- size limit is reached. After that, the destination switches- -- to a 'ThreadedSink'.+ | {- | Bytes are accumulated in a contiguous buffer until the+ size limit is reached. After that, the destination switches+ to a 'ThreadedSink'.+ -} BoundedGrowingBuffer {-# UNPACK #-} !(ForeignPtr Word8) !Int {-bound-} @@ -195,8 +215,9 @@ -} data StreamQueue = StreamQueue { sqBase :: {-# UNPACK #-} !(Ptr Word8)- -- ^ Buffer start, extracted once at init. The hot-path flush- -- reads this instead of dereferencing the 'ForeignPtr'.+ {- ^ Buffer start, extracted once at init. The hot-path flush+ reads this instead of dereferencing the 'ForeignPtr'.+ -} , sqFptr :: !(ForeignPtr Word8) -- ^ Kept alive for GC. Never dereferenced on the hot path. , sqCap :: !Int@@ -208,6 +229,70 @@ } +{- | Mutable state for the ring-direct sink.++The builder writes directly into the ring's double-mapped memory.+On overflow the handler first checks whether the ring already has+room (deferred publish). Only when the ring is actually full does+it publish ('rsPublishHead') and wait ('rsEnsureRoom'). This+batches writes so that a single 'sendBuilderDirect' call produces+at most one 'sendPublishHead' per ring-size worth of data, matching+the syscall count of the old materialize-then-copy path.++The callbacks are closed over the 'SendTransport' at construction+time (in "Wireform.Transport.Send"), so 'FastBuilder' does not+depend on the transport types.+-}+data RingSinkState = RingSinkState+ { rsHead :: {-# UNPACK #-} !Word64+ {- ^ Logical head: start of the current buffer region.+ Tracks the builder's logical write position; may be ahead+ of the last published head.+ -}+ , rsPublished :: {-# UNPACK #-} !Word64+ {- ^ Last position passed to 'rsPublishHead'. Everything+ before this has been handed off to the ring consumer.+ -}+ , rsBase :: {-# UNPACK #-} !(Ptr Word8)+ -- ^ Ring base address (first mapping).+ , rsMask :: {-# UNPACK #-} !Int+ -- ^ @ringSize - 1@.+ , rsRingSize :: {-# UNPACK #-} !Int+ -- ^ Physical ring size (N, not 2N).+ , rsChunkSize :: {-# UNPACK #-} !Int+ -- ^ Default chunk offered to the builder on overflow.+ , rsPublishHead :: !(Word64 -> IO ())+ -- ^ Publish the head position to the ring consumer.+ , rsEnsureRoom :: !(Word64 -> IO ())+ {- ^ Block until the ring has room for head to advance to this+ position. Throws on transport failure / close.+ -}+ , rsLoadTail :: !(IO Word64)+ {- ^ Read the consumer's current tail position. Used to check+ whether the ring has room without publishing first.+ -}+ }+++{- | Publish the final bytes written by a builder into the ring.++After 'runBuilder' returns a final cursor, call this to publish+the last unpublished bytes. Returns the final head position.+-}+finalizeRingSink :: IORef RingSinkState -> Ptr Word8 -> IO Word64+finalizeRingSink rsRef finalCur = do+ rs <- readIORef rsRef+ let !off = fromIntegral (rsHead rs) .&. rsMask rs+ !startPtr = rsBase rs `plusPtr` off+ !written = finalCur `minusPtr` startPtr+ !finalHead = rsHead rs + fromIntegral written+ -- Publish everything from the last-published position to+ -- the builder's final cursor in a single call.+ when (finalHead > rsPublished rs) $+ rsPublishHead rs finalHead+ pure finalHead++ {- | A streaming byte transform — middleware that processes chunks as a 'Builder' produces them. Used for compression, encryption, checksumming, or any incremental byte transform.@@ -237,23 +322,25 @@ -} data StreamSink = StreamSink { ssFeedRaw :: !(Ptr Word8 -> Int -> IO ())- -- ^ Feed a raw pointer region to the transform. The pointer- -- points into the builder's current buffer. The region is- -- valid for the duration of this call only — implementations- -- must consume or copy the data before returning.- --- -- @ssFeedRaw ptr len@ — @ptr@ is the start of the filled- -- region, @len@ is the number of valid bytes.+ {- ^ Feed a raw pointer region to the transform. The pointer+ points into the builder's current buffer. The region is+ valid for the duration of this call only — implementations+ must consume or copy the data before returning.++ @ssFeedRaw ptr len@ — @ptr@ is the start of the filled+ region, @len@ is the number of valid bytes.+ -} , ssFinish :: !(IO Builder)- -- ^ Flush the transform and return a 'Builder' that emits the- -- transformed output. This builder is run directly inside the- -- outer builder's buffer — no intermediate 'S.ByteString' is- -- materialised for the output either.- --- -- Typical implementation: collect compressed chunks in an- -- 'IORef', then return @mconcat (map byteStringCopy chunks)@.- -- Or for a single contiguous output buffer, return- -- @unsafeCStringLen (outPtr, outLen)@.+ {- ^ Flush the transform and return a 'Builder' that emits the+ transformed output. This builder is run directly inside the+ outer builder's buffer — no intermediate 'S.ByteString' is+ materialised for the output either.++ Typical implementation: collect compressed chunks in an+ 'IORef', then return @mconcat (map byteStringCopy chunks)@.+ Or for a single contiguous output buffer, return+ @unsafeCStringLen (outPtr, outLen)@.+ -} } @@ -280,12 +367,14 @@ Error E.SomeException | -- | The builder thread has completed. Done !(Ptr Word8)- | -- | The builder thread has finished generating one chunk,- -- and waits for another request with the specified minimum size.+ | {- | The builder thread has finished generating one chunk,+ and waits for another request with the specified minimum size.+ -} MoreBuffer !(Ptr Word8) !Int- | -- | The builder thread has partially filled the current chunk,- -- and wants to emit the bytestring to be included in the final- -- output.+ | {- | The builder thread has partially filled the current chunk,+ and wants to emit the bytestring to be included in the final+ output.+ -} InsertByteString !(Ptr Word8) !S.ByteString deriving (Show) @@ -416,8 +505,8 @@ -- 'BuilderState' is an unlifted tuple, so the @(\\s -> w1 (w0 s))@ / -- @(\\s -> s)@ continuations here can't go through @(.)@ or 'id' — -- those have lifted-kind type parameters and don't apply.-{-# HLINT ignore "Avoid lambda" #-}-{-# HLINT ignore "Use id" #-}+{- HLINT ignore "Avoid lambda" -}+{- HLINT ignore "Use id" -} instance Sem.Semigroup Write where Write s0 w0 <> Write s1 w1 = Write (s0 + s1) (\s -> w1 (w0 s)) @@ -956,6 +1045,50 @@ -- Reset cur to base — buffer is free after flush sq <- io $ readIORef sqRef setCur (sqBase sq)+ RingSink rsRef -> do+ cur <- getCur+ rs <- io $ readIORef rsRef+ let !off = fromIntegral (rsHead rs) .&. rsMask rs+ !startPtr = rsBase rs `plusPtr` off+ !written = cur `minusPtr` startPtr+ !newHead = rsHead rs + fromIntegral written+ !bsLen = S.length bstr+ !ringSz = rsRingSize rs+ -- Publish accumulated builder bytes before the BS copy.+ when (written > 0) $ io $ do+ when (newHead > rsPublished rs) $+ rsPublishHead rs newHead+ -- Copy the BS into the ring, chunking at ringSize if+ -- the BS is larger than the ring can hold contiguously.+ io $ S.unsafeUseAsCString bstr $ \src -> do+ let loop !srcOff !hd !remaining+ | remaining <= 0 = pure hd+ | otherwise = do+ let !chunk = min remaining ringSz+ !wh = hd + fromIntegral chunk+ rsEnsureRoom rs wh+ let !dstOff = fromIntegral hd .&. rsMask rs+ !dst = rsBase rs `plusPtr` dstOff+ copyBytes dst (castPtr src `plusPtr` srcOff) chunk+ rsPublishHead rs wh+ loop (srcOff + chunk) wh (remaining - chunk)+ afterBs <- loop 0 newHead bsLen+ let !pub = afterBs+ writeIORef+ rsRef+ rs+ { rsHead = afterBs+ , rsPublished = pub+ }+ -- Set up a fresh region for subsequent builder writes.+ rs' <- io $ readIORef rsRef+ let !contChunk = min ringSz (rsChunkSize rs')+ !contWant = rsHead rs' + fromIntegral contChunk+ io $ rsEnsureRoom rs' contWant+ let !contOff = fromIntegral (rsHead rs') .&. rsMask rs'+ !contPtr = rsBase rs' `plusPtr` contOff+ setCur contPtr+ setEnd (contPtr `plusPtr` contChunk) {-# NOINLINE byteStringInsert_ #-} @@ -1049,6 +1182,55 @@ } setCur newBase setEnd (newBase `plusPtr` I# n)+ RingSink rsRef -> do+ cur <- getCur+ rs <- io $ readIORef rsRef+ -- The ring provides at most ringSize contiguous bytes.+ -- ensureBytes requests larger than that are a programming+ -- error — use byteStringInsert (triggered automatically+ -- by byteString / byteStringThreshold) for large payloads.+ when (I# n > rsRingSize rs) $+ io $+ error $+ "Wireform.Builder: RingSink getBytes request ("+ ++ show (I# n)+ ++ ") exceeds ring size ("+ ++ show (rsRingSize rs)+ ++ "); use byteString/byteStringInsert for large payloads"+ let !off = fromIntegral (rsHead rs) .&. rsMask rs+ !startPtr = rsBase rs `plusPtr` off+ !written = cur `minusPtr` startPtr+ !newHead = rsHead rs + fromIntegral written+ -- Try to extend without publishing: the ring may already+ -- have room beyond what we initially reserved.+ let !chunkSz =+ min+ (rsRingSize rs)+ (max (I# n) (rsChunkSize rs))+ !wantHead = newHead + fromIntegral chunkSz+ tl <- io $ rsLoadTail rs+ let !ringCap = fromIntegral (rsRingSize rs) :: Word64+ published' <-+ if wantHead - tl <= ringCap+ then+ io $ pure (rsPublished rs)+ else do+ io $ do+ when (newHead > rsPublished rs) $+ rsPublishHead rs newHead+ rsEnsureRoom rs wantHead+ io $ pure newHead+ let !newOff = fromIntegral newHead .&. rsMask rs+ !newPtr = rsBase rs `plusPtr` newOff+ io $+ writeIORef+ rsRef+ rs+ { rsHead = newHead+ , rsPublished = published'+ }+ setCur newPtr+ setEnd (newPtr `plusPtr` chunkSz) {-# NOINLINE getBytes_ #-}
src/Wireform/Encode/Direct.hs view
@@ -1,69 +1,74 @@ {-# LANGUAGE BangPatterns #-} {-# OPTIONS_GHC -Wno-unused-top-binds #-}--- | Direct-write encoder: bypasses ByteString.Builder entirely.------ Uses offset-based writes to a pre-allocated buffer. The offset is--- an unboxed Int threaded through all operations, avoiding any heap--- allocation for the write context.------ For field writes (tag + value), uses single C FFI calls that--- handle the entire field in one shot, minimizing Haskell\/C--- call overhead.------ @--- import Proto.Encode.Direct (directEncode, dWord8, dVarint)------ let bytes = directEncode 3 (\\p off -> do--- off1 <- dWord8 p off 0x42--- dVarint p off1 12345)--- @-module Wireform.Encode.Direct- ( -- * Core- directEncode - -- * Offset-based write primitives (return new offset)- , dWord8- , dVarint- , dWord32LE- , dWord64LE- , dFloatLE- , dDoubleLE- , dBytes- , dText+{- | Direct-write encoder: bypasses ByteString.Builder entirely. - -- * Field-level writes (tag + value in one call)- , dVarintField- , dBoolField- , dStringField- , dBytesField- , dFixed32Field- , dFixed64Field- , dFloatField- , dDoubleField- ) where+Uses offset-based writes to a pre-allocated buffer. The offset is+an unboxed Int threaded through all operations, avoiding any heap+allocation for the write context. -import Data.Bits ((.&.), (.|.), shiftR)+For field writes (tag + value), uses single C FFI calls that+handle the entire field in one shot, minimizing Haskell\/C+call overhead.++@+import Proto.Encode.Direct (directEncode, dWord8, dVarint)++let bytes = directEncode 3 (\\p off -> do+ off1 <- dWord8 p off 0x42+ dVarint p off1 12345)+@+-}+module Wireform.Encode.Direct (+ -- * Core+ directEncode,++ -- * Offset-based write primitives (return new offset)+ dWord8,+ dVarint,+ dWord32LE,+ dWord64LE,+ dFloatLE,+ dDoubleLE,+ dBytes,+ dText,++ -- * Field-level writes (tag + value in one call)+ dVarintField,+ dBoolField,+ dStringField,+ dBytesField,+ dFixed32Field,+ dFixed64Field,+ dFloatField,+ dDoubleField,+) where++import Data.Bits (shiftR, (.&.), (.|.)) import Data.ByteString (ByteString)-import qualified Data.ByteString as BS-import qualified Data.ByteString.Internal as BSI+import Data.ByteString qualified as BS+import Data.ByteString.Internal qualified as BSI import Data.Text (Text)-import qualified Data.Text.Encoding as TE-import Data.Word (Word8, Word32, Word64)+import Data.Text.Encoding qualified as TE+import Data.Word (Word32, Word64, Word8) import Foreign.ForeignPtr (withForeignPtr)+import Foreign.Marshal.Utils (copyBytes) import Foreign.Ptr (Ptr, plusPtr) import Foreign.Storable (pokeByteOff)-import GHC.Float (castFloatToWord32, castDoubleToWord64)+import GHC.Float (castDoubleToWord64, castFloatToWord32)+import Wireform.FFI (encodeBoolFieldC, encodeLengthDelimitedC, encodeVarintFieldC) -import Wireform.FFI (encodeVarintFieldC, encodeBoolFieldC, encodeLengthDelimitedC) --- | Allocate a buffer of exactly @sz@ bytes, run the writer, return ByteString.--- The writer takes (Ptr Word8, offset) and returns the final offset.+{- | Allocate a buffer of exactly @sz@ bytes, run the writer, return ByteString.+The writer takes (Ptr Word8, offset) and returns the final offset.+-} directEncode :: Int -> (Ptr Word8 -> Int -> IO Int) -> ByteString directEncode !sz writer = BSI.unsafeCreate sz $ \ptr -> do _ <- writer ptr 0 pure () {-# INLINE directEncode #-} + -- Offset-based primitives: take (Ptr, offset), return new offset. dWord8 :: Ptr Word8 -> Int -> Word8 -> IO Int@@ -72,26 +77,31 @@ pure $! off + 1 {-# INLINE dWord8 #-} + dWord32LE :: Ptr Word8 -> Int -> Word32 -> IO Int dWord32LE !p !off !v = do pokeByteOff p off v pure $! off + 4 {-# INLINE dWord32LE #-} + dWord64LE :: Ptr Word8 -> Int -> Word64 -> IO Int dWord64LE !p !off !v = do pokeByteOff p off v pure $! off + 8 {-# INLINE dWord64LE #-} + dFloatLE :: Ptr Word8 -> Int -> Float -> IO Int dFloatLE !p !off !v = dWord32LE p off (castFloatToWord32 v) {-# INLINE dFloatLE #-} + dDoubleLE :: Ptr Word8 -> Int -> Double -> IO Int dDoubleLE !p !off !v = dWord64LE p off (castDoubleToWord64 v) {-# INLINE dDoubleLE #-} + -- | Write a varint at offset, return new offset. dVarint :: Ptr Word8 -> Int -> Word64 -> IO Int dVarint !p !off !n@@ -112,14 +122,16 @@ pure $! off + n' {-# INLINE dVarint #-} + -- | Write raw bytes at offset (no length prefix). Returns new offset. dBytes :: Ptr Word8 -> Int -> ByteString -> IO Int dBytes !p !off (BSI.BS fp len) = do withForeignPtr fp $ \src ->- BSI.memcpy (p `plusPtr` off) src len+ copyBytes (p `plusPtr` off) src len pure $! off + len {-# INLINE dBytes #-} + -- | Write text (length-prefixed UTF-8 bytes) at offset. dText :: Ptr Word8 -> Int -> Text -> IO Int dText !p !off !t = do@@ -129,6 +141,7 @@ dBytes p off1 bs {-# INLINE dText #-} + -- ============================================================ -- Field-level writes: tag + value in minimal calls -- ============================================================@@ -140,6 +153,7 @@ pure $! off + n {-# INLINE dVarintField #-} + -- | Bool field: tag_byte + 0/1. Always 2 bytes. dBoolField :: Ptr Word8 -> Int -> Word8 -> Bool -> IO Int dBoolField !p !off !tag !val = do@@ -147,6 +161,7 @@ pure $! off + 2 {-# INLINE dBoolField #-} + -- | String field: tag + varint(len) + UTF-8 bytes. Single C call. dStringField :: Ptr Word8 -> Int -> Word8 -> Text -> IO Int dStringField !p !off !tag !t = do@@ -156,6 +171,7 @@ pure $! off + n {-# INLINE dStringField #-} + -- | Bytes field: tag + varint(len) + raw bytes. Single C call. dBytesField :: Ptr Word8 -> Int -> Word8 -> ByteString -> IO Int dBytesField !p !off !tag (BSI.BS fp len) = do@@ -164,6 +180,7 @@ pure $! off + n {-# INLINE dBytesField #-} + -- | Fixed32 field: tag + 4 LE bytes. Always 5 bytes. dFixed32Field :: Ptr Word8 -> Int -> Word8 -> Word32 -> IO Int dFixed32Field !p !off !tag !val = do@@ -172,6 +189,7 @@ pure $! off + 5 {-# INLINE dFixed32Field #-} + -- | Fixed64 field: tag + 8 LE bytes. Always 9 bytes. dFixed64Field :: Ptr Word8 -> Int -> Word8 -> Word64 -> IO Int dFixed64Field !p !off !tag !val = do@@ -180,10 +198,12 @@ pure $! off + 9 {-# INLINE dFixed64Field #-} + -- | Float field: tag + 4 LE bytes. dFloatField :: Ptr Word8 -> Int -> Word8 -> Float -> IO Int dFloatField !p !off !tag !val = dFixed32Field p off tag (castFloatToWord32 val) {-# INLINE dFloatField #-}+ -- | Double field: tag + 8 LE bytes. dDoubleField :: Ptr Word8 -> Int -> Word8 -> Double -> IO Int
src/Wireform/FFI.hs view
@@ -50,6 +50,15 @@ findByte, findByteBS, + -- * SIMD 4-byte repeating-key XOR (WebSocket masking,++ -- RFC 6455 sec 5.3)+ xorRepeatingKey,+ xorRepeatingKeyBS,++ -- * Thread-local xoshiro256++ PRNG+ fastRandomWord64,+ -- * SIMD ASCII check (general purpose) isAscii, isAsciiBS,@@ -312,6 +321,75 @@ ------------------------------------------------------------------------+-- 4-byte repeating-key XOR (WebSocket masking)+------------------------------------------------------------------------++foreign import ccall unsafe "hs_ws_mask"+ c_ws_mask :: Ptr Word8 -> CInt -> Word32 -> IO ()+++{- | In-place XOR of the 4-byte repeating @key@ over @buf[0..len)@.++The key is interpreted in network byte order: byte 0 of the+key sits in the high byte of @key@. This matches RFC 6455 sec+5.3's masking-key layout.++Used by 'wireform-websocket' for frame masking; exposed+generically because the same primitive shows up in any protocol+that masks with a periodic 4-byte key. Implemented in+@cbits\/fast_scan.c@ as an SSE2 (via simde) loop processing+16 bytes per iteration with a tiled mask vector.+-}+xorRepeatingKey :: Ptr Word8 -> Int -> Word32 -> IO ()+xorRepeatingKey ptr len key = c_ws_mask ptr (fromIntegral len) key+{-# INLINE xorRepeatingKey #-}+++{- | 'ByteString' variant: XOR the 4-byte repeating key in-place+over the bytes the 'ByteString' references. The 'ByteString'+itself is shared with the caller, so the mutation is visible to+everyone else holding the same 'ByteString'. Callers must own+the backing memory exclusively at this point.+-}+xorRepeatingKeyBS :: ByteString -> Word32 -> IO ()+xorRepeatingKeyBS bs key = BSU.unsafeUseAsCStringLen bs $ \(ptr, len) ->+ c_ws_mask (castPtr ptr) (fromIntegral len) key+{-# INLINE xorRepeatingKeyBS #-}+++------------------------------------------------------------------------+-- Thread-local xoshiro256++ PRNG+------------------------------------------------------------------------++foreign import ccall unsafe "hs_xoshiro256pp_next"+ c_xoshiro256pp_next :: IO Word64+++{- | Pull a non-cryptographically-random 'Word64' from the calling+OS thread's xoshiro256++ generator.++State is per-OS-thread (@__thread@), seeded from @getrandom(2)@+(@arc4random_buf@ on BSDs, @\/dev\/urandom@ everywhere else) on+first use. Once seeded, each call is a handful of register-only+arithmetic ops — typically ~1 ns including the FFI boundary,+versus ~50 ns for the global @splitmix@ generator that takes an+@MVar@ on every call.++Caveat: because Haskell threads are multiplexed across OS+threads, the per-Haskell-thread sequence is not reproducible+(an HS thread may resume on a different capability and draw+from a different RNG stream). This is the right trade-off for+the non-deterministic-randomness needs that motivated the helper+(WebSocket frame masking, retry jitter, …); for reproducible+streams use a 'System.Random.Stateful' generator the caller+owns.+-}+fastRandomWord64 :: IO Word64+fastRandomWord64 = c_xoshiro256pp_next+{-# INLINE fastRandomWord64 #-}+++------------------------------------------------------------------------ -- SIMD ASCII check ------------------------------------------------------------------------ @@ -387,14 +465,14 @@ let !escPos = findJsonEscapeBS bs pos !safeLen = escPos - pos in ( if safeLen > 0- then B.byteString (BSU.unsafeTake safeLen (BSU.unsafeDrop pos bs))- else mempty+ then B.byteString (BSU.unsafeTake safeLen (BSU.unsafeDrop pos bs))+ else mempty )- <> if escPos >= len- then mempty- else- let !b = BSU.unsafeIndex bs escPos- in escByte b <> go (escPos + 1)+ <> if escPos >= len+ then mempty+ else+ let !b = BSU.unsafeIndex bs escPos+ in escByte b <> go (escPos + 1) escByte :: Word8 -> B.Builder escByte 0x22 = B.byteString "\\\"" -- "
src/Wireform/Hash.hs view
@@ -1,49 +1,54 @@ {-# LANGUAGE BangPatterns #-}--- | Shared C/SIMDe-backed hash and Roaring-bitmap kernels used across the--- wireform format packages.------ - 'murmur3_32' / 'bucketLong' — byte-compatible with Apache Iceberg's--- @org.apache.iceberg.util.BucketUtil@ (used both by the Iceberg--- @bucket[N]@ partition transform and by the Parquet bloom filter's--- feeder hash variants in some integrations).--- - 'xxh64' — XXH64 with a caller-supplied seed, byte-exact against the--- upstream xxHash 0.6.x reference. This is the hash function Parquet's--- split-block bloom filter uses (seed @0@) and that Iceberg manifest--- tooling uses for content-addressing.--- - 'roaring*' — portable Roaring 32-bit container kernels used by Iceberg--- V3 deletion vectors and (potentially) Parquet column-index null pages.------ The implementations live in @cbits/wireform_hash_simd.c@. All entry--- points are 'unsafePerformIO' / 'unsafeDupablePerformIO' wrapped so--- callers don't think about IO.-module Wireform.Hash- ( -- * Murmur3 32-bit (Iceberg @BucketUtil@)- murmur3_32- , bucketLong- , bucketBytes- -- * XXH64- , xxh64- -- * Roaring 32-bit container- , roaringDecodeArray- , roaringDecodeBitset- , roaringContains- , roaringEncodeArray- , roaringEncodeBitset- , RoaringContainerKind(..)- ) where +{- | Shared C/SIMDe-backed hash and Roaring-bitmap kernels used across the+wireform format packages.++- 'murmur3_32' / 'bucketLong' — byte-compatible with Apache Iceberg's+ @org.apache.iceberg.util.BucketUtil@ (used both by the Iceberg+ @bucket[N]@ partition transform and by the Parquet bloom filter's+ feeder hash variants in some integrations).+- 'xxh64' — XXH64 with a caller-supplied seed, byte-exact against the+ upstream xxHash 0.6.x reference. This is the hash function Parquet's+ split-block bloom filter uses (seed @0@) and that Iceberg manifest+ tooling uses for content-addressing.+- 'roaring*' — portable Roaring 32-bit container kernels used by Iceberg+ V3 deletion vectors and (potentially) Parquet column-index null pages.++The implementations live in @cbits/wireform_hash_simd.c@. All entry+points are 'unsafePerformIO' / 'unsafeDupablePerformIO' wrapped so+callers don't think about IO.+-}+module Wireform.Hash (+ -- * Murmur3 32-bit (Iceberg @BucketUtil@)+ murmur3_32,+ bucketLong,+ bucketBytes,++ -- * XXH64+ xxh64,++ -- * Roaring 32-bit container+ roaringDecodeArray,+ roaringDecodeBitset,+ roaringContains,+ roaringEncodeArray,+ roaringEncodeBitset,+ RoaringContainerKind (..),+) where+ import Data.Bits ((.&.))-import qualified Data.ByteString as BS-import qualified Data.ByteString.Internal as BSI+import Data.ByteString qualified as BS+import Data.ByteString.Internal qualified as BSI import Data.ByteString.Unsafe (unsafeUseAsCStringLen) import Data.Int (Int32, Int64)-import qualified Data.Vector.Storable as VS-import qualified Data.Vector.Storable.Mutable as VSM+import Data.Vector.Storable qualified as VS+import Data.Vector.Storable.Mutable qualified as VSM import Data.Word (Word16, Word32, Word64, Word8) import Foreign.ForeignPtr (withForeignPtr) import Foreign.Ptr (Ptr, castPtr) import System.IO.Unsafe (unsafeDupablePerformIO, unsafePerformIO) + -- ============================================================ -- C entry points -- ============================================================@@ -51,27 +56,35 @@ foreign import ccall unsafe "hs_wf_murmur3_32" c_murmur3_32 :: Ptr Word8 -> Int32 -> Int32 + foreign import ccall unsafe "hs_wf_bucket_long" c_bucket_long :: Int64 -> Int32 -> Int32 + foreign import ccall unsafe "hs_wf_xxh64" c_xxh64 :: Ptr Word8 -> Int64 -> Word64 -> Word64 + foreign import ccall unsafe "hs_wf_roaring_decode_array" c_roaring_decode_array :: Ptr Word8 -> Int32 -> Word32 -> Ptr Int32 -> Int32 + foreign import ccall unsafe "hs_wf_roaring_decode_bitset" c_roaring_decode_bitset :: Ptr Word8 -> Word32 -> Ptr Int32 -> Int32 + foreign import ccall unsafe "hs_wf_roaring_contains" c_roaring_contains :: Int32 -> Ptr Word8 -> Int32 -> Word16 -> Int32 + foreign import ccall unsafe "hs_wf_roaring_encode_array" c_roaring_encode_array :: Ptr Word16 -> Int32 -> Ptr Word8 -> Int32 + foreign import ccall unsafe "hs_wf_roaring_encode_bitset" c_roaring_encode_bitset :: Ptr Word16 -> Int32 -> Ptr Word8 -> IO () + -- ============================================================ -- Public wrappers -- ============================================================@@ -83,27 +96,32 @@ pure $! c_murmur3_32 (castPtr p) (fromIntegral len) {-# INLINE murmur3_32 #-} --- | Iceberg @bucket[N]@ partition transform on a 64-bit signed integer.--- The C kernel inlines the 8-byte little-endian serialisation, the--- murmur3 hash, and the @& Integer.MAX_VALUE % N@ reduction.++{- | Iceberg @bucket[N]@ partition transform on a 64-bit signed integer.+The C kernel inlines the 8-byte little-endian serialisation, the+murmur3 hash, and the @& Integer.MAX_VALUE % N@ reduction.+-} bucketLong :: Int -> Int64 -> Int bucketLong n v- | n <= 0 = 0+ | n <= 0 = 0 | otherwise = fromIntegral (c_bucket_long v (fromIntegral n)) {-# INLINE bucketLong #-} --- | Iceberg @bucket[N]@ on raw bytes (binary / fixed / uuid / decimal /--- string columns once UTF-8-encoded). Hash is murmur3-32 over the bytes--- with seed 0, then @(hash & Integer.MAX_VALUE) % N@.++{- | Iceberg @bucket[N]@ on raw bytes (binary / fixed / uuid / decimal /+string columns once UTF-8-encoded). Hash is murmur3-32 over the bytes+with seed 0, then @(hash & Integer.MAX_VALUE) % N@.+-} bucketBytes :: Int -> BS.ByteString -> Int bucketBytes n bs- | n <= 0 = 0+ | n <= 0 = 0 | otherwise =- let !h = murmur3_32 bs+ let !h = murmur3_32 bs !hu = fromIntegral h .&. (0x7FFFFFFF :: Word32)- in fromIntegral hu `mod` n+ in fromIntegral hu `mod` n {-# INLINE bucketBytes #-} + -- | XXH64 with caller-supplied seed. xxh64 :: Word64 -> BS.ByteString -> Word64 xxh64 seed bs = unsafePerformIO $@@ -111,6 +129,7 @@ pure $! c_xxh64 (castPtr p) (fromIntegral len) seed {-# INLINE xxh64 #-} + -- ============================================================ -- Roaring containers -- ============================================================@@ -118,12 +137,17 @@ data RoaringContainerKind = ArrayContainer | BitsetContainer deriving (Show, Eq) --- | Decode a Roaring ARRAY container payload into a vector of 32-bit--- positions, OR'd with @hi << 16@.++{- | Decode a Roaring ARRAY container payload into a vector of 32-bit+positions, OR'd with @hi << 16@.+-} roaringDecodeArray- :: BS.ByteString -- ^ Raw payload (cardinality * 2 bytes).- -> Int -- ^ Cardinality.- -> Word32 -- ^ High 32 bits to OR onto each position.+ :: BS.ByteString+ -- ^ Raw payload (cardinality * 2 bytes).+ -> Int+ -- ^ Cardinality.+ -> Word32+ -- ^ High 32 bits to OR onto each position. -> VS.Vector Int32 roaringDecodeArray bs card hi = unsafeDupablePerformIO $ do mv <- VSM.unsafeNew card@@ -134,11 +158,15 @@ VS.unsafeFreeze mv {-# INLINE roaringDecodeArray #-} --- | Decode a Roaring BITSET container payload (exactly 8192 bytes) into--- a vector of set positions, OR'd with @hi << 16@.++{- | Decode a Roaring BITSET container payload (exactly 8192 bytes) into+a vector of set positions, OR'd with @hi << 16@.+-} roaringDecodeBitset- :: BS.ByteString -- ^ 8192 raw bytes.- -> Word32 -- ^ High 32 bits.+ :: BS.ByteString+ -- ^ 8192 raw bytes.+ -> Word32+ -- ^ High 32 bits. -> VS.Vector Int32 roaringDecodeBitset bs hi = unsafeDupablePerformIO $ do mv <- VSM.unsafeNew 65536@@ -149,11 +177,13 @@ VS.unsafeFreeze (VSM.unsafeSlice 0 n mv) {-# INLINE roaringDecodeBitset #-} + -- | Test whether a 16-bit low value is present in a Roaring container. roaringContains :: RoaringContainerKind -> BS.ByteString- -> Int -- ^ Cardinality (only used for ARRAY).+ -> Int+ -- ^ Cardinality (only used for ARRAY). -> Word16 -> Bool roaringContains kind bs card v = unsafePerformIO $@@ -162,22 +192,27 @@ in pure $! c_roaring_contains k (castPtr p) (fromIntegral card) v /= 0 {-# INLINE roaringContains #-} --- | Encode a sorted vector of 16-bit positions as a Roaring ARRAY--- container payload.++{- | Encode a sorted vector of 16-bit positions as a Roaring ARRAY+container payload.+-} roaringEncodeArray :: VS.Vector Word16 -> BS.ByteString roaringEncodeArray vs = unsafePerformIO $ do let !len = VS.length vs- !sz = len * 2+ !sz = len * 2 fp <- BSI.mallocByteString sz withForeignPtr fp $ \dst -> VS.unsafeWith vs $ \src ->- do _ <- pure $! c_roaring_encode_array src (fromIntegral len) dst- pure ()+ do+ _ <- pure $! c_roaring_encode_array src (fromIntegral len) dst+ pure () pure (BSI.BS fp sz) {-# INLINE roaringEncodeArray #-} --- | Encode a sorted vector of 16-bit positions as a Roaring BITSET--- container payload (exactly 8192 bytes).++{- | Encode a sorted vector of 16-bit positions as a Roaring BITSET+container payload (exactly 8192 bytes).+-} roaringEncodeBitset :: VS.Vector Word16 -> BS.ByteString roaringEncodeBitset vs = unsafePerformIO $ do fp <- BSI.mallocByteString 8192
+ src/Wireform/Parser.hs view
@@ -0,0 +1,1221 @@+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE BlockArguments #-}+{-# LANGUAGE CPP #-}+{-# LANGUAGE MagicHash #-}+{-# LANGUAGE PatternSynonyms #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE UnboxedSums #-}+{-# LANGUAGE UnboxedTuples #-}++-- | Fast streaming parser with flatparse-grade inner-loop performance.+module Wireform.Parser (+ -- * Parser type+ Parser,++ -- * Byte primitives+ anyWord8,+ anyWord8_,+ anyWord16,+ anyWord32,+ anyWord64,+ anyWord16le,+ anyWord32le,+ anyWord64le,+ anyWord16be,+ anyWord32be,+ anyWord64be,+ anyWord16_,+ anyWord32_,+ anyWord64_,+ anyFloatle,+ anyFloatbe,+ anyDoublele,+ anyDoublebe,++ -- * CPS byte primitives (avoid intermediate boxing)+ withAnyWord8,+ withAnyWord16,+ withAnyWord32,+ withAnyWord64,+ withSatisfyAscii,++ -- * Byte matching+ word8,+ byteString,++ -- * ByteString operations+ takeBs,+ takeBsCopy,+ skip,+ takeRest,+ skipBack,++ -- * Signed integers+ anyInt8,+ anyInt16,+ anyInt32,+ anyInt64,+ anyInt16le,+ anyInt32le,+ anyInt64le,+ anyInt16be,+ anyInt32be,+ anyInt64be,++ -- * UTF-8 characters+ anyChar,+ anyChar_,+ satisfy,+ satisfy_,+ satisfyAscii,+ satisfyAscii_,+ skipSatisfyAscii,+ fusedSatisfy,++ -- * UTF-8 characters (additional)+ anyAsciiChar,+ skipAnyAsciiChar,++ -- * Character classes+ isDigit,+ isLatinLetter,+ isGreekLetter,++ -- * ASCII numeric+ anyAsciiDecimalWord,+ anyAsciiDecimalInt,+ anyAsciiDecimalInteger,+ anyAsciiHexWord,+ anyAsciiHexInt,++ -- * Control flow+ (<|>),+ empty,+ branch,+ lookahead,+ fails,+ try,+ optional,+ optional_,+ many_,+ some_,+ many,+ some,+ skipMany,+ skipSome,+ withOption,+ chainl,+ chainr,+ notFollowedBy,+ isolate,++ -- * Error handling+ err,+ cut,+ cutting,+ ensureNOrEof,+ withError,++ -- * Position and span+ Pos (..),+ Span (..),+ getPos,+ withSpan,+ byteStringOf,+ spanToByteString,+ withByteString,+ inSpan,+ subPos,++ -- * Marks+ Mark,+ mark,+ restore,+ release,++ -- * Low-level+ ensureN#,+ checkpoint,+ eof,+ atEnd,+ remaining,++ -- * Re-exports+ module Wireform.Parser.Error,+) where++import Control.Monad (when)+import Data.ByteString (ByteString)+import Data.ByteString qualified as BS+import Data.ByteString.Internal qualified as BSI+import Data.Int+import Data.Word+import Foreign.Marshal.Utils (copyBytes)+import GHC.Exts+import GHC.Float (castWord32ToFloat, castWord64ToDouble)+import GHC.ForeignPtr (ForeignPtr (..), mallocPlainForeignPtrBytes, unsafeWithForeignPtr)+import GHC.IO (IO (..))+import GHC.Word (Word16 (..), Word32 (..), Word64 (..), Word8 (..))+import System.IO.Unsafe (unsafeDupablePerformIO)+import Wireform.Parser.Error+import Wireform.Parser.Internal+import Wireform.Parser.Mark+import Wireform.Parser.Position+++------------------------------------------------------------------------+-- Internal: withEnsure# (bounds check then run continuation)+------------------------------------------------------------------------++{- | Ensure @n#@ bytes available, then run the body.++Fast path (1 register comparison, no memory access).+Semi-fast path: re-read 'peEndPtr' to catch stale @eob@ after a+prior streaming resume; avoids the NOINLINE 'ensureNSlow' call.+Slow path: genuine suspension via 'onEnsureFail'.+-}+withEnsure# :: forall m e a. ParserMode m => Int# -> Parser m e a -> Parser m e a+withEnsure# n# (Parser p) = Parser \env eob s st ->+ case n# <=# minusAddr# eob s of+ 1# -> p env eob s st+ _ -> case readEnd# env st of+ (# st', eob' #) -> case n# <=# minusAddr# eob' s of+ 1# -> p env eob' s st'+ _ -> case onEnsureFail @m env eob' s n# st' of+ (# st'', OK# _ s'' #) -> case readEnd# env st'' of+ (# st''', eob'' #) -> p env eob'' s'' st'''+ (# st'', x #) -> (# st'', unsafeCoerce# x #)+{-# INLINE withEnsure# #-}+++------------------------------------------------------------------------+-- CPS byte primitives (match flatparse's withAnyWord8 etc)+------------------------------------------------------------------------++{- | Read one byte and pass it to the continuation. Most efficient+form — avoids allocating an intermediate @Word8@ on the heap.+-}+withAnyWord8 :: ParserMode m => (Word8 -> Parser m e r) -> Parser m e r+withAnyWord8 p = withEnsure# 1# $ Parser \env eob s st ->+ case indexWord8OffAddr# s 0# of+ w# -> runParser# (p (W8# w#)) env eob (plusAddr# s 1#) st+{-# INLINE withAnyWord8 #-}+++withAnyWord16 :: ParserMode m => (Word16 -> Parser m e r) -> Parser m e r+withAnyWord16 p = withEnsure# 2# $ Parser \env eob s st ->+ case indexWord16OffAddr# s 0# of+ w# -> runParser# (p (W16# w#)) env eob (plusAddr# s 2#) st+{-# INLINE withAnyWord16 #-}+++withAnyWord32 :: ParserMode m => (Word32 -> Parser m e r) -> Parser m e r+withAnyWord32 p = withEnsure# 4# $ Parser \env eob s st ->+ case indexWord32OffAddr# s 0# of+ w# -> runParser# (p (W32# w#)) env eob (plusAddr# s 4#) st+{-# INLINE withAnyWord32 #-}+++withAnyWord64 :: ParserMode m => (Word64 -> Parser m e r) -> Parser m e r+withAnyWord64 p = withEnsure# 8# $ Parser \env eob s st ->+ case indexWord64OffAddr# s 0# of+ w# -> runParser# (p (W64# w#)) env eob (plusAddr# s 8#) st+{-# INLINE withAnyWord64 #-}+++------------------------------------------------------------------------+-- Non-CPS byte primitives+------------------------------------------------------------------------++anyWord8 :: ParserMode m => Parser m e Word8+anyWord8 = withAnyWord8 pure+{-# INLINE anyWord8 #-}+++anyWord8_ :: ParserMode m => Parser m e ()+anyWord8_ = withEnsure# 1# $ Parser \_ _ s st ->+ (# st, OK# () (plusAddr# s 1#) #)+{-# INLINE anyWord8_ #-}+++anyWord16 :: ParserMode m => Parser m e Word16+anyWord16 = withAnyWord16 pure+{-# INLINE anyWord16 #-}+++anyWord32 :: ParserMode m => Parser m e Word32+anyWord32 = withAnyWord32 pure+{-# INLINE anyWord32 #-}+++anyWord64 :: ParserMode m => Parser m e Word64+anyWord64 = withAnyWord64 pure+{-# INLINE anyWord64 #-}+++-- Skip variants+anyWord16_ :: ParserMode m => Parser m e ()+anyWord16_ = withEnsure# 2# $ Parser \_ _ s st -> (# st, OK# () (plusAddr# s 2#) #)+{-# INLINE anyWord16_ #-}+++anyWord32_ :: ParserMode m => Parser m e ()+anyWord32_ = withEnsure# 4# $ Parser \_ _ s st -> (# st, OK# () (plusAddr# s 4#) #)+{-# INLINE anyWord32_ #-}+++anyWord64_ :: ParserMode m => Parser m e ()+anyWord64_ = withEnsure# 8# $ Parser \_ _ s st -> (# st, OK# () (plusAddr# s 8#) #)+{-# INLINE anyWord64_ #-}++------------------------------------------------------------------------+-- Endianness variants+------------------------------------------------------------------------++#if defined(WORDS_BIGENDIAN)+anyWord16le = withAnyWord16 (pure . byteSwap16)+anyWord32le = withAnyWord32 (pure . byteSwap32)+anyWord64le = withAnyWord64 (pure . byteSwap64)+anyWord16be = anyWord16+anyWord32be = anyWord32+anyWord64be = anyWord64+#else+anyWord16le :: ParserMode m => Parser m e Word16+anyWord16le = anyWord16+{-# INLINE anyWord16le #-}+anyWord32le :: ParserMode m => Parser m e Word32+anyWord32le = anyWord32+{-# INLINE anyWord32le #-}+anyWord64le :: ParserMode m => Parser m e Word64+anyWord64le = anyWord64+{-# INLINE anyWord64le #-}+anyWord16be :: ParserMode m => Parser m e Word16+anyWord16be = withAnyWord16 (pure . byteSwap16)+{-# INLINE anyWord16be #-}+anyWord32be :: ParserMode m => Parser m e Word32+anyWord32be = withAnyWord32 (pure . byteSwap32)+{-# INLINE anyWord32be #-}+anyWord64be :: ParserMode m => Parser m e Word64+anyWord64be = withAnyWord64 (pure . byteSwap64)+{-# INLINE anyWord64be #-}+#endif+++------------------------------------------------------------------------+-- Floating-point+------------------------------------------------------------------------++anyFloatle :: ParserMode m => Parser m e Float+anyFloatle = castWord32ToFloat <$> anyWord32le+{-# INLINE anyFloatle #-}+++anyFloatbe :: ParserMode m => Parser m e Float+anyFloatbe = castWord32ToFloat <$> anyWord32be+{-# INLINE anyFloatbe #-}+++anyDoublele :: ParserMode m => Parser m e Double+anyDoublele = castWord64ToDouble <$> anyWord64le+{-# INLINE anyDoublele #-}+++anyDoublebe :: ParserMode m => Parser m e Double+anyDoublebe = castWord64ToDouble <$> anyWord64be+{-# INLINE anyDoublebe #-}+++------------------------------------------------------------------------+-- Byte matching+------------------------------------------------------------------------++word8 :: ParserMode m => Word8 -> Parser m e ()+word8 (W8# expected) = withEnsure# 1# $ Parser \_ _ s st ->+ case eqWord8# (indexWord8OffAddr# s 0#) expected of+ 1# -> (# st, OK# () (plusAddr# s 1#) #)+ _ -> (# st, Fail# #)+{-# INLINE word8 #-}+++-- | Match a literal 'ByteString'. Uses word-at-a-time comparison.+byteString :: ByteString -> Parser m e ()+byteString bs = Parser \env eob s st ->+ let !(BSI.BS _ (I# len#)) = bs+ in case len# <=# minusAddr# eob s of+ 1# -> case memcmpAddr# s (bsAddr# bs) len# of+ 0# -> (# st, OK# () (plusAddr# s len#) #)+ _ -> (# st, Fail# #)+ _ -> case ensureNSlow env eob s len# st of+ (# st', OK# _ s' #) -> case readEnd# env st' of+ (# st'', _ #) ->+ case memcmpAddr# s' (bsAddr# bs) len# of+ 0# -> (# st'', OK# () (plusAddr# s' len#) #)+ _ -> (# st'', Fail# #)+ (# st', x #) -> (# st', unsafeCoerce# x #)+{-# INLINE byteString #-}+++bsAddr# :: ByteString -> Addr#+bsAddr# (BSI.BS (ForeignPtr addr _) _) = addr+{-# INLINE bsAddr# #-}+++memcmpAddr# :: Addr# -> Addr# -> Int# -> Int#+memcmpAddr# a b n = case unsafeDupablePerformIO (c_memcmp (Ptr a) (Ptr b) (I# n)) of+ r -> if r == 0 then 0# else 1#+{-# INLINE memcmpAddr# #-}+++foreign import ccall unsafe "memcmp"+ c_memcmp :: Ptr Word8 -> Ptr Word8 -> Int -> IO Int+++------------------------------------------------------------------------+-- ByteString operations+------------------------------------------------------------------------++{- | Take @n@ bytes.++Returns a /zero-copy slice/ into the ring (in streaming mode) or+the input 'ByteString' (in 'parseByteString' mode) whenever @n@+fits in the current backing buffer in one go. When @n@ exceeds+the magic ring's capacity, falls back to draining the bytes into+a fresh heap allocation: 'takeBs' still hands the caller all @n@+bytes, the cost is one allocation + memcpy and the result no+longer aliases the ring.++== Lifetime++For the zero-copy slice case the returned 'ByteString' holds a+reference to the ring's backing memory. The slice is valid as+long as the ring exists (managed by 'withMagicRing' / 'withRecvTransport').+If the ring is freed while the 'ByteString' is still alive, the+slice becomes a dangling pointer. In practice this is safe because+'runParser' / 'runParserLoop' run within the transport's scope.++For the drained-into-a-fresh-allocation case (@n@ > ring size) the+result is an ordinary heap 'ByteString' with no ring dependency+and may outlive 'withMagicRing' freely.++A type-system-enforced alternative for callers that prefer to+/prove/ slices cannot outlive a refill lives in "Wireform.Ring":+use 'Wireform.Ring.RingSlice' values and 'Wireform.Ring.copyRingSlice'+as the explicit escape hatch.+-}+takeBs :: forall m e. ParserMode m => Int -> Parser m e ByteString+takeBs total+ | total <= 0 = pure BS.empty+ | otherwise = Parser \env eob s st ->+ let !(I# mask#) = peMask env+ in case (case total of I# n# -> (n# -# 1#) <=# mask#) of+ 1# -> runParser# (takeBsSlice @m total) env eob s st+ -- Drain path is Stream-monomorphic so that 'modeCheckpoint'+ -- in the loop body resolves to 'checkpoint' at compile time+ -- (no per-iteration dictionary indirection). 'coerce' is+ -- safe because the 'm' phantom parameter of 'Parser' has+ -- role phantom — 'Parser Stream e a' and 'Parser m e a'+ -- have identical runtime representation. Pure mode never+ -- reaches this branch at runtime (peMask is maxBound for+ -- Pure, so the bounds check always picks the fast slice).+ _ ->+ runParser#+ (coerce (takeBsDrainStream @e total) :: Parser m e ByteString)+ env+ eob+ s+ st+{-# INLINE takeBs #-}+++{- | Take @n@ bytes, always copying. Use when you need the result+to outlive the transport's scope.++Like 'takeBs', accepts any @n@ — for @n@ larger than the ring+size, the bytes are drained chunk-by-chunk through the ring into+the destination, advancing the consumer tail between chunks so+the producer has room to keep filling.+-}+takeBsCopy :: forall m e. ParserMode m => Int -> Parser m e ByteString+takeBsCopy total+ | total <= 0 = pure BS.empty+ | otherwise = Parser \env eob s st ->+ let !(I# mask#) = peMask env+ in case (case total of I# n# -> (n# -# 1#) <=# mask#) of+ 1# -> runParser# (takeBsCopySingle @m total) env eob s st+ -- See 'takeBs' for why the drain path is Stream-monomorphic+ -- + 'coerce'd back to 'Parser m'.+ _ ->+ runParser#+ (coerce (takeBsDrainStream @e total) :: Parser m e ByteString)+ env+ eob+ s+ st+{-# INLINE takeBsCopy #-}+++-- | Single-shot zero-copy slice (fast path of 'takeBs').+takeBsSlice :: forall m e. ParserMode m => Int -> Parser m e ByteString+takeBsSlice (I# n#) = withEnsure# @m n# $ Parser \env _ s st ->+ let !bs = BSI.BS (ForeignPtr s (peBackingFp env)) (I# n#)+ in (# st, OK# bs (plusAddr# s n#) #)+{-# INLINE takeBsSlice #-}+++-- | Single-shot memcpy into a fresh allocation (fast path of 'takeBsCopy').+takeBsCopySingle :: forall m e. ParserMode m => Int -> Parser m e ByteString+takeBsCopySingle (I# n#) = withEnsure# @m n# $ Parser \_ _ s st ->+ let !bs =+ unsafeDupablePerformIO+ (BSI.create (I# n#) \dst -> copyBytes dst (Ptr s) (I# n#))+ in (# st, OK# bs (plusAddr# s n#) #)+{-# INLINE takeBsCopySingle #-}+++{- | Drain @total@ bytes through the ring into a fresh heap+allocation. Used by 'takeBs' and 'takeBsCopy' when @total@ does+not fit in the ring at once.++Strategy:++1. Allocate a pinned 'ForeignPtr' of size @total@.+2. 'checkpoint' — advance tail to the parser's current position+ so the producer has the whole ring to refill into.+3. Loop: 'ensureN#' a ring-sized chunk, memcpy from the cursor+ into the destination, advance the cursor, 'checkpoint' if+ more chunks remain.++The loop performs roughly @ceil(total / ringSize)@+producer/consumer round-trips.++Deliberately monomorphic in @Parser Stream e@ rather than+polymorphic over @ParserMode m@: 'checkpoint' / 'ensureN#' need+to resolve at compile time so the inner loop carries no+dictionary indirection. Pure-mode callers can't reach this+function anyway (their bounds check always picks the fast+slice), so the polymorphic dispatch lives in 'takeBs' /+'takeBsCopy' which 'coerce' the 'Parser Stream' result into+'Parser m' across the (phantom) @m@ parameter.+-}+takeBsDrainStream :: forall e. Int -> Parser Stream e ByteString+takeBsDrainStream total = do+ ringSz <- readRingSize+ fp <- allocPinnedFp total+ checkpoint+ drainLoopStream fp 0 total ringSz+ pure $! BSI.BS fp total+++{- | Pull at most 'ringSize' bytes per iteration into @fp@; advance+cur and tail between iterations.++Bang patterns on every argument so the strictness analyzer+unboxes 'copied' / 'total' / 'ringSz' into 'Int#' across the+recursive call. Without the bang on @ringSz@ in particular the+worker shipped it as a boxed 'Int' and paid one @case I#@ per+iteration to unbox it.+-}+drainLoopStream+ :: forall e+ . ForeignPtr Word8+ -> Int+ -> Int+ -> Int+ -> Parser Stream e ()+drainLoopStream !fp !copied !total !ringSz+ | copied >= total = pure ()+ | otherwise = do+ let !(I# c#) = min (total - copied) ringSz+ ensureN# @Stream c#+ memcpyFromCur fp copied (I# c#)+ let !copied' = copied + I# c#+ when (copied' < total) checkpoint+ drainLoopStream fp copied' total ringSz+++{- | 'peMask + 1' — the streaming ring's capacity, or 'maxBound' /+arbitrary in whole-input mode (the dispatch in 'takeBs' / 'takeBsCopy'+never enters the drain path in whole-input mode, so the value is+not actually consumed there).+-}+readRingSize :: Parser m e Int+readRingSize = Parser \env _eob s st ->+ let !rsz = case peMask env of+ m -> if m == maxBound then m else m + 1+ in (# st, OK# rsz s #)+{-# INLINE readRingSize #-}+++{- | Allocate a pinned 'ForeignPtr' of the given size, threading the+parser's 'State#' so allocation order is well-defined.+-}+allocPinnedFp :: Int -> Parser m e (ForeignPtr Word8)+allocPinnedFp n = Parser \_env _eob s st0 ->+ case unIO (mallocPlainForeignPtrBytes n) st0 of+ (# st1, fp #) -> (# st1, OK# fp s #)+ where+ unIO (IO f) = f+{-# INLINE allocPinnedFp #-}+++{- | Copy @len@ bytes from the parser's cursor into @fp + dstOff@,+advancing the cursor by @len@ bytes. Caller must have called+'ensureN#' (or equivalent) for at least @len@ bytes.++Uses 'unsafeWithForeignPtr' rather than 'withForeignPtr': the+former skips the per-call 'keepAlive#' barrier and the fresh+closure allocation that goes with it. Safe here because @fp@+is bound by the outer @do@-block in 'takeBsDrain' and stays live+for the entire drain — the returned @BSI.BS fp _@ keeps it+referenced past the loop's last iteration, and the drain loop+itself only ever reads through the address.+-}+memcpyFromCur :: ForeignPtr Word8 -> Int -> Int -> Parser m e ()+memcpyFromCur fp (I# dstOff#) (I# len#) = Parser \_env _eob s st0 ->+ case unIO+ ( unsafeWithForeignPtr fp \(Ptr dst#) ->+ copyBytes+ (Ptr (plusAddr# dst# dstOff#))+ (Ptr s)+ (I# len#)+ )+ st0 of+ (# st1, () #) -> (# st1, OK# () (plusAddr# s len#) #)+ where+ unIO (IO f) = f+{-# INLINE memcpyFromCur #-}+++skip :: ParserMode m => Int -> Parser m e ()+skip (I# n#) = withEnsure# n# $ Parser \_ _ s st ->+ (# st, OK# () (plusAddr# s n#) #)+{-# INLINE skip #-}+++-- | Consume all remaining bytes in the current window.+takeRest :: Parser m e ByteString+takeRest = Parser \env eob s st ->+ let !len = I# (minusAddr# eob s)+ !bs =+ if len <= 0+ then BS.empty+ else BSI.BS (ForeignPtr s (peBackingFp env)) len+ in (# st, OK# bs eob #)+{-# INLINE takeRest #-}+++{- | Skip backward @n@ bytes (takes a positive integer).++In the ring buffer context, you can only skip back to bytes that+haven't been overwritten — i.e., bytes between the consumer tail+and the current position. For 'parseByteString' this is the+entire input. For streaming, it's bounded by whatever the driver+hasn't advanced the tail past (at minimum, the start of the+current 'runParser' invocation).++Fails if @n@ would move before the most recent anchor (the start+of the current parse run for non-streaming, or the latest+'checkpoint' for streaming). Does NOT check the ring tail+directly — the driver guarantees tail <= anchor.+-}+skipBack :: Int -> Parser m e ()+skipBack (I# n#) = Parser \env _ s st ->+ let !target = plusAddr# s (negateInt# n#)+ !(Ptr anchorPtr#) = peAnchorCur env+ in case readAddrOffAddr# anchorPtr# 0# st of+ (# st', anchorCur# #) ->+ case leAddr# anchorCur# target of+ 1# -> (# st', OK# () target #)+ _ -> (# st', Fail# #)+{-# INLINE skipBack #-}+++------------------------------------------------------------------------+-- Signed integers+------------------------------------------------------------------------++anyInt8 :: ParserMode m => Parser m e Int8+anyInt8 = fromIntegral <$> anyWord8+{-# INLINE anyInt8 #-}+++anyInt16 :: ParserMode m => Parser m e Int16+anyInt16 = fromIntegral <$> anyWord16+{-# INLINE anyInt16 #-}+++anyInt32 :: ParserMode m => Parser m e Int32+anyInt32 = fromIntegral <$> anyWord32+{-# INLINE anyInt32 #-}+++anyInt64 :: ParserMode m => Parser m e Int64+anyInt64 = fromIntegral <$> anyWord64+{-# INLINE anyInt64 #-}+++anyInt16le :: ParserMode m => Parser m e Int16+anyInt16le = fromIntegral <$> anyWord16le+{-# INLINE anyInt16le #-}+++anyInt32le :: ParserMode m => Parser m e Int32+anyInt32le = fromIntegral <$> anyWord32le+{-# INLINE anyInt32le #-}+++anyInt64le :: ParserMode m => Parser m e Int64+anyInt64le = fromIntegral <$> anyWord64le+{-# INLINE anyInt64le #-}+++anyInt16be :: ParserMode m => Parser m e Int16+anyInt16be = fromIntegral <$> anyWord16be+{-# INLINE anyInt16be #-}+++anyInt32be :: ParserMode m => Parser m e Int32+anyInt32be = fromIntegral <$> anyWord32be+{-# INLINE anyInt32be #-}+++anyInt64be :: ParserMode m => Parser m e Int64+anyInt64be = fromIntegral <$> anyWord64be+{-# INLINE anyInt64be #-}+++------------------------------------------------------------------------+-- UTF-8 / ASCII+------------------------------------------------------------------------++-- | Parse any UTF-8 character.+anyChar :: ParserMode m => Parser m e Char+anyChar = withEnsure# 1# $ Parser \_ eob s st ->+ case indexWord8OffAddr# s 0# of+ c1 -> case leWord8# c1 (wordToWord8# 0x7F##) of+ 1# -> (# st, OK# (C# (chr# (word2Int# (word8ToWord# c1)))) (plusAddr# s 1#) #)+ _ -> case leWord8# c1 (wordToWord8# 0xBF##) of+ 1# -> (# st, Fail# #)+ _ -> case leWord8# c1 (wordToWord8# 0xDF##) of+ 1# -> case 2# <=# minusAddr# eob s of+ 1# -> case indexWord8OffAddr# s 1# of+ c2 ->+ let !cp =+ ((word2Int# (word8ToWord# c1) -# 0xC0#) `uncheckedIShiftL#` 6#)+ +# (word2Int# (word8ToWord# c2) -# 0x80#)+ in (# st, OK# (C# (chr# cp)) (plusAddr# s 2#) #)+ _ -> (# st, Fail# #)+ _ -> case leWord8# c1 (wordToWord8# 0xEF##) of+ 1# -> case 3# <=# minusAddr# eob s of+ 1# -> case indexWord8OffAddr# s 1# of+ c2 ->+ case indexWord8OffAddr# s 2# of+ c3 ->+ let !cp =+ ((word2Int# (word8ToWord# c1) -# 0xE0#) `uncheckedIShiftL#` 12#)+ +# ((word2Int# (word8ToWord# c2) -# 0x80#) `uncheckedIShiftL#` 6#)+ +# (word2Int# (word8ToWord# c3) -# 0x80#)+ in (# st, OK# (C# (chr# cp)) (plusAddr# s 3#) #)+ _ -> (# st, Fail# #)+ _ -> case 4# <=# minusAddr# eob s of+ 1# -> case indexWord8OffAddr# s 1# of+ c2 ->+ case indexWord8OffAddr# s 2# of+ c3 ->+ case indexWord8OffAddr# s 3# of+ c4 ->+ let !cp =+ ((word2Int# (word8ToWord# c1) -# 0xF0#) `uncheckedIShiftL#` 18#)+ +# ((word2Int# (word8ToWord# c2) -# 0x80#) `uncheckedIShiftL#` 12#)+ +# ((word2Int# (word8ToWord# c3) -# 0x80#) `uncheckedIShiftL#` 6#)+ +# (word2Int# (word8ToWord# c4) -# 0x80#)+ in (# st, OK# (C# (chr# cp)) (plusAddr# s 4#) #)+ _ -> (# st, Fail# #)+{-# INLINE anyChar #-}+++anyChar_ :: ParserMode m => Parser m e ()+anyChar_ = () <$ anyChar+{-# INLINE anyChar_ #-}+++-- | CPS variant: parse an ASCII char and pass to continuation.+withSatisfyAscii :: ParserMode m => (Char -> Bool) -> (Char -> Parser m e r) -> Parser m e r+withSatisfyAscii f p = withEnsure# 1# $ Parser \env eob s st ->+ case indexCharOffAddr# s 0# of+ c1 -> case leChar# c1 '\x7F'# of+ 1# ->+ let !ch = C# c1+ in if f ch+ then runParser# (p ch) env eob (plusAddr# s 1#) st+ else (# st, Fail# #)+ _ -> (# st, Fail# #)+{-# INLINE withSatisfyAscii #-}+++-- | Parse an ASCII character matching a predicate.+satisfyAscii :: ParserMode m => (Char -> Bool) -> Parser m e Char+satisfyAscii f = withSatisfyAscii f pure+{-# INLINE satisfyAscii #-}+++satisfyAscii_ :: ParserMode m => (Char -> Bool) -> Parser m e ()+satisfyAscii_ f = withSatisfyAscii f (\_ -> pure ())+{-# INLINE satisfyAscii_ #-}+++skipSatisfyAscii :: ParserMode m => (Char -> Bool) -> Parser m e ()+skipSatisfyAscii f = withEnsure# 1# $ Parser \_ _ s st ->+ case indexCharOffAddr# s 0# of+ c1 -> case leChar# c1 '\x7F'# of+ 1# ->+ if f (C# c1)+ then (# st, OK# () (plusAddr# s 1#) #)+ else (# st, Fail# #)+ _ -> (# st, Fail# #)+{-# INLINE skipSatisfyAscii #-}+++-- | Parse any ASCII character (< 0x80).+anyAsciiChar :: ParserMode m => Parser m e Char+anyAsciiChar = satisfyAscii (const True)+{-# INLINE anyAsciiChar #-}+++-- | Skip one ASCII character.+skipAnyAsciiChar :: ParserMode m => Parser m e ()+skipAnyAsciiChar = skipSatisfyAscii (const True)+{-# INLINE skipAnyAsciiChar #-}+++-- | Parse a UTF-8 character matching a predicate.+satisfy :: ParserMode m => (Char -> Bool) -> Parser m e Char+satisfy f = do+ c <- anyChar+ if f c then pure c else empty+{-# INLINE satisfy #-}+++satisfy_ :: ParserMode m => (Char -> Bool) -> Parser m e ()+satisfy_ f = () <$ satisfy f+{-# INLINE satisfy_ #-}+++{- | Fused satisfy: uses the ASCII fast path when the byte is < 0x80,+the full UTF-8 path otherwise. Two separate predicates for each case.+-}+fusedSatisfy :: forall m e. ParserMode m => (Char -> Bool) -> (Word8 -> Bool) -> Parser m e Char+fusedSatisfy charPred _bytePred = withEnsure# 1# $ Parser \env eob s st ->+ case indexWord8OffAddr# s 0# of+ w -> case leWord8# w (wordToWord8# 0x7F##) of+ 1# ->+ let !ch = C# (chr# (word2Int# (word8ToWord# w)))+ in if charPred ch+ then (# st, OK# ch (plusAddr# s 1#) #)+ else (# st, Fail# #)+ _ -> case runParser# (anyChar @m) env eob s st of+ (# st', OK# c s' #) -> if charPred c then (# st', OK# c s' #) else (# st', Fail# #)+ x -> x+{-# INLINE fusedSatisfy #-}+++------------------------------------------------------------------------+-- Character classes+------------------------------------------------------------------------++isDigit :: Char -> Bool+isDigit c = c >= '0' && c <= '9'+{-# INLINE isDigit #-}+++isLatinLetter :: Char -> Bool+isLatinLetter c = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')+{-# INLINE isLatinLetter #-}+++------------------------------------------------------------------------+-- ASCII numeric+------------------------------------------------------------------------++anyAsciiDecimalWord :: ParserMode m => Parser m e Word+anyAsciiDecimalWord = withEnsure# 1# $ Parser \_ eob s st ->+ case indexWord8OffAddr# s 0# of+ c -> case leWord8# (wordToWord8# 0x30##) c of+ 0# -> (# st, Fail# #)+ _ -> case leWord8# c (wordToWord8# 0x39##) of+ 0# -> (# st, Fail# #)+ _ ->+ let !d0 = W# (word8ToWord# c) - 0x30+ in goDecWord eob (plusAddr# s 1#) st d0+{-# INLINE anyAsciiDecimalWord #-}+++goDecWord :: Addr# -> Addr# -> State# RealWorld -> Word -> StRes# e Word+goDecWord eob s st !acc =+ case eqAddr# eob s of+ 1# -> (# st, OK# acc s #)+ _ -> case indexWord8OffAddr# s 0# of+ c -> case leWord8# (wordToWord8# 0x30##) c of+ 0# -> (# st, OK# acc s #)+ _ -> case leWord8# c (wordToWord8# 0x39##) of+ 0# -> (# st, OK# acc s #)+ _ ->+ goDecWord+ eob+ (plusAddr# s 1#)+ st+ (acc * 10 + (W# (word8ToWord# c) - 0x30))+{-# NOINLINE goDecWord #-}+++anyAsciiDecimalInt :: ParserMode m => Parser m e Int+anyAsciiDecimalInt = fromIntegral <$> anyAsciiDecimalWord+{-# INLINE anyAsciiDecimalInt #-}+++------------------------------------------------------------------------+-- Control flow+------------------------------------------------------------------------++empty :: Parser m e a+empty = Parser \_ _ _ st -> (# st, Fail# #)+{-# INLINE empty #-}+++infixr 6 <|>+++(<|>) :: Parser m e a -> Parser m e a -> Parser m e a+(<|>) (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 [1] (<|>) #-}+++{-# RULES "wireform/reassoc-alt" forall l m r. (l <|> m) <|> r = l <|> (m <|> r) #-}+++branch :: Parser m e a -> Parser m e b -> Parser m e b -> Parser m e b+branch (Parser p) (Parser t) (Parser f) = Parser \env eob s st ->+ case p env eob s st of+ (# st', OK# _ s' #) -> t env eob s' st'+ (# st', Fail# #) -> f env eob s st'+ (# st', x #) -> (# st', unsafeCoerce# x #)+{-# INLINE branch #-}+++lookahead :: Parser m e a -> Parser m e a+lookahead (Parser f) = Parser \env eob s st ->+ case f env eob s st of+ (# st', OK# a _ #) -> (# st', OK# a s #)+ x -> x+{-# INLINE lookahead #-}+++fails :: Parser m e a -> Parser m e ()+fails (Parser f) = Parser \env eob s st ->+ case f env eob s st of+ (# st', OK# _ _ #) -> (# st', Fail# #)+ (# st', Fail# #) -> (# st', OK# () s #)+ (# st', x #) -> (# st', unsafeCoerce# x #)+{-# INLINE fails #-}+++try :: Parser m e a -> Parser m e a+try (Parser f) = Parser \env eob s st ->+ case f env eob s st of+ (# st', Err# _ #) -> (# st', Fail# #)+ x -> x+{-# INLINE try #-}+++optional :: Parser m e a -> Parser m e (Maybe a)+optional p = (Just <$> p) <|> pure Nothing+{-# INLINE optional #-}+++optional_ :: Parser m e a -> Parser m e ()+optional_ p = (() <$ p) <|> pure ()+{-# INLINE optional_ #-}+++withOption :: Parser m e a -> (a -> Parser m e b) -> Parser m e b -> Parser m e b+withOption (Parser p) just (Parser nothing) = Parser \env eob s st ->+ case p env eob s st of+ (# st', OK# a s' #) -> runParser# (just a) env eob s' st'+ (# st', Fail# #) -> nothing env eob s st'+ (# st', x #) -> (# st', unsafeCoerce# x #)+{-# INLINE withOption #-}+++many_ :: Parser m e a -> Parser m e ()+many_ (Parser f) = Parser go+ where+ go env eob s st = case f env eob s st of+ (# st', OK# _ s' #) -> go env eob s' st'+ (# st', Fail# #) -> (# st', OK# () s #)+ (# st', x #) -> (# st', unsafeCoerce# x #)+{-# INLINE many_ #-}+++some_ :: Parser m e a -> Parser m e ()+some_ p = p *> many_ p+{-# INLINE some_ #-}+++skipMany :: Parser m e a -> Parser m e ()+skipMany = many_+{-# INLINE skipMany #-}+++skipSome :: Parser m e a -> Parser m e ()+skipSome = some_+{-# INLINE skipSome #-}+++-- | Run @p@ zero or more times, collecting results into a list.+many :: Parser m e a -> Parser m e [a]+many (Parser f) = Parser go+ where+ go env eob s st = case f env eob s st of+ (# st', OK# a s' #) -> case go env eob s' st' of+ (# st'', OK# as s'' #) -> (# st'', OK# (a : as) s'' #)+ x -> x+ (# st', Fail# #) -> (# st', OK# [] s #)+ (# st', x #) -> (# st', unsafeCoerce# x #)+{-# INLINE many #-}+++-- | Run @p@ one or more times, collecting results.+some :: Parser m e a -> Parser m e [a]+some p = (:) <$> p <*> many p+{-# INLINE some #-}+++-- | Succeed iff @p@ fails. Does not consume input.+notFollowedBy :: Parser m e a -> Parser m e ()+notFollowedBy = fails+{-# INLINE notFollowedBy #-}+++{- | Run a parser on exactly @n@ bytes. The inner parser must consume+all @n@ bytes or the overall parse fails.+-}+isolate :: ParserMode m => Int -> Parser m e a -> Parser m e a+isolate (I# n#) (Parser p) = withEnsure# n# $ Parser \env _ s st ->+ let !isolEnd = plusAddr# s n#+ in case p env isolEnd s st of+ (# st', OK# a s' #) -> case eqAddr# s' isolEnd of+ 1# -> (# st', OK# a s' #)+ _ -> (# st', Fail# #)+ (# st', x #) -> (# st', unsafeCoerce# x #)+{-# INLINE isolate #-}+++------------------------------------------------------------------------+-- Error handling+------------------------------------------------------------------------++-- | Catch an @Err#@ and handle it.+withError :: (e -> Parser m e a) -> Parser m e a -> Parser m e a+withError handler (Parser f) = Parser \env eob s st ->+ case f env eob s st of+ (# st', Err# e #) -> runParser# (handler e) env eob s st'+ x -> x+{-# INLINE withError #-}+++err :: e -> Parser m e a+err e = Parser \_ _ _ st -> (# st, Err# e #)+{-# INLINE err #-}+++cut :: Parser m e a -> e -> Parser m e a+cut (Parser f) e = Parser \env eob s st ->+ case f env eob s st of+ (# st', Fail# #) -> (# st', Err# e #)+ x -> x+{-# INLINE cut #-}+++cutting :: Parser m e a -> e -> (e -> e -> e) -> Parser m e a+cutting (Parser f) e merge = Parser \env eob s st ->+ case f env eob s st of+ (# st', Fail# #) -> (# st', Err# e #)+ (# st', Err# e' #) -> (# st', Err# (merge e' e) #)+ x -> x+{-# INLINE cutting #-}+++------------------------------------------------------------------------+-- EOF+------------------------------------------------------------------------++{- | Succeed iff at end of input.+In streaming mode, this may suspend to check if more data is coming.+Uses ensureN# 1 internally: if ensure fails (no more data), we're at EOF.+-}+eof :: forall m e. ParserMode m => Parser m e ()+eof = Parser \env eob s st ->+ case eqAddr# eob s of+ 1# -> case onEnsureFail @m env eob s 1# st of+ (# st', OK# _ _ #) -> (# st', Fail# #) -- data arrived+ (# st', Fail# #) -> (# st', OK# () s #) -- genuine EOF+ (# st', x #) -> (# st', unsafeCoerce# x #)+ _ -> (# st, Fail# #)+{-# INLINE eof #-}+++{- | Non-consuming check: 'True' if at end of input.+In streaming mode, may suspend to determine.+-}+atEnd :: ParserMode m => Parser m e Bool+atEnd = (eof *> pure True) <|> pure False+{-# INLINE atEnd #-}+++-- | Number of bytes remaining in the current window (cheap, no suspend).+remaining :: Parser m e Int+remaining = Parser \_ eob s st ->+ (# st, OK# (I# (minusAddr# eob s)) s #)+{-# INLINE remaining #-}+++------------------------------------------------------------------------+-- Chain combinators+------------------------------------------------------------------------++{- | Left-associative chain.+@chainl f p q@ parses @p@ then zero or more @q@, folding left with @f@.+-}+chainl :: (b -> a -> b) -> Parser m e b -> Parser m e a -> Parser m e b+chainl f (Parser p) (Parser q) = Parser \env eob s st ->+ case p env eob s st of+ (# st', OK# b s' #) -> chainlGo f q env eob s' st' b+ (# st', x #) -> (# st', unsafeCoerce# x #)+{-# INLINE chainl #-}+++chainlGo+ :: forall e b a+ . (b -> a -> b)+ -> (ParserEnv -> Addr# -> Addr# -> State# RealWorld -> StRes# e a)+ -> ParserEnv+ -> Addr#+ -> Addr#+ -> State# RealWorld+ -> b+ -> StRes# e b+chainlGo f q env eob s st !acc =+ case q env eob s st of+ (# st', OK# a s' #) -> chainlGo f q env eob s' st' (f acc a)+ (# st', Fail# #) -> (# st', OK# acc s #)+ (# st', x #) -> (# st', unsafeCoerce# x #)+{-# NOINLINE chainlGo #-}+++{- | Right-associative chain.+@chainr f p q@ parses zero or more @p@, then @q@ at the end,+folding right with @f@.+-}+chainr :: (a -> b -> b) -> Parser m e a -> Parser m e b -> Parser m e b+chainr f (Parser p) (Parser q) = Parser go+ where+ go env eob s st =+ case p env eob s st of+ (# st', OK# a s' #) -> case go env eob s' st' of+ (# st'', OK# b s'' #) -> (# st'', OK# (f a b) s'' #)+ (# st'', x #) -> (# st'', unsafeCoerce# x #)+ (# st', Fail# #) -> q env eob s st'+ (# st', x #) -> (# st', unsafeCoerce# x #)+{-# INLINE chainr #-}+++------------------------------------------------------------------------+-- Additional error handling+------------------------------------------------------------------------++{- | Like 'ensureN#' but on EOF, errors with a specific error+instead of failing recoverably.+-}+ensureNOrEof :: ParserMode m => Int -> e -> Parser m e ()+ensureNOrEof n e = ensureN# (case n of I# n# -> n#) <|> err e+{-# INLINE ensureNOrEof #-}+++------------------------------------------------------------------------+-- Additional position combinators+------------------------------------------------------------------------++-- | Parse something and return both the result and the bytes consumed.+withByteString :: Parser m e a -> (a -> ByteString -> Parser m e b) -> Parser m e b+withByteString (Parser p) f = Parser \env eob s st ->+ case p env eob s st of+ (# st', OK# a s' #) ->+ let !len = I# (minusAddr# s' s)+ !bs = BSI.BS (ForeignPtr s (peBackingFp env)) len+ in runParser# (f a bs) env eob s' st'+ (# st', x #) -> (# st', unsafeCoerce# x #)+{-# INLINE withByteString #-}+++------------------------------------------------------------------------+-- Additional character classes+------------------------------------------------------------------------++isGreekLetter :: Char -> Bool+isGreekLetter c = (c >= '\x0391' && c <= '\x03A9') || (c >= '\x03B1' && c <= '\x03C9')+{-# INLINE isGreekLetter #-}+++------------------------------------------------------------------------+-- Additional ASCII numeric+------------------------------------------------------------------------++anyAsciiHexWord :: ParserMode m => Parser m e Word+anyAsciiHexWord = withEnsure# 1# $ Parser \_ eob s st ->+ case hexDigit (indexWord8OffAddr# s 0#) of+ (# | (# #) #) -> (# st, Fail# #)+ (# (# d #) | #) -> goHexWord eob (plusAddr# s 1#) st (W# (word8ToWord# d))+{-# INLINE anyAsciiHexWord #-}+++hexDigit :: Word8# -> (# (# Word8# #) | (# #) #)+hexDigit w+ | isTrue# (leWord8# (wordToWord8# 0x30##) w)+ , isTrue# (leWord8# w (wordToWord8# 0x39##)) =+ (# (# wordToWord8# (word8ToWord# w `minusWord#` 0x30##) #) | #)+ | isTrue# (leWord8# (wordToWord8# 0x41##) w)+ , isTrue# (leWord8# w (wordToWord8# 0x46##)) =+ (# (# wordToWord8# (word8ToWord# w `minusWord#` 0x37##) #) | #)+ | isTrue# (leWord8# (wordToWord8# 0x61##) w)+ , isTrue# (leWord8# w (wordToWord8# 0x66##)) =+ (# (# wordToWord8# (word8ToWord# w `minusWord#` 0x57##) #) | #)+ | otherwise = (# | (# #) #)+{-# INLINE hexDigit #-}+++goHexWord :: Addr# -> Addr# -> State# RealWorld -> Word -> StRes# e Word+goHexWord eob s st !acc =+ case eqAddr# eob s of+ 1# -> (# st, OK# acc s #)+ _ -> case hexDigit (indexWord8OffAddr# s 0#) of+ (# | (# #) #) -> (# st, OK# acc s #)+ (# (# d #) | #) ->+ goHexWord+ eob+ (plusAddr# s 1#)+ st+ (acc * 16 + W# (word8ToWord# d))+{-# NOINLINE goHexWord #-}+++anyAsciiHexInt :: ParserMode m => Parser m e Int+anyAsciiHexInt = fromIntegral <$> anyAsciiHexWord+{-# INLINE anyAsciiHexInt #-}+++anyAsciiDecimalInteger :: ParserMode m => Parser m e Integer+anyAsciiDecimalInteger = fromIntegral <$> anyAsciiDecimalWord+{-# INLINE anyAsciiDecimalInteger #-}
+ src/Wireform/Parser/Adapter.hs view
@@ -0,0 +1,166 @@+{-# 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)
+ src/Wireform/Parser/Driver.hs view
@@ -0,0 +1,354 @@+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE BlockArguments #-}+{-# LANGUAGE MagicHash #-}+{-# LANGUAGE PatternSynonyms #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE UnboxedSums #-}+{-# LANGUAGE UnboxedTuples #-}++module Wireform.Parser.Driver (+ runParser,+ runParserLoop,+ LoopControl (..),+ parseByteString,+ runParserInternal,+ InternalResult (..),+) where++import Control.Exception (SomeException, mask)+import Data.Bits ((.&.))+import Data.ByteString (ByteString)+import Data.ByteString.Internal qualified as BSI+import Data.IORef+import Data.Word (Word64, Word8)+import Foreign.ForeignPtr (withForeignPtr)+import Foreign.Marshal.Alloc (allocaBytes)+import Foreign.Ptr (Ptr, castPtr, minusPtr, plusPtr)+import Foreign.Storable (poke)+import GHC.Exts+import GHC.ForeignPtr (ForeignPtr (..), ForeignPtrContents (..))+import GHC.IO (IO (..))+import System.IO.Unsafe (unsafeDupablePerformIO)+import Wireform.Parser.Error+import Wireform.Parser.Internal+import Wireform.Ring.Internal (ringBase, ringMask, ringSize)+import Wireform.Transport+++data LoopControl = Continue | Stop+ deriving stock (Eq, Show)+++data InternalResult e a+ = IRDone {-# UNPACK #-} !Word64 !a+ | IRFail !Word64+ | IRErr !Word64 !e+ | IRUnexpectedEof !Word64 !Int+ | IRTransportError !SomeException+ | IRCleanEof+ | -- | position, requested bytes, ring size — see 'ParseRingOverflow'.+ IRRingOverflow {-# UNPACK #-} !Word64 {-# UNPACK #-} !Int {-# UNPACK #-} !Int+++data TransportState+ = TSOpen+ | TSClosedEof+ | TSClosedErr !SomeException+++------------------------------------------------------------------------+-- runParser+------------------------------------------------------------------------++runParser :: forall e a. ReceiveTransport -> Parser Stream e a -> IO (Either (ParseError e) a)+runParser t p = do+ startPos <- receiveLoadHead t+ ir <- runParserInternal t p startPos+ pure $ case ir of+ IRDone _ a -> Right a+ IRFail pos -> Left (ParseFail pos)+ IRErr pos e -> Left (ParseErr pos e)+ IRUnexpectedEof pos n -> Left (ParseUnexpectedEof pos n)+ IRTransportError exc -> Left (ParseTransportError exc)+ IRCleanEof -> Left (ParseUnexpectedEof 0 0)+ IRRingOverflow pos n sz -> Left (ParseRingOverflow pos n sz)+++runParserInternal :: forall e a. ReceiveTransport -> Parser Stream e a -> Word64 -> IO (InternalResult e a)+runParserInternal t p startPos = mask \restore -> do+ let ring = receiveRing t+ !base = ringBase ring+ !msk = ringMask ring+ !sz = ringSize ring++ currentHead <- receiveLoadHead t+ let !curOffset = fromIntegral startPos .&. msk+ !(Ptr initCur#) = base `plusPtr` curOffset+ -- See StepCheckpoint / StepSuspend for the rationale on+ -- computing eob from @cur + avail@ rather than+ -- @base + (head .&. msk)@: when @head - startPos == ringSize@,+ -- the masked offsets coincide and eob collapses onto cur.+ --+ -- Clamp to 'sz' (the ring size). A well-behaved transport+ -- maintains @head <= tail + ringSize@ so the clamp is a no-op,+ -- but a misbehaving transport (e.g. a benchmark fixture that+ -- prefills more bytes than the ring holds, or a bug in a+ -- recv loop) can claim a much larger head — without the+ -- clamp 'eob' would point past the double mapping and the+ -- first parser read past 'base + 2*ringSize' would segfault.+ !avail = min sz (fromIntegral (currentHead - startPos))+ !(Ptr initEnd#) = (Ptr initCur#) `plusPtr` avail++ -- One stack-allocated buffer holds the three mutable cells the+ -- parser shares with the driver: end pointer, anchor pos, anchor+ -- cur. The cells live for the duration of the parse run and+ -- never escape — neither 'ParserEnv' nor any 'control0#'+ -- continuation can outlive the surrounding 'prompt#' frame, which+ -- is set up inside this 'allocaBytes' scope.+ --+ -- The cells are mutated by the parser-side resume bodies (see+ -- 'checkpoint' / 'ensureNSlow' in "Wireform.Parser.Internal") with+ -- no lock or fence: those bodies only run while the parser is+ -- suspended between 'control0#' and the matching 'prompt#'. See+ -- the 'ParserEnv' haddock for the full argument.+ allocaBytes 24 \cells -> do+ let !endPtr = cells+ !anchorPos = cells `plusPtr` 8+ !anchorCur = cells `plusPtr` 16+ poke (castPtr endPtr :: Ptr (Ptr Word8)) (Ptr initEnd#)+ poke (castPtr anchorPos :: Ptr Word64) startPos+ poke (castPtr anchorCur :: Ptr (Ptr Word8)) (Ptr initCur#)++ highWaterRef <- newIORef startPos+ tsRef <- newIORef TSOpen++ -- Allocate tag and construct env inside IO so the tag+ -- is available for peTag, then return both.+ (env, step0) <- IO \s0 -> case newPromptTag# s0 of+ (# s1, (tag :: PromptTag# (Step e a)) #) ->+ let !env' =+ ParserEnv+ { peEndPtr = castPtr endPtr+ , peBaseAddr = base+ , peMask = msk+ , peAnchorPos = castPtr anchorPos+ , peAnchorCur = castPtr anchorCur+ , peBackingFp = FinalPtr+ , peTag = tagToAny tag+ }+ body :: State# RealWorld -> (# State# RealWorld, Step e a #)+ body s = case runParser# p env' initEnd# initCur# s of+ (# s', OK# a cur' #) ->+ case curToPos env' cur' s' of+ (# s'', newPos #) -> (# s'', StepDone newPos a #)+ (# s', Fail# #) ->+ (# s', StepFail startPos #)+ (# s', Err# e #) ->+ (# s', StepErr startPos e #)+ in case prompt# tag body s1 of+ (# s2, step #) -> (# s2, (env', step) #)++ driverLoop restore t env base msk sz startPos highWaterRef tsRef step0+++driverLoop+ :: forall e a+ . (forall x. IO x -> IO x)+ -> ReceiveTransport+ -> ParserEnv+ -> Ptr Word8+ -> Int+ -> Int+ -> Word64+ -> IORef Word64+ -> IORef TransportState+ -> Step e a+ -> IO (InternalResult e a)+driverLoop restore t env base msk sz startPos hwRef tsRef = go+ where+ go :: Step e a -> IO (InternalResult e a)+ go step = case step of+ StepDone newPos a -> do+ receiveAdvanceTail t newPos+ pure (IRDone newPos a)+ StepErr pos e -> pure (IRErr pos e)+ StepFail pos -> do+ ts <- readIORef tsRef+ hw <- readIORef hwRef+ pure $ case ts of+ TSOpen -> IRFail pos+ TSClosedErr e -> IRTransportError e+ TSClosedEof+ | hw == startPos -> IRCleanEof+ | otherwise -> IRUnexpectedEof pos 0+ StepCheckpoint pos resume -> do+ receiveAdvanceTail t pos+ h <- receiveLoadHead t+ -- Compute eob relative to the new cur so that a wrap (where+ -- @pos .&. msk == h .&. msk@ because exactly a ring's worth+ -- of bytes are in flight) does not collapse eob onto cur and+ -- make the parser see zero bytes when there are actually+ -- ringSize bytes available. The double mapping guarantees+ -- @newCur + avail@ is addressable for any @avail <= ringSize@.+ -- The 'min sz' guards against transports that mis-report+ -- head past 'tail + ringSize'; without it the parser could+ -- read past the double mapping and segfault.+ -- 'resumeContinue' re-anchors the env so 'curToPos' stays+ -- correct after the cur wrap.+ let !curOff = fromIntegral pos .&. msk+ !newCur = base `plusPtr` curOff+ !avail = min sz (fromIntegral (h - pos))+ !newEnd = newCur `plusPtr` avail+ nextStep <- resumeContinue resume newCur newEnd+ go nextStep+ StepSuspend pausedAt needed resume+ | needed > sz ->+ -- The parser asked for more bytes than the entire ring can+ -- ever hold. No amount of refilling will satisfy this;+ -- waiting would deadlock (producer can't make room because+ -- the consumer is suspended waiting for it). Fail loudly+ -- instead.+ pure (IRRingOverflow pausedAt needed sz)+ | otherwise -> do+ modifyIORef' hwRef (max pausedAt)+ result <- restore (waitUntilAvailable t tsRef pausedAt needed sz)+ case result of+ WAMoreData newHead -> do+ modifyIORef' hwRef (max newHead)+ -- See the comment on StepCheckpoint above: compute eob+ -- as @newCur + (newHead - pausedAt)@ rather than as+ -- @base + (newHead .&. msk)@. The latter collapses to+ -- @newCur@ when the producer has filled exactly one+ -- ring-worth of bytes since @pausedAt@, hiding all the+ -- newly-available bytes from the parser.+ -- 'min sz' guards against misbehaving transports —+ -- see the matching comment in 'runParserInternal'.+ -- 'resumeContinue' re-anchors the env so 'curToPos'+ -- stays correct after the cur wrap.+ let !newCurOff = fromIntegral pausedAt .&. msk+ !newCur = base `plusPtr` newCurOff+ !avail = min sz (fromIntegral (newHead - pausedAt))+ !newEnd = newCur `plusPtr` avail+ nextStep <- resumeContinue resume newCur newEnd+ go nextStep+ WAEndOfInput -> do+ nextStep <- resumeEof resume+ go nextStep+ WATransportError exc ->+ pure (IRTransportError exc)+++data WaitAvail+ = WAMoreData {-# UNPACK #-} !Word64+ | WAEndOfInput+ | WATransportError !SomeException+++waitUntilAvailable+ :: ReceiveTransport+ -> IORef TransportState+ -> Word64+ -> Int+ -> Int+ -> IO WaitAvail+waitUntilAvailable t tsRef pos needed _ringSize = do+ h0 <- receiveLoadHead t+ loop (max pos h0)+ where+ -- 'waitFrom' is the position we ask the transport to advance past.+ -- It advances on each iteration so a transport that delivers data+ -- in dribs (a chunked feeder, a TLS layer that yields partial+ -- records, an io_uring SQ that hands us one CQE at a time)+ -- doesn't spin: each 'receiveWaitData' is asked to wait /past/+ -- the head we already observed, so it must either pull fresh+ -- bytes or report ReceiveEndOfInput.+ loop !waitFrom = do+ h <- receiveLoadHead t+ if h - pos >= fromIntegral needed+ then pure (WAMoreData h)+ else do+ r <- receiveWaitData t waitFrom+ case r of+ ReceiveMoreData h' -> loop (max waitFrom h')+ ReceiveEndOfInput -> do writeIORef tsRef TSClosedEof; pure WAEndOfInput+ ReceiveFailed exc -> do writeIORef tsRef (TSClosedErr exc); pure (WATransportError exc)+++------------------------------------------------------------------------+-- runParserLoop+------------------------------------------------------------------------++runParserLoop+ :: forall e a+ . ReceiveTransport+ -> Parser Stream e a+ -> (a -> IO LoopControl)+ -> IO (Either (ParseError e) ())+runParserLoop t p k = do+ startPos <- receiveLoadHead t+ loop startPos+ where+ loop pos = do+ r <- runParserInternal t p pos+ case r of+ IRDone newPos a -> do+ ctl <- k a+ case ctl of Continue -> loop newPos; Stop -> pure (Right ())+ IRCleanEof -> pure (Right ())+ IRFail fpos -> pure (Left (ParseFail fpos))+ IRErr fpos e -> pure (Left (ParseErr fpos e))+ IRUnexpectedEof fpos n -> pure (Left (ParseUnexpectedEof fpos n))+ IRTransportError exc -> pure (Left (ParseTransportError exc))+ IRRingOverflow fpos n sz -> pure (Left (ParseRingOverflow fpos n sz))+++------------------------------------------------------------------------+-- parseByteString (non-streaming, flatparse-equivalent)+------------------------------------------------------------------------++{- | Run a parser against a whole 'ByteString'.+The hot path is bit-identical to flatparse — no suspension overhead.+-}+parseByteString :: forall e a. Parser Pure e a -> ByteString -> Either (ParseError e) a+parseByteString p b = unsafeDupablePerformIO $ do+ -- withForeignPtr keeps the ByteString's backing memory alive+ let !(BSI.BS (ForeignPtr buf# fp) (I# len#)) = b+ !end# = plusAddr# buf# len#++ withForeignPtr (ForeignPtr buf# fp) \_ ->+ allocaBytes 24 \cells -> do+ let !endPtr = cells+ !anchorPos = cells `plusPtr` 8+ !anchorCur = cells `plusPtr` 16+ poke (castPtr endPtr :: Ptr (Ptr Word8)) (Ptr end#)+ poke (castPtr anchorPos :: Ptr Word64) 0+ poke (castPtr anchorCur :: Ptr (Ptr Word8)) (Ptr buf#)++ IO \s0 -> case newPromptTag# s0 of+ (# s1, (tag :: PromptTag# (Step e a)) #) ->+ let env =+ ParserEnv+ { peEndPtr = castPtr endPtr+ , peBaseAddr = Ptr buf#+ , peMask = maxBound+ , peAnchorPos = castPtr anchorPos+ , peAnchorCur = castPtr anchorCur+ , peBackingFp = fp+ , peTag = tagToAny tag+ }+ body :: State# RealWorld -> (# State# RealWorld, Step e a #)+ body s = case runParser# p env end# buf# s of+ (# s', OK# a cur' #) ->+ let !pos = fromIntegral (I# (minusAddr# cur' buf#))+ in (# s', StepDone pos a #)+ (# s', Fail# #) -> (# s', StepFail 0 #)+ (# s', Err# e #) -> (# s', StepErr 0 e #)+ in case prompt# tag body s1 of+ (# s2, step #) -> unIO (classifyStep step) s2+ where+ classifyStep (StepDone _ a) = pure (Right a)+ classifyStep (StepFail pos) = pure (Left (ParseFail pos))+ classifyStep (StepErr pos e) = pure (Left (ParseErr pos e))+ classifyStep (StepSuspend _ _ r) = resumeEof r >>= classifyStep+ classifyStep (StepCheckpoint _ r) = resumeEof r >>= classifyStep+ unIO (IO f) = f
+ src/Wireform/Parser/Error.hs view
@@ -0,0 +1,46 @@+module Wireform.Parser.Error (+ ParseError (..),+ CleanEof (..),+) where++import Control.Exception (Exception, SomeException)+import Data.Typeable (Typeable)+import Data.Word (Word64)+++-- | The result of a failed parse.+data ParseError e+ = {- | The parser failed (recoverably) at the given absolute position+ and the failure was not caught by an enclosing alternative.+ -}+ ParseFail !Word64+ | -- | The parser failed unrecoverably (via cut\/commit) with user error @e@.+ ParseErr !Word64 !e+ | {- | Transport reported EOF while the parser was mid-message.+ The position is where the parser was; the 'Int' is how many+ additional bytes it needed.+ -}+ ParseUnexpectedEof !Word64 !Int+ | -- | Transport-level failure (broken connection, closed socket, etc.).+ ParseTransportError !SomeException+ | {- | The parser asked for more bytes than the magic ring can ever+ hold (or, more precisely, more than the remaining capacity from+ the parser's current position). Continuing would deadlock —+ the producer cannot make room because the consumer is suspended+ waiting for bytes the producer cannot deliver. Fields: parser+ position, requested bytes, ring size.+ -}+ ParseRingOverflow !Word64 !Int !Int+ deriving stock (Show, Functor)+++{- | Internal sentinel: clean EOF before any bytes were consumed for+this parse. Never exposed to users — the driver converts it to+either clean loop termination or 'ParseUnexpectedEof' depending on+context.+-}+data CleanEof = CleanEof+ deriving stock (Show, Typeable)+++instance Exception CleanEof
+ src/Wireform/Parser/Internal.hs view
@@ -0,0 +1,572 @@+{-# 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'
+ src/Wireform/Parser/Mark.hs view
@@ -0,0 +1,42 @@+{-# LANGUAGE BlockArguments #-}+{-# LANGUAGE MagicHash #-}+{-# LANGUAGE PatternSynonyms #-}+{-# LANGUAGE UnboxedSums #-}+{-# LANGUAGE UnboxedTuples #-}++module Wireform.Parser.Mark (+ Mark (..),+ mark,+ restore,+ release,+) where++import Data.Bits ((.&.))+import Data.Word (Word64)+import Foreign.Ptr (plusPtr)+import GHC.Exts+import Wireform.Parser.Internal+++newtype Mark = Mark {unMark :: Word64}+ deriving stock (Eq, Ord, Show)+++mark :: Parser m e Mark+mark = Parser \env _ s st ->+ case curToPos env s st of+ (# st', pos #) -> (# st', OK# (Mark pos) s #)+{-# INLINE mark #-}+++restore :: Mark -> Parser m e ()+restore (Mark pos) = Parser \env _ _ st ->+ let !offset = fromIntegral pos .&. peMask env+ !(Ptr newCur) = peBaseAddr env `plusPtr` offset+ in (# st, OK# () newCur #)+{-# INLINE restore #-}+++release :: Mark -> Parser m e ()+release _ = Parser \_ _ s st -> (# st, OK# () s #)+{-# INLINE release #-}
+ src/Wireform/Parser/Position.hs view
@@ -0,0 +1,101 @@+{-# LANGUAGE BlockArguments #-}+{-# LANGUAGE MagicHash #-}+{-# LANGUAGE PatternSynonyms #-}+{-# LANGUAGE UnboxedSums #-}+{-# LANGUAGE UnboxedTuples #-}++module Wireform.Parser.Position (+ Pos (..),+ Span (..),+ subPos,+ getPos,+ withSpan,+ byteStringOf,+ spanToByteString,+ inSpan,+) where++import Data.Bits ((.&.))+import Data.ByteString (ByteString)+import Data.ByteString.Internal qualified as BSI+import Data.Word (Word64)+import Foreign.Marshal.Utils (copyBytes)+import Foreign.Ptr (plusPtr)+import GHC.Exts+import GHC.ForeignPtr (ForeignPtr (..))+import System.IO.Unsafe (unsafeDupablePerformIO)+import Wireform.Parser.Internal+++newtype Pos = Pos {unPos :: Word64}+ deriving stock (Eq, Ord, Show)+ deriving newtype (Num)+++data Span = Span !Pos !Pos+ deriving stock (Eq, Show)+++subPos :: Pos -> Pos -> Int+subPos (Pos a) (Pos b) = fromIntegral (a - b)+++getPos :: Parser m e Pos+getPos = Parser \env _ s st ->+ case curToPos env s st of+ (# st', pos #) -> (# st', OK# (Pos pos) s #)+{-# INLINE getPos #-}+++withSpan :: Parser m e a -> (a -> Span -> Parser m e b) -> Parser m e b+withSpan (Parser p) f = Parser \env eob s st ->+ case p env eob s st of+ (# st', OK# a s' #) ->+ case curToPos env s st' of+ (# st'', startPos #) ->+ case curToPos env s' st'' of+ (# st''', endPos #) ->+ let !sp = Span (Pos startPos) (Pos endPos)+ in runParser# (f a sp) env eob s' st'''+ (# st', x #) -> (# st', unsafeCoerce# x #)+{-# INLINE withSpan #-}+++byteStringOf :: Parser m e a -> Parser m e ByteString+byteStringOf (Parser p) = Parser \env eob s st ->+ case p env eob s st of+ (# st', OK# _ s' #) ->+ let !len = I# (minusAddr# s' s)+ !bs = BSI.BS (ForeignPtr s (peBackingFp env)) len+ in (# st', OK# bs s' #)+ (# st', x #) -> (# st', unsafeCoerce# x #)+{-# INLINE byteStringOf #-}+++spanToByteString :: Span -> Parser m e ByteString+spanToByteString (Span (Pos start) (Pos end)) = Parser \env _ s st ->+ let !len = fromIntegral (end - start)+ base = peBaseAddr env+ mask = peMask env+ off = fromIntegral start .&. mask+ !(Ptr ptr) = base `plusPtr` off+ !bs = unsafeDupablePerformIO (BSI.create len \dst -> copyBytes dst (Ptr ptr) len)+ in (# st, OK# bs s #)+++{- | Run a parser within an explicitly bounded byte window.+Temporarily restricts @eob@ to the span's end position, then+restores it on completion.+-}+inSpan :: Span -> Parser m e a -> Parser m e a+inSpan (Span (Pos start) (Pos end)) (Parser p) = Parser \env _ s st ->+ let base = peBaseAddr env+ mask = peMask env+ !endOff = fromIntegral end .&. mask+ !(Ptr spanEnd) = base `plusPtr` endOff+ !startOff = fromIntegral start .&. mask+ !(Ptr spanStart) = base `plusPtr` startOff+ in case p env spanEnd spanStart st of+ (# st', OK# a _ #) -> (# st', OK# a s #)+ (# st', x #) -> (# st', unsafeCoerce# x #)+{-# INLINE inSpan #-}
+ src/Wireform/Parser/Stateful.hs view
@@ -0,0 +1,252 @@+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE BlockArguments #-}+{-# LANGUAGE CPP #-}+{-# LANGUAGE ExplicitNamespaces #-}+{-# LANGUAGE MagicHash #-}+{-# LANGUAGE PatternSynonyms #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE UnboxedSums #-}+{-# LANGUAGE UnboxedTuples #-}++{- | Stateful parser variant with a reader environment @r@ and mutable+state @s@, following flatparse's @FlatParse.Stateful@ design.++State is stored in a raw @MutVar#@ — no IORef boxing overhead.+Reader is a plain boxed value.+-}+module Wireform.Parser.Stateful (+ ParserS (..),++ -- * Running+ runParserS,+ parseByteStringS,++ -- * Reader operations+ ask,+ asks,+ local,++ -- * State operations+ get,+ put,+ modify,+ modify',++ -- * Lifting basic parsers+ liftParser,++ -- * Re-exports+ module Wireform.Parser,+) where++import Data.ByteString.Internal qualified as BSI+import Data.Word (Word64, Word8)+import Foreign.ForeignPtr (withForeignPtr)+import Foreign.Marshal.Alloc (allocaBytes)+import Foreign.Ptr (castPtr, plusPtr)+import Foreign.Storable (poke)+import GHC.Exts+import GHC.ForeignPtr (ForeignPtr (..))+import GHC.IO (IO (..))+import System.IO.Unsafe (unsafeDupablePerformIO)+import Wireform.Parser+import Wireform.Parser.Internal (+ Parser (..),+ ParserEnv (..),+ Resume (..),+ Step (..),+ tagToAny,+ pattern Err#,+ pattern Fail#,+ pattern OK#,+ type StRes#,+ )+++------------------------------------------------------------------------+-- Stateful parser type+------------------------------------------------------------------------++{- | Parser with reader @r@ and mutable state @s@ stored in a raw+@MutVar#@. One pointer deref per 'get'/'put', no IORef boxing.+-}+newtype ParserS r s e a = ParserS+ { runParserS#+ :: forall result+ . PromptTag# (Step e result)+ -> ParserEnv+ -> r+ -> MutVar# RealWorld s+ -> Addr#+ -> Addr#+ -> State# RealWorld+ -> StRes# e a+ }+++instance Functor (ParserS r s e) where+ fmap f (ParserS g) = ParserS \tag env r mv eob s rw ->+ case g tag env r mv eob s rw of+ (# rw', OK# a s' #) -> let !b = f a in (# rw', OK# b s' #)+ (# rw', x #) -> (# rw', unsafeCoerce# x #)+ {-# INLINE fmap #-}+++instance Applicative (ParserS r s e) where+ pure !a = ParserS \_tag _env _r _mv _eob s rw -> (# rw, OK# a s #)+ {-# INLINE pure #-}+ ParserS ff <*> ParserS fa = ParserS \tag env r mv eob s rw ->+ case ff tag env r mv eob s rw of+ (# rw', OK# f s' #) -> case fa tag env r mv eob s' rw' of+ (# rw'', OK# a s'' #) -> let !b = f a in (# rw'', OK# b s'' #)+ (# rw'', x #) -> (# rw'', unsafeCoerce# x #)+ (# rw', x #) -> (# rw', unsafeCoerce# x #)+ {-# INLINE (<*>) #-}+ ParserS fa *> ParserS fb = ParserS \tag env r mv eob s rw ->+ case fa tag env r mv eob s rw of+ (# rw', OK# _ s' #) -> fb tag env r mv eob s' rw'+ (# rw', x #) -> (# rw', unsafeCoerce# x #)+ {-# INLINE (*>) #-}+++instance Monad (ParserS r s e) where+ ParserS fa >>= f = ParserS \tag env r mv eob s rw ->+ case fa tag env r mv eob s rw of+ (# rw', OK# a s' #) -> runParserS# (f a) tag env r mv eob s' rw'+ (# rw', x #) -> (# rw', unsafeCoerce# x #)+ {-# INLINE (>>=) #-}+ (>>) = (*>)+ {-# INLINE (>>) #-}+++------------------------------------------------------------------------+-- Reader operations+------------------------------------------------------------------------++ask :: ParserS r s e r+ask = ParserS \_tag _env r _mv _eob s rw -> (# rw, OK# r s #)+{-# INLINE ask #-}+++asks :: (r -> a) -> ParserS r s e a+asks f = ParserS \_tag _env r _mv _eob s rw -> let !a = f r in (# rw, OK# a s #)+{-# INLINE asks #-}+++local :: (r -> r) -> ParserS r s e a -> ParserS r s e a+local f (ParserS p) = ParserS \tag env r mv eob s rw ->+ p tag env (f r) mv eob s rw+{-# INLINE local #-}+++------------------------------------------------------------------------+-- State operations (raw MutVar#, no IORef)+------------------------------------------------------------------------++get :: ParserS r s e s+get = ParserS \_tag _env _r mv _eob s rw ->+ case readMutVar# mv rw of+ (# rw', val #) -> (# rw', OK# val s #)+{-# INLINE get #-}+++put :: s -> ParserS r s e ()+put !v = ParserS \_tag _env _r mv _eob s rw ->+ case writeMutVar# mv v rw of+ rw' -> (# rw', OK# () s #)+{-# INLINE put #-}+++modify :: (s -> s) -> ParserS r s e ()+modify f = ParserS \_tag _env _r mv _eob s rw ->+ case readMutVar# mv rw of+ (# rw', val #) -> case writeMutVar# mv (f val) rw' of+ rw'' -> (# rw'', OK# () s #)+{-# INLINE modify #-}+++modify' :: (s -> s) -> ParserS r s e ()+modify' f = ParserS \_tag _env _r mv _eob s rw ->+ case readMutVar# mv rw of+ (# rw', val #) ->+ let !val' = f val+ in case writeMutVar# mv val' rw' of+ rw'' -> (# rw'', OK# () s #)+{-# INLINE modify' #-}+++------------------------------------------------------------------------+-- Lifting basic parsers+------------------------------------------------------------------------++liftParser :: Parser m e a -> ParserS r s e a+liftParser (Parser p) = ParserS \_tag env _r _mv eob s rw ->+ p env eob s rw+{-# INLINE liftParser #-}+++------------------------------------------------------------------------+-- Running+------------------------------------------------------------------------++runParserS+ :: ParserS r s e a+ -> r+ -> s+ -> BSI.ByteString+ -> Either (ParseError e) (a, s)+runParserS p r s0 b = unsafeDupablePerformIO $ do+ let !(BSI.BS (ForeignPtr buf# fp) (I# len#)) = b+ !end# = plusAddr# buf# len#++ withForeignPtr (ForeignPtr buf# fp) \_ ->+ allocaBytes 24 \cells -> do+ let !endPtr = cells+ !anchorPos = cells `plusPtr` 8+ !anchorCur = cells `plusPtr` 16+ poke (castPtr endPtr :: Ptr (Ptr Word8)) (Ptr end#)+ poke (castPtr anchorPos :: Ptr Word64) 0+ poke (castPtr anchorCur :: Ptr (Ptr Word8)) (Ptr buf#)++ IO \rw0 -> case newMutVar# s0 rw0 of+ (# rw1, mv #) -> case newPromptTag# rw1 of+ (# rw2, (tag :: PromptTag# (Step e a)) #) ->+ let env =+ ParserEnv+ { peEndPtr = castPtr endPtr+ , peBaseAddr = Ptr buf#+ , peMask = maxBound+ , peAnchorPos = castPtr anchorPos+ , peAnchorCur = castPtr anchorCur+ , peBackingFp = fp+ , peTag = tagToAny tag+ }+ body :: State# RealWorld -> (# State# RealWorld, Step e a #)+ body rw = case runParserS# p tag env r mv end# buf# rw of+ (# rw', OK# a _cur #) -> (# rw', StepDone 0 a #)+ (# rw', Fail# #) -> (# rw', StepFail 0 #)+ (# rw', Err# e #) -> (# rw', StepErr 0 e #)+ in case prompt# tag body rw2 of+ (# rw3, step #) -> case readMutVar# mv rw3 of+ (# rw4, finalState #) ->+ (# rw4, classifyStep step finalState #)+ where+ classifyStep (StepDone _ a) s = Right (a, s)+ classifyStep (StepFail pos) _ = Left (ParseFail pos)+ classifyStep (StepErr pos e) _ = Left (ParseErr pos e)+ classifyStep (StepSuspend _ _ res) s =+ unsafeDupablePerformIO $+ resumeEof res >>= \step -> pure (classifyStep step s)+ classifyStep (StepCheckpoint _ res) s =+ unsafeDupablePerformIO $+ resumeEof res >>= \step -> pure (classifyStep step s)+++parseByteStringS+ :: ParserS r s e a+ -> r+ -> s+ -> BSI.ByteString+ -> Either (ParseError e) (a, s)+parseByteStringS = runParserS
+ src/Wireform/Parser/Switch.hs view
@@ -0,0 +1,288 @@+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE BlockArguments #-}+{-# LANGUAGE CPP #-}+{-# LANGUAGE MagicHash #-}+{-# LANGUAGE PatternSynonyms #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE UnboxedSums #-}+{-# LANGUAGE UnboxedTuples #-}++-- \$(switch [| case _ of+-- \"if\" -> pure TIf+-- \"else\" -> pure TElse+-- _ -> identifier |])+-- @++{- | Efficient literal branching using Template Haskell.++Compiles string-literal case branches into a trie of primitive byte+comparisons with grouped bounds checks.++@+-}+module Wireform.Parser.Switch (+ switch,+ switchWithPost,++ -- * TH literal splices+ char,+ string,++ -- * Helpers used by generated code (not for direct use)+ switchFailed,+ switchBranch,+ switchAnyWord8Unsafe,+ switchPeekWord8Unsafe,+ switchSkip1,+) where++import Control.Monad (forM)+import Data.ByteString.Char8 qualified as BSC+import Data.Char (ord)+import Data.Foldable (foldl')+import Data.Map.Strict (Map)+import Data.Map.Strict qualified as M+import Data.Word (Word8)+import GHC.Exts+import GHC.Word (Word8 (..))+import Language.Haskell.TH+import Language.Haskell.TH.Syntax (Lift (..))+import Wireform.Parser (byteString, word8)+import Wireform.Parser.Internal+++------------------------------------------------------------------------+-- Helpers called by generated code+------------------------------------------------------------------------++-- | Fail without consuming input.+switchFailed :: Parser m e a+switchFailed = Parser \_ _ _ st -> (# st, Fail# #)+{-# INLINE switchFailed #-}+++-- | Unsafe read — caller must have ensured at least 1 byte.+switchAnyWord8Unsafe :: Parser m e Word8+switchAnyWord8Unsafe = Parser \_ _ s st ->+ case indexWord8OffAddr# s 0# of+ w# -> (# st, OK# (W8# w#) (plusAddr# s 1#) #)+{-# INLINE switchAnyWord8Unsafe #-}+++{- | Peek the next byte without consuming. Used by the trie code+generator so that a wildcard-branch falling back to the+current node's terminal action does not eat a byte that+belongs to whatever runs after the switch.++(Previously the generator used 'switchAnyWord8Unsafe' for the+byte dispatch, which consumed the byte and then ran the+terminal action /past/ that byte. For input like @\"1,…\"@ on a+switch with literals @\"1\", \"1.0\", \"1.00\"@, that meant the+@\"1\"@ terminal action ran from after the comma — silently+breaking every quality-weighted Accept-* parser whose first+entry used @q=1@.)+-}+switchPeekWord8Unsafe :: Parser m e Word8+switchPeekWord8Unsafe = Parser \_env _eob s st ->+ case indexWord8OffAddr# s 0# of+ w# -> (# st, OK# (W8# w#) s #)+{-# INLINE switchPeekWord8Unsafe #-}+++{- | Skip one byte (used after 'switchPeekWord8Unsafe' has+decided a child branch matches).+-}+switchSkip1 :: Parser m e ()+switchSkip1 = Parser \_env _eob s st ->+ (# st, OK# () (plusAddr# s 1#) #)+{-# INLINE switchSkip1 #-}+++-- | Branch on an ensure check: if enough bytes, run @t@; else @f@.+switchBranch :: Int -> Parser m e a -> Parser m e a -> Parser m e a+switchBranch (I# n#) (Parser t) (Parser f) = Parser \env eob s st ->+ case n# <=# minusAddr# eob s of+ 1# -> t env eob s st+ _ -> f env eob s st+{-# INLINE switchBranch #-}+++------------------------------------------------------------------------+-- Trie+------------------------------------------------------------------------++type Rule = Maybe Int+++data Trie a = Branch !a !(Map Word8 (Trie a))+++nilTrie :: Trie Rule+nilTrie = Branch Nothing mempty+++insertTrie :: Int -> [Word8] -> Trie Rule -> Trie Rule+insertTrie rule = go+ where+ go [] (Branch r ts) = Branch (Just $ maybe rule (min rule) r) ts+ go (c : cs) (Branch r ts) =+ Branch r (M.alter (Just . maybe (go cs nilTrie) (go cs)) c ts)+++listToTrie :: [(Int, String)] -> Trie Rule+listToTrie = foldl' (\t (!r, !s) -> insertTrie r (charToBytes s) t) nilTrie+++charToBytes :: String -> [Word8]+charToBytes = concatMap go+ where+ go c+ | n < 0x80 = [fromIntegral n]+ | n < 0x800 =+ [ fromIntegral (0xC0 + n `div` 64)+ , fromIntegral (0x80 + n `mod` 64)+ ]+ | otherwise = [fromIntegral n] -- simplified; ASCII-only for v1+ where+ n = ord c+++mindepths :: Trie Rule -> Trie (Rule, Int)+mindepths (Branch rule ts)+ | M.null ts = Branch (rule, 0) mempty+ | otherwise =+ let ts' = M.map mindepths ts+ d =+ minimum $+ M.elems $+ M.map+ ( \(Branch (r, depth) _) ->+ maybe (depth + 1) (const 1) r+ )+ ts'+ in Branch (rule, d) ts'++------------------------------------------------------------------------+-- TH code generation+------------------------------------------------------------------------++#if MIN_VERSION_base(4,15,0)+mkDoE :: [Stmt] -> Exp+mkDoE = DoE Nothing+#else+mkDoE :: [Stmt] -> Exp+mkDoE = DoE+#endif+++switch :: Q Exp -> Q Exp+switch = switchWithPost Nothing+++switchWithPost :: Maybe (Q Exp) -> Q Exp -> Q Exp+switchWithPost postAction qexp = do+ post <- sequence postAction+ (cases, deflt) <- parseSwitchExp qexp+ genSwitch post cases deflt+++parseSwitchExp :: Q Exp -> Q ([(String, Exp)], Maybe Exp)+parseSwitchExp qexp =+ qexp >>= \case+ CaseE (UnboundVarE _) [] -> fail "switch: empty case list"+ CaseE (UnboundVarE _) matches -> do+ let (ini, lst) = (init matches, last matches)+ cases <- forM ini \case+ Match (LitP (StringL s)) (NormalB rhs) [] -> pure (s, rhs)+ _ -> fail "switch: expected string literal pattern"+ (cases', deflt) <- case lst of+ Match (LitP (StringL s)) (NormalB rhs) [] -> pure (cases <> [(s, rhs)], Nothing)+ Match WildP (NormalB rhs) [] -> pure (cases, Just rhs)+ _ -> fail "switch: expected string literal or wildcard pattern"+ pure (cases', deflt)+ _ -> fail "switch: expected 'case _ of' expression"+++genSwitch :: Maybe Exp -> [(String, Exp)] -> Maybe Exp -> Q Exp+genSwitch post cases deflt = do+ let indexed = zip [0 :: Int ..] cases+ trie = mindepths (listToTrie [(i, s) | (i, (s, _)) <- indexed])++ let ruleMap =+ M.fromList $+ (Nothing, VarE 'switchFailed)+ : [(Just i, applyPost post rhs) | (i, (_, rhs)) <- indexed]+ defltExp = maybe (VarE 'switchFailed) id deflt++ bindings <- forM (M.toList (M.insert Nothing defltExp ruleMap)) \(k, e) -> do+ n <- newName ("r" <> maybe "D" show k)+ pure (k, n, e)++ let nameMap = M.fromList [(k, n) | (k, n, _) <- bindings]++ body <- genTrieCode nameMap trie+ letE+ [valD (varP n) (normalB (pure e)) [] | (_, n, e) <- bindings]+ (pure body)+++genTrieCode :: Map (Maybe Int) Name -> Trie (Rule, Int) -> Q Exp+genTrieCode names = go 0+ where+ ruleName r = case M.lookup r names of+ Just n -> n+ Nothing -> error "switch: missing rule"++ go :: Int -> Trie (Rule, Int) -> Q Exp+ go ensured (Branch (rule, depth) ts)+ | M.null ts = pure (VarE (ruleName rule))+ | otherwise = do+ let need = ensured < 1+ branches <- forM (M.toList ts) \(w, sub) -> do+ e <- go (if need then depth - 1 else ensured - 1) sub+ pure (w, InfixE (Just (VarE 'switchSkip1)) (VarE '(>>)) (Just e))++ let defE = VarE (ruleName rule)+ body =+ mkDoE+ [ BindS (VarP (mkName "c")) (VarE 'switchPeekWord8Unsafe)+ , NoBindS+ ( CaseE+ (VarE (mkName "c"))+ ( [ Match (LitP (IntegerL (fromIntegral w))) (NormalB e) []+ | (w, e) <- branches+ ]+ <> [Match WildP (NormalB defE) []]+ )+ )+ ]++ if need+ then [|switchBranch depth $(pure body) $(pure defE)|]+ else pure body+++applyPost :: Maybe Exp -> Exp -> Exp+applyPost Nothing rhs = rhs+applyPost (Just p) rhs = InfixE (Just p) (VarE '(>>)) (Just rhs)+++------------------------------------------------------------------------+-- TH literal splices (flatparse-compatible)+------------------------------------------------------------------------++{- | @$(char \'x\')@ compiles to @word8 0xNN@ for ASCII characters,+or the appropriate multi-byte UTF-8 sequence otherwise.+Only ASCII is supported for now.+-}+char :: Char -> Q Exp+char c+ | n < 0x80 = [|word8 $(lift (fromIntegral n :: Word8))|]+ | otherwise = error ("Wireform.Parser.Switch.char: non-ASCII character: " <> show c)+ where+ n = ord c+++-- | @$(string \"foo\")@ compiles to @byteString \"foo\"@.+string :: String -> Q Exp+string s = [|byteString $(lift (BSC.pack s))|]
+ src/Wireform/Ring.hs view
@@ -0,0 +1,48 @@+{-# LANGUAGE CApiFFI #-}+{-# LANGUAGE RankNTypes #-}++{- | Double-mapped (\"magic\") ring buffer.++A buffer of size @N@ (power of two, page-aligned) backed by a single+shared-memory object mapped twice into a contiguous @2N@-byte virtual+region. Any read of up to @N@ bytes starting anywhere in+@[base, base + N)@ is contiguous in virtual memory — the MMU handles+the wrap transparently.++The parser's pointer-bumping primitives rely on this property: they+never contain wrap logic.++== Scoping++'MagicRing' carries a phantom type parameter @s@ that works like the+@s@ on 'Control.Monad.ST.ST'. 'withMagicRing' is rank-2:++> withMagicRing :: Int -> (forall s. MagicRing s -> IO a) -> IO a++Slices produced from the ring ('RingSlice') inherit @s@ and therefore+cannot appear in @a@ — they are unable to outlive the buffer that+backs them and would otherwise alias bytes that subsequent refills+overwrite. When a slice has to leave the scope, 'copyRingSlice'+materialises a fresh 'ByteString' by memcpy.+-}+module Wireform.Ring (+ -- * The ring+ MagicRing,+ withMagicRing,+ ringBase,+ ringSize,+ ringMask,+ MagicRingException (..),++ -- * Slices+ RingSlice,+ ringSlice,+ ringSliceAtPos,+ ringSliceLength,+ withRingSlice,+ peekRingSliceByte,+ copyRingSlice,+) where++import Wireform.Ring.Internal+
+ src/Wireform/Ring/Internal.hs view
@@ -0,0 +1,315 @@+{-# LANGUAGE CApiFFI #-}+{-# LANGUAGE MagicHash #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE RoleAnnotations #-}+{-# LANGUAGE UnboxedTuples #-}++{- | Internal module exposing the unsafe machinery behind 'MagicRing'.++The phantom @s@ on 'MagicRing' acts like the @s@ on 'Control.Monad.ST.ST':+it is sealed inside 'withMagicRing' with a rank-2 @forall@, so values+that carry the @s@ — most importantly 'RingSlice' — cannot leak out+of the scope in which the ring is alive (and therefore cannot+dangle through a refill that overwrites the bytes they point at).++Code that genuinely needs the un-scoped variants ('newMagicRing',+'destroyMagicRing', the raw @ringBase@ pointer) lives here.+Day-to-day callers should prefer the safer surface in "Wireform.Ring".+-}+module Wireform.Ring.Internal (+ -- * The ring+ MagicRing (..),+ newMagicRing,+ destroyMagicRing,+ withMagicRing,+ ringBase,+ ringSize,+ ringMask,+ MagicRingException (..),++ -- * Slices+ RingSlice (..),+ ringSlice,+ ringSliceAtPos,+ ringSliceLength,+ ringSliceBase,+ withRingSlice,+ peekRingSliceByte,+ copyRingSlice,++ -- * Unsafe internals (no scoping guarantees)+ unsafeRingSliceFromPtr,+ unsafeRingScope,+) where++import Control.Exception (Exception, bracket, throwIO)+import Data.Bits ((.&.))+import Data.ByteString (ByteString)+import Data.ByteString.Internal qualified as BSI+import Data.Typeable (Typeable)+import Data.Word (Word64, Word8)+import Foreign.C.Types (CInt (..), CLong (..), CSize (..))+import Foreign.Marshal.Alloc (alloca)+import Foreign.Marshal.Utils (copyBytes)+import Foreign.Ptr (Ptr, nullPtr, plusPtr)+import Foreign.Storable (Storable (..), peek, peekByteOff, poke, pokeByteOff)+++------------------------------------------------------------------------+-- MagicRing+------------------------------------------------------------------------++{- | Opaque handle to a double-mapped ring buffer.++The phantom @s@ is /nominal/: it has no run-time content, only the+type-level role of preventing slices from escaping their ring's+scope (see 'withMagicRing' / 'RingSlice').+-}+data MagicRing s = MagicRing+ { mrBase :: {-# UNPACK #-} !(Ptr Word8)+ , mrSize :: {-# UNPACK #-} !Int+ }+++type role MagicRing nominal+++-- C struct layout: { void *base; size_t size; }+-- On Windows there's an extra HANDLE field, but we don't need it from+-- the Haskell side — hs_ring_destroy handles cleanup.+data CRing = CRing+ { crBase :: {-# UNPACK #-} !(Ptr Word8)+ , crSize :: {-# UNPACK #-} !CSize+ }+++instance Storable CRing where+ sizeOf _ = sizeOf (undefined :: Ptr ()) + sizeOf (undefined :: CSize)+++ -- conservative: ignores padding. On all target platforms+ -- both fields are pointer-sized and naturally aligned.+ alignment _ = alignment (undefined :: Ptr ())+ peek p = do+ b <- peekByteOff p 0+ s <- peekByteOff p (sizeOf (undefined :: Ptr ()))+ pure (CRing b s)+ poke p (CRing b s) = do+ pokeByteOff p 0 b+ pokeByteOff p (sizeOf (undefined :: Ptr ())) s+++foreign import capi unsafe "magic_ring.h hs_ring_create"+ c_ring_create :: CSize -> Ptr CRing -> IO CInt+++foreign import capi unsafe "magic_ring.h hs_ring_destroy"+ c_ring_destroy :: Ptr CRing -> IO ()+++data MagicRingException+ = MagicRingUnavailable !String+ deriving stock (Show, Typeable)+++instance Exception MagicRingException+++{- | Allocate a magic ring of at least the requested size (in bytes).+The actual size is rounded up to the nearest page-size multiple and+power of two. Throws 'MagicRingUnavailable' on failure.++The returned ring is polymorphic in @s@: callers either pair it+with 'destroyMagicRing' manually (no scope safety) or, preferably,+use 'withMagicRing' which binds @s@ inside a rank-2 scope.+-}+newMagicRing :: Int -> IO (MagicRing s)+newMagicRing requested = alloca $ \p -> do+ poke p (CRing nullPtr 0)+ rc <- c_ring_create (fromIntegral requested) p+ if rc /= 0+ then+ throwIO+ ( MagicRingUnavailable $+ "hs_ring_create failed for requested size " <> show requested+ )+ else do+ cr <- peek p+ pure+ MagicRing+ { mrBase = crBase cr+ , mrSize = fromIntegral (crSize cr)+ }+++-- | Release the ring's virtual mappings. Safe to call multiple times.+destroyMagicRing :: MagicRing s -> IO ()+destroyMagicRing mr = alloca $ \p -> do+ poke p (CRing (mrBase mr) (fromIntegral (mrSize mr)))+ c_ring_destroy p+++{- | Scoped allocation, modelled after 'Control.Monad.ST.runST'.++The body is rank-2: the @s@ inside the body is a fresh skolem that+cannot unify with anything outside the @forall@. Any value carrying+that @s@ — slices, sub-buffers, anything we add later — therefore+cannot appear in the result type @a@ and cannot escape.++> withMagicRing 4096 $ \\ring -> do+> let slice = ringSlice ring 0 16+> copyRingSlice slice -- OK, copy escapes as a fresh ByteString+> pure slice -- type error: RingSlice s escapes+-}+withMagicRing :: Int -> (forall s. MagicRing s -> IO a) -> IO a+withMagicRing n action = bracket (newMagicRing n) destroyMagicRing action+++-- | Base pointer to the start of the ring (first of the two mappings).+ringBase :: MagicRing s -> Ptr Word8+ringBase = mrBase+{-# INLINE ringBase #-}+++-- | Physical size of the ring in bytes (N, not 2N).+ringSize :: MagicRing s -> Int+ringSize = mrSize+{-# INLINE ringSize #-}+++{- | Bitmask for cheap @pos .&. ringMask@ indexing.+Always @ringSize - 1@ since the size is a power of two.+-}+ringMask :: MagicRing s -> Int+ringMask mr = mrSize mr - 1+{-# INLINE ringMask #-}+++------------------------------------------------------------------------+-- RingSlice+------------------------------------------------------------------------++{- | A contiguous slice into a 'MagicRing'.++'RingSlice' is deliberately /not/ a 'ByteString': the only way to+materialise a 'ByteString' from one is 'copyRingSlice', which performs+a memcpy into freshly-allocated memory. This makes the cost of+escaping the ring's scope visible at the call site and prevents+accidental retention of a pointer that subsequent refills will+overwrite.++The phantom @s@ ties the slice to its originating ring's scope;+because 'withMagicRing' seals @s@ inside a rank-2 @forall@, a slice+can never outlive the ring it came from when constructed through+the safe API.++The base pointer lies inside the first mapping (@[base, base + N)@)+but, by virtue of the double mapping, callers may read up to+'ringSliceLength' bytes contiguously from it without wrap logic.+-}+data RingSlice s = RingSlice+ { _rsPtr :: {-# UNPACK #-} !(Ptr Word8)+ , _rsLen :: {-# UNPACK #-} !Int+ }+++type role RingSlice nominal+++{- | Build a slice from a byte offset within the ring and a length.++The offset is taken modulo 'ringSize' so callers can pass an+absolute consumer position directly. The length may exceed+@ringSize - (offset \`mod\` ringSize)@ — the double mapping makes+the read contiguous as long as @len <= ringSize@.+-}+ringSlice :: MagicRing s -> Int -> Int -> RingSlice s+ringSlice mr offset len =+ let !msk = mrSize mr - 1+ !off = offset .&. msk+ !p = mrBase mr `plusPtr` off+ in RingSlice p len+{-# INLINE ringSlice #-}+++{- | Build a slice from an absolute (monotonic) producer/consumer+position and a length — the form the transport layer naturally+speaks.+-}+ringSliceAtPos :: MagicRing s -> Word64 -> Int -> RingSlice s+ringSliceAtPos mr pos len =+ let !msk = mrSize mr - 1+ !off = fromIntegral pos .&. msk+ !p = mrBase mr `plusPtr` off+ in RingSlice p len+{-# INLINE ringSliceAtPos #-}+++-- | Slice length in bytes.+ringSliceLength :: RingSlice s -> Int+ringSliceLength (RingSlice _ n) = n+{-# INLINE ringSliceLength #-}+++{- | Base pointer of the slice.++Exposed in @.Internal@ only. The pointer is /not/ tagged with @s@,+so once you reach for it you are responsible for not retaining it+past the ring's lifetime. Prefer 'withRingSlice' for scoped access.+-}+ringSliceBase :: RingSlice s -> Ptr Word8+ringSliceBase (RingSlice p _) = p+{-# INLINE ringSliceBase #-}+++{- | Borrow the slice's pointer + length for an IO action. The @s@+on 'RingSlice' guarantees the call site is inside the ring's scope;+the body is conventionally responsible for not stashing the pointer+somewhere it could outlive that scope.+-}+withRingSlice :: RingSlice s -> (Ptr Word8 -> Int -> IO a) -> IO a+withRingSlice (RingSlice p n) k = k p n+{-# INLINE withRingSlice #-}+++-- | Read a single byte at an offset within the slice. No bounds check.+peekRingSliceByte :: RingSlice s -> Int -> IO Word8+peekRingSliceByte (RingSlice p _) i = peek (p `plusPtr` i)+{-# INLINE peekRingSliceByte #-}+++{- | The escape hatch. Allocates a fresh 'ByteString' and memcpys the+slice's bytes into it. The result has no dependency on the ring,+so it can leave the 'withMagicRing' scope freely.+-}+copyRingSlice :: RingSlice s -> IO ByteString+copyRingSlice (RingSlice p n)+ | n <= 0 = pure mempty+ | otherwise = BSI.create n $ \dst -> copyBytes dst p n+{-# INLINE copyRingSlice #-}+++------------------------------------------------------------------------+-- Unsafe internals+------------------------------------------------------------------------++{- | Construct a 'RingSlice' directly from a raw pointer + length.++This is the back door used by the parser internals, which already+track bounds against the live ring. It bypasses the scope check+because the @s@ is freely chosen by the caller; only call this+when you can guarantee, by other means, that the pointer is+inside a live ring.+-}+unsafeRingSliceFromPtr :: Ptr Word8 -> Int -> RingSlice s+unsafeRingSliceFromPtr = RingSlice+{-# INLINE unsafeRingSliceFromPtr #-}+++{- | Re-tag a ring under a different @s@. Like 'unsafeCoerce' for the+phantom — useful when wrapping a long-lived (unsafely-allocated)+ring in a scoped action. Do not export from "Wireform.Ring".+-}+unsafeRingScope :: MagicRing s -> MagicRing s'+unsafeRingScope (MagicRing b sz) = MagicRing b sz+{-# INLINE unsafeRingScope #-}
+ src/Wireform/Ring/Pool.hs view
@@ -0,0 +1,102 @@+{- | A pool of pre-allocated 'MagicRing' buffers for reuse.++Allocating a magic ring costs @memfd_create@ + @ftruncate@ + 3×+@mmap@ + @memset@ per ring. This pool keeps a bounded free list of+rings so connection churn doesn't repeat that syscall work.++Rings are stored as raw @(Ptr Word8, Int)@ pairs — no IORefs, no+per-ring mutable state. The transport-layer cursor state (head,+tail, open\/closed) is always fresh per connection; only the+underlying mmap'd memory is recycled.+-}+module Wireform.Ring.Pool (+ RingPool,+ RingPoolConfig (..),+ defaultRingPoolConfig,+ newRingPool,+ closeRingPool,+ acquireRing,+ releaseRing,+) where++import Control.Concurrent.MVar+import Control.Exception (SomeException, mask_, try)+import Control.Monad (forM_)+import GHC.Exts (RealWorld)+import Wireform.Ring.Internal (MagicRing (..), destroyMagicRing, newMagicRing)+++data RingPoolConfig = RingPoolConfig+ { ringPoolMaxIdle :: !Int+ }+++defaultRingPoolConfig :: RingPoolConfig+defaultRingPoolConfig =+ RingPoolConfig+ { ringPoolMaxIdle = 64+ }+++{- | The pool is just a bounded free list protected by an MVar.+All rings in the list have the same allocated size (the first+allocation's rounded-up size sets the class).+-}+data RingPool = RingPool+ { rpMaxIdle :: !Int+ , rpFree :: !(MVar [MagicRing RealWorld])+ }+++newRingPool :: RingPoolConfig -> IO RingPool+newRingPool cfg = do+ free <- newMVar []+ pure+ RingPool+ { rpMaxIdle = ringPoolMaxIdle cfg+ , rpFree = free+ }+++{- | Destroy all idle rings. The pool remains usable (acquire+allocates fresh, release destroys immediately).+-}+closeRingPool :: RingPool -> IO ()+closeRingPool pool = mask_ $ do+ rings <- swapMVar (rpFree pool) []+ forM_ rings $ \r ->+ () <$ try @SomeException (destroyMagicRing r)+++-- | Take a ring from the pool or allocate a fresh one.+acquireRing :: RingPool -> Int -> IO (MagicRing s)+acquireRing pool requested = mask_ $ do+ rings <- takeMVar (rpFree pool)+ case rings of+ (r : rs) | mrSize r >= requested -> do+ putMVar (rpFree pool) rs+ pure (coerceRing r)+ _ -> do+ putMVar (rpFree pool) rings+ newMagicRing requested+++{- | Return a ring to the pool. Destroyed immediately if the pool+is full.+-}+releaseRing :: RingPool -> MagicRing s -> IO ()+releaseRing pool ring = mask_ $ do+ rings <- takeMVar (rpFree pool)+ if length rings >= rpMaxIdle pool+ then do+ putMVar (rpFree pool) rings+ destroyMagicRing ring+ else+ putMVar (rpFree pool) (coerceRing ring : rings)+++-- MagicRing's phantom s is nominal, but the pool stores them as+-- RealWorld and hands them out polymorphically. The underlying+-- data is just (Ptr Word8, Int) — the coercion is safe.+coerceRing :: MagicRing s -> MagicRing s'+coerceRing (MagicRing p n) = MagicRing p n
+ src/Wireform/Transport.hs view
@@ -0,0 +1,17 @@+{- | Magic-ring transports — receive and send.++This module is a thin umbrella over the symmetric pair+'Wireform.Transport.Receive.ReceiveTransport' (consumer of bytes+the producer side wrote into a ring) and+'Wireform.Transport.Send.SendTransport' (producer of bytes the+consumer side will drain from a ring). See the per-direction+modules for the full doc.+-}+module Wireform.Transport (+ module Wireform.Transport.Receive,+ module Wireform.Transport.Send,+) where++import Wireform.Transport.Receive+import Wireform.Transport.Send+
+ src/Wireform/Transport/Capabilities.hs view
@@ -0,0 +1,306 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE BlockArguments #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-}+{-# OPTIONS_GHC -Wno-unused-top-binds -Wno-unused-local-binds #-}++module Wireform.Transport.Capabilities+ ( SystemCapabilities (..)+ , IOUringFeatures (..)+ , NumaNodeInfo (..)+ , CoreTopology (..)+ , Placement (..)+ , detectCapabilities+ , recommendPlacement+ ) where++import qualified Control.Exception as CE+import Data.ByteString (ByteString)+import qualified Data.ByteString as BS+import Data.IntMap.Strict (IntMap)+import qualified Data.IntMap.Strict as IntMap+import Data.IORef+import Foreign.C.Types (CLong (..))+import System.IO.Unsafe (unsafePerformIO)++import Wireform.Parser+import Wireform.Parser.Internal (Pure)+import Wireform.Parser.Driver (parseByteString)++data SystemCapabilities = SystemCapabilities+ { capPageSize :: !Int+ , capHugePageSizes :: ![Int]+ , capHasIOUring :: !Bool+ , capIOUringFeatures :: !IOUringFeatures+ , capNumaNodes :: ![NumaNodeInfo]+ , capIsolatedCores :: ![Int]+ , capCoreCount :: !Int+ , capCoreTopology :: !CoreTopology+ } deriving stock (Show)++data IOUringFeatures = IOUringFeatures+ { ioUringFeatureProvidedBuffers :: !Bool+ , ioUringFeatureMultishotRecv :: !Bool+ , ioUringFeatureSQPoll :: !Bool+ } deriving stock (Show)++data NumaNodeInfo = NumaNodeInfo+ { numaNodeId :: !Int+ , numaNodeCores :: ![Int]+ , numaNodeMemoryMB :: !Int+ } deriving stock (Show)++data CoreTopology = CoreTopology+ { topoCoreToPackage :: !(IntMap Int)+ , topoCoreToL3 :: !(IntMap Int)+ , topoSiblings :: !(IntMap [Int])+ } deriving stock (Show)++data Placement = Placement+ { placementNumaNode :: !(Maybe Int)+ , placementCore :: !(Maybe Int)+ } deriving stock (Show)++{-# NOINLINE cachedCapabilities #-}+cachedCapabilities :: IORef (Maybe SystemCapabilities)+cachedCapabilities = unsafePerformIO (newIORef Nothing)++detectCapabilities :: IO SystemCapabilities+detectCapabilities = do+ cached <- readIORef cachedCapabilities+ case cached of+ Just c -> pure c+ Nothing -> do+ c <- detectCapabilitiesUncached+ writeIORef cachedCapabilities (Just c)+ pure c++detectCapabilitiesUncached :: IO SystemCapabilities+detectCapabilitiesUncached = do+ ps <- getPageSize+ hugeSizes <- getHugePageSizes+ cores <- getCoreCount+ isolated <- getIsolatedCores+ hasUring <- getHasIOUring+ uringFeats <- getIOUringFeatures+ numa <- getNumaNodes+ pure SystemCapabilities+ { capPageSize = ps+ , capHugePageSizes = hugeSizes+ , capHasIOUring = hasUring+ , capIOUringFeatures = uringFeats+ , capNumaNodes = numa+ , capIsolatedCores = isolated+ , capCoreCount = cores+ , capCoreTopology = CoreTopology IntMap.empty IntMap.empty IntMap.empty+ }++recommendPlacement :: Maybe Int -> IO Placement+recommendPlacement _fdNumaNode = pure (Placement Nothing Nothing)++------------------------------------------------------------------------+-- Parsers for /proc and /sys files (using our own parser)+------------------------------------------------------------------------++type P = Parser Pure ()++-- | Parse a CPU list like "0-3,5,7-9" into [0,1,2,3,5,7,8,9].+pCpuList :: P [Int]+pCpuList = do+ first <- pRange+ rest <- many (word8 0x2C *> pRange) -- ','+ pure (concat (first : rest))+ where+ pRange :: P [Int]+ pRange = do+ lo <- anyAsciiDecimalInt+ withOption (word8 0x2D *> anyAsciiDecimalInt) -- '-'+ (\hi -> pure [lo..hi])+ (pure [lo])++-- | Parse a decimal integer, skipping leading whitespace.+pDecimal :: P Int+pDecimal = skipWs *> anyAsciiDecimalInt++-- | Skip ASCII whitespace.+skipWs :: P ()+skipWs = skipMany (satisfyAscii (\c -> c == ' ' || c == '\t' || c == '\n' || c == '\r'))++-- | Skip to after a keyword, then parse what follows.+pAfterKeyword :: ByteString -> P a -> P a+pAfterKeyword kw p = go+ where+ go = (byteString kw *> p) <|> (anyWord8 *> go)++-- | Count lines starting with a given prefix.+pCountPrefix :: ByteString -> P Int+pCountPrefix pfx = go 0+ where+ go !n = (byteString pfx *> pSkipLine *> go (n + 1))+ <|> (pSkipLine *> go n) -- pSkipLine fails at EOF, terminating the loop+ <|> pure n++-- | Skip bytes until newline (inclusive). Fails at EOF.+pSkipLine :: P ()+pSkipLine = go+ where+ go = withAnyWord8 \w ->+ if w == 0x0A then pure () else go++-- | Parse "Linux version X.Y.Z ..." from /proc/version.+pKernelVersion :: P (Int, Int)+pKernelVersion = pAfterKeyword "version " $ do+ major <- anyAsciiDecimalInt+ word8 0x2E -- '.'+ minor <- anyAsciiDecimalInt+ pure (major, minor)++-- | Parse "Hugepagesize: 2048 kB" lines from /proc/meminfo.+pHugePageSize :: P [Int]+pHugePageSize = go []+ where+ go !acc =+ (do byteString "Hugepagesize:"+ skipWs+ n <- anyAsciiDecimalInt+ skipWs+ byteString "kB"+ pSkipLine+ go (n * 1024 : acc))+ <|> (pSkipLine *> go acc)+ <|> pure (reverse acc)++-- | Parse "MemTotal: 12345 kB" from a NUMA node's meminfo.+pNodeMemMB :: P Int+pNodeMemMB = go+ where+ go = (do byteString "MemTotal:"+ skipWs+ n <- anyAsciiDecimalInt+ pure (n `div` 1024))+ <|> (pSkipLine *> go)+ <|> pure 0++------------------------------------------------------------------------+-- File reading + parsing+------------------------------------------------------------------------++tryReadFileBS :: FilePath -> IO (Maybe ByteString)+tryReadFileBS path = do+ r <- CE.try @CE.SomeException (BS.readFile path)+ case r of+ Left _ -> pure Nothing+ Right s -> pure (Just s)++runP :: P a -> ByteString -> Maybe a+runP p bs = case parseByteString p bs of+ Right a -> Just a+ Left _ -> Nothing++------------------------------------------------------------------------+-- Platform-specific detection+------------------------------------------------------------------------++foreign import ccall unsafe "hs_page_size"+ c_page_size :: IO CLong++getPageSize :: IO Int+getPageSize = fromIntegral <$> c_page_size++getCoreCount :: IO Int+getCoreCount = do+#if defined(linux_HOST_OS)+ r <- tryReadFileBS "/proc/cpuinfo"+ case r of+ Nothing -> pure 1+ Just bs -> pure $ max 1 (maybe 1 id (runP (pCountPrefix "processor") bs))+#else+ pure 1+#endif++getHugePageSizes :: IO [Int]+getHugePageSizes = do+#if defined(linux_HOST_OS)+ r <- tryReadFileBS "/proc/meminfo"+ case r of+ Nothing -> pure []+ Just bs -> pure $ maybe [] id (runP pHugePageSize bs)+#else+ pure []+#endif++getIsolatedCores :: IO [Int]+getIsolatedCores = do+#if defined(linux_HOST_OS)+ r <- tryReadFileBS "/sys/devices/system/cpu/isolated"+ case r of+ Nothing -> pure []+ Just bs -> pure $ maybe [] id (runP (skipWs *> pCpuList) (BS.filter (/= 0x0A) bs))+#else+ pure []+#endif++getHasIOUring :: IO Bool+getHasIOUring = do+#if defined(linux_HOST_OS)+ ver <- kernelVersion+ pure (ver >= (5, 1))+#else+ pure False+#endif++getIOUringFeatures :: IO IOUringFeatures+getIOUringFeatures = do+#if defined(linux_HOST_OS)+ hasUring <- getHasIOUring+ if not hasUring+ then pure (IOUringFeatures False False False)+ else do+ ver <- kernelVersion+ pure IOUringFeatures+ { ioUringFeatureProvidedBuffers = ver >= (5, 19)+ , ioUringFeatureMultishotRecv = ver >= (6, 0)+ , ioUringFeatureSQPoll = ver >= (5, 1)+ }+#else+ pure (IOUringFeatures False False False)+#endif++getNumaNodes :: IO [NumaNodeInfo]+getNumaNodes = do+#if defined(linux_HOST_OS)+ r <- tryReadFileBS "/sys/devices/system/node/online"+ case r of+ Nothing -> pure []+ Just bs -> do+ let nodeIds = maybe [] id (runP (skipWs *> pCpuList) (BS.filter (/= 0x0A) bs))+ mapM getNodeInfo nodeIds+#else+ pure []+#endif+ where+ getNodeInfo nid = do+ cpuList <- tryReadFileBS ("/sys/devices/system/node/node" <> bsShow nid <> "/cpulist")+ memInfo <- tryReadFileBS ("/sys/devices/system/node/node" <> bsShow nid <> "/meminfo")+ let cores = case cpuList of+ Nothing -> []+ Just bs -> maybe [] id (runP pCpuList (BS.filter (/= 0x0A) bs))+ memMB = case memInfo of+ Nothing -> 0+ Just bs -> maybe 0 id (runP pNodeMemMB bs)+ pure (NumaNodeInfo nid cores memMB)++ bsShow :: Int -> FilePath+ bsShow = show++kernelVersion :: IO (Int, Int)+kernelVersion = do+#if defined(linux_HOST_OS)+ r <- tryReadFileBS "/proc/version"+ pure $ case r of+ Nothing -> (0, 0)+ Just bs -> maybe (0, 0) id (runP pKernelVersion bs)+#else+ pure (0, 0)+#endif
+ src/Wireform/Transport/Config.hs view
@@ -0,0 +1,221 @@+module Wireform.Transport.Config (+ -- * Profiles+ Profile (..),+ profileConfig,++ -- * Full configuration+ TransportConfig (..),+ defaultTransportConfig,++ -- * Wait policy+ WaitPolicy (..),+ SpinBudget (..),++ -- * Pinning+ PinningPolicy (..),++ -- * Pages+ PagePolicy (..),++ -- * NUMA+ NumaPolicy (..),++ -- * io_uring+ IOUringConfig (..),+ SQPollPolicy (..),+ CompletionWaitMode (..),+ defaultIOUringConfig,++ -- * Capability handling+ CapabilityAction (..),+) where+++------------------------------------------------------------------------+-- Profiles+------------------------------------------------------------------------++{- | High-level performance profile. Most users pick one of these+and never touch 'TransportConfig' directly.+-}+data Profile+ = -- | IO-manager-parked. Good citizen on shared systems.+ Throughput+ | -- | Brief spin before parking, pinned thread, huge pages where available.+ LowLatency+ | {- | Pure busy-poll, pinned to isolated core, huge pages, mlock.+ Burns one CPU core permanently.+ -}+ UltraLowLatency+ deriving stock (Eq, Ord, Show, Read, Bounded, Enum)+++-- | Expand a profile into a full 'TransportConfig'.+profileConfig :: Profile -> TransportConfig+profileConfig Throughput = defaultTransportConfig+profileConfig LowLatency =+ defaultTransportConfig+ { waitPolicy = WaitSpinThenPark (SpinNanos 5000)+ , pinning = PinNearFd+ , pages = PreferHugePages+ , memoryLocking = True+ , numaPlacement = NumaAutoFromFd+ }+profileConfig UltraLowLatency =+ defaultTransportConfig+ { waitPolicy = WaitBusyPoll+ , pinning = PinIsolated+ , pages = PreferHugePages+ , memoryLocking = True+ , numaPlacement = NumaAutoFromFd+ , ioUring =+ defaultIOUringConfig+ { ioUringQueueDepth = 1024+ , ioUringSQPoll = SQPollWithIdle 100+ , ioUringCompletionWait = WaitViaBusyPoll+ }+ }+++------------------------------------------------------------------------+-- Full configuration+------------------------------------------------------------------------++data TransportConfig = TransportConfig+ { ringSizeHint :: !Int+ {- ^ Requested ring size in bytes. Rounded up to page-size multiple+ and power of two. Default: 1 MiB.+ -}+ , waitPolicy :: !WaitPolicy+ , pinning :: !PinningPolicy+ , pages :: !PagePolicy+ , memoryLocking :: !Bool+ -- ^ @mlock@ the ring. Requires sufficient RLIMIT_MEMLOCK.+ , numaPlacement :: !NumaPolicy+ , ioUring :: !IOUringConfig+ -- ^ Linux io_uring tuning (silently ignored elsewhere).+ , onCapabilityLimit :: !CapabilityAction+ -- ^ What to do when a knob cannot be applied on this platform.+ }+ deriving stock (Show)+++defaultTransportConfig :: TransportConfig+defaultTransportConfig =+ TransportConfig+ { ringSizeHint = 1024 * 1024+ , waitPolicy = WaitParkImmediately+ , pinning = NoPinning+ , pages = StandardPages+ , memoryLocking = False+ , numaPlacement = NoNumaPreference+ , ioUring = defaultIOUringConfig+ , onCapabilityLimit = LogAndContinue+ }+++------------------------------------------------------------------------+-- Wait policy+------------------------------------------------------------------------++data WaitPolicy+ = -- | Park on the IO manager as soon as caught up.+ WaitParkImmediately+ | -- | Busy-spin for the budget, then park.+ WaitSpinThenPark !SpinBudget+ | -- | Never park. Burns a core.+ WaitBusyPoll+ deriving stock (Show)+++data SpinBudget+ = SpinIterations !Int+ | SpinNanos !Int+ | SpinUntilPaused+ deriving stock (Show)+++------------------------------------------------------------------------+-- Pinning+------------------------------------------------------------------------++data PinningPolicy+ = NoPinning+ | PinToCore !Int+ | PinToCoreSet ![Int]+ | -- | Auto-detect: pin near the NIC / fd's NUMA node.+ PinNearFd+ | -- | Pick an isolated core (Linux @isolcpus=@); fall back to 'PinNearFd'.+ PinIsolated+ deriving stock (Show)+++------------------------------------------------------------------------+-- Page policy+------------------------------------------------------------------------++data PagePolicy+ = StandardPages+ | -- | Request huge pages; fall back to standard if unavailable.+ PreferHugePages+ | -- | Fail at ring creation if huge pages are unavailable.+ RequireHugePages+ deriving stock (Show)+++------------------------------------------------------------------------+-- NUMA+------------------------------------------------------------------------++data NumaPolicy+ = NoNumaPreference+ | NumaNode !Int+ | NumaAutoFromFd+ | NumaAutoFromCurrentCore+ deriving stock (Show)+++------------------------------------------------------------------------+-- io_uring+------------------------------------------------------------------------++data IOUringConfig = IOUringConfig+ { ioUringQueueDepth :: !Int+ , ioUringSQPoll :: !SQPollPolicy+ , ioUringProvidedBuffers :: !Bool+ , ioUringCompletionWait :: !CompletionWaitMode+ }+ deriving stock (Show)+++defaultIOUringConfig :: IOUringConfig+defaultIOUringConfig =+ IOUringConfig+ { ioUringQueueDepth = 128+ , ioUringSQPoll = NoSQPoll+ , ioUringProvidedBuffers = True+ , ioUringCompletionWait = WaitViaEventFd+ }+++data SQPollPolicy+ = NoSQPoll+ | SQPollWithIdle !Int+ deriving stock (Show)+++data CompletionWaitMode+ = WaitViaEventFd+ | WaitViaBusyPoll+ deriving stock (Show)+++------------------------------------------------------------------------+-- Capability action+------------------------------------------------------------------------++data CapabilityAction+ = SilentlyIgnore+ | LogAndContinue+ | FailHard+ deriving stock (Eq, Ord, Show, Read, Bounded, Enum)
+ src/Wireform/Transport/Receive.hs view
@@ -0,0 +1,94 @@+{-# LANGUAGE RankNTypes #-}++{- | Receive-side magic-ring transport.++The parser consumes bytes from a 'ReceiveTransport'. The producer+(a network recv loop, a TLS context, an io_uring backend, an+in-memory test fixture, …) writes into the magic ring at @head@+and the parser drains @[tail, head)@.++== Cursor model++'Word64' positions are monotonic (never wrap during process+lifetime). The byte at logical position @p@ lives at+@ringBase + (p .&. ringMask)@ in the ring.++== Why the ring is stored as three primitive fields++'ReceiveTransport' embeds the ring's base pointer, size, and mask+directly rather than carrying a 'Wireform.Ring.Internal.MagicRing'.+That keeps the type free of the ring's phantom @s@ parameter, so+the rank-2 scope safety of 'Wireform.Ring.withMagicRing' is something+callers opt into at the slice layer rather than something that+ripples through every 'ReceiveTransport'-using package.+'receiveRing' reconstructs a polymorphic 'MagicRing' for callers+that still want to call 'ringBase' / 'ringSize' / 'ringMask' on a+record value.+-}+module Wireform.Transport.Receive (+ ReceiveTransport (..),+ ReceiveWait (..),+ receiveRing,+) where++import Control.Exception (SomeException)+import Data.Word (Word64, Word8)+import Foreign.Ptr (Ptr)+import Wireform.Ring.Internal (MagicRing (..))+++{- | A producer-side cursor + a slot to wait on more data.++The parser interacts with data exclusively through this record.+Implementations bind one to a socket, TLS context, io_uring+instance, or an in-memory fixture.+-}+data ReceiveTransport = ReceiveTransport+ { receiveRingBase :: {-# UNPACK #-} !(Ptr Word8)+ -- ^ Base address of the ring's first mapping.+ , receiveRingSize :: {-# UNPACK #-} !Int+ -- ^ Physical ring size (N, not 2N). Always a power of two.+ , receiveRingMask :: {-# UNPACK #-} !Int+ -- ^ @ringSize - 1@, cached for cheap @pos .&. mask@.+ , receiveLoadHead :: !(IO Word64)+ {- ^ Read the producer's current head position+ (monotonically increasing).+ -}+ , receiveAdvanceTail :: !(Word64 -> IO ())+ {- ^ Consumer publishes a new tail position+ (monotonically increasing). The transport may use this for+ backpressure or buffer reuse.+ -}+ , receiveWaitData :: !(Word64 -> IO ReceiveWait)+ {- ^ Block until head advances past the given position.+ Parks on the IO manager when blocking (same thread as the+ parser).+ -}+ , receiveClose :: !(IO ())+ -- ^ Release resources. Idempotent.+ }+++-- | Outcome of waiting for more data.+data ReceiveWait+ = -- | New head position.+ ReceiveMoreData {-# UNPACK #-} !Word64+ | {- | Clean end (sticky: once returned, all subsequent calls+ return this).+ -}+ ReceiveEndOfInput+ | -- | Producer-side failure (sticky).+ ReceiveFailed !SomeException+ deriving stock (Show)+++{- | Reconstruct the underlying 'MagicRing'. Polymorphic in the+phantom @s@: the resulting handle does /not/ inherit the scope of+whatever ring originally produced these bytes, so callers should+treat it as un-scoped (i.e. equivalent to the pre-@MagicRing s@+world). Use 'Wireform.Ring.withMagicRing' directly when you need+the type-system-enforced safety.+-}+receiveRing :: ReceiveTransport -> MagicRing s+receiveRing t = MagicRing (receiveRingBase t) (receiveRingSize t)+{-# INLINE receiveRing #-}
+ src/Wireform/Transport/Send.hs view
@@ -0,0 +1,451 @@+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE RankNTypes #-}++{- | Send-side magic-ring transport.++The dual of 'Wireform.Transport.Receive.ReceiveTransport':++* Producer = the encoder (a 'Wireform.Builder.Builder', a hand-+ written pointer-bumping codec, a 'ByteString' source, …).+ Advances @head@ by writing bytes into the ring at+ @[head, head + n)@ and publishing.+* Consumer = the wire (a network @sendmsg@ loop, a TLS+ @SSL_write@ loop, an io_uring @prep_send@ submission, an in-+ memory test sink, …). Advances @tail@ as it drains.++The encoder never sees a wrap point: a single 'reserveSend' may+span the ring's wrap boundary, but the double mapping makes the+reserved pointer contiguous in virtual memory.+-}+module Wireform.Transport.Send (+ -- * The transport+ SendTransport (..),+ SendWait (..),+ sendRing,++ -- * Encoder-facing API+ reserveSend,+ withSendReservation,+ sendByteString,+ sendByteStringMany,+ sendBuilder,+ sendBuilderDirect,+ sendBuilderViaByteString,++ -- * Corking (batch multiple sends into one publish)+ withSendCork,++ -- * Exceptions+ SendRingFull (..),+ SendReservationTooLarge (..),+) where++import Control.Exception (Exception, SomeException, throwIO)+import Control.Monad (when)+import Data.Bits ((.&.))+import Data.ByteString (ByteString)+import Data.ByteString qualified as BS+import Data.ByteString.Unsafe qualified as BSU+import Data.IORef+import Data.Typeable (Typeable)+import Data.Word (Word64, Word8)+import Foreign.Marshal.Utils (copyBytes)+import Foreign.Ptr (Ptr, castPtr, plusPtr)+import Wireform.Builder qualified as B+import Wireform.Builder.FastBuilder (+ DataSink (RingSink),+ RingSinkState (..),+ finalizeRingSink,+ runBuilder,+ )+import Wireform.Ring.Internal (MagicRing (..))+++------------------------------------------------------------------------+-- SendTransport+------------------------------------------------------------------------++{- | A consumer-side cursor + a slot to wait on more room.++Encoders interact with the wire exclusively through this record.+Implementations bind one to a socket, a TLS context, an io_uring+instance, an in-memory fixture, etc.+-}+data SendTransport = SendTransport+ { sendRingBase :: {-# UNPACK #-} !(Ptr Word8)+ -- ^ Base address of the send ring's first mapping.+ , sendRingSize :: {-# UNPACK #-} !Int+ -- ^ Physical ring size (N, not 2N). Always a power of two.+ , sendRingMask :: {-# UNPACK #-} !Int+ -- ^ @sendRingSize - 1@, cached for cheap @pos .&. mask@.+ , sendLoadTail :: !(IO Word64)+ {- ^ Read the consumer's current tail position+ (monotonically increasing).+ -}+ , sendLoadHead :: !(IO Word64)+ {- ^ Read the current head position. Encoders maintain this+ locally between commits; exposed here so debugging tools+ and background workers can observe progress.+ -}+ , sendPublishHead :: !(Word64 -> IO ())+ {- ^ Producer publishes a new head after writing bytes into the+ ring. Implementations may use this as the flush trigger+ (e.g. an io_uring SQ submit) or rely on an explicit+ 'sendFlush'.+ -}+ , sendWaitSpace :: !(Word64 -> IO SendWait)+ {- ^ Block until 'sendLoadTail >= pos - sendRingSize' — i.e.+ until there is room to advance head to @pos@. The dual of+ 'Wireform.Transport.Receive.receiveWaitData'.+ -}+ , sendFlush :: !(IO ())+ {- ^ Request the consumer side drain everything published so+ far. For an inline (synchronous @sendmsg@ / @SSL_write@)+ consumer this is @pure ()@ because 'sendPublishHead' already+ drained. For a background-worker / io_uring consumer this+ kicks the SQ / signals the worker.+ -}+ , sendShutdownWrite :: !(IO ())+ {- ^ Half-close (@shutdown(SHUT_WR)@ on TCP, @close_notify@+ on TLS), keeping the recv side alive so we can still read+ the peer's final reply.+ -}+ , sendClose :: !(IO ())+ {- ^ Full release: tear down the ring + any background worker.+ Idempotent.+ -}+ }+++-- | Outcome of waiting for more room.+data SendWait+ = -- | New tail position. Room is @sendRingSize - (head - tail)@.+ SendSpaceAvailable {-# UNPACK #-} !Word64+ | {- | Consumer closed (peer sent RST, FIN-after-shutdown, etc).+ Sticky.+ -}+ SendPeerClosed+ | -- | Wire-side failure. Sticky.+ SendFailed !SomeException+ deriving stock (Show)+++{- | Reconstruct the underlying send ring as a 'MagicRing'.+Polymorphic in @s@ for the same reason 'receiveRing' is — the+resulting handle does not inherit any scope.+-}+sendRing :: SendTransport -> MagicRing s+sendRing t = MagicRing (sendRingBase t) (sendRingSize t)+{-# INLINE sendRing #-}+++------------------------------------------------------------------------+-- Exceptions+------------------------------------------------------------------------++{- | The send ring is full and the consumer has not yet drained any+room within the wait policy's budget. Surfaced as a sticky+'SendFailed' (and re-thrown by 'reserveSend' / 'sendByteString')+instead of letting the producer/consumer pair spin forever.+-}+data SendRingFull = SendRingFull+ { sendRingFullSize :: !Int+ -- ^ Physical ring size (N).+ , sendRingFullHead :: !Word64+ , sendRingFullTail :: !Word64+ }+ deriving stock (Show, Typeable)+++instance Exception SendRingFull+++{- | A 'reserveSend' / 'sendByteString' asked for more bytes in a+single contiguous reservation than the ring physically holds.+The ring is sized at construction time; raise the+'Wireform.Transport.Config.ringSizeHint' for the connection if+you need to stage a larger payload in one shot.+-}+data SendReservationTooLarge = SendReservationTooLarge+ { sendReservationRequested :: !Int+ , sendReservationRingSize :: !Int+ }+ deriving stock (Show, Typeable)+++instance Exception SendReservationTooLarge+++------------------------------------------------------------------------+-- Encoder-facing API+------------------------------------------------------------------------++{- | Reserve a contiguous span of @n@ bytes at the current head.+Blocks (via 'sendWaitSpace') until there is room. Returns a+pointer into the ring's double mapping (so the encoder can write+@n@ bytes contiguously regardless of wrap) and the new head that+'commitSend' / 'sendPublishHead' should publish once the write+is complete.++Throws 'SendReservationTooLarge' if @n > sendRingSize@.+Throws the sticky 'SendRingFull' / underlying 'SendFailed'+exception if the transport has been closed (concretely: the+ring's backing mmap may already be gone, so we must not hand+out a pointer into it).+-}+reserveSend+ :: SendTransport+ -> Int+ -> IO (Ptr Word8, Word64)+reserveSend t n+ | n < 0 = error "Wireform.Transport.Send.reserveSend: negative length"+ | n > sendRingSize t =+ throwIO+ ( SendReservationTooLarge+ { sendReservationRequested = n+ , sendReservationRingSize = sendRingSize t+ }+ )+ | otherwise = do+ h <- sendLoadHead t+ let !newHead = h + fromIntegral n+ ensureRoom t newHead+ let !off = fromIntegral h .&. sendRingMask t+ !writeP = sendRingBase t `plusPtr` off+ pure (writeP, newHead)+{-# INLINE reserveSend #-}+++{- | Reserve up to @maxLen@ bytes, run the fill callback, publish+head by the number of bytes the callback actually wrote.+Convenient bracket-style wrapper around 'reserveSend' ++'sendPublishHead'.+-}+withSendReservation+ :: SendTransport+ -> Int+ -- ^ max bytes to reserve+ -> (Ptr Word8 -> Int -> IO Int)+ -- ^ fill callback, returns bytes written+ -> IO Int+withSendReservation t maxLen fill+ | maxLen <= 0 = pure 0+ | otherwise = do+ (p, _) <- reserveSend t maxLen+ written <- fill p maxLen+ if written < 0 || written > maxLen+ then error "Wireform.Transport.Send.withSendReservation: callback returned out-of-range length"+ else do+ when_ (written > 0) $ do+ h <- sendLoadHead t+ sendPublishHead t (h + fromIntegral written)+ pure written+ where+ when_ True m = m+ when_ False _ = pure ()+{-# INLINE withSendReservation #-}+++{- | Copy a 'ByteString' into the send ring as one contiguous+reservation, then publish head. The bytes are committed by the+time this returns; whether they have hit the wire by then+depends on the transport's consumer (an inline socket consumer+has already drained; an io_uring consumer has at least submitted+the SQE).+-}+sendByteString :: SendTransport -> ByteString -> IO ()+sendByteString t bs = do+ let !len = BS.length bs+ if len == 0+ then pure ()+ else do+ (p, newHead) <- reserveSend t len+ BSU.unsafeUseAsCStringLen bs $ \(src, _) ->+ copyBytes p (castPtr src) len+ sendPublishHead t newHead+{-# INLINE sendByteString #-}+++{- | Stage many byte strings as one merged reservation, then+publish head once. Lets the consumer coalesce them into a+single @sendmsg@ / io_uring SQE.+-}+sendByteStringMany :: SendTransport -> [ByteString] -> IO ()+sendByteStringMany _ [] = pure ()+sendByteStringMany t bss = do+ let !total = sum (fmap BS.length bss)+ if total == 0+ then pure ()+ else do+ (p, newHead) <- reserveSend t total+ copyAll p bss+ sendPublishHead t newHead+ where+ copyAll _ [] = pure ()+ copyAll dst (b : bs) = do+ let !n = BS.length b+ BSU.unsafeUseAsCStringLen b $ \(src, _) ->+ copyBytes dst (castPtr src) n+ copyAll (dst `plusPtr` n) bs+{-# INLINEABLE sendByteStringMany #-}+++{- | Stage a 'B.Builder' into the send ring. Equivalent to+'sendBuilderDirect' -- writes directly into ring memory with+no intermediate 'ByteString'.+-}+sendBuilder :: SendTransport -> B.Builder -> IO ()+sendBuilder = sendBuilderDirect+{-# INLINE sendBuilder #-}+++{- | The pre-optimisation path: materialise to a strict+'ByteString', then memcpy into the ring. Exported so+benchmarks can compare the two strategies side-by-side.+-}+sendBuilderViaByteString :: SendTransport -> B.Builder -> IO ()+sendBuilderViaByteString t b =+ sendByteString t (B.toStrictByteStringWith 4096 b)+{-# INLINE sendBuilderViaByteString #-}+++{- | Run a 'B.Builder' directly into the send ring with no+intermediate 'ByteString' allocation or copy.++The builder writes into the ring's double-mapped memory.+When the current region fills, the already-written bytes are+published and the builder continues into freshly-available ring+space. The final bytes are published before this function+returns.++For a typical small frame (< 32 KiB) this is one @runBuilder@+call with zero allocations beyond a single 'IORef'. For larger+payloads the builder publishes in chunks, keeping memory+pressure proportional to the ring size rather than the payload.+-}+sendBuilderDirect :: SendTransport -> B.Builder -> IO ()+sendBuilderDirect t b = do+ h <- sendLoadHead t+ let !chunkSz = min (sendRingSize t) defaultDirectChunk+ !wantHead = h + fromIntegral chunkSz+ ensureRoom t wantHead+ let !off = fromIntegral h .&. sendRingMask t+ !ptr = sendRingBase t `plusPtr` off+ rsRef <-+ newIORef+ RingSinkState+ { rsHead = h+ , rsPublished = h+ , rsBase = sendRingBase t+ , rsMask = sendRingMask t+ , rsRingSize = sendRingSize t+ , rsChunkSize = chunkSz+ , rsPublishHead = sendPublishHead t+ , rsEnsureRoom = ensureRoom t+ , rsLoadTail = sendLoadTail t+ }+ finalCur <- runBuilder b (RingSink rsRef) ptr (ptr `plusPtr` chunkSz)+ _ <- finalizeRingSink rsRef finalCur+ pure ()+{-# INLINE sendBuilderDirect #-}+++-- 32 KiB: large enough that most protocol frames never overflow,+-- small enough that the initial ensureRoom resolves immediately on+-- a typical 64-256 KiB ring.+defaultDirectChunk :: Int+defaultDirectChunk = 32768+++------------------------------------------------------------------------+-- Corking+------------------------------------------------------------------------++{- | Run an action with deferred publishing. Bytes written via+'sendByteString' \/ 'sendBuilderDirect' \/ etc. accumulate in the+ring without triggering the consumer (no @sendmsg@, no io_uring+SQE submission). On scope exit, a single 'sendPublishHead' covers+everything written.++The cork is demand-driven: 'sendPublishHead' is a no-op inside the+scope, and the cork only breaks when 'sendWaitSpace' detects the+ring is genuinely full and needs a drain to continue. This means+the cork batches as much as the ring can hold — for typical HTTP+responses (headers + body < ringSize) that is one publish total.+For payloads larger than ringSize, publishes happen only when+strictly necessary to free ring space.++@+withSendCork transport $ \corked -> do+ sendBuilderDirect corked (responseBuilder resp)+ sendByteString corked bodyBytes+-- single publish \/ sendmsg here+@+-}+withSendCork :: SendTransport -> (SendTransport -> IO a) -> IO a+withSendCork t action = do+ h0 <- sendLoadHead t+ headRef <- newIORef h0+ publishedRef <- newIORef h0+ let corkedPublish h = writeIORef headRef h+ corkedWaitSpace pos = do+ r <- sendWaitSpace t pos+ case r of+ SendSpaceAvailable tl+ | pos - tl <= fromIntegral (sendRingSize t) -> pure r+ | otherwise -> do+ -- Ring full — publish accumulated bytes to trigger+ -- drain, then retry.+ cur <- readIORef headRef+ pub <- readIORef publishedRef+ when (cur > pub) $ do+ sendPublishHead t cur+ writeIORef publishedRef cur+ sendWaitSpace t pos+ _ -> pure r+ let corked =+ t+ { sendPublishHead = corkedPublish+ , sendLoadHead = readIORef headRef+ , sendWaitSpace = corkedWaitSpace+ }+ result <- action corked+ finalHead <- readIORef headRef+ pub <- readIORef publishedRef+ when (finalHead > pub) $ sendPublishHead t finalHead+ pure result+++------------------------------------------------------------------------+-- Internal: wait for room+------------------------------------------------------------------------++{- | Block until the ring has room for head to advance to @newHead@.++Always consults 'sendWaitSpace' once, even when the local cursor+arithmetic says room is already available. This is what surfaces+a sticky closed state (e.g. after 'sendClose') BEFORE the caller+reaches for the ring's base pointer — without this probe,+'reserveSend' on a closed inline transport would happily hand out+a pointer into the ring's already-unmapped backing memory.+-}+ensureRoom :: SendTransport -> Word64 -> IO ()+ensureRoom t newHead = loop+ where+ sz = fromIntegral (sendRingSize t) :: Word64+ loop = do+ r <- sendWaitSpace t newHead+ case r of+ SendSpaceAvailable tl+ | newHead - tl <= sz -> pure ()+ | otherwise -> loop+ SendPeerClosed ->+ throwIO+ ( SendRingFull+ { sendRingFullSize = sendRingSize t+ , sendRingFullHead = newHead+ , sendRingFullTail = newHead -- unknown; report newHead+ }+ )+ SendFailed e -> throwIO e+{-# INLINE ensureRoom #-}
+ test/Spec.hs view
@@ -0,0 +1,15 @@+module Main where++import Test.Syd+import Wireform.Base64.Test qualified as Base64+import Wireform.Parser.Test qualified as Parser+import Wireform.Ring.Test qualified as Ring+import Wireform.Transport.SendTest qualified as Send+++main :: IO ()+main = sydTest $ do+ Ring.spec+ Parser.spec+ Send.spec+ Base64.spec
+ test/Wireform/Base64/Test.hs view
@@ -0,0 +1,72 @@+{-# LANGUAGE OverloadedStrings #-}++module Wireform.Base64.Test (spec) where++import Data.ByteString qualified as BS+import Data.Word (Word8)+import Test.QuickCheck+import Test.Syd+import Wireform.Base64+++spec :: Spec+spec = describe "Wireform.Base64" $ do+ describe "RFC 4648 sec 10 test vectors" $ do+ it "encodes the canonical vectors" $ do+ encodeBase64 "" `shouldBe` ""+ encodeBase64 "f" `shouldBe` "Zg=="+ encodeBase64 "fo" `shouldBe` "Zm8="+ encodeBase64 "foo" `shouldBe` "Zm9v"+ encodeBase64 "foob" `shouldBe` "Zm9vYg=="+ encodeBase64 "fooba" `shouldBe` "Zm9vYmE="+ encodeBase64 "foobar" `shouldBe` "Zm9vYmFy"++ it "decodes the canonical vectors" $ do+ decodeBase64 "" `shouldBe` Just ""+ decodeBase64 "Zg==" `shouldBe` Just "f"+ decodeBase64 "Zm8=" `shouldBe` Just "fo"+ decodeBase64 "Zm9v" `shouldBe` Just "foo"+ decodeBase64 "Zm9vYg==" `shouldBe` Just "foob"+ decodeBase64 "Zm9vYmE=" `shouldBe` Just "fooba"+ decodeBase64 "Zm9vYmFy" `shouldBe` Just "foobar"++ describe "round-trip" $ do+ it "decodes its own encoding for inputs up to 256 bytes" $+ property $ \(bytes :: [Word8Wrap]) ->+ let bs = BS.pack (map unwrap (take 256 bytes))+ in decodeBase64 (encodeBase64 bs) === Just bs++ it "drives the SIMD body (>= 16 chars)" $ do+ let bs = BS.pack [0 .. 47]+ decodeBase64 (encodeBase64 bs) `shouldBe` Just bs++ it "drives the SIMD body and tail (4096 bytes)" $ do+ let bs = BS.pack (take 4096 (cycle [0 .. 255]))+ decodeBase64 (encodeBase64 bs) `shouldBe` Just bs++ describe "rejection" $ do+ it "rejects non-multiple-of-4 inputs" $+ decodeBase64 "Zm9v=" `shouldBe` Nothing++ it "rejects out-of-alphabet bytes" $+ decodeBase64 "Zm9*" `shouldBe` Nothing++ it "rejects bad padding position" $+ decodeBase64 "Z===" `shouldBe` Nothing++ describe "length helpers" $ do+ it "encodeBase64Length matches the actual encoded length" $+ property $ \n ->+ let n' = abs n `mod` 1024+ bs = BS.replicate n' 0+ in BS.length (encodeBase64 bs) === encodeBase64Length n'+++-- Small wrapper to bound Arbitrary Word8 in the property test+-- without dragging in QuickCheck-instances.+newtype Word8Wrap = Word8Wrap {unwrap :: Word8}+ deriving stock (Show)+++instance Arbitrary Word8Wrap where+ arbitrary = Word8Wrap . fromIntegral <$> chooseInt (0, 255)
+ test/Wireform/Parser/Test.hs view
@@ -0,0 +1,275 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}++module Wireform.Parser.Test (spec) where++import Data.ByteString (ByteString)+import Data.ByteString qualified as BS+import Data.Int+import Data.Word+import Test.QuickCheck+import Test.Syd+import Wireform.Parser+import Wireform.Parser.Driver (parseByteString)+import Wireform.Parser.Error+import Wireform.Parser.Internal (Pure)+++type P = Parser Pure String+++ok :: (Show a, Eq a) => Either (ParseError String) a -> a -> Expectation+ok (Right a) expected = a `shouldBe` expected+ok (Left e) _ = expectationFailure ("parse failed: " <> show e)+++bad :: (Show a) => Either (ParseError String) a -> Expectation+bad (Left _) = pure ()+bad (Right a) = expectationFailure ("expected failure, got: " <> show a)+++spec :: Spec+spec = describe "Parser" $ do+ describe "byte primitives" $ do+ it "anyWord8" $+ ok (parseByteString (anyWord8 :: P Word8) "\x42") 0x42+ it "anyWord8 on empty" $+ bad (parseByteString (anyWord8 :: P Word8) "")+ it "anyWord16 native" $+ ok (parseByteString (anyWord16 :: P Word16) "\x01\x02") (if isLE then 0x0201 else 0x0102)+ it "anyWord32be" $+ ok (parseByteString (anyWord32be :: P Word32) "\x00\x00\x01\x00") 256+ it "anyWord32le" $+ ok (parseByteString (anyWord32le :: P Word32) "\x00\x01\x00\x00") 256+ it "anyWord64le" $+ ok (parseByteString (anyWord64le :: P Word64) "\x01\x00\x00\x00\x00\x00\x00\x00") 1+ it "anyWord64be" $+ ok (parseByteString (anyWord64be :: P Word64) "\x00\x00\x00\x00\x00\x00\x00\x01") 1++ describe "signed integers" $ do+ it "anyInt8" $+ ok (parseByteString (anyInt8 :: P Int8) "\xFF") (-1)+ it "anyInt16be" $+ ok (parseByteString (anyInt16be :: P Int16) "\xFF\xFE") (-2)+ it "anyInt32le" $+ ok (parseByteString (anyInt32le :: P Int32) "\xFE\xFF\xFF\xFF") (-2)++ describe "floating point" $ do+ it "anyFloatle parses 1.0" $+ ok (parseByteString (anyFloatle :: P Float) "\x00\x00\x80\x3F") 1.0+ it "anyDoublebe parses 1.0" $+ ok (parseByteString (anyDoublebe :: P Double) "\x3F\xF0\x00\x00\x00\x00\x00\x00") 1.0++ describe "sequencing" $ do+ it "two bytes" $ do+ let p = (,) <$> anyWord8 <*> anyWord8 :: P (Word8, Word8)+ ok (parseByteString p "\xAA\xBB") (0xAA, 0xBB)+ it "insufficient for sequence" $ do+ let p = (,) <$> anyWord8 <*> anyWord8 :: P (Word8, Word8)+ bad (parseByteString p "\xAA")+ it "three-way sequence" $ do+ let p = (,,) <$> anyWord8 <*> anyWord16be <*> anyWord32be :: P (Word8, Word16, Word32)+ ok (parseByteString p "\x01\x00\x02\x00\x00\x00\x03") (1, 2, 3)++ describe "alternatives" $ do+ it "second branch" $ do+ let p = (word8 0x01 *> pure "a") <|> (word8 0x02 *> pure "b") :: P String+ ok (parseByteString p "\x02") "b"+ it "first branch" $ do+ let p = (word8 0x01 *> pure "a") <|> (word8 0x02 *> pure "b") :: P String+ ok (parseByteString p "\x01") "a"+ it "both fail" $ do+ let p = (word8 0x01 *> pure "a") <|> (word8 0x02 *> pure "b") :: P String+ bad (parseByteString p "\x03")+ it "backtracking restores position" $ do+ let p = (word8 0x01 *> word8 0x99) <|> (word8 0x01 *> word8 0x02) :: P ()+ ok (parseByteString p "\x01\x02") ()+ it "three-way alternative" $ do+ let p = word8 0x01 <|> word8 0x02 <|> word8 0x03 :: P ()+ ok (parseByteString p "\x03") ()++ describe "cut/err" $ do+ it "cut converts Fail to Err" $ do+ let p = cut (word8 0x01 *> anyWord8 *> word8 0xFF) "bad" :: P ()+ case parseByteString p "\x01\x42\xAA" of+ Left (ParseErr _ e) -> e `shouldBe` "bad"+ other -> expectationFailure (show other)+ it "err produces Err" $ do+ let p = err "fatal" :: P ()+ case parseByteString p "x" of+ Left (ParseErr _ e) -> e `shouldBe` "fatal"+ other -> expectationFailure (show other)+ it "Err bypasses alternatives" $ do+ let p = (err "x" :: P ()) <|> pure ()+ case parseByteString p "x" of+ Left (ParseErr _ _) -> pure ()+ other -> expectationFailure (show other)+ it "try converts Err to Fail" $ do+ let p = try (err "x" :: P ()) <|> pure ()+ ok (parseByteString p "x") ()++ describe "withError" $ do+ it "catches and handles Err" $ do+ let p = withError (\e -> pure (e <> "!")) (err "boom") :: P String+ ok (parseByteString p "x") "boom!"++ describe "byte matching" $ do+ it "byteString match" $+ ok (parseByteString (byteString "hello" :: P ()) "hello") ()+ it "byteString mismatch" $+ bad (parseByteString (byteString "hello" :: P ()) "hxllo")+ it "byteString empty" $+ ok (parseByteString (byteString "" :: P ()) "anything") ()++ describe "takeBs" $ do+ it "takes n bytes" $+ ok (parseByteString (takeBs 3 :: P ByteString) "abcde") "abc"+ it "fails if short" $+ bad (parseByteString (takeBs 10 :: P ByteString) "abc")+ it "take 0 bytes" $+ ok (parseByteString (takeBs 0 :: P ByteString) "x") ""++ describe "skip" $ do+ it "skip and continue" $ do+ let p = skip 2 *> anyWord8 :: P Word8+ ok (parseByteString p "\x00\x00\x42") 0x42++ describe "takeRest" $ do+ it "consumes all" $+ ok (parseByteString (takeRest :: P ByteString) "hello") "hello"+ it "empty on empty" $+ ok (parseByteString (takeRest :: P ByteString) "") ""++ describe "eof" $ do+ it "succeeds at end" $+ ok (parseByteString (eof :: P ()) "") ()+ it "fails with remaining" $+ bad (parseByteString (eof :: P ()) "x")++ describe "atEnd / remaining" $ do+ it "atEnd true on empty" $+ ok (parseByteString (atEnd :: P Bool) "") True+ it "atEnd false with data" $+ ok (parseByteString (atEnd :: P Bool) "x") False+ it "remaining counts bytes" $+ ok (parseByteString (remaining :: P Int) "hello") 5++ describe "UTF-8" $ do+ it "ASCII char" $+ ok (parseByteString (anyAsciiChar :: P Char) "A") 'A'+ it "rejects non-ASCII" $+ bad (parseByteString (satisfyAscii (const True) :: P Char) "\xC3\xA9")+ it "2-byte UTF-8 (é)" $+ ok (parseByteString (anyChar :: P Char) "\xC3\xA9") '\x00E9'+ it "3-byte UTF-8 (€)" $+ ok (parseByteString (anyChar :: P Char) "\xE2\x82\xAC") '\x20AC'+ it "4-byte UTF-8 (😀)" $+ ok (parseByteString (anyChar :: P Char) "\xF0\x9F\x98\x80") '\x1F600'++ describe "satisfy" $ do+ it "satisfy matches" $+ ok (parseByteString (satisfy (== 'A') :: P Char) "A") 'A'+ it "satisfy rejects" $+ bad (parseByteString (satisfy (== 'A') :: P Char) "B")++ describe "ASCII decimal" $ do+ it "parses number" $+ ok (parseByteString (anyAsciiDecimalWord :: P Word) "12345x") 12345+ it "fails on non-digit" $+ bad (parseByteString (anyAsciiDecimalWord :: P Word) "abc")+ it "single digit" $+ ok (parseByteString (anyAsciiDecimalWord :: P Word) "0") 0++ describe "hex" $ do+ it "parses hex" $+ ok (parseByteString (anyAsciiHexWord :: P Word) "FF") 255+ it "mixed case" $+ ok (parseByteString (anyAsciiHexWord :: P Word) "aB") 0xAB++ describe "lookahead and negative lookahead" $ do+ it "lookahead does not consume" $ do+ let p = lookahead anyWord8 *> anyWord8 :: P Word8+ ok (parseByteString p "\x42") 0x42+ it "fails succeeds" $ do+ let p = fails (word8 0x01) :: P ()+ ok (parseByteString p "\x02") ()+ it "notFollowedBy" $ do+ let p = notFollowedBy (word8 0x01) :: P ()+ ok (parseByteString p "\x02") ()++ describe "many/some/many_/some_" $ do+ it "many_ then read" $ do+ let p = many_ (word8 0x41) *> anyWord8 :: P Word8+ ok (parseByteString p "\x41\x41\x42") 0x42+ it "many_ on empty" $+ ok (parseByteString (many_ (word8 0x41) *> eof :: P ()) "") ()+ it "many collects" $ do+ let p = many (word8 0x41 *> pure 'A') :: P [Char]+ ok (parseByteString p "\x41\x41\x42") "AA"+ it "some requires one" $+ bad (parseByteString (some (word8 0x41) :: P [()]) "\x42")++ describe "isolate" $ do+ it "isolate consumes exact bytes" $ do+ let p = isolate 3 (takeBs 3) :: P ByteString+ ok (parseByteString p "abcdef") "abc"+ it "isolate fails if inner underconsumed" $ do+ let p = isolate 3 (takeBs 2) :: P ByteString+ bad (parseByteString p "abcdef")++ describe "chainl" $ do+ it "left-associative chain" $ do+ let digit = anyAsciiDecimalInt :: P Int+ plus = word8 0x2B *> digit+ p = chainl (+) digit plus+ ok (parseByteString p "1+2+3x") 6++ describe "position and span" $ do+ it "getPos returns 0 at start" $ do+ ok (parseByteString (getPos :: P Pos) "hello") (Pos 0)+ it "getPos advances" $ do+ let p = skip 3 *> getPos :: P Pos+ ok (parseByteString p "hello") (Pos 3)+ it "byteStringOf captures consumed bytes" $ do+ let p = byteStringOf (skip 3) :: P ByteString+ ok (parseByteString p "hello") "hel"+ it "withSpan captures span" $ do+ let p = withSpan (skip 3) (\_ (Span s e) -> pure (subPos e s)) :: P Int+ ok (parseByteString p "hello") 3++ describe "skipBack" $ do+ it "skips backward and re-reads" $ do+ let p = do+ _ <- anyWord8+ _ <- anyWord8+ skipBack 2+ anyWord8 :: P Word8+ ok (parseByteString p "\xAA\xBB") 0xAA+ it "fails when skipping past start" $ do+ let p = skipBack 1 :: P ()+ bad (parseByteString p "x")+ it "skip forward then back" $ do+ let p = do+ skip 3+ skipBack 2+ anyWord8 :: P Word8+ -- "hello" -> skip 3 -> at 'l' -> back 2 -> at 'e'+ ok (parseByteString p "hello") (fromIntegral (fromEnum 'e'))++ describe "marks" $ do+ it "mark and restore" $ do+ let p = do+ m <- mark+ _ <- anyWord8+ _ <- anyWord8+ restore m+ anyWord8 :: P Word8+ ok (parseByteString p "\xAA\xBB") 0xAA+ where+ isLE :: Bool+ isLE =+ BS.pack [1, 0]+ == BS.pack+ ( let w = 1 :: Word16+ in [fromIntegral w, fromIntegral (w `div` 256)]+ )
+ test/Wireform/Ring/Test.hs view
@@ -0,0 +1,214 @@+module Wireform.Ring.Test (spec) where++import Control.Exception (SomeException, catch, evaluate)+import Control.Monad (forM_, when)+import Data.Bits qualified+import Data.ByteString qualified as BS+import Data.IORef+import Data.Word (Word8)+import Foreign.Marshal.Array (pokeArray)+import Foreign.Ptr (Ptr, minusPtr, nullPtr, plusPtr)+import Foreign.Storable (peek, poke)+import Test.QuickCheck+import Test.Syd+import Wireform.Ring+import Wireform.Ring.Internal (destroyMagicRing, newMagicRing)+import Wireform.Transport.Capabilities (capCoreCount, capPageSize, detectCapabilities)+++spec :: Spec+spec = describe "MagicRing" $ do+ describe "basic operations" $ do+ it "creates and destroys a ring" $ do+ withMagicRing 4096 $ \ring -> do+ ringSize ring `shouldSatisfy` (>= 4096)+ ringMask ring `shouldBe` (ringSize ring - 1)+ ringBase ring `shouldSatisfy` (/= nullPtr)++ it "ring size is power of two" $ do+ withMagicRing 5000 $ \ring ->+ let s = ringSize ring+ in (s Data.Bits..&. (s - 1)) `shouldBe` 0++ it "ring size >= requested" $ do+ withMagicRing 12345 $ \ring ->+ ringSize ring `shouldSatisfy` (>= 12345)++ describe "double-mapping correctness" $ do+ it "write at end, read from second mapping" $ do+ withMagicRing 4096 $ \ring -> do+ let n = ringSize ring+ base = ringBase ring+ poke (base `plusPtr` (n - 1)) (0xAB :: Word8)+ -- Second mapping: base + n points to same physical page+ v <- peek (base `plusPtr` (n - 1)) :: IO Word8+ v `shouldBe` 0xAB++ it "contiguous read spanning boundary" $ do+ withMagicRing 4096 $ \ring -> do+ let n = ringSize ring+ base = ringBase ring+ forM_ [0 .. 19] $ \i ->+ poke (base `plusPtr` (n - 10 + i)) (fromIntegral (i + 1) :: Word8)+ bytes <- mapM (\i -> peek (base `plusPtr` (n - 10 + i)) :: IO Word8) [0 .. 19]+ bytes `shouldBe` [1 .. 20]++ it "write in first mapping, read from second" $ do+ withMagicRing 4096 $ \ring -> do+ let n = ringSize ring+ base = ringBase ring+ poke (base `plusPtr` 42) (0xCD :: Word8)+ v <- peek (base `plusPtr` (n + 42)) :: IO Word8+ v `shouldBe` 0xCD++ it "write in second mapping, read from first" $ do+ withMagicRing 4096 $ \ring -> do+ let n = ringSize ring+ base = ringBase ring+ poke (base `plusPtr` (n + 100)) (0xEF :: Word8)+ v <- peek (base `plusPtr` 100) :: IO Word8+ v `shouldBe` 0xEF++ it "full ring write + wrap-around read" $ do+ withMagicRing 4096 $ \ring -> do+ let n = ringSize ring+ base = ringBase ring+ forM_ [0 .. n - 1] $ \i ->+ poke (base `plusPtr` i) (fromIntegral (i `mod` 256) :: Word8)+ forM_ [0 .. n - 1] $ \i -> do+ v <- peek (base `plusPtr` (n + i)) :: IO Word8+ v `shouldBe` fromIntegral (i `mod` 256)++ describe "multiple sizes" $ do+ it "works with 64KB ring" $ do+ withMagicRing (64 * 1024) $ \ring -> do+ let n = ringSize ring+ base = ringBase ring+ poke (base `plusPtr` (n - 1)) (0xFF :: Word8)+ v <- peek (base `plusPtr` (n - 1)) :: IO Word8+ v `shouldBe` 0xFF++ it "works with 1MB ring" $ do+ withMagicRing (1024 * 1024) $ \ring -> do+ let n = ringSize ring+ base = ringBase ring+ poke (base `plusPtr` (n - 1)) (0x42 :: Word8)+ v <- peek (base `plusPtr` (2 * n - 1)) :: IO Word8+ v `shouldBe` 0x42++ it "minimum size is at least page-sized" $ do+ withMagicRing 1 $ \ring ->+ ringSize ring `shouldSatisfy` (>= 4096)++ describe "resource management" $ do+ it "sequential allocation: no FD/VA leaks (1000 rings)" $ do+ forM_ [1 .. 1000 :: Int] $ \_ ->+ withMagicRing 4096 $ \ring ->+ ringSize ring `shouldSatisfy` (>= 4096)++ it "absurd size throws MagicRingUnavailable" $ do+ let absurdSize = 1024 * 1024 * 1024 * 1024 * 1024+ result <-+ (newMagicRing absurdSize >> pure False)+ `catch` (\(MagicRingUnavailable _) -> pure True)+ result `shouldBe` True++ it "destroy is idempotent" $ do+ ring <- newMagicRing 4096+ destroyMagicRing ring+ destroyMagicRing ring -- should not crash+ describe "stress tests" $ do+ it "ring used as circular buffer (100k writes)" $ do+ withMagicRing 4096 $ \ring -> do+ let n = ringSize ring+ base = ringBase ring+ mask = ringMask ring+ forM_ [0 .. 99999 :: Int] $ \i -> do+ let off = i Data.Bits..&. mask+ poke (base `plusPtr` off) (fromIntegral (i `mod` 251) :: Word8)+ v <- peek (base `plusPtr` off) :: IO Word8+ when (v /= fromIntegral (i `mod` 251)) $+ error ("mismatch at iteration " <> show i)++ describe "capabilities detection" $ do+ it "detects page size > 0" $ do+ caps <- detectCapabilities+ capPageSize caps `shouldSatisfy` (> 0)++ it "detects at least 1 core" $ do+ caps <- detectCapabilities+ capCoreCount caps `shouldSatisfy` (>= 1)++ describe "stress tests (continued)" $ do+ it "alternating write/read at boundary" $ do+ withMagicRing 4096 $ \ring -> do+ let n = ringSize ring+ base = ringBase ring+ forM_ [0 .. 9999 :: Int] $ \i -> do+ let off = (n - 4 + (i `mod` 8))+ poke (base `plusPtr` off) (fromIntegral i :: Word8)+ v <- peek (base `plusPtr` off) :: IO Word8+ v `shouldBe` fromIntegral i++ describe "RingSlice (runST-like scoping)" $ do+ it "ringSliceLength reports the requested length" $+ withMagicRing 4096 $ \ring -> do+ let s = ringSlice ring 0 17+ ringSliceLength s `shouldBe` 17++ it "copyRingSlice produces a ByteString with the slice's bytes" $+ withMagicRing 4096 $ \ring -> do+ let base = ringBase ring+ forM_ [0 .. 31 :: Int] $ \i ->+ poke (base `plusPtr` i) (fromIntegral (i + 1) :: Word8)+ copied <- copyRingSlice (ringSlice ring 0 32)+ BS.unpack copied `shouldBe` map fromIntegral [1 .. 32 :: Int]++ it "copyRingSlice handles slices that cross the ring boundary" $+ withMagicRing 4096 $ \ring -> do+ let n = ringSize ring+ base = ringBase ring+ -- Lay down a marker pattern that wraps across the boundary by+ -- writing through the second mapping (offsets n-8 .. n+7).+ forM_ [0 .. 15 :: Int] $ \i ->+ poke (base `plusPtr` (n - 8 + i)) (fromIntegral (i + 1) :: Word8)+ -- Slice starting at (n-8), length 16 — reads contiguously+ -- through the double mapping.+ copied <- copyRingSlice (ringSliceAtPos ring (fromIntegral (n - 8)) 16)+ BS.unpack copied `shouldBe` map fromIntegral [1 .. 16 :: Int]++ it "copyRingSlice on an empty slice returns mempty" $+ withMagicRing 4096 $ \ring -> do+ copied <- copyRingSlice (ringSlice ring 0 0)+ copied `shouldBe` BS.empty++ it "copied ByteString outlives the ring's scope" $ do+ -- This compiles iff copyRingSlice's result is independent of @s@.+ -- The whole point of the scoping mechanism is that a raw+ -- RingSlice s cannot escape but a fresh ByteString can.+ bs <- withMagicRing 4096 $ \ring -> do+ let base = ringBase ring+ forM_ [0 .. 7 :: Int] $ \i ->+ poke (base `plusPtr` i) (fromIntegral (0xA0 + i) :: Word8)+ copyRingSlice (ringSlice ring 0 8)+ BS.unpack bs `shouldBe` [0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7]++ it "withRingSlice exposes pointer and length to the body" $+ withMagicRing 4096 $ \ring -> do+ let base = ringBase ring+ poke (base `plusPtr` 4) (0x42 :: Word8)+ let s = ringSlice ring 4 1+ v <- withRingSlice s $ \p n -> do+ n `shouldBe` 1+ peek p :: IO Word8+ v `shouldBe` 0x42++ it "peekRingSliceByte reads at the given offset" $+ withMagicRing 4096 $ \ring -> do+ let base = ringBase ring+ forM_ [0 .. 15 :: Int] $ \i ->+ poke (base `plusPtr` (100 + i)) (fromIntegral (i * 3) :: Word8)+ let s = ringSlice ring 100 16+ forM_ [0 .. 15 :: Int] $ \i -> do+ v <- peekRingSliceByte s i+ v `shouldBe` fromIntegral (i * 3)
+ test/Wireform/Transport/SendTest.hs view
@@ -0,0 +1,534 @@+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE ScopedTypeVariables #-}++module Wireform.Transport.SendTest (spec) where++import Control.Exception (catch)+import Data.Bits ((.&.))+import Data.ByteString qualified as BS+import Data.IORef+import Data.Word (Word64, Word8)+import Foreign.Ptr (castPtr, plusPtr)+import System.IO.Unsafe (unsafePerformIO)+import Test.QuickCheck hiding ((.&.))+import Test.QuickCheck.Monadic (assert, monadicIO, run)+import Test.Syd+import Wireform.Builder (+ Builder,+ byteString,+ byteStringCopy,+ byteStringInsert,+ toStrictByteString,+ word32BE,+ word64BE,+ word8,+ )+import Wireform.Ring.Internal (ringBase, ringMask, ringSize, withMagicRing)+import Wireform.Transport.Send+++------------------------------------------------------------------------+-- Helpers: generate non-trivial byte patterns+------------------------------------------------------------------------++patternBytes :: Int -> BS.ByteString+patternBytes n = BS.pack (fmap (\i -> fromIntegral (i `mod` 251) :: Word8) [0 .. n - 1])+++patternBuilder :: Int -> Builder+patternBuilder n = byteString (patternBytes n)+++mixedBuilder :: Int -> Builder+mixedBuilder n =+ let header = word32BE 0xDEADBEEF <> word64BE (fromIntegral n)+ payload = byteString (patternBytes (max 0 (n - 12)))+ in header <> payload+++mixedBuilderBS :: Int -> BS.ByteString+mixedBuilderBS n = toStrictByteString (mixedBuilder n)+++------------------------------------------------------------------------+-- Tests+------------------------------------------------------------------------++spec :: Spec+spec = describe "SendTransport" $ do+ -- ----------------------------------------------------------------+ -- sendBuilderDirect: basic correctness+ -- ----------------------------------------------------------------+ describe "sendBuilderDirect" $ do+ it "sends empty builder" $ do+ (bytes, pubs) <- withTestTransport 4096 $ \t ->+ sendBuilderDirect t mempty+ readChunks bytes `shouldBe` BS.empty+ readIORef pubs `shouldReturn` 0++ it "sends a single byte" $ do+ (bytes, _) <- withTestTransport 4096 $ \t ->+ sendBuilderDirect t (word8 0x42)+ readChunks bytes `shouldBe` BS.singleton 0x42++ it "sends two bytes" $ do+ (bytes, _) <- withTestTransport 4096 $ \t ->+ sendBuilderDirect t (word8 0x42 <> word8 0x43)+ readChunks bytes `shouldBe` BS.pack [0x42, 0x43]++ it "sends a pattern payload (100 bytes)" $ do+ (bytes, _) <- withTestTransport 4096 $ \t ->+ sendBuilderDirect t (patternBuilder 100)+ readChunks bytes `shouldBe` patternBytes 100++ it "sends exactly ringSize bytes" $ do+ sz <- ringSizeAt 4096+ (bytes, _) <- withTestTransport 4096 $ \t ->+ sendBuilderDirect t (patternBuilder sz)+ readChunks bytes `shouldBe` patternBytes sz++ it "sends ringSize - 1 bytes" $ do+ sz <- ringSizeAt 4096+ (bytes, _) <- withTestTransport 4096 $ \t ->+ sendBuilderDirect t (patternBuilder (sz - 1))+ readChunks bytes `shouldBe` patternBytes (sz - 1)++ it "sends ringSize + 1 bytes (forces overflow)" $ do+ sz <- ringSizeAt 16384+ (bytes, _) <- withTestTransport 16384 $ \t ->+ sendBuilderDirect t (patternBuilder (sz + 1))+ readChunks bytes `shouldBe` patternBytes (sz + 1)++ it "sends 2x ringSize bytes" $ do+ sz <- ringSizeAt 4096+ let n = sz * 2+ (bytes, _) <- withTestTransport 4096 $ \t ->+ sendBuilderDirect t (patternBuilder n)+ readChunks bytes `shouldBe` patternBytes n++ it "sends 5x ringSize bytes" $ do+ sz <- ringSizeAt 4096+ let n = sz * 5+ (bytes, _) <- withTestTransport 4096 $ \t ->+ sendBuilderDirect t (patternBuilder n)+ readChunks bytes `shouldBe` patternBytes n++ it "sends 10x ringSize bytes" $ do+ sz <- ringSizeAt 4096+ let n = sz * 10+ (bytes, _) <- withTestTransport 4096 $ \t ->+ sendBuilderDirect t (patternBuilder n)+ readChunks bytes `shouldBe` patternBytes n++ it "sends via byteString insert (large BS, > threshold)" $ do+ let payload = patternBytes 12000+ (bytes, _) <- withTestTransport 4096 $ \t ->+ sendBuilderDirect t (byteString payload)+ readChunks bytes `shouldBe` payload++ it "sends via byteString insert larger than ring" $ do+ let payload = patternBytes 50000+ (bytes, _) <- withTestTransport 4096 $ \t ->+ sendBuilderDirect t (byteString payload)+ readChunks bytes `shouldBe` payload++ it "sends mixed builder (header + payload)" $ do+ (bytes, _) <- withTestTransport 4096 $ \t ->+ sendBuilderDirect t (mixedBuilder 500)+ readChunks bytes `shouldBe` mixedBuilderBS 500++ it "sends many small builders concatenated" $ do+ let builders = fmap (\i -> word8 (fromIntegral (i :: Int))) [0 .. 255]+ b = mconcat builders+ (bytes, _) <- withTestTransport 4096 $ \t ->+ sendBuilderDirect t b+ readChunks bytes `shouldBe` BS.pack [0 .. 255]++ it "sends builder with explicit byteStringInsert" $ do+ let payload = patternBytes 20000+ (bytes, _) <- withTestTransport 4096 $ \t ->+ sendBuilderDirect t (byteStringInsert payload)+ readChunks bytes `shouldBe` payload++ -- ----------------------------------------------------------------+ -- sendBuilderDirect vs sendBuilderViaByteString equivalence+ -- ----------------------------------------------------------------+ describe "sendBuilderDirect vs sendBuilderViaByteString" $ do+ it "identical output for small payload" $ do+ let b = mixedBuilder 200+ (directBytes, _) <- withTestTransport 65536 $ \t ->+ sendBuilderDirect t b+ (viaBytes, _) <- withTestTransport 65536 $ \t ->+ sendBuilderViaByteString t b+ readChunks directBytes `shouldBe` readChunks viaBytes++ it "identical output for medium payload" $ do+ let b = mixedBuilder 5000+ (directBytes, _) <- withTestTransport 65536 $ \t ->+ sendBuilderDirect t b+ (viaBytes, _) <- withTestTransport 65536 $ \t ->+ sendBuilderViaByteString t b+ readChunks directBytes `shouldBe` readChunks viaBytes++ it "identical output for large payload (multi-overflow)" $ do+ let b = mixedBuilder 40000+ (directBytes, _) <- withTestTransport 65536 $ \t ->+ sendBuilderDirect t b+ (viaBytes, _) <- withTestTransport 65536 $ \t ->+ sendBuilderViaByteString t b+ readChunks directBytes `shouldBe` readChunks viaBytes++ it "identical output for byteString insert path" $ do+ let b = byteString (patternBytes 25000)+ (directBytes, _) <- withTestTransport 32768 $ \t ->+ sendBuilderDirect t b+ (viaBytes, _) <- withTestTransport 32768 $ \t ->+ sendBuilderViaByteString t b+ readChunks directBytes `shouldBe` readChunks viaBytes++ -- ----------------------------------------------------------------+ -- sendByteString+ -- ----------------------------------------------------------------+ describe "sendByteString" $ do+ it "sends empty BS (no-op)" $ do+ (bytes, pubs) <- withTestTransport 4096 $ \t ->+ sendByteString t BS.empty+ readChunks bytes `shouldBe` BS.empty+ readIORef pubs `shouldReturn` 0++ it "sends 1 byte" $ do+ (bytes, _) <- withTestTransport 4096 $ \t ->+ sendByteString t (BS.singleton 0xAB)+ readChunks bytes `shouldBe` BS.singleton 0xAB++ it "sends exactly ringSize bytes" $ do+ sz <- ringSizeAt 4096+ (bytes, _) <- withTestTransport 4096 $ \t ->+ sendByteString t (patternBytes sz)+ readChunks bytes `shouldBe` patternBytes sz++ it "sendByteStringMany coalesces" $ do+ let bss = [patternBytes 100, patternBytes 200, patternBytes 50]+ (bytes, pubs) <- withTestTransport 4096 $ \t ->+ sendByteStringMany t bss+ readChunks bytes `shouldBe` BS.concat bss+ readIORef pubs `shouldReturn` 1++ -- ----------------------------------------------------------------+ -- sendByteString: reservation too large+ -- ----------------------------------------------------------------+ describe "sendByteString limits" $ do+ it "throws SendReservationTooLarge for BS > ringSize" $ do+ threw <- withMagicRing 4096 $ \ring -> do+ let payload = patternBytes (ringSize ring + 1)+ headRef' <- newIORef (0 :: Word64)+ tailRef' <- newIORef (0 :: Word64)+ let t' =+ SendTransport+ { sendRingBase = ringBase ring+ , sendRingSize = ringSize ring+ , sendRingMask = ringMask ring+ , sendLoadTail = readIORef tailRef'+ , sendLoadHead = readIORef headRef'+ , sendPublishHead = \h -> writeIORef headRef' h >> writeIORef tailRef' h+ , sendWaitSpace = \_ -> SendSpaceAvailable <$> readIORef tailRef'+ , sendFlush = pure ()+ , sendShutdownWrite = pure ()+ , sendClose = pure ()+ }+ (sendByteString t' payload >> pure False)+ `catch` (\(SendReservationTooLarge _ _) -> pure True)+ threw `shouldBe` True++ -- ----------------------------------------------------------------+ -- withSendCork: batching+ -- ----------------------------------------------------------------+ describe "withSendCork" $ do+ it "empty cork: zero publishes, zero bytes" $ do+ (bytes, pubs) <- withTestTransport 4096 $ \t ->+ withSendCork t $ \_ -> pure ()+ readChunks bytes `shouldBe` BS.empty+ readIORef pubs `shouldReturn` 0++ it "single send inside cork: one publish" $ do+ (bytes, pubs) <- withTestTransport 65536 $ \t ->+ withSendCork t $ \corked ->+ sendByteString corked (patternBytes 100)+ readChunks bytes `shouldBe` patternBytes 100+ readIORef pubs `shouldReturn` 1++ it "batches two sendByteString into one publish" $ do+ let a = patternBytes 100+ b = patternBytes 200+ (bytes, pubs) <- withTestTransport 65536 $ \t ->+ withSendCork t $ \corked -> do+ sendByteString corked a+ sendByteString corked b+ readChunks bytes `shouldBe` BS.append a b+ readIORef pubs `shouldReturn` 1++ it "batches three sendByteString into one publish" $ do+ let a = patternBytes 100+ b = patternBytes 200+ c = patternBytes 300+ (bytes, pubs) <- withTestTransport 65536 $ \t ->+ withSendCork t $ \corked -> do+ sendByteString corked a+ sendByteString corked b+ sendByteString corked c+ readChunks bytes `shouldBe` BS.concat [a, b, c]+ readIORef pubs `shouldReturn` 1++ it "batches builder + bytestring into one publish" $ do+ let header = word8 0x01 <> word32BE 0x12345678+ body = patternBytes 50+ (bytes, pubs) <- withTestTransport 65536 $ \t ->+ withSendCork t $ \corked -> do+ sendBuilderDirect corked header+ sendByteString corked body+ let result = readChunks bytes+ BS.take 5 result `shouldBe` BS.pack [0x01, 0x12, 0x34, 0x56, 0x78]+ BS.drop 5 result `shouldBe` body+ readIORef pubs `shouldReturn` 1++ it "batches sendBuilderDirect + sendBuilderDirect" $ do+ (bytes, pubs) <- withTestTransport 65536 $ \t ->+ withSendCork t $ \corked -> do+ sendBuilderDirect corked (mixedBuilder 200)+ sendBuilderDirect corked (mixedBuilder 300)+ readChunks bytes+ `shouldBe` BS.append (mixedBuilderBS 200) (mixedBuilderBS 300)+ readIORef pubs `shouldReturn` 1++ it "corked payload exactly ring size" $ do+ sz <- ringSizeAt 4096+ (bytes, _) <- withTestTransport 4096 $ \t ->+ withSendCork t $ \corked ->+ sendBuilderDirect corked (patternBuilder sz)+ readChunks bytes `shouldBe` patternBytes sz++ -- ----------------------------------------------------------------+ -- withSendCork: backpressure+ -- ----------------------------------------------------------------+ describe "withSendCork backpressure" $ do+ it "builder payload 2x ring" $ do+ sz <- ringSizeAt 4096+ let n = sz * 2+ (bytes, pubs) <- withTestTransport 4096 $ \t ->+ withSendCork t $ \corked ->+ sendBuilderDirect corked (patternBuilder n)+ readChunks bytes `shouldBe` patternBytes n+ readIORef pubs >>= (`shouldSatisfy` (> 1))++ it "builder payload 5x ring" $ do+ sz <- ringSizeAt 4096+ let n = sz * 5+ (bytes, _) <- withTestTransport 4096 $ \t ->+ withSendCork t $ \corked ->+ sendBuilderDirect corked (patternBuilder n)+ readChunks bytes `shouldBe` patternBytes n++ it "builder payload 10x ring" $ do+ sz <- ringSizeAt 4096+ let n = sz * 10+ (bytes, _) <- withTestTransport 4096 $ \t ->+ withSendCork t $ \corked ->+ sendBuilderDirect corked (patternBuilder n)+ readChunks bytes `shouldBe` patternBytes n++ it "byteString insert > ring inside cork" $ do+ sz <- ringSizeAt 4096+ let payload = patternBytes (sz + 1)+ (bytes, pubs) <- withTestTransport 4096 $ \t ->+ withSendCork t $ \corked ->+ sendBuilderDirect corked (byteString payload)+ readChunks bytes `shouldBe` payload+ readIORef pubs >>= (`shouldSatisfy` (> 1))++ it "many small sends exceeding ring" $ do+ sz <- ringSizeAt 4096+ let chunk = patternBytes 500+ numChunks = sz `quot` BS.length chunk + 1+ (bytes, _) <- withTestTransport 4096 $ \t ->+ withSendCork t $ \corked ->+ mapM_ (\_ -> sendByteString corked chunk) [1 .. numChunks]+ readChunks bytes `shouldBe` BS.concat (replicate numChunks chunk)++ it "many small builders exceeding ring" $ do+ sz <- ringSizeAt 4096+ let numBuilders = sz `quot` 4 + 1+ (bytes, _) <- withTestTransport 4096 $ \t ->+ withSendCork t $ \corked ->+ mapM_+ (sendBuilderDirect corked . word32BE . fromIntegral)+ [0 .. numBuilders - 1]+ let expected =+ toStrictByteString $+ mconcat (fmap (word32BE . fromIntegral) [0 .. numBuilders - 1 :: Int])+ readChunks bytes `shouldBe` expected++ it "interleaved builder + BS exceeding ring" $ do+ sz <- ringSizeAt 4096+ let n = sz `quot` 204 + 1+ (bytes, _) <- withTestTransport 4096 $ \t ->+ withSendCork t $ \corked ->+ mapM_+ ( \i -> do+ sendBuilderDirect corked (word32BE (fromIntegral i))+ sendByteString corked (patternBytes 200)+ )+ [0 .. n - 1]+ BS.length (readChunks bytes) `shouldBe` n * (4 + 200)++ -- ----------------------------------------------------------------+ -- Sequential cork + uncork operations+ -- ----------------------------------------------------------------+ describe "sequential cork/uncork" $ do+ it "cork then uncorked send" $ do+ (bytes, _) <- withTestTransport 65536 $ \t -> do+ withSendCork t $ \corked ->+ sendByteString corked (BS.pack [0x01, 0x02])+ sendByteString t (BS.pack [0x03, 0x04])+ readChunks bytes `shouldBe` BS.pack [0x01, 0x02, 0x03, 0x04]++ it "two sequential corks" $ do+ (bytes, pubs) <- withTestTransport 65536 $ \t -> do+ withSendCork t $ \corked -> do+ sendByteString corked (BS.pack [0x01])+ sendByteString corked (BS.pack [0x02])+ withSendCork t $ \corked -> do+ sendByteString corked (BS.pack [0x03])+ sendByteString corked (BS.pack [0x04])+ readChunks bytes `shouldBe` BS.pack [0x01, 0x02, 0x03, 0x04]+ readIORef pubs `shouldReturn` 2++ it "three sequential corks with varying sizes" $ do+ (bytes, pubs) <- withTestTransport 65536 $ \t -> do+ withSendCork t $ \c -> sendBuilderDirect c (patternBuilder 100)+ withSendCork t $ \c -> sendBuilderDirect c (patternBuilder 500)+ withSendCork t $ \c -> sendBuilderDirect c (patternBuilder 1000)+ readChunks bytes+ `shouldBe` BS.concat [patternBytes 100, patternBytes 500, patternBytes 1000]+ readIORef pubs `shouldReturn` 3++ it "cork then many uncorked sends" $ do+ (bytes, _) <- withTestTransport 65536 $ \t -> do+ withSendCork t $ \c ->+ sendByteString c (patternBytes 50)+ mapM_ (sendByteString t . BS.singleton . fromIntegral) [0 .. 9 :: Int]+ readChunks bytes+ `shouldBe` BS.append (patternBytes 50) (BS.pack [0 .. 9])++ -- ----------------------------------------------------------------+ -- Property-based: arbitrary payload sizes+ -- ----------------------------------------------------------------+ describe "property-based" $ do+ it "sendBuilderDirect produces correct output for arbitrary sizes" $+ property $ \(Positive (n :: Int)) -> monadicIO $ do+ let sz = min n 100000+ (bytes, _) <- run $ withTestTransport 4096 $ \t ->+ sendBuilderDirect t (patternBuilder sz)+ assert (readChunks bytes == patternBytes sz)++ it "sendBuilderDirect == sendBuilderViaByteString for arbitrary builders" $+ property $ \(Positive (n :: Int)) -> monadicIO $ do+ let sz = min n 50000+ b = mixedBuilder sz+ (directBytes, _) <- run $ withTestTransport 65536 $ \t ->+ sendBuilderDirect t b+ (viaBytes, _) <- run $ withTestTransport 65536 $ \t ->+ sendBuilderViaByteString t b+ assert (readChunks directBytes == readChunks viaBytes)++ it "corked sendBuilderDirect produces correct output for arbitrary sizes" $+ property $ \(Positive (n :: Int)) -> monadicIO $ do+ let sz = min n 100000+ (bytes, _) <- run $ withTestTransport 4096 $ \t ->+ withSendCork t $ \corked ->+ sendBuilderDirect corked (patternBuilder sz)+ assert (readChunks bytes == patternBytes sz)++ it "corked multi-send produces correct output" $+ property $ \(Positive (n :: Int)) -> monadicIO $ do+ let numSends = min n 50+ chunk = patternBytes 100+ (bytes, _) <- run $ withTestTransport 4096 $ \t ->+ withSendCork t $ \corked ->+ mapM_ (\_ -> sendByteString corked chunk) [1 .. numSends]+ assert (readChunks bytes == BS.concat (replicate numSends chunk))+++------------------------------------------------------------------------+-- Test infrastructure+------------------------------------------------------------------------++ringSizeAt :: Int -> IO Int+ringSizeAt requested = withMagicRing requested (pure . ringSize)+++withTestTransport+ :: Int+ -> (SendTransport -> IO a)+ -> IO (IORef [BS.ByteString], IORef Int)+withTestTransport requestedRingSz action =+ withMagicRing requestedRingSz $ \ring -> do+ let !base = ringBase ring+ !sz = ringSize ring+ !msk = ringMask ring+ chunksRef <- newIORef ([] :: [BS.ByteString])+ publishCount <- newIORef (0 :: Int)+ headRef <- newIORef (0 :: Word64)+ tailRef <- newIORef (0 :: Word64)+ stateRef <- newIORef True++ let drainTo !hd = do+ tl <- readIORef tailRef+ drainLoop tl+ where+ drainLoop !cur+ | cur >= hd = pure ()+ | otherwise = do+ let !off = fromIntegral cur .&. msk+ !want = min (fromIntegral (hd - cur)) (sz - off)+ !ptr = base `plusPtr` off+ bs <- BS.packCStringLen (castPtr ptr, want)+ modifyIORef' chunksRef (bs :)+ let !newTail = cur + fromIntegral want+ writeIORef tailRef newTail+ drainLoop newTail++ let publish h = do+ modifyIORef' publishCount (+ 1)+ writeIORef headRef h+ drainTo h++ let waitSpace _pos = do+ isOpen <- readIORef stateRef+ if isOpen+ then do+ tl <- readIORef tailRef+ pure (SendSpaceAvailable tl)+ else pure SendPeerClosed++ let transport =+ SendTransport+ { sendRingBase = base+ , sendRingSize = sz+ , sendRingMask = msk+ , sendLoadTail = readIORef tailRef+ , sendLoadHead = readIORef headRef+ , sendPublishHead = publish+ , sendWaitSpace = waitSpace+ , sendFlush = readIORef headRef >>= drainTo+ , sendShutdownWrite = pure ()+ , sendClose = writeIORef stateRef False+ }+ _ <- action transport+ pure (chunksRef, publishCount)+++readChunks :: IORef [BS.ByteString] -> BS.ByteString+readChunks ref = unsafePerformIO $ do+ chunks <- readIORef ref+ pure (BS.concat (reverse chunks))+{-# NOINLINE readChunks #-}
wireform-core.cabal view
@@ -1,11 +1,24 @@ cabal-version: 3.0 name: wireform-core-version: 0.1.0.0+version: 0.2.0.0 synopsis: Shared FFI primitives for wireform format packages description: Cross-format SWAR / SIMD-accelerated primitives used by every @wireform-*@ format package. .+ Modules:+ .+ * @Wireform.FFI@ -- C FFI surface: packed-varint pre-scan + decode,+ SWAR UTF-8 validation, SIMD NUL / byte / ASCII / whitespace+ scanners, SIMD JSON escape scanner, SIMD Iceberg bounds compare,+ Arrow IPC buffer validation, endianness helpers.+ * @Wireform.Encode.Direct@ -- shared direct-write encode buffer+ used by the per-format encoders.+ * @Wireform.Hash@ -- SIMD-accelerated hashing helpers.+ .+ Pulls in C sources (@cbits\/fast_decode.c@, @cbits\/fast_scan.c@,+ @cbits\/wireform_hash_simd.c@) and the vendored @simde@ headers.+ Contains no format-specific code. homepage: https://github.com/iand675/wireform- bug-reports: https://github.com/iand675/wireform-/issues license: BSD-3-Clause@@ -45,11 +58,13 @@ DeriveAnyClass OverloadedStrings LambdaCase+ BlockArguments library import: defaults hs-source-dirs: src exposed-modules:+ -- Existing builder modules Wireform.Builder Wireform.Builder.FastBuilder Wireform.Builder.Internal.Prim@@ -57,13 +72,103 @@ Wireform.FFI Wireform.Encode.Direct Wireform.Hash+ Wireform.Base64++ -- Magic ring+ Wireform.Ring+ Wireform.Ring.Internal+ Wireform.Ring.Pool++ -- Transport abstraction (symmetric send / receive)+ Wireform.Transport+ Wireform.Transport.Receive+ Wireform.Transport.Send+ Wireform.Transport.Config+ Wireform.Transport.Capabilities++ -- Parser+ Wireform.Parser+ Wireform.Parser.Internal+ Wireform.Parser.Stateful+ Wireform.Parser.Switch+ Wireform.Parser.Position+ Wireform.Parser.Mark+ Wireform.Parser.Error++ -- Driver+ Wireform.Parser.Driver++ -- External-parser adapter+ Wireform.Parser.Adapter+ c-sources: cbits/fast_decode.c cbits/fast_scan.c cbits/wireform_hash_simd.c+ cbits/base64.c+ cbits/fast_rng.c include-dirs: include/simde+ include+ cc-options: -O3++ if os(linux)+ c-sources: cbits/magic_ring_linux.c+ if os(darwin) || os(freebsd) || os(netbsd) || os(openbsd)+ c-sources: cbits/magic_ring_posix.c+ if os(windows)+ c-sources: cbits/magic_ring_windows.c+ extra-libraries: kernel32+ build-depends:- base >= 4.16 && < 5,+ base >= 4.18 && < 5, bytestring >= 0.11 && < 0.13,- ghc-prim >= 0.10 && < 0.12,+ containers,+ ghc-prim,+ template-haskell, text >= 2.0 && < 2.2, vector >= 0.13 && < 0.14++test-suite wireform-core-test+ import: defaults+ type: exitcode-stdio-1.0+ hs-source-dirs: test+ main-is: Spec.hs+ other-modules:+ Wireform.Ring.Test+ Wireform.Parser.Test+ Wireform.Transport.SendTest+ Wireform.Base64.Test+ build-depends:+ base,+ bytestring,+ wireform-core,+ sydtest,+ QuickCheck++benchmark parser-bench+ import: defaults+ type: exitcode-stdio-1.0+ hs-source-dirs: bench+ main-is: ParserBench.hs+ other-modules:+ Common+ WFBasic+ FPBasic+ ghc-options: -O2 -rtsopts -threaded+ build-depends:+ base,+ bytestring,+ wireform-core,+ flatparse >= 0.5 && < 0.6,+ criterion >= 1.5 && < 1.7++benchmark send-builder-bench+ import: defaults+ type: exitcode-stdio-1.0+ hs-source-dirs: bench+ main-is: SendBuilderBench.hs+ ghc-options: -O2 -rtsopts -threaded+ build-depends:+ base,+ bytestring,+ wireform-core,+ criterion >= 1.5 && < 1.7