jordan-0.2.0.0: lib/Jordan/FromJSON/Internal/UnboxedReporting.hs
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiWayIf #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE NoMonomorphismRestriction #-}
module Jordan.FromJSON.Internal.UnboxedReporting where
import Control.Applicative (Alternative (empty, (<|>)))
import Control.Applicative.Combinators (sepBy)
import Control.Monad (when)
import Data.Bifunctor
import qualified Data.ByteString as BS
import Data.ByteString.Unsafe as BS
import Data.Char (chr, isControl, ord)
import Data.Functor (void, ($>))
import qualified Data.Map.Strict as Map
import Data.Maybe (isJust)
import Data.Monoid (Alt (..))
import Data.Scientific (Scientific)
import qualified Data.Scientific as Scientific
import qualified Data.Text as T
import Data.Text.Encoding (decodeUtf8', encodeUtf8)
import Data.Word (Word8)
import Debug.Trace (traceM)
import Jordan.FromJSON.Class
import Jordan.FromJSON.Internal.Attoparsec (bsToInteger)
import Jordan.FromJSON.Internal.Permutation
import Jordan.FromJSON.Internal.UnboxedParser as UP hiding (AccumE (..), AccumEL, AccumER)
import Jordan.Types.Internal.AccumE (AccumE (AccumE))
import Jordan.Types.JSONError
( JSONArrayError (..),
JSONError
( ErrorBadArray,
ErrorBadObject,
ErrorBadTextConstant,
ErrorBadType,
ErrorInvalidJSON,
ErrorMesage,
ErrorNoValue
),
JSONObjectError (..),
)
import Jordan.Types.JSONType (JSONType (..))
import Numeric (showHex)
skipWithFailure :: JSONError -> Parser JSONError a
skipWithFailure err =
UP.asFailure $
skipAnything $> err
lexeme :: Semigroup err => Parser err a -> Parser err a
lexeme p = p <* UP.skipWhitespace
{-# INLINE lexeme #-}
jsonTypeFromWord :: Word8 -> Maybe JSONType
jsonTypeFromWord jt
| jt == 34 = pure JSONTypeText
| jt == 116 || jt == 102 = pure JSONTypeBool
| jt == 110 = pure JSONTypeNull
| jt >= 48 && jt <= 57 = pure JSONTypeNumber
| jt == 45 = pure JSONTypeNumber
| jt == 91 = pure JSONTypeArray
| jt == 123 = pure JSONTypeObject
| otherwise = Nothing
{-# INLINE jsonTypeFromWord #-}
peekJSONType :: (Monoid err) => Parser err JSONType
peekJSONType = UP.orFail (jsonTypeFromWord <$> UP.peekWord)
{-# INLINE peekJSONType #-}
skipNullExpecting :: JSONType -> Parser JSONError a
skipNullExpecting jt =
UP.asFailure $ nullParser $> ErrorBadType jt JSONTypeNull
-- | Parse a NULL value.
nullParser :: Semigroup err => Parser err ()
nullParser = lexeme $ UP.parseChunk "null" $> ()
{-# INLINE nullParser #-}
skipBoolExpecting :: JSONType -> Parser JSONError a
skipBoolExpecting jt =
UP.asFailure $
boolParser $> ErrorBadType jt JSONTypeBool
{-# INLINE skipBoolExpecting #-}
boolParser :: (Monoid err) => Parser err Bool
boolParser =
lexeme $
(UP.parseChunk "true" $> True)
<|> (UP.parseChunk "false" $> False)
{-# INLINE boolParser #-}
skipTextExpecting :: JSONType -> Parser JSONError a
skipTextExpecting jt =
UP.asFailure $
textParser $> ErrorBadType jt JSONTypeText
{-# INLINE skipTextExpecting #-}
textParser :: (Monoid err) => Parser err T.Text
textParser = lexeme $ do
UP.specificWord 34
parseAfterQuote
{-# INLINE textParser #-}
sepByVoid :: Alternative f => f a1 -> f a2 -> f ()
sepByVoid elem sep = void $ sepBy (void elem) sep
{-# INLINE sepByVoid #-}
skipNumber :: (Monoid err) => Parser err ()
skipNumber = void scientific
{-# INLINE skipNumber #-}
skipNumberExpecting :: JSONType -> Parser JSONError a
skipNumberExpecting jt =
UP.asFailure $
skipNumber $> ErrorBadType jt JSONTypeNumber
skipAnything :: Monoid err => Parser err ()
skipAnything = do
r <- UP.peekWord
if
| r == 110 -> lexeme $ UP.parseChunk "null"
| r == 116 -> lexeme $ UP.parseChunk "true" -- t -> true
| r == 102 -> lexeme $ UP.parseChunk "false"
| r == 34 -> void textParser -- " -> text
| r == 123 -> skipObject -- { -> object
| r == 91 -> skipArray -- [ -> array
| r == 45 || (r >= 48 && r <= 57) -> skipNumber
| otherwise -> (orFail $ pure Nothing)
{-# INLINE skipAnything #-}
skipArray :: (Monoid err) => Parser err ()
skipArray = do
startArray
sepByVoid skipAnything comma
endArray
{-# INLINE skipArray #-}
kvSep :: Semigroup err => Parser err ()
kvSep = lexeme $ UP.specificWord 58
skipAnyKV :: Monoid err => Parser err ()
skipAnyKV = do
textParser
kvSep
skipAnything
comma :: Semigroup err => Parser err ()
comma = lexeme $ UP.specificWord 44
skipObject :: Monoid err => Parser err ()
skipObject = do
lexeme $ UP.specificWord 123
sepByVoid skipAnyKV comma
lexeme $ UP.specificWord 125
{-# INLINE skipObject #-}
failOnError :: (Monoid err) => Either a T.Text -> Parser err T.Text
failOnError = \case
Left _ -> failParse
Right txt -> pure txt
parseAfterQuote :: (Monoid err) => Parser err T.Text
parseAfterQuote = do
chunk <- UP.takeWord8Cont (\c -> c /= 92 && c /= 34) decodeUtf8'
decoded <- failOnError chunk
(lexeme (specificWord 34) $> decoded) <|> do
specificWord 92
escape <- parseEscape
res <- parseAfterQuote
pure $ decoded <> escape <> res
{-# INLINE parseAfterQuote #-}
hexDigit :: Semigroup err => Parser err Word8
hexDigit = do
r <- UP.word
orFail $
if
| r >= 48 && r <= 57 -> pure $ Just (r - 48)
| r >= 97 && r <= 103 -> pure $ Just ((r - 97) + 10)
| otherwise -> pure Nothing
{-# INLINE hexDigit #-}
parseEscape :: (Monoid err) => UP.Parser err T.Text
parseEscape =
quote
<|> backslash
<|> solidus
<|> backspace
<|> formfeed
<|> linefeed
<|> carriage
<|> tab
<|> unicode
where
quote = specificWord 34 $> "\""
backslash = specificWord 92 $> "\\"
solidus = specificWord 47 $> "/"
backspace = specificWord 98 $> "\b"
formfeed = specificWord 102 $> "\f"
linefeed = specificWord 110 $> "\n"
carriage = specificWord 114 $> "\r"
tab = specificWord 116 $> "\t"
unicode = do
specificWord 117
a <- hexDigit
b <- hexDigit
c <- hexDigit
d <- hexDigit
let res = (((fromIntegral a * 16) + fromIntegral b) * 16 + fromIntegral c) * 16 + fromIntegral d
pure $ T.pack [chr res]
{-# INLINE parseEscape #-}
parseCharInText (c :: Char) = parseLit c <|> escaped c
where
parseLit = \case
'\\' -> UP.parseChunk "\\\\"
'"' -> UP.parseChunk "\\\""
'/' -> UP.parseChunk "/" <|> UP.parseChunk "\\/"
'\b' -> UP.parseChunk "\\b"
'\f' -> UP.parseChunk "\\f"
'\n' -> UP.parseChunk "\\n"
'\r' -> UP.parseChunk "\\r"
'\t' -> UP.parseChunk "\\t"
a -> if isControl a then empty else UP.parseChunk (encodeUtf8 $ T.singleton a)
escaped c = UP.parseChunk $ encodeUtf8 $ "\\u" <> T.justifyRight 4 '0' (T.pack $ showHex (ord c) mempty)
{-# INLINE parseCharInText #-}
parseSpecificKeyInQuotes :: Monoid err => T.Text -> Parser err ()
parseSpecificKeyInQuotes t = UP.specificWord 34 *> parseSpecificKeyAfterQuote t
{-# INLINE parseSpecificKeyInQuotes #-}
parseSpecificKeyAfterQuote :: Monoid err => T.Text -> Parser err ()
parseSpecificKeyAfterQuote key = (parseRaw <|> parseChars) *> lexeme (UP.specificWord 34)
where
parseChars = T.foldr (\c a -> parseCharInText c *> a) (pure ()) key
parseRaw =
if isJust $ T.findIndex invalidTextChar key
then empty
else UP.parseChunk (encodeUtf8 key)
startBracket :: Semigroup err => Parser err ()
startBracket = lexeme $ UP.specificWord 123
endBracket :: Semigroup err => Parser err ()
endBracket = lexeme $ UP.specificWord 125
startArray :: Semigroup err => Parser err ()
startArray = lexeme $ UP.specificWord 91
endArray :: Semigroup err => Parser err ()
endArray = lexeme $ UP.specificWord 93
parseObjectKV :: Monoid err => T.Text -> Parser err b -> Parser err b
parseObjectKV key v = do
lexeme $ parseSpecificKeyInQuotes key
lexeme $ UP.specificWord 58
v
invalidTextChar :: Char -> Bool
invalidTextChar c =
c == '"'
|| c == '\\'
|| isControl c
data SP = SP !Integer {-# UNPACK #-} !Int
-- A sub-quadratic algorithm for Integer. Pairs of adjacent radix b
-- digits are combined into a single radix b^2 digit. This process is
-- repeated until we are left with a single digit. This algorithm
-- performs well only on large inputs, so we use the simple algorithm
-- for smaller inputs.
valInteger :: Integer -> Int -> [Integer] -> Integer
valInteger = go
where
go :: Integer -> Int -> [Integer] -> Integer
go _ _ [] = 0
go _ _ [d] = d
go b l ds
| l > 40 = b' `seq` go b' l' (combine b ds')
| otherwise = valSimple b ds
where
-- ensure that we have an even number of digits
-- before we call combine:
ds' = if even l then ds else 0 : ds
b' = b * b
l' = (l + 1) `quot` 2
combine b (d1 : d2 : ds) = d `seq` (d : combine b ds)
where
d = d1 * b + d2
combine _ [] = []
combine _ [_] = errorWithoutStackTrace "this should not happen"
{-# INLINE valInteger #-}
-- The following algorithm is only linear for types whose Num operations
-- are in constant time.
valSimple :: Integer -> [Integer] -> Integer
valSimple base = go 0
where
go r [] = r
go r (d : ds) = r' `seq` go r' ds
where
r' = r * base + fromIntegral d
{-# INLINE valSimple #-}
isDigitWord8 :: Word8 -> Bool
isDigitWord8 c = c >= 48 && c <= 57
{-# INLINE isDigitWord8 #-}
decimal0 :: Semigroup err => Parser err Integer
decimal0 = do
let zero = 48
digits <- UP.takeWord8 isDigitWord8
let !length = BS.length digits
when (length == 0) UP.failParse
if length > 1 && BS.unsafeHead digits == zero
then UP.failParse
else return (bsToInteger digits)
{-# INLINE decimal0 #-}
scientific :: (Monoid err) => UP.Parser err Scientific
scientific = lexeme $ do
let minus = 45
plus = 43
sign <- UP.peekWord
let !positive = sign == plus || sign /= minus
when (sign == plus || sign == minus) $
void UP.word
n <- decimal0
let f fracDigits =
SP
(BS.foldl' step n fracDigits)
(negate $ BS.length fracDigits)
step a w = a * 10 + fromIntegral (w - 48)
dotty <- UP.peekWordMaybe
SP c e <- case dotty of
Just 46 -> UP.word *> UP.takeWord81Cont isDigitWord8 f
_ -> pure (SP n 0)
let !signedCoeff
| positive = c
| otherwise = - c
( (UP.specificWord 101 <|> UP.specificWord 69)
*> fmap (Scientific.scientific signedCoeff . (e +)) (UP.signed (snd <$> UP.parseIntegral))
)
<|> pure (Scientific.scientific signedCoeff e)
{-# INLINE scientific #-}