packages feed

gigaparsec-0.2.4.0: src/Text/Gigaparsec/Internal/Token/Text.hs

{-# LANGUAGE Safe #-}
{-# LANGUAGE OverloadedLists, OverloadedStrings #-}
{-# OPTIONS_GHC -Wno-incomplete-record-updates -Wno-incomplete-uni-patterns #-}
{-# OPTIONS_HADDOCK hide #-}
module Text.Gigaparsec.Internal.Token.Text (module Text.Gigaparsec.Internal.Token.Text) where

import Text.Gigaparsec (Parsec, void, (<|>), empty, somel, (<~>), ($>), atomic, some)
import Text.Gigaparsec.Char (char, digit, hexDigit, octDigit, bit, satisfy, trie, string)
import Text.Gigaparsec.Token.Descriptions (
    TextDesc(TextDesc, characterLiteralEnd, graphicCharacter),
    EscapeDesc(EscapeDesc, escBegin, emptyEscape, gapsSupported, mapping, literals,
               decimalEscape, hexadecimalEscape, octalEscape, binaryEscape),
    NumericEscape(NumericSupported, NumericIllegal, numDigits, maxValue, prefix),
    CharPredicate,
    NumberOfDigits(Exactly, AtMost, Unbounded)
  )
import Text.Gigaparsec.Token.Errors (
    ErrorConfig(verifiedCharBadCharsUsedInLiteral, verifiedStringBadCharsUsedInLiteral,
                filterCharNonAscii, filterCharNonLatin1,
                labelCharAscii, labelCharAsciiEnd, labelCharLatin1, labelCharLatin1End,
                labelCharUnicodeEnd, labelCharUnicode,
                labelGraphicCharacter, labelStringCharacter,
                filterStringNonAscii, filterStringNonLatin1,
                labelEscapeEnd, labelEscapeSequence, filterEscapeCharNumericSequenceIllegal,
                filterEscapeCharRequiresExactDigits, labelEscapeNumericEnd, labelEscapeNumeric,
                labelStringEscapeGap, labelStringEscapeGapEnd, labelStringEscapeEmpty,
                labelStringAscii, labelStringAsciiEnd, labelStringLatin1, labelStringLatin1End,
                labelStringUnicode, labelStringUnicodeEnd),
    NotConfigurable (notConfigured)
  )
import Text.Gigaparsec.Internal.Token.Errors (
    checkBadChar, filterS, annotate, mapMaybeS, mapMaybeS',
    LabelWithExplainConfig, LabelConfig
  )
import Text.Gigaparsec.Internal.Token.Generic (
    GenericNumeric(zeroAllowedDecimal, zeroAllowedHexadecimal, zeroAllowedOctal, zeroAllowedBinary)
  )
import Data.Char (isSpace, chr, ord, digitToInt, isAscii, isLatin1)
import Data.Map qualified as Map (insert, map)
import Data.Set (Set)
import Data.Set qualified as Set (toList)
import Data.List.NonEmpty (NonEmpty((:|)), sort)
import Text.Gigaparsec.Registers (Reg, make, unsafeMake, gets, modify, put, get)
import Text.Gigaparsec.Combinator (guardS, choice, manyTill)
import Control.Applicative (liftA3)
import Data.Maybe (catMaybes)

-- TODO: is it possible to /actually/ support Text/Bytestring in future?
-- Perhaps something like the Numeric stuff?
type TextParsers :: * -> *
data TextParsers t = TextParsers { unicode :: Parsec t
                                 , ascii :: Parsec t
                                 , latin1 :: Parsec t
                                 }

-- I want the convenient naming, sue me
type StringParsers :: *
type StringParsers = TextParsers String

type CharacterParsers :: *
type CharacterParsers = TextParsers Char

mkCharacterParsers :: TextDesc -> Escape -> ErrorConfig -> CharacterParsers
mkCharacterParsers TextDesc{..} escape !err = TextParsers {..}
  where unicode = lit (labelCharUnicode err) (labelCharUnicodeEnd err) uncheckedUniLetter
        ascii = lit (labelCharAscii err) (labelCharAsciiEnd err) (filterS (filterCharNonAscii err) (> '\x7f') uncheckedUniLetter)
        latin1 = lit (labelCharLatin1 err) (labelCharLatin1End err) (filterS (filterCharNonLatin1 err) (> '\xff') uncheckedUniLetter)

        quote = char characterLiteralEnd
        lit label endLabel c = annotate label quote *> c <* annotate endLabel quote
        uncheckedUniLetter = escapeChar escape <|> graphic <|> checkBadChar (verifiedCharBadCharsUsedInLiteral err)

        graphic = annotate (labelGraphicCharacter err) $ maybe empty satisfy (letter characterLiteralEnd False graphicCharacter)

type StringChar :: *
data StringChar = RawChar
                | EscapeChar {-# UNPACK #-} !Char (Parsec (Maybe Char))

mkEscapeChar :: EscapeDesc -> Escape -> Parsec () -> ErrorConfig -> StringChar
mkEscapeChar !desc !esc !space !err = EscapeChar (escBegin desc) stringEsc
  where stringEsc = escapeBegin esc *> (escapeGap $> Nothing <|>
                                        escapeEmpty $> Nothing <|>
                                        Just <$> escapeCode esc)
        escapeEmpty = maybe empty (annotate (labelStringEscapeEmpty err) . char) (emptyEscape desc)
        escapeGap
          | gapsSupported desc = some (annotate (labelStringEscapeGap err) space)
                              *> annotate (labelStringEscapeGapEnd err) (escapeBegin esc)
          | otherwise = empty

mkChar :: StringChar -> ErrorConfig -> CharPredicate -> Parsec (Maybe Char)
mkChar RawChar !err = maybe empty ((<|> checkBadChar (verifiedStringBadCharsUsedInLiteral err)) . fmap Just . annotate (labelStringCharacter err) . satisfy)
mkChar (EscapeChar escBegin stringEsc) err =
  foldr (\p -> annotate (labelStringCharacter err) . (<|> checkBadChar (verifiedStringBadCharsUsedInLiteral err)) . (<|> fmap Just (annotate (labelGraphicCharacter err) (satisfy (\c -> p c && c /= escBegin)))))
        stringEsc

isRawChar :: StringChar -> Bool
isRawChar RawChar = True
isRawChar EscapeChar{} = False

ensureAscii :: ErrorConfig -> Parsec String -> Parsec String
ensureAscii !err = filterS (filterStringNonAscii err) (not . all isAscii)

ensureLatin1 :: ErrorConfig -> Parsec String -> Parsec String
ensureLatin1 !err = filterS (filterStringNonLatin1 err) (not . all isLatin1)

mkStringParsers :: Set (String, String) -> StringChar -> CharPredicate -> Bool -> ErrorConfig -> StringParsers
mkStringParsers !ends !stringChar !isGraphic !allowsAllSpace !err = TextParsers {..}
  where ascii = stringLiteral (ensureAscii err) (labelStringAscii err) (labelStringAsciiEnd err)
        latin1 = stringLiteral (ensureLatin1 err) (labelStringLatin1 err) (labelStringLatin1End err)
        unicode = stringLiteral id (labelStringUnicode err) (labelStringUnicodeEnd err)

        stringLiteral :: (Parsec String -> Parsec String)
                      -> (Bool -> Bool -> LabelWithExplainConfig)
                      -> (Bool -> Bool -> LabelConfig)
                      -> Parsec String
        stringLiteral valid openLabel closeLabel =
          choice (map (uncurry (makeStringParser valid openLabel closeLabel)) (Set.toList ends))

        makeStringParser :: (Parsec String -> Parsec String)
                         -> (Bool -> Bool -> LabelWithExplainConfig)
                         -> (Bool -> Bool -> LabelConfig)
                         -> String -> String -> Parsec String
        makeStringParser valid openLabel closeLabel begin end@(terminalInit : _) =
          let strChar = mkChar stringChar err (letter terminalInit allowsAllSpace isGraphic)
          in (annotate (openLabel allowsAllSpace (isRawChar stringChar)) (string begin) *>) . valid $
               catMaybes <$> manyTill (Just <$> char terminalInit <|> strChar)
                                      (annotate (closeLabel allowsAllSpace (isRawChar stringChar)) (atomic (string end)))
        makeStringParser _ _ _ _ [] = error "string terminals cannot be empty"

letter :: Char -> Bool -> CharPredicate -> CharPredicate
letter !terminalLead !allowsAllSpace (Just g)
  | allowsAllSpace = Just $ \c -> c /= terminalLead && (g c || isSpace c)
  | otherwise      = Just $ \c -> c /= terminalLead && g c
letter _ _ Nothing = Nothing

type Escape :: *
data Escape = Escape { escapeCode :: !(Parsec Char)
                     , escapeBegin :: !(Parsec ())
                     , escapeChar :: !(Parsec Char)
                     }

mkEscape :: EscapeDesc -> GenericNumeric -> ErrorConfig -> Escape
mkEscape EscapeDesc{..} gen !err = Escape {..}
  where
    escapeBegin = annotate (labelEscapeSequence err) $ void (char escBegin)
    escapeCode = annotate (labelEscapeEnd err) $
      escMapped <|> numericEscape
    escapeChar = escapeBegin *> escapeCode

    escs = foldr (\c -> Map.insert [c] c) mapping literals
    escMapped = trie (Map.map pure escs)

    numericEscape = decimalEsc <|> hexadecimalEsc <|> octalEsc <|> binaryEsc

    decimalEsc = fromDesc 10 decimalEscape (zeroAllowedDecimal gen notConfigured) digit
    hexadecimalEsc = fromDesc 16 hexadecimalEscape (zeroAllowedHexadecimal gen notConfigured) hexDigit
    octalEsc = fromDesc 8 octalEscape (zeroAllowedOctal gen notConfigured) octDigit
    binaryEsc = fromDesc 2 binaryEscape (zeroAllowedBinary gen notConfigured) bit

    boundedChar :: Parsec Integer -> Char -> Maybe Char -> Int -> Parsec Char
    boundedChar p maxValue prefix radix = annotate (labelEscapeNumeric err radix) $
      foldr (\c t -> char c *> annotate (labelEscapeNumericEnd err c radix) t)
            (mapMaybeS config f p)
            prefix
      where config = filterEscapeCharNumericSequenceIllegal err maxValue radix
            f c
             | c < toInteger (ord maxValue) = Just (chr (fromInteger c))
             | otherwise = Nothing

    atMost' :: Int -> Parsec Char -> Reg r Word -> Parsec Integer
    atMost' radix dig atMostR =
      -- FIXME: surely this is an inefficient mess with the translations?
      somel (\n d -> n * toInteger radix + toInteger (digitToInt d)) 0
            (guardS (gets atMostR (> 0)) *> dig <* modify atMostR pred)

    atMost :: Word -> Int -> Parsec Char -> Parsec Integer
    atMost n radix dig = make n (atMost' radix dig)

    exactly :: Word -> Word -> Int -> Parsec Char -> NonEmpty Word -> Parsec Integer
    exactly n full radix dig reqDigits = make n $ \atMostR ->
      mapMaybeS' snd (filterEscapeCharRequiresExactDigits err radix reqDigits)
                 (\(num, m) -> if m == full then Just num else Nothing)
                 (atMost' radix dig atMostR <~> gets atMostR (full -))

    oneOfExactly' :: NonEmpty Word -> Word -> Word -> [Word] -> Int -> Parsec Char -> Reg r Word -> Parsec Integer
    oneOfExactly' reqDigits digits m [] radix dig digitsParsed =
      exactly digits m radix dig reqDigits <* put digitsParsed digits
    oneOfExactly' reqDigits digits m (n:ns) radix dig digitsParsed =
      let theseDigits = exactly digits m radix dig reqDigits
          restDigits =
                atomic (Just <$> oneOfExactly' reqDigits (n - m) n ns radix dig digitsParsed
                     <* modify digitsParsed (+ digits))
            <|> put digitsParsed digits $> Nothing
          combine !x Nothing !_ = x
          -- digits is removed here, because it's been added before the get
          combine x (Just y) e = x * toInteger radix ^ (e - digits) + y
      in liftA3 combine theseDigits restDigits (get digitsParsed)

    oneOfExactly :: NonEmpty Word -> Int -> Parsec Char -> Parsec Integer
    oneOfExactly ns radix dig =
      let reqDigits@(m :| ms) = sort ns
      in unsafeMake (oneOfExactly' reqDigits m m ms radix dig)

    fromDesc :: Int -> NumericEscape -> Parsec Integer -> Parsec Char -> Parsec Char
    fromDesc !_ NumericIllegal !_ !_ = empty
    fromDesc radix NumericSupported{..} integer dig = case numDigits of
      Unbounded  -> boundedChar integer maxValue prefix radix
      AtMost n   -> boundedChar (atMost n radix dig) maxValue prefix radix
      Exactly ns -> boundedChar (oneOfExactly ns radix dig) maxValue prefix radix

lexeme :: (forall a. Parsec a -> Parsec a) -> TextParsers t -> TextParsers t
lexeme lexe TextParsers{..} = TextParsers {
    unicode = lexe unicode,
    ascii = lexe ascii,
    latin1 = lexe latin1
  }