bytesmith (empty) → 0.1.0.0
raw patch · 6 files changed
+953/−0 lines, 6 filesdep +basedep +byteslicedep +bytesmithsetup-changed
Dependencies added: base, byteslice, bytesmith, primitive
Files
- CHANGELOG.md +5/−0
- LICENSE +30/−0
- Setup.hs +2/−0
- bytesmith.cabal +39/−0
- src/Data/Bytes/Parser.hs +834/−0
- test/Main.hs +43/−0
+ CHANGELOG.md view
@@ -0,0 +1,5 @@+# Revision history for bytesmith++## 0.1.0.0 -- 2019-08-22++* First version.
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright (c) 2019, Andrew Martin++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++ * Redistributions of source code must retain the above copyright+ notice, this list of conditions and the following disclaimer.++ * Redistributions in binary form must reproduce the above+ copyright notice, this list of conditions and the following+ disclaimer in the documentation and/or other materials provided+ with the distribution.++ * Neither the name of Andrew Martin nor the names of other+ contributors may be used to endorse or promote products derived+ from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ bytesmith.cabal view
@@ -0,0 +1,39 @@+cabal-version: 2.2+name: bytesmith+version: 0.1.0.0+synopsis: Nonresumable byte parser+description:+ Parse bytes as fast as possible. This is a nonresumable parser+ that aggresively uses `UnboxedSums` to avoid performing any+ allocations.+homepage: https://github.com/andrewthad/bytesmith+bug-reports: https://github.com/andrewthad/bytesmith/issues+license: BSD-3-Clause+license-file: LICENSE+author: Andrew Martin+maintainer: andrew.thaddeus@gmail.com+copyright: 2019 Andrew Martin+category: Data+extra-source-files: CHANGELOG.md++library+ exposed-modules:+ Data.Bytes.Parser+ build-depends:+ , base >=4.12 && <5+ , byteslice >=0.1.1 && <2+ , primitive >=0.7 && <0.8+ hs-source-dirs: src+ ghc-options: -O2 -Wall+ default-language: Haskell2010++test-suite test+ default-language: Haskell2010+ type: exitcode-stdio-1.0+ hs-source-dirs: test+ main-is: Main.hs+ build-depends:+ , base >=4.12.0.0 && <5+ , bytesmith+ , primitive+ , byteslice
+ src/Data/Bytes/Parser.hs view
@@ -0,0 +1,834 @@+{-# language BangPatterns #-}+{-# language BinaryLiterals #-}+{-# language DataKinds #-}+{-# language DeriveFunctor #-}+{-# language DerivingStrategies #-}+{-# language GADTSyntax #-}+{-# language KindSignatures #-}+{-# language LambdaCase #-}+{-# language MagicHash #-}+{-# language MultiWayIf #-}+{-# language PolyKinds #-}+{-# language RankNTypes #-}+{-# language ScopedTypeVariables #-}+{-# language StandaloneDeriving #-}+{-# language TypeApplications #-}+{-# language UnboxedSums #-}+{-# language UnboxedTuples #-}++module Data.Bytes.Parser+ ( -- * Types+ Parser(..)+ , Result(..)+ -- * Run Parsers+ , parseByteArray+ , parseBytes+ , parseBytesST+ -- * Build Parsers+ , fail+ , peekAnyAscii+ , ascii+ , ascii3+ , ascii4+ , any+ , anyAscii+ , anyAscii#+ , anyUtf8#+ , anyAsciiOpt+ , decWord+ , decWord8+ , decWord16+ , decWord32+ , hexWord16+ , decPositiveInteger+ , endOfInput+ , isEndOfInput+ , skipUntilAsciiConsume+ , skipWhile+ , skipAscii+ , skipAscii1+ , skipAlphaAscii+ , skipAlphaAscii1+ , skipDigitsAscii+ , skipDigitsAscii1+ -- * Lift Effects+ , effect+ -- * Expose Internals+ , cursor+ , expose+ , unconsume+ -- * Cut down on boxing+ , unboxWord32+ , boxWord32+ -- * Specialized Bind+ -- $bind+ , bindChar+ -- * Alternative+ , orElse+ ) where++import Prelude hiding (length,any,fail)++import Data.Char (ord)+import Data.Bits ((.&.),(.|.),unsafeShiftL,xor)+import Data.Kind (Type)+import GHC.ST (ST(..),runST)+import GHC.Exts (Word(W#),Word#,TYPE,State#,Int#,ByteArray#)+import GHC.Exts (Int(I#),Char(C#),chr#,RuntimeRep)+import GHC.Exts (Char#,(+#),(-#),(<#),(>#),word2Int#)+import GHC.Exts (indexCharArray#,indexWord8Array#,ord#)+import GHC.Exts (timesWord#,plusWord#)+import GHC.Word (Word16(W16#),Word8(W8#),Word32(W32#))+import Data.Bytes.Types (Bytes(..))+import Data.Primitive (ByteArray(..))++import qualified Data.Primitive as PM+import qualified Control.Monad++type Bytes# = (# ByteArray#, Int#, Int# #)+type ST# s (a :: TYPE r) = State# s -> (# State# s, a #)+type Result# e (a :: TYPE r) =+ (# e+ | (# a, Int#, Int# #) #) -- ints are offset and length++-- | A non-resumable parser.+newtype Parser :: forall (r :: RuntimeRep). Type -> Type -> TYPE r -> Type where+ Parser :: forall (r :: RuntimeRep) (e :: Type) (s :: Type) (a :: TYPE r).+ { runParser :: (# ByteArray#, Int#, Int# #) -> ST# s (Result# e a) } -> Parser e s a++-- | The result of running a parser.+data Result e a+ = Failure e+ -- ^ An error message indicating what went wrong.+ | Success !a !Int !Int+ -- ^ The parsed value, the offset after the last consumed byte, and the+ -- number of bytes remaining in parsed slice.++-- | Parse a slice of a byte array. This can succeed even if the+-- entire slice was not consumed by the parser.+parseBytes :: forall e a. (forall s. Parser e s a) -> Bytes -> Result e a+parseBytes p !b = runST action+ where+ action :: forall s. ST s (Result e a)+ action = case p @s of+ Parser f -> ST+ (\s0 -> case f (unboxBytes b) s0 of+ (# s1, r #) -> (# s1, boxResult r #)+ )++-- | Variant of 'parseBytes' that accepts an unsliced 'ByteArray'.+parseByteArray :: (forall s. Parser e s a) -> ByteArray -> Result e a+parseByteArray p b =+ parseBytes p (Bytes b 0 (PM.sizeofByteArray b))++-- | Variant of 'parseBytes' that allows the parser to be run+-- as part of an existing effectful context.+parseBytesST :: Parser e s a -> Bytes -> ST s (Result e a)+parseBytesST (Parser f) !b = ST+ (\s0 -> case f (unboxBytes b) s0 of+ (# s1, r #) -> (# s1, boxResult r #)+ )++instance Functor (Parser e s) where+ {-# inline fmap #-}+ fmap f (Parser g) = Parser+ (\x s0 -> case g x s0 of+ (# s1, r #) -> case r of+ (# e | #) -> (# s1, (# e | #) #)+ (# | (# a, b, c #) #) -> (# s1, (# | (# f a, b, c #) #) #)+ )++instance Applicative (Parser e s) where+ pure = pureParser+ (<*>) = Control.Monad.ap++instance Monad (Parser e s) where+ {-# inline return #-}+ {-# inline (>>=) #-}+ return = pureParser+ Parser f >>= g = Parser+ (\x@(# arr, _, _ #) s0 -> case f x s0 of+ (# s1, r0 #) -> case r0 of+ (# e | #) -> (# s1, (# e | #) #)+ (# | (# y, b, c #) #) ->+ runParser (g y) (# arr, b, c #) s1+ )++pureParser :: a -> Parser e s a+pureParser a = Parser+ (\(# _, b, c #) s -> (# s, (# | (# a, b, c #) #) #))++upcastUnitSuccess :: (# Int#, Int# #) -> Result# e ()+upcastUnitSuccess (# b, c #) = (# | (# (), b, c #) #)++upcastWordResult :: Result# e Word# -> Result# e Word+upcastWordResult (# e | #) = (# e | #)+upcastWordResult (# | (# a, b, c #) #) = (# | (# W# a, b, c #) #)++-- Precondition: the word is small enough+upcastWord16Result :: Result# e Word# -> Result# e Word16+upcastWord16Result (# e | #) = (# e | #)+upcastWord16Result (# | (# a, b, c #) #) = (# | (# W16# a, b, c #) #)++-- Precondition: the word is small enough+upcastWord32Result :: Result# e Word# -> Result# e Word32+upcastWord32Result (# e | #) = (# e | #)+upcastWord32Result (# | (# a, b, c #) #) = (# | (# W32# a, b, c #) #)++-- Precondition: the word is small enough+upcastWord8Result :: Result# e Word# -> Result# e Word8+upcastWord8Result (# e | #) = (# e | #)+upcastWord8Result (# | (# a, b, c #) #) = (# | (# W8# a, b, c #) #)++c2w :: Char -> Word8+c2w = fromIntegral . ord++-- | Get the current offset into the chunk. Using this makes+-- it possible to observe the internal difference between 'Bytes'+-- that refer to equivalent slices. Be careful.+cursor :: Parser e s Int+cursor = uneffectful $ \chunk ->+ Success (offset chunk) (offset chunk) (length chunk)++-- | Return the byte array being parsed. This includes bytes+-- that preceed the current offset and may include bytes that+-- go beyond the length. This is somewhat dangerous, so only+-- use this is you know what you're doing.+expose :: Parser e s ByteArray+expose = uneffectful $ \chunk ->+ Success (array chunk) (offset chunk) (length chunk)++-- | Move the cursor back by @n@ bytes. Precondition: you+-- must have previously consumed at least @n@ bytes.+unconsume :: Int -> Parser e s ()+unconsume n = uneffectful $ \chunk ->+ Success () (offset chunk - n) (length chunk + n)++uneffectful :: (Bytes -> Result e a) -> Parser e s a+{-# inline uneffectful #-}+uneffectful f = Parser+ ( \b s0 -> (# s0, unboxResult (f (boxBytes b)) #) )++uneffectful# :: (Bytes -> Result# e a) -> Parser e s a+uneffectful# f = Parser+ ( \b s0 -> (# s0, (f (boxBytes b)) #) )++uneffectfulWord# :: (Bytes -> Result# e Word#) -> Parser e s Word#+uneffectfulWord# f = Parser+ ( \b s0 -> (# s0, (f (boxBytes b)) #) )++-- | Lift an effectful computation into a parser.+effect :: ST s a -> Parser e s a+effect (ST f) = Parser+ ( \(# _, off, len #) s0 -> case f s0 of+ (# s1, a #) -> (# s1, (# | (# a, off, len #) #) #)+ )++-- | Only valid for characters with a Unicode code point lower+-- than 128. This consumes a single byte, decoding it as an ASCII+-- character.+ascii :: e -> Char -> Parser e s ()+-- GHC should decide to inline this after optimization.+ascii e !c = uneffectful $ \chunk -> if length chunk > 0+ then if PM.indexByteArray (array chunk) (offset chunk) == c2w c+ then Success () (offset chunk + 1) (length chunk - 1)+ else Failure e+ else Failure e++-- | Parse three bytes in succession.+ascii3 :: e -> Char -> Char -> Char -> Parser e s ()+-- GHC should decide to inline this after optimization.+ascii3 e !c0 !c1 !c2 = uneffectful $ \chunk ->+ if | length chunk > 2+ , PM.indexByteArray (array chunk) (offset chunk) == c2w c0+ , PM.indexByteArray (array chunk) (offset chunk + 1) == c2w c1+ , PM.indexByteArray (array chunk) (offset chunk + 2) == c2w c2+ -> Success () (offset chunk + 3) (length chunk - 3)+ | otherwise -> Failure e++-- | Parse four bytes in succession.+ascii4 :: e -> Char -> Char -> Char -> Char -> Parser e s ()+-- GHC should decide to inline this after optimization.+ascii4 e !c0 !c1 !c2 !c3 = uneffectful $ \chunk ->+ if | length chunk > 3+ , PM.indexByteArray (array chunk) (offset chunk) == c2w c0+ , PM.indexByteArray (array chunk) (offset chunk + 1) == c2w c1+ , PM.indexByteArray (array chunk) (offset chunk + 2) == c2w c2+ , PM.indexByteArray (array chunk) (offset chunk + 3) == c2w c3+ -> Success () (offset chunk + 4) (length chunk - 4)+ | otherwise -> Failure e++-- | Fail with the provided error message.+fail ::+ e -- ^ Error message+ -> Parser e s a+fail e = uneffectful $ \_ -> Failure e++-- | Interpret the next byte as an ASCII-encoded character.+-- Fails if the byte corresponds to a number above 127.+peekAnyAscii :: e -> Parser e s Char+peekAnyAscii e = uneffectful $ \chunk -> if length chunk > 0+ then+ let w = PM.indexByteArray (array chunk) (offset chunk) :: Word8+ in if w < 128+ then Success+ (C# (chr# (unI (fromIntegral w))))+ (offset chunk)+ (length chunk)+ else Failure e+ else Failure e++-- | Consumes and returns the next byte in the input.+-- Fails if no characters are left.+any :: e -> Parser e s Word8+{-# inline any #-}+any e = uneffectful $ \chunk -> if length chunk > 0+ then+ let w = PM.indexByteArray (array chunk) (offset chunk) :: Word8+ in Success w (offset chunk + 1) (length chunk - 1)+ else Failure e++-- Interpret the next byte as an ASCII-encoded character.+-- Does not check to see if any characters are left. This+-- is not exported.+anyUnsafe :: Parser e s Word8+{-# inline anyUnsafe #-}+anyUnsafe = uneffectful $ \chunk ->+ let w = PM.indexByteArray (array chunk) (offset chunk) :: Word8+ in Success w (offset chunk + 1) (length chunk - 1)++-- | Interpret the next byte as an ASCII-encoded character.+-- Fails if the byte corresponds to a number above 127.+anyAscii :: e -> Parser e s Char+{-# inline anyAscii #-}+anyAscii e = uneffectful $ \chunk -> if length chunk > 0+ then+ let w = PM.indexByteArray (array chunk) (offset chunk) :: Word8+ in if w < 128+ then Success+ (C# (chr# (unI (fromIntegral w))))+ (offset chunk + 1)+ (length chunk - 1)+ else Failure e+ else Failure e++-- | Interpret the next byte as an ASCII-encoded character.+-- Fails if the byte corresponds to a number above 127.+anyAscii# :: e -> Parser e s Char#+{-# inline anyAscii# #-}+anyAscii# e = Parser+ (\(# arr, off, len #) s0 -> case len of+ 0# -> (# s0, (# e | #) #)+ _ ->+ let !w = indexCharArray# arr off+ in case ord# w <# 128# of+ 1# -> (# s0, (# | (# w, off +# 1#, len -# 1# #) #) #)+ _ -> (# s0, (# e | #) #)+ )++-- | Interpret the next one to four bytes as a UTF-8-encoded character.+-- Fails if the decoded codepoint is in the range U+D800 through U+DFFF.+anyUtf8# :: e -> Parser e s Char#+{-# noinline anyUtf8# #-}+anyUtf8# e = Parser+ (\(# arr, off, len #) s0 -> case len ># 0# of+ 1# ->+ let !w0 = indexWord8Array# arr off+ in if | oneByteChar (W8# w0) -> + (# s0, (# | (# chr# (word2Int# w0), off +# 1#, len -# 1# #) #) #)+ | twoByteChar (W8# w0) ->+ if | I# len > 1+ , w1 <- indexWord8Array# arr (off +# 1#)+ , followingByte (W8# w1)+ , C# c <- codepointFromTwoBytes (W8# w0) (W8# w1)+ -> (# s0, (# | (# c, off +# 2#, len -# 2# #) #) #)+ | otherwise -> (# s0, (# e | #) #)+ | threeByteChar (W8# w0) ->+ if | I# len > 2+ , w1 <- indexWord8Array# arr (off +# 1# )+ , w2 <- indexWord8Array# arr (off +# 2# )+ , followingByte (W8# w1)+ , !c@(C# c#) <- codepointFromThreeBytes (W8# w0) (W8# w1) (W8# w2)+ , c < '\xD800' || c > '\xDFFF'+ -> (# s0, (# | (# c#, off +# 3#, len -# 3# #) #) #)+ | otherwise -> (# s0, (# e | #) #)+ | fourByteChar (W8# w0) ->+ if | I# len > 3+ , w1 <- indexWord8Array# arr (off +# 1# )+ , w2 <- indexWord8Array# arr (off +# 2# )+ , w3 <- indexWord8Array# arr (off +# 3# )+ , followingByte (W8# w1)+ , !(C# c#) <- codepointFromFourBytes (W8# w0) (W8# w1) (W8# w2) (W8# w3)+ -> (# s0, (# | (# c#, off +# 4#, len -# 4# #) #) #)+ | otherwise -> (# s0, (# e | #) #)+ | otherwise -> (# s0, (# e | #) #)+ _ -> (# s0, (# e | #) #)+ )++-- | Interpret the next byte as an ASCII-encoded character.+-- Fails if the byte corresponds to a number above 127. Returns+-- nothing if the end of the input has been reached.+anyAsciiOpt :: e -> Parser e s (Maybe Char)+{-# inline anyAsciiOpt #-}+anyAsciiOpt e = uneffectful $ \chunk -> if length chunk > 0+ then+ let w = PM.indexByteArray (array chunk) (offset chunk) :: Word8+ in if w < 128+ then Success+ (Just (C# (chr# (unI (fromIntegral w)))))+ (offset chunk + 1)+ (length chunk - 1)+ else Failure e+ else Success Nothing (offset chunk) (length chunk)++-- | Skip while the predicate is matched. This is always inlined.+skipWhile :: (Word8 -> Bool) -> Parser e s ()+{-# inline skipWhile #-}+skipWhile f = go where+ go = isEndOfInput >>= \case+ True -> pure ()+ False -> do+ w <- anyUnsafe+ if f w+ then go+ else unconsume 1++-- | Parse exactly four ASCII-encoded characters, interpretting+-- them as the hexadecimal encoding of a 32-bit number. Note that+-- this rejects a sequence such as @5A9@, requiring @05A9@ instead.+-- This is insensitive to case.+hexWord16 :: e -> Parser e s Word16+{-# inline hexWord16 #-}+hexWord16 e = Parser+ (\x s0 -> case runParser (hexWord16# e) x s0 of+ (# s1, r #) -> case r of+ (# err | #) -> (# s1, (# err | #) #)+ (# | (# a, b, c #) #) -> (# s1, (# | (# W16# a, b, c #) #) #)+ )++hexWord16# :: e -> Parser e s Word#+{-# noinline hexWord16# #-}+hexWord16# e = uneffectfulWord# $ \chunk -> if length chunk >= 4+ then+ let !w0@(W# n0) = oneHex $ PM.indexByteArray (array chunk) (offset chunk)+ !w1@(W# n1) = oneHex $ PM.indexByteArray (array chunk) (offset chunk + 1)+ !w2@(W# n2) = oneHex $ PM.indexByteArray (array chunk) (offset chunk + 2)+ !w3@(W# n3) = oneHex $ PM.indexByteArray (array chunk) (offset chunk + 3)+ in if | w0 .|. w1 .|. w2 .|. w3 /= maxBound ->+ (# |+ (# (n0 `timesWord#` 4096##) `plusWord#`+ (n1 `timesWord#` 256##) `plusWord#`+ (n2 `timesWord#` 16##) `plusWord#`+ n3+ , unI (offset chunk) +# 4#+ , unI (length chunk) -# 4# #) #)+ | otherwise -> (# e | #)+ else (# e | #)+++-- Returns the maximum machine word if the argument is not+-- the ASCII encoding of a hexadecimal digit.+oneHex :: Word8 -> Word+oneHex w+ | w >= 48 && w < 58 = (fromIntegral w - 48)+ | w >= 65 && w < 71 = (fromIntegral w - 55)+ | w >= 97 && w < 103 = (fromIntegral w - 87)+ | otherwise = maxBound++-- | Skip ASCII-encoded digits until a non-digit is encountered.+skipDigitsAscii :: Parser e s ()+skipDigitsAscii = uneffectful# $ \c ->+ upcastUnitSuccess (skipDigitsAsciiLoop c)++-- | Skip uppercase and lowercase letters until a non-alpha+-- character is encountered.+skipDigitsAscii1 :: e -> Parser e s ()+skipDigitsAscii1 e = uneffectful# $ \c ->+ skipDigitsAscii1LoopStart e c++-- | Skip uppercase and lowercase letters until a non-alpha+-- character is encountered.+skipAlphaAscii :: Parser e s ()+skipAlphaAscii = uneffectful# $ \c ->+ upcastUnitSuccess (skipAlphaAsciiLoop c)++-- | Skip uppercase and lowercase letters until a non-alpha+-- character is encountered.+skipAlphaAscii1 :: e -> Parser e s ()+skipAlphaAscii1 e = uneffectful# $ \c ->+ skipAlphaAsciiLoop1Start e c++-- | Skip the character any number of times. This succeeds+-- even if the character was not present.+skipAscii :: Char -> Parser e s ()+skipAscii !w = uneffectful# $ \c ->+ upcastUnitSuccess (skipLoop (c2w w) c)++-- | Skip the character any number of times. It must occur+-- at least once or else this will fail.+skipAscii1 :: e -> Char -> Parser e s ()+skipAscii1 e !w = uneffectful# $ \c ->+ skipLoop1Start e (c2w w) c++skipDigitsAsciiLoop ::+ Bytes -- Chunk+ -> (# Int#, Int# #)+skipDigitsAsciiLoop !c = if length c > 0+ then+ let w = PM.indexByteArray (array c) (offset c) :: Word8+ in if w >= c2w '0' && w <= c2w '9'+ then skipDigitsAsciiLoop (advance 1 c)+ else (# unI (offset c), unI (length c) #)+ else (# unI (offset c), unI (length c) #)++skipAlphaAsciiLoop ::+ Bytes -- Chunk+ -> (# Int#, Int# #)+skipAlphaAsciiLoop !c = if length c > 0+ then+ let w = PM.indexByteArray (array c) (offset c) :: Word8+ in if (w >= c2w 'a' && w <= c2w 'z') || (w >= c2w 'A' && w <= c2w 'Z')+ then skipAlphaAsciiLoop (advance 1 c)+ else (# unI (offset c), unI (length c) #)+ else (# unI (offset c), unI (length c) #)++skipAlphaAsciiLoop1Start ::+ e+ -> Bytes -- chunk+ -> Result# e ()+skipAlphaAsciiLoop1Start e !c = if length c > 0+ then + let w = PM.indexByteArray (array c) (offset c) :: Word8+ in if (w >= c2w 'a' && w <= c2w 'z') || (w >= c2w 'A' && w <= c2w 'Z')+ then upcastUnitSuccess (skipAlphaAsciiLoop (advance 1 c))+ else (# e | #)+ else (# e | #)++skipDigitsAscii1LoopStart ::+ e+ -> Bytes -- chunk+ -> Result# e ()+skipDigitsAscii1LoopStart e !c = if length c > 0+ then + let w = PM.indexByteArray (array c) (offset c) :: Word8+ in if w >= c2w '0' && w <= c2w '9'+ then upcastUnitSuccess (skipDigitsAsciiLoop (advance 1 c))+ else (# e | #)+ else (# e | #)++skipLoop ::+ Word8 -- byte to match+ -> Bytes -- Chunk+ -> (# Int#, Int# #)+skipLoop !w !c = if length c > 0+ then if PM.indexByteArray (array c) (offset c) == w+ then skipLoop w (advance 1 c)+ else (# unI (offset c), unI (length c) #)+ else (# unI (offset c), unI (length c) #)++skipLoop1Start ::+ e+ -> Word8 -- byte to match+ -> Bytes -- chunk+ -> Result# e ()+skipLoop1Start e !w !chunk0 = if length chunk0 > 0+ then if PM.indexByteArray (array chunk0) (offset chunk0) == w+ then upcastUnitSuccess (skipLoop w (advance 1 chunk0))+ else (# e | #)+ else (# e | #)++-- | Skip bytes until the character from the ASCII plane is encountered.+-- This does not ensure that the skipped bytes were ASCII-encoded+-- characters.+skipUntilAsciiConsume :: e -> Char -> Parser e s ()+skipUntilAsciiConsume e !w = uneffectful# $ \c ->+ skipUntilConsumeLoop e (c2w w) c++skipUntilConsumeLoop ::+ e -- Error message+ -> Word8 -- byte to match+ -> Bytes -- Chunk+ -> Result# e ()+skipUntilConsumeLoop e !w !c = if length c > 0+ then if PM.indexByteArray (array c) (offset c) /= w+ then skipUntilConsumeLoop e w (advance 1 c)+ else (# | (# (), unI (offset c + 1), unI (length c - 1) #) #)+ else (# e | #)++-- | Fails if there is still more input remaining.+endOfInput :: e -> Parser e s ()+-- GHC should decide to inline this after optimization.+endOfInput e = uneffectful $ \chunk -> if length chunk == 0+ then Success () (offset chunk) 0+ else Failure e++-- | Returns true if there are no more bytes in the input. Returns+-- false otherwise. Always succeeds.+isEndOfInput :: Parser e s Bool+-- GHC should decide to inline this after optimization.+isEndOfInput = uneffectful $ \chunk ->+ Success (length chunk == 0) (offset chunk) (length chunk)++-- | Parse a decimal-encoded 8-bit word. If the number is larger+-- than 255, this parser fails.+decWord8 :: e -> Parser e s Word8+decWord8 e = Parser+ (\chunk0 s0 -> case decSmallWordStart e 256 (boxBytes chunk0) s0 of+ (# s1, r #) -> (# s1, upcastWord8Result r #)+ )++-- | Parse a decimal-encoded 16-bit word. If the number is larger+-- than 65535, this parser fails.+decWord16 :: e -> Parser e s Word16+decWord16 e = Parser+ (\chunk0 s0 -> case decSmallWordStart e 65536 (boxBytes chunk0) s0 of+ (# s1, r #) -> (# s1, upcastWord16Result r #)+ )++-- | Parse a decimal-encoded 32-bit word. If the number is larger+-- than 4294967295, this parser fails.+decWord32 :: e -> Parser e s Word32+-- This will not work on 32-bit platforms.+decWord32 e = Parser+ (\chunk0 s0 -> case decSmallWordStart e 4294967296 (boxBytes chunk0) s0 of+ (# s1, r #) -> (# s1, upcastWord32Result r #)+ )++-- | Parse a decimal-encoded number. If the number is too large to be+-- represented by a machine word, this overflows rather than failing.+-- This may be changed in a future release.+decWord :: e -> Parser e s Word+decWord e = Parser+ (\chunk0 s0 -> case decWordStart e (boxBytes chunk0) s0 of+ (# s1, r #) -> (# s1, upcastWordResult r #)+ )++-- | Parse a decimal-encoded positive integer of arbitrary+-- size. Note: this is not implemented efficiently. This+-- pulls in one digit at a time, multiplying the accumulator+-- by ten each time and adding the new digit. Since+-- arithmetic involving arbitrary-precision integers is+-- somewhat expensive, it would be better to pull in several+-- digits at a time, convert those to a machine-sized integer,+-- then upcast and perform the multiplication and addition.+decPositiveInteger :: e -> Parser e s Integer+decPositiveInteger e = Parser+ (\chunk0 s0 -> decPositiveIntegerStart e (boxBytes chunk0) s0)++decWordStart ::+ e -- Error message+ -> Bytes -- Chunk+ -> ST# s (Result# e Word# )+decWordStart e !chunk0 s0 = if length chunk0 > 0+ then+ let !w = fromIntegral @Word8 @Word+ (PM.indexByteArray (array chunk0) (offset chunk0)) - 48+ in if w < 10+ then (# s0, decWordMore e w (advance 1 chunk0) #)+ else (# s0, (# e | #) #)+ else (# s0, (# e | #) #)++-- No limit on length for integers.+decPositiveIntegerStart ::+ e+ -> Bytes+ -> ST# s (Result# e Integer)+decPositiveIntegerStart e !chunk0 s0 = if length chunk0 > 0+ then+ let !w = (PM.indexByteArray (array chunk0) (offset chunk0)) - 48+ in if w < (10 :: Word8)+ then (# s0, decIntegerMore e (fromIntegral w) (advance 1 chunk0) #)+ else (# s0, (# e | #) #)+ else (# s0, (# e | #) #)++decSmallWordStart ::+ e -- Error message+ -> Word -- Upper Bound+ -> Bytes -- Chunk+ -> ST# s (Result# e Word# )+decSmallWordStart e !limit !chunk0 s0 = if length chunk0 > 0+ then+ let !w = fromIntegral @Word8 @Word+ (PM.indexByteArray (array chunk0) (offset chunk0)) - 48+ in if w < 10+ then (# s0, decSmallWordMore e w limit (advance 1 chunk0) #)+ else (# s0, (# e | #) #)+ else (# s0, (# e | #) #)++-- This will not inline since it is recursive, but worker+-- wrapper will still happen.+decWordMore ::+ e -- Error message+ -> Word -- Accumulator+ -> Bytes -- Chunk+ -> Result# e Word#+decWordMore e !acc !chunk0 = if length chunk0 > 0+ then+ let !w = fromIntegral @Word8 @Word+ (PM.indexByteArray (array chunk0) (offset chunk0)) - 48+ in if w < 10+ then decWordMore e (acc * 10 + w)+ (advance 1 chunk0)+ else (# | (# unW acc, unI (offset chunk0), unI (length chunk0) #) #)+ else (# | (# unW acc, unI (offset chunk0), 0# #) #)++decSmallWordMore ::+ e -- Error message+ -> Word -- Accumulator+ -> Word -- Upper Bound+ -> Bytes -- Chunk+ -> Result# e Word#+decSmallWordMore e !acc !limit !chunk0 = if length chunk0 > 0+ then+ let !w = fromIntegral @Word8 @Word+ (PM.indexByteArray (array chunk0) (offset chunk0)) - 48+ in if w < 10+ then+ let w' = acc * 10 + w+ in if w' < limit+ then decSmallWordMore e w' limit (advance 1 chunk0)+ else (# e | #)+ else (# | (# unW acc, unI (offset chunk0), unI (length chunk0) #) #)+ else (# | (# unW acc, unI (offset chunk0), 0# #) #)++decIntegerMore ::+ e -- Error message+ -> Integer -- Accumulator+ -> Bytes -- Chunk+ -> Result# e Integer+decIntegerMore e !acc !chunk0 = if length chunk0 > 0+ then+ let w :: Word8+ !w = (PM.indexByteArray (array chunk0) (offset chunk0)) - 48+ in if w < 10+ then+ let w' = acc * 10 + fromIntegral w+ in decIntegerMore e w' (advance 1 chunk0)+ else (# | (# acc, unI (offset chunk0), unI (length chunk0) #) #)+ else (# | (# acc, unI (offset chunk0), 0# #) #)++advance :: Int -> Bytes -> Bytes+advance n (Bytes arr off len) = Bytes arr (off + n) (len - n)++unW :: Word -> Word#+unW (W# w) = w++unI :: Int -> Int#+unI (I# w) = w++boxBytes :: Bytes# -> Bytes+boxBytes (# a, b, c #) = Bytes (ByteArray a) (I# b) (I# c)++unboxBytes :: Bytes -> Bytes#+unboxBytes (Bytes (ByteArray a) (I# b) (I# c)) = (# a,b,c #)++unboxResult :: Result e a -> Result# e a+unboxResult (Success a (I# b) (I# c)) = (# | (# a, b, c #) #)+unboxResult (Failure e) = (# e | #)++boxResult :: Result# e a -> Result e a+boxResult (# | (# a, b, c #) #) = Success a (I# b) (I# c)+boxResult (# e | #) = Failure e++-- | Convert a 'Word32' parser to a 'Word#' parser.+unboxWord32 :: Parser s e Word32 -> Parser s e Word#+unboxWord32 (Parser f) = Parser+ (\x s0 -> case f x s0 of+ (# s1, r #) -> case r of+ (# e | #) -> (# s1, (# e | #) #)+ (# | (# W32# a, b, c #) #) -> (# s1, (# | (# a, b, c #) #) #)+ )++-- | Convert a 'Word#' parser to a 'Word32' parser. Precondition:+-- the argument parser only returns words less than 4294967296.+boxWord32 :: Parser s e Word# -> Parser s e Word32+boxWord32 (Parser f) = Parser+ (\x s0 -> case f x s0 of+ (# s1, r #) -> case r of+ (# e | #) -> (# s1, (# e | #) #)+ (# | (# a, b, c #) #) -> (# s1, (# | (# W32# a, b, c #) #) #)+ )++-- | There is a law-abiding instance of @Alternative@ for 'Parser'.+-- However, it is not terribly useful since error messages seldom+-- have a 'Monoid' instance. This function is a right-biased+-- variant of @\<|\>@. Consequently, it lacks an identity.+-- See <https://github.com/bos/attoparsec/issues/122 attoparsec #122>+-- for more discussion of this topic.+orElse :: Parser s e a -> Parser s e a -> Parser s e a+orElse (Parser f) (Parser g) = Parser+ (\x s0 -> case f x s0 of+ (# s1, r0 #) -> case r0 of+ (# _ | #) -> g x s1+ (# | r #) -> (# s1, (# | r #) #)+ )++codepointFromFourBytes :: Word8 -> Word8 -> Word8 -> Word8 -> Char+codepointFromFourBytes w1 w2 w3 w4 = C#+ ( chr#+ ( unI $ fromIntegral+ ( unsafeShiftL (word8ToWord w1 .&. 0b00001111) 18 .|. + unsafeShiftL (word8ToWord w2 .&. 0b00111111) 12 .|. + unsafeShiftL (word8ToWord w3 .&. 0b00111111) 6 .|. + (word8ToWord w4 .&. 0b00111111)+ )+ )+ )++codepointFromThreeBytes :: Word8 -> Word8 -> Word8 -> Char+codepointFromThreeBytes w1 w2 w3 = C#+ ( chr#+ ( unI $ fromIntegral+ ( unsafeShiftL (word8ToWord w1 .&. 0b00001111) 12 .|. + unsafeShiftL (word8ToWord w2 .&. 0b00111111) 6 .|. + (word8ToWord w3 .&. 0b00111111)+ )+ )+ )++codepointFromTwoBytes :: Word8 -> Word8 -> Char+codepointFromTwoBytes w1 w2 = C#+ ( chr#+ ( unI $ fromIntegral @Word @Int+ ( unsafeShiftL (word8ToWord w1 .&. 0b00011111) 6 .|. + (word8ToWord w2 .&. 0b00111111)+ )+ )+ )++oneByteChar :: Word8 -> Bool+oneByteChar !w = w .&. 0b10000000 == 0++twoByteChar :: Word8 -> Bool+twoByteChar !w = w .&. 0b11100000 == 0b11000000++threeByteChar :: Word8 -> Bool+threeByteChar !w = w .&. 0b11110000 == 0b11100000++fourByteChar :: Word8 -> Bool+fourByteChar !w = w .&. 0b11111000 == 0b11110000++word8ToWord :: Word8 -> Word+word8ToWord = fromIntegral++followingByte :: Word8 -> Bool+followingByte !w = xor w 0b01000000 .&. 0b11000000 == 0b11000000++{- $bind+Sometimes, GHC ends up building join points in a way that+boxes arguments unnecessarily. In this situation, special variants+of monadic @>>=@ can be helpful. If @C#@, @I#@, etc. never+get used in you original source code, GHC cannot introduce them.+-}++-- | Specialization of monadic bind for parsers that return 'Char#'.+bindChar :: Parser s e Char# -> (Char# -> Parser s e a) -> Parser s e a+{-# inline bindChar #-}+bindChar (Parser f) g = Parser+ (\x@(# arr, _, _ #) s0 -> case f x s0 of+ (# s1, r0 #) -> case r0 of+ (# e | #) -> (# s1, (# e | #) #)+ (# | (# y, b, c #) #) ->+ runParser (g y) (# arr, b, c #) s1+ )+
+ test/Main.hs view
@@ -0,0 +1,43 @@+{-# language BangPatterns #-}+{-# language MultiWayIf #-}+{-# language ScopedTypeVariables #-}+{-# language TypeApplications #-}++import Control.Exception (throwIO)+import Data.Primitive (ByteArray)+import Data.Word (Word8)+import Data.Char (ord)+import Data.Bytes.Types (Bytes(Bytes))+import Data.Bytes.Parser (Parser,Result(..))++import qualified Data.Bytes.Parser as P+import qualified Data.Primitive as PM+import qualified GHC.Exts as Exts++main :: IO ()+main = do+ putStrLn "Start"+ putStrLn "A"+ testA+ putStrLn "Finished"++testA :: IO ()+testA =+ let r = P.parseByteArray+ ( pure (,)+ <*> P.decWord ()+ <* P.ascii () '.'+ <*> P.decWord ()+ <* P.ascii () '.'+ ) (pack "42.8.")+ in case r of+ Failure () -> fail "test A failed parsing"+ Success (42,8) 5 0 -> pure ()+ Success _ _ _ -> fail "test A wrong result"++bytes :: String -> Bytes+bytes s = let b = pack s in Bytes b 0 (PM.sizeofByteArray b)++pack :: String -> ByteArray+pack = Exts.fromList . map (fromIntegral @Int @Word8 . ord)+