glualint-1.26.0: src/GLua/Lexer.hs
{-# LANGUAGE CPP #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE Rank2Types #-}
{-# LANGUAGE NoMonomorphismRestriction #-}
-- | Lex GLua into MTokens
module GLua.Lexer where
import GLua.AG.Token (MToken (..), Region (..), Token (..))
import Data.Char (ord)
import Text.ParserCombinators.UU (
ExtAlternative (opt, (<<|>)),
P,
micro,
pEnd,
pErrors,
pMany,
pPos,
pReturn,
pSome,
parse,
(<$$>),
(<**>),
)
import Text.ParserCombinators.UU.BasicInstances (
Error,
Insertion (Insertion),
LineColPos (..),
Str,
createStr,
pMunch,
pSatisfy,
pSym,
pToken,
)
import Text.ParserCombinators.UU.Utils (pDigit)
-- | 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 "--"
<**> (const <$> (parseBlockComment <<|> DashComment <$> pUntilEnd)) -- Dash block comment and dash comment both start with "--"
<<|> 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 '\\'
<**> ((\c' str esc -> esc : c' ++ str) <$> escapeSequence <*> innerString) -- Escaped character in string always starts with backslash
<<|> const ""
<$> pSym c
<<|> (:)
<$> pNoNewline
<*> innerString -- the end of the string
-- 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 Token
parseLabel =
Label
<$ pToken "::"
<*> parseOptionalWhitespace
<*> parseIdentifier
<*> parseOptionalWhitespace
<* 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 ">"
<<|> parseLabel
<<|> Colon
<$ pToken ":"
<<|> Comma
<$ pToken ","
<<|> Hash
<$ pToken "#"
`micro` 10
<<|> CAnd -- Add micro cost to prevent conflict with parseHashBang
<$ 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)