packages feed

ron-0.13: lib/Attoparsec/Extra.hs

{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE ImportQualifiedPost #-}
{-# LANGUAGE OverloadedStrings #-}

module Attoparsec.Extra (
    module Attoparsec,
    char,
    endOfInputEx,
    isSuccessful,
    label,
    takeL,
    definiteDouble,
    withInputSize,
    (??),
    (<+>),
) where

import RON.Prelude

import Data.Attoparsec.ByteString.Char8 (
    anyChar,
    decimal,
    isDigit_w8,
    signed,
 )
import Data.Attoparsec.ByteString.Lazy as Attoparsec
import Data.Attoparsec.Internal.Types qualified as Internal
import Data.ByteString qualified as BS
import Data.ByteString.Lazy (fromStrict)
import Data.Maybe (isJust)
import Data.Scientific qualified as Sci
import GHC.Real (toInteger)

-- | 'Attoparsec.take' adapter to 'ByteStringL'
takeL :: Int -> Parser ByteStringL
takeL = fmap fromStrict . Attoparsec.take

getPos :: Parser Int
getPos =
    Internal.Parser $ \t pos more _ suc -> suc t pos more $ Internal.fromPos pos

withInputSize :: Parser a -> Parser (Int, a)
withInputSize p = do
    posBefore <- getPos
    r <- p
    posAfter <- getPos
    pure (posAfter - posBefore, r)

label :: String -> Parser a -> Parser a
label = flip (<?>)

-- label' :: String -> Parser a -> Parser a
-- label' name p = do
--     pos <- getPos
--     label (name ++ ':' : show pos) p

-- | Variant of 'endOfInput' with a more debuggable message.
endOfInputEx :: Parser ()
endOfInputEx = do
    weAreAtEnd <- atEnd
    unless weAreAtEnd $ do
        pos <- getPos
        rest <- takeAtMost 11
        let cite
                | BS.length rest < 11 = rest
                | otherwise = BS.take 10 rest <> "..."
        fail $ show pos <> ": extra input: " <> show cite

takeAtMost :: Int -> Parser ByteString
takeAtMost limit = do
    pos0 <- getPos
    BS.pack <$> manyTill anyWord8 (checkLimit $ pos0 + limit)
  where
    checkLimit maxPos = do
        pos <- getPos
        guard (pos >= maxPos) <|> endOfInput

(??) :: (Applicative f) => Maybe a -> f a -> f a
a ?? alt = maybe alt pure a

{- | Apply parser and check it is applied successfully.
Kinda opposite to 'guard'.
-}
isSuccessful :: (Alternative f) => f a -> f Bool
isSuccessful p = p $> True <|> pure False

char :: Char -> Parser Char
char c = do
    c' <- anyChar
    if c == c' then
        pure c
    else
        fail $ "Expected " ++ show c ++ ", got " ++ show c'

{- | Parses a definite double, i.e. it is not an integer.
For this, the double has either a '.', and 'e'/'E' part or both.
-}
{-# INLINE definiteDouble #-}
definiteDouble :: Parser Double
definiteDouble = withDot <|> noDot
  where
    dot = char '.'
    minus = char '-'

    integerPart = decimal

    fractionalPartWithLength =
        BS.foldl' step (0, 0) `fmap` Attoparsec.takeWhile1 isDigit_w8
      where
        step (a, l) w = (a * 10 + fromIntegral (w - 48), l + 1)

    exponent = (char 'e' <|> char 'E') *> signed decimal

    withDot = do
        m <- isJust <$> optional minus
        i <- integerPart
        _ <- dot
        (f, l) <- fractionalPartWithLength
        e <- optional exponent
        pure $ buildDouble m i f l (fromMaybe 0 e)

    noDot = do
        m <- isJust <$> optional minus
        i <- integerPart
        buildDouble m i 0 0 <$> exponent

buildDouble :: Bool -> Integer -> Integer -> Int -> Int -> Double
buildDouble
    isNegative
    integerPart
    fractionalPart
    fractionalPartLength
    exponentPart =
        Sci.toRealFloat $ Sci.scientific coeff exponent
      where
        addOrSubFractionalPart
            | integerPart < 0 = -fractionalPart
            | otherwise = fractionalPart
        coeff =
            (if isNegative then negate else id) $
                integerPart * 10 ^ toInteger fractionalPartLength
                    + addOrSubFractionalPart
        exponent = exponentPart - fractionalPartLength

(<+>) :: Parser a -> Parser a -> Parser a
(<+>) p1 p2 =
    Internal.Parser \t pos more lose suc ->
        let lose1 t' _pos more1 ctx1 msg1 =
                Internal.runParser p2 t' pos more1 lose2 suc
              where
                lose2 t2 pos2 more2 ctx2 msg2 =
                    lose t2 pos2 more2 [] $
                        unwords
                            [ "Many fails:\n"
                            , intercalate " > " ctx1
                            , ":"
                            , msg1
                            , "|\n"
                            , intercalate " > " ctx2
                            , ":"
                            , msg2
                            ]
        in  Internal.runParser p1 t pos more lose1 suc
infixl 3 <+>