packages feed

glualint-1.24.6: src/GLua/Lexer.hs

{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE Rank2Types #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE NoMonomorphismRestriction #-}
{-# LANGUAGE CPP #-}

-- | Lex GLua into MTokens
module GLua.Lexer where

import GLua.AG.Token ( Region(..), MToken(..), Token(..) )

import Data.Char (ord)
import Text.ParserCombinators.UU
    ( P,
      pEnd,
      pErrors,
      (<**>),
      micro,
      pPos,
      parse,
      (<$$>),
      pMany,
      pMaybe,
      pReturn,
      pSome,
      ExtAlternative(opt, (<<|>)) )
import Text.ParserCombinators.UU.Utils ( pDigit )
import Text.ParserCombinators.UU.BasicInstances
    ( Error,
      LineColPos(..),
      Str,
      createStr,
      pMunch,
      pSatisfy,
      pSym,
      pToken,
      Insertion(Insertion) )

-- | String parser that maintains positions.
type LParser a = P (Str Char String LineColPos) a

-- | Whitespace parser that requires at least one whitespace character
parseWhitespace :: LParser String
parseWhitespace = pSome $ pSatisfy (`elem` " \r\n\t") (Insertion "Whitespace" ' ' 5)

-- | Whitespace parser that requires 0 or more whitespace characters
parseOptionalWhitespace :: LParser String
parseOptionalWhitespace = pMany $ pSatisfy (`elem` " \r\n\t") (Insertion "Whitespace" ' ' 5)

-- | Blanco parser. Parses anything. Used in parsing comments.
parseAnyChar :: LParser Char
parseAnyChar = pSatisfy (const True) (Insertion "Any character" 'y' 5)

-- See luajit's src/lj_char.c and src/lj_char.h
pIdentifierCharacter :: LParser Char
pIdentifierCharacter = pSatisfy validChar (Insertion "Identifying character (letter, number, emoji)" 'a' 5)
    where
        validChar :: Char -> Bool
        validChar c =
            between c '0' '9' ||
            between c 'A' 'Z' ||
            c == '_'          ||
            between c 'a' 'z' ||
            ord c >= 128


pNonDigitIdentifierCharacter :: LParser Char
pNonDigitIdentifierCharacter = pSatisfy validChar (Insertion "Identifying character (letter, emoji)" 'a' 5)
    where
        validChar :: Char -> Bool
        validChar c =
            between c 'A' 'Z' ||
            c == '_'          ||
            between c 'a' 'z' ||
            ord c >= 128

between :: Char -> Char -> Char -> Bool
between c left right = c >= left && c <= right

-- | Parses a C-style block comment.
parseCBlockComment :: LParser String
parseCBlockComment = const "" <$> pToken "*/" <<|>
            (:) <$> parseAnyChar <*> parseCBlockComment

-- | Try to parse a block comment.
-- Might actually return a single line Dash comment, because for example
-- the following line is a line comment, rather than a block comment
--[===== <- missing the last '[' bracket.
parseBlockComment :: LParser Token
parseBlockComment = pToken "[" *> nested 0
    where
        -- The amount of =-signs in the string delimiter is n
        nested :: Int -> LParser Token
        nested n = pToken "=" *> nested (n + 1) <<|>
                   DashBlockComment n <$ pToken "[" <*> restString n <<|>
                   lineComment n <$> pUntilEnd

        -- Turns out we were describing a line comment all along, cheeky bastard!
        -- (the last [ of the block comment start token is missing)
        lineComment :: Int -> String -> Token
        lineComment n str = DashComment $ '[' : replicate n '=' ++ str

        -- Right-recursive grammar. This part searches for the rest of the string until it finds the ]=^n] token
        restString :: Int -> LParser String
        restString n = const "" <$> pToken ("]" ++ replicate n '=' ++ "]") <<|>
                       (:) <$> parseAnyChar <*> restString n

-- | Parse the string until the end. Used in parseLineComment among others.
pUntilEnd :: LParser String
pUntilEnd = pMunch (\c -> c /= '\n' && c /= '\r')

-- | A comment that spans until the end of the line.
parseLineComment :: String -> LParser String
parseLineComment prefix = flip const <$> pToken prefix <*> pUntilEnd

-- | Parses a multiline string except for its first character (e.g. =[ string ]=])
-- This is because the first [ could also just be parsed as a square bracket.
nestedString :: LParser String
nestedString = nested 0
    where
        -- The amount of =-signs in the string delimiter is n
        nested :: Int -> LParser String
        nested n = (\str -> "=" ++ str) <$ pToken "=" <*> nested (n + 1) <<|>
                   ('[' :) <$ pToken "[" <*> restString n

        -- Right-recursive grammar. This part searches for the rest of the string until it finds the ]=^n] token
        restString :: Int -> LParser String
        restString n = pToken ("]" ++ replicate n '=' ++ "]") <<|>
                       (:) <$> parseAnyChar <*> restString n

