bytesmith-0.3.11.1: src/Data/Bytes/Parser/Ascii.hs
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE BinaryLiterals #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE GADTSyntax #-}
{-# LANGUAGE MagicHash #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE UnboxedTuples #-}
{- | Parse input as ASCII-encoded text. Some parsers in this module,
like 'any' and 'peek', fail if they encounter a byte above @0x7F@.
Others, like numeric parsers and skipping parsers, leave the cursor
at the position of the offending byte without failing.
-}
module Data.Bytes.Parser.Ascii
( -- * Matching
Latin.char
, Latin.char2
, Latin.char3
, Latin.char4
-- * Case-Insensitive Matching
, charInsensitive
-- * Get Character
, any
, any#
, peek
, opt
-- * Match Many
, shortTrailedBy
, takeShortWhile
-- * Skip
, Latin.skipDigits
, Latin.skipDigits1
, Latin.skipChar
, Latin.skipChar1
, skipAlpha
, skipAlpha1
, skipTrailedBy
, skipWhile
-- * Numbers
, Latin.decWord
, Latin.decWord8
, Latin.decWord16
, Latin.decWord32
) where
import Prelude hiding (any, fail, length, takeWhile)
import Control.Monad.ST (runST)
import Data.Bits (clearBit)
import Data.Bytes.Parser.Internal (Parser (..), Result (..), Result#, indexLatinCharArray, uneffectful, uneffectful#, upcastUnitSuccess)
import Data.Bytes.Types (Bytes (..))
import Data.Char (ord)
import Data.Text.Short (ShortText)
import Data.Word (Word8)
import GHC.Exts (Char (C#), Char#, Int (I#), Int#, chr#, gtChar#, indexCharArray#, ord#, (+#), (-#), (<#))
import qualified Data.ByteString.Short.Internal as BSS
import qualified Data.Bytes as Bytes
import qualified Data.Bytes.Parser.Latin as Latin
import qualified Data.Bytes.Parser.Unsafe as Unsafe
import qualified Data.Primitive as PM
import qualified Data.Text.Short.Unsafe as TS
{- | Consume the next character, failing if it does not match the expected
value or if there is no more input. This check for equality is case
insensitive.
Precondition: The argument must be a letter (@[a-zA-Z]@). Behavior is
undefined if it is not.
-}
charInsensitive :: e -> Char -> Parser e s ()
{-# INLINE charInsensitive #-}
charInsensitive e !c = uneffectful $ \chunk ->
if length chunk > 0
then
if clearBit (PM.indexByteArray (array chunk) (offset chunk) :: Word8) 5 == w
then Success () (offset chunk + 1) (length chunk - 1)
else Failure e
else Failure e
where
w = clearBit (fromIntegral @Int @Word8 (ord c)) 5
{- | Consume input until the trailer is found. Then, consume
the trailer as well. This fails if the trailer is not
found or if any non-ASCII characters are encountered.
-}
skipTrailedBy :: e -> Char -> Parser e s ()
{-# INLINE skipTrailedBy #-}
skipTrailedBy e !c = do
let go = do
!d <- any e
if d == c
then pure ()
else go
go
{- | Consume characters matching the predicate. The stops when it
encounters a non-matching character or when it encounters a byte
above @0x7F@. This never fails.
-}
takeShortWhile :: (Char -> Bool) -> Parser e s ShortText
{-# INLINE takeShortWhile #-}
takeShortWhile p = do
!start <- Unsafe.cursor
skipWhile p
end <- Unsafe.cursor
src <- Unsafe.expose
let len = end - start
!r = runST $ do
marr <- PM.newByteArray len
PM.copyByteArray marr 0 src start len
PM.unsafeFreezeByteArray marr
pure $
TS.fromShortByteStringUnsafe $
byteArrayToShortByteString $
r
{- | Consume input through the next occurrence of the target
character and return the consumed input, excluding the
target character, as a 'ShortText'. This fails if it
encounters any bytes above @0x7F@.
-}
shortTrailedBy :: e -> Char -> Parser e s ShortText
shortTrailedBy e !c = do
!start <- Unsafe.cursor
skipTrailedBy e c
end <- Unsafe.cursor
src <- Unsafe.expose
let len = end - start - 1
!r = runST $ do
marr <- PM.newByteArray len
PM.copyByteArray marr 0 src start len
PM.unsafeFreezeByteArray marr
pure $
TS.fromShortByteStringUnsafe $
byteArrayToShortByteString $
r
-- | Consumes and returns the next character in the input.
any :: e -> Parser e s Char
{-# INLINE any #-}
any e = uneffectful $ \chunk ->
if length chunk > 0
then
let c = indexLatinCharArray (array chunk) (offset chunk)
in if c < '\128'
then Success c (offset chunk + 1) (length chunk - 1)
else Failure e
else Failure e
-- | Variant of 'any' with unboxed result.
any# :: e -> Parser e s Char#
{-# INLINE any# #-}
any# 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 | #) #)
)
unI :: Int -> Int#
{-# INLINE unI #-}
unI (I# w) = w
{- | Examine the next byte without consuming it, interpret it as an
ASCII-encoded character. This fails if the byte is above @0x7F@ or
if the end of input has been reached.
-}
peek :: e -> Parser e s Char
{-# INLINE peek #-}
peek 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
{- | Consume the next byte, interpreting it as an ASCII-encoded character.
Fails if the byte is above @0x7F@. Returns @Nothing@ if the
end of the input has been reached.
-}
opt :: e -> Parser e s (Maybe Char)
{-# INLINE opt #-}
opt 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)
{- | Consume characters matching the predicate. The stops when it
encounters a non-matching character or when it encounters a byte
above @0x7F@. This never fails.
-}
skipWhile :: (Char -> Bool) -> Parser e s ()
{-# INLINE skipWhile #-}
skipWhile p =
Parser
( \(# arr, off0, len0 #) s0 ->
let go off len = case len of
0# -> (# (), off, 0# #)
_ ->
let c = indexCharArray# arr off
in case p (C# c) of
True -> case gtChar# c '\x7F'# of
1# -> (# (), off, len #)
_ -> go (off +# 1#) (len -# 1#)
False -> (# (), off, len #)
in (# s0, (# | go off0 len0 #) #)
)
{- | Skip uppercase and lowercase letters until a non-alpha
character is encountered.
-}
skipAlpha :: Parser e s ()
{-# INLINE skipAlpha #-}
skipAlpha = uneffectful# $ \c ->
upcastUnitSuccess (skipAlphaAsciiLoop c)
{- | Skip uppercase and lowercase letters until a non-alpha
character is encountered.
-}
skipAlpha1 :: e -> Parser e s ()
{-# INLINE skipAlpha1 #-}
skipAlpha1 e = uneffectful# $ \c ->
skipAlphaAsciiLoop1Start e c
skipAlphaAsciiLoop ::
Bytes -> -- Chunk
(# Int#, Int# #)
{-# INLINE skipAlphaAsciiLoop #-}
skipAlphaAsciiLoop !c =
if length c > 0
then
let w = indexLatinCharArray (array c) (offset c)
in if (w >= 'a' && w <= 'z') || (w >= 'A' && w <= 'Z')
then skipAlphaAsciiLoop (Bytes.unsafeDrop 1 c)
else (# unI (offset c), unI (length c) #)
else (# unI (offset c), unI (length c) #)
skipAlphaAsciiLoop1Start ::
e ->
Bytes -> -- chunk
Result# e ()
{-# INLINE skipAlphaAsciiLoop1Start #-}
skipAlphaAsciiLoop1Start e !c =
if length c > 0
then
let w = indexLatinCharArray (array c) (offset c)
in if (w >= 'a' && w <= 'z') || (w >= 'A' && w <= 'Z')
then upcastUnitSuccess (skipAlphaAsciiLoop (Bytes.unsafeDrop 1 c))
else (# e | #)
else (# e | #)
byteArrayToShortByteString :: PM.ByteArray -> BSS.ShortByteString
{-# INLINE byteArrayToShortByteString #-}
byteArrayToShortByteString (PM.ByteArray x) = BSS.SBS x