-- | Parse any kind of comment.
parseComment :: LParser Token
parseComment =  pToken "--" <**> -- Dash block comment and dash comment both start with "--"
                    (const <$> (parseBlockComment <<|> DashComment <$> pUntilEnd)) <<|>
                pToken "/" <**>
                    (const <$>
                        (SlashBlockComment <$ pToken "*" <*> parseCBlockComment <<|>
                        SlashComment <$> parseLineComment "/" <<|>
                        pReturn Divide -- The /-sign is here because it also starts with '/'
                        ))

-- | Parse single line strings e.g. "sdf", 'werf'.
parseLineString :: Char -> LParser String
parseLineString c = pSym c *> innerString
    where
        innerString :: LParser String
        innerString = pSym '\\' <**> -- Escaped character in string always starts with backslash
                         ((\c' str esc -> esc : c' ++ str) <$> escapeSequence <*> innerString) <<|>
                      const "" <$> pSym c <<|> -- the end of the string
                      (:) <$> pNoNewline <*> innerString -- the next character in the string

        escapeSequence :: LParser String
        escapeSequence = (:) <$> pSym 'z' <*> parseOptionalWhitespace <<|> (:[]) <$> parseAnyChar

        pNoNewline :: LParser Char
        pNoNewline = pSatisfy (/= '\n') (Insertion "Anything but a newline" c 5)


-- | Single and multiline strings.
parseString :: LParser Token
parseString = DQString <$> parseLineString '"' <<|>
              SQString <$> parseLineString '\'' <<|>
              -- Parse either a multiline string or just a bracket.
              -- Placed here because they have the first token '[' in common
              pSym '[' <**> ((\_ -> MLString . (:) '[') <$$> nestedString <<|>
                        const <$> pReturn LSquare)

-- | Parse any kind of number.
parseNumber :: LParser Token
parseNumber = TNumber <$> ((++) <$> (pZeroPrefixedNumber <<|> pNumber) <*> (pLLULL <<|> opt parseNumberSuffix ""))
    where
        -- Numbers starting with 0 can be regular numbers, hexadecimals or binary
        pZeroPrefixedNumber :: LParser String
        pZeroPrefixedNumber =
            pSym '0' <**> (
                (\hex _0 -> _0 : hex) <$> pHexadecimal <<|>
                (\bin _0 -> _0 : bin) <$> pBinary <<|>
                (\digits _0 -> _0 : digits) <$> (pDecimal <<|> pNumber) <<|>
                pure (:[])
            )

        pNumber :: LParser String
        pNumber = (++) <$> pSome pDigit <*> opt pDecimal ""

        pDecimal :: LParser String
        pDecimal = (:) <$> pSym '.' <*> pMany pDigit

        pHexDecimal :: LParser String
        pHexDecimal = (:) <$> pSym '.' <*> pMany pHex

        pHexadecimal :: LParser String
        pHexadecimal = (:) <$> (pSym 'x' <<|> pSym 'X') <*> ((++) <$> pSome pHex <*> opt pHexDecimal "")

        pBinary :: LParser String
        pBinary = (:) <$> (pSym 'b' <<|> pSym 'B') <*> ((++) <$> pSome pBin <*> opt pDecimal "")

        pHex :: LParser Char
        pHex = pDigit <<|> pSym 'a' <<|> pSym 'b' <<|> pSym 'c' <<|> pSym 'd' <<|> pSym 'e' <<|> pSym 'f'
                      <<|> pSym 'A' <<|> pSym 'B' <<|> pSym 'C' <<|> pSym 'D' <<|> pSym 'E' <<|> pSym 'F'

        pBin :: LParser Char
        pBin = pSym '0' <<|> pSym '1'

        -- LL/ULL suffix of a number, making it signed/unsigned int64 respectively
        -- http://luajit.org/ext_ffi_api.html#literals
        pLLULL :: LParser String
        pLLULL = pULL <<|> pLL

        pLL :: LParser String
        pLL = "LL" <$ (pSym 'L' <<|> pSym 'l') <* (pSym 'L' <<|> pSym 'l')

        pULL :: LParser String
        pULL = "ULL" <$ (pSym 'U' <<|> pSym 'u') <* pLL

-- Parse the suffix of a number
parseNumberSuffix :: LParser String
parseNumberSuffix = imaginary <<|> extension
    where
        imaginary = (:[]) <$> (pSym 'i' <<|> pSym 'I')

        extension = (\e s d -> e : s ++ d) <$> (pSym 'e' <<|> pSym 'E' <<|> pSym 'p' <<|> pSym 'P')
            <*> opt (pToken "+" <<|> pToken "-") ""
            <*> pSome pDigit

-- | Parse a keyword. Note: It must really a key/word/! This parser makes sure to return an identifier when
-- it's actually an identifier that starts with that keyword.
parseKeyword :: Token -> String -> LParser Token
parseKeyword tok word = pToken word <**>
                            ((\k -> Identifier . (++) k) <$$> pSome pIdentifierCharacter <<|> const <$> pReturn tok)

-- | Parse just an identifier.
parseIdentifier :: LParser String
parseIdentifier = (:) <$> pNonDigitIdentifierCharacter <*> pMany pIdentifierCharacter

-- | Parse a label.
parseLabel :: LParser String
parseLabel = pToken "::" *> (pMaybe parseWhitespace *> parseIdentifier <* pMaybe parseWhitespace) <* pToken "::"

-- | Parse anything to do with dots. Indexaction (.), concatenation (..) or varargs (...)
parseDots :: LParser Token
parseDots = pToken "." <**> ( -- A dot means it's either a VarArg (...), concatenation (..) or just a dot (.)
                const <$> (pToken "." <**> (
                    const VarArg <$ pToken "." <<|>
                    const <$> pReturn Concatenate
                )) <<|>
                (\ds sfx dot -> TNumber $ dot ++ ds ++ sfx) <$> pSome pDigit <*> opt parseNumberSuffix "" <<|>
                const <$> pReturn Dot
                )

-- | Parse any kind of token.
parseToken :: LParser Token
parseToken =    parseComment                                 <<|>

                -- Constants
                parseString                                  <<|>
                parseNumber                                  <<|>
                parseKeyword TTrue "true"                    <<|>
                parseKeyword TFalse "false"                  <<|>
                parseKeyword Nil "nil"                       <<|>
                parseKeyword Not "not"                       <<|>
                parseKeyword And "and"                       <<|>
                parseKeyword Or "or"                         <<|>
                parseKeyword Function "function"             <<|>
                parseKeyword Local "local"                   <<|>
                parseKeyword If "if"                         <<|>
                parseKeyword Then "then"                     <<|>
                parseKeyword Elseif "elseif"                 <<|>
                parseKeyword Else "else"                     <<|>
                parseKeyword For "for"                       <<|>
                parseKeyword In "in"                         <<|>
                parseKeyword Do "do"                         <<|>
                parseKeyword While "while"                   <<|>
                parseKeyword Until "until"                   <<|>
                parseKeyword Repeat "repeat"                 <<|>
                parseKeyword Continue "continue"             <<|>
                parseKeyword Break "break"                   <<|>
                parseKeyword Return "return"                 <<|>
                parseKeyword End "end"                       <<|>

                Identifier <$> parseIdentifier               <<|>

                Semicolon <$ pToken ";"                      <<|>
                parseDots                                    <<|>

                -- Operators
                Plus <$ pToken "+"                           <<|>
                Minus <$ pToken "-"                          <<|>
                Multiply <$ pToken "*"                       <<|>
                Modulus <$ pToken "%"                        <<|>
                Power <$ pToken "^"                          <<|>
                TEq <$ pToken "=="                           <<|>
                Equals <$ pToken "="                         <<|>
                TNEq <$ pToken "~="                          <<|>
                TCNEq <$ pToken "!="                         <<|>
                CNot <$ pToken "!"                           <<|>
                TLEQ <$ pToken "<="                          <<|>
                TLT <$ pToken "<"                            <<|>
                TGEQ <$ pToken ">="                          <<|>
                TGT <$ pToken ">"                            <<|>
                Label <$> parseLabel                         <<|>
                Colon <$ pToken ":"                          <<|>
                Comma <$ pToken ","                          <<|>
                Hash <$ pToken "#"  `micro` 10               <<|> -- Add micro cost to prevent conflict with parseHashBang
                CAnd <$ pToken "&&"                          <<|>
                COr <$ pToken "||"                           <<|>

                LRound <$ pToken "("                         <<|>
                RRound <$ pToken ")"                         <<|>
                LCurly <$ pToken "{"                         <<|>
                RCurly <$ pToken "}"                         <<|>
                -- Other square bracket is parsed in parseString
                RSquare <$ pToken "]"                        <<|>
                Whitespace <$> parseWhitespace


-- | A thing of which the region is to be parsed
annotated :: (Region -> a -> b) -> LParser a -> LParser b
annotated f p = (\s t e -> f (Region s e) t) <$> pPos <*> p <*> pPos

-- | parse located MToken
parseMToken :: LParser MToken
parseMToken = annotated MToken parseToken

-- | Parse a list of tokens and turn them into MTokens.
parseTokens :: LParser [MToken]
parseTokens = pMany parseMToken

-- | Parse the potential #!comment on the first line
-- Lua ignores the first line if it starts with #
parseHashBang :: LParser String
parseHashBang = opt (pToken "#" <* pUntilEnd) ""

-- | Lex a string with a given lexer
lexFromString :: LParser a -> String -> (a, [Error LineColPos])
lexFromString p = parse ((,) <$> p <*> pErrors <* pEnd) . createStr (LineColPos 0 0 0)

-- | Parse a string into MTokens. Also returns parse errors.
execParseTokens :: String -> ([MToken], [Error LineColPos])
execParseTokens = parse ((,) <$ parseHashBang <*> parseTokens <*> pErrors <* pEnd) . createStr (LineColPos 0 0 0)