glualint-1.24.6: src/GLua/PSLexer.hs
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
module GLua.PSLexer where
import GLua.AG.Token ( Region(..), MToken(..), Token(..) )
import Data.Char(ord)
import Text.Parsec
( SourcePos,
ParseError,
char,
anyChar,
digit,
endOfLine,
noneOf,
oneOf,
satisfy,
space,
string,
eof,
many1,
manyTill,
option,
optionMaybe,
sourceColumn,
sourceLine,
(<?>),
(<|>),
getPosition,
lookAhead,
many,
parse,
try )
import Text.Parsec.String ( Parser )
import Text.Parsec.Pos ( newPos )
import Text.ParserCombinators.UU.BasicInstances(LineColPos(..))
-- | Region start to SourcePos
rgStart2sp :: Region -> SourcePos
rgStart2sp (Region start _) = lcp2sp start
-- | Region end to SourcePos
rgEnd2sp :: Region -> SourcePos
rgEnd2sp (Region _ end) = lcp2sp end
-- | SourcePos to region
sp2Rg :: SourcePos -> Region
sp2Rg sp = Region (sp2lcp sp) (sp2lcp sp)
-- | LineColPos to SourcePos
lcp2sp :: LineColPos -> SourcePos
lcp2sp (LineColPos l c _) = newPos "source.lua" (l + 1) (c + 1)
-- | SourcePos to LineColPos
sp2lcp :: SourcePos -> LineColPos
sp2lcp pos = LineColPos (sourceLine pos - 1) (sourceColumn pos - 1) 0
-- | Get the source position
pPos :: Parser LineColPos
pPos = sp2lcp <$> getPosition
-- See luajit's src/lj_char.c and src/lj_char.h
pIdentifierCharacter :: Parser Char
pIdentifierCharacter = satisfy validChar
where
validChar :: Char -> Bool
validChar c =
cBetween c '0' '9' ||
cBetween c 'A' 'Z' ||
c == '_' ||
cBetween c 'a' 'z' ||
ord c >= 128
pNonDigitIdentifierCharacter :: Parser Char
pNonDigitIdentifierCharacter = satisfy validChar
where
validChar :: Char -> Bool
validChar c =
cBetween c 'A' 'Z' ||
c == '_' ||
cBetween c 'a' 'z' ||
ord c >= 128
cBetween :: Char -> Char -> Char -> Bool
cBetween c left right = c >= left && c <= right
-- | Parse the string until the end. Used in parseLineComment among others.
pUntilEnd :: Parser String
pUntilEnd = manyTill anyChar (lookAhead (endOfLine <|> const '\n' <$> eof))
-- | Whitespace parser that requires at least one whitespace character
parseWhitespace :: Parser String
parseWhitespace = many1 space <?> "whitespace"
-- | Whitespace parser that requires 0 or more whitespace characters
parseOptionalWhitespace :: Parser String
parseOptionalWhitespace = many space <?> "whitespace"
-- | An escaped character of a single line string
-- Takes the delimiter of the string as parameter
escapedStringChar :: Char -> Parser String
escapedStringChar delim = (\escaped -> '\\' : escaped) <$ char '\\' <*> escapedChar <|> (:[]) <$> noneOf ['\n', delim]
where
escapedChar = (:) <$> char 'z' <*> parseOptionalWhitespace <|> (:[]) <$> anyChar
-- | The start of a nested string
-- Returns the amount of =-signs at the start of the nested string
startNestedString :: Parser String
startNestedString = do
_ <- char '['
depth <- many (char '=')
_ <- char '['
return depth
-- | Parses a multiline string
-- returns the amount of =-signs in the string delimiter and the contents
nestedString :: String -> Parser String
nestedString depth = do
let endStr = "]" ++ depth ++ "]"
manyTill anyChar (try $ string endStr)
-- | Parse Lua style comments
parseLuaComment :: Parser Token
parseLuaComment = do
_ <- string "--"
-- When a nested comment is started, it must be finished
startNested <- optionMaybe (try startNestedString)
case startNested of
Nothing -> DashComment <$> pUntilEnd
Just depth -> do
contents <- nestedString depth
return $ DashBlockComment (length depth) contents
-- | Parse C-Style comments
parseCComment :: Parser Token
parseCComment = do
_ <- char '/'
try (SlashComment <$ char '/' <*> pUntilEnd) <|>
SlashBlockComment <$ char '*' <*> manyTill anyChar (try (string "*/") <|> const "\n" <$> eof)
-- | Parse any kind of comment.
parseComment :: Parser Token
parseComment = parseLuaComment <|> parseCComment <?> "Comment"
-- | Convert the result of the nestedString parser to a MultiLine string
nestedStringToMLString :: String -> String -> Token
nestedStringToMLString depth contents = MLString (showChar '[' . showString depth . showChar '[' . showString contents . showChar ']' . showString depth . showChar ']' $ "")
-- | Parse single line strings e.g. "sdf", 'werf'.
parseLineString :: Char -> Parser String
parseLineString c = concat <$ char c <*> many (escapedStringChar c) <* char c
-- | Single and multiline strings.
parseString :: Parser Token
parseString = SQString <$> parseLineString '\'' <|>
DQString <$> parseLineString '"' <|> do
depth <- startNestedString
nestedStringToMLString depth <$> nestedString depth <?> "String"
-- | Parse any kind of number.
-- Except for numbers that start with a '.'. That's handled by parseDots to solve ambiguity.
parseNumber :: Parser Token
parseNumber = TNumber <$> ((++) <$> (pHexadecimal <|> pBinary <|> pNumber) <*> (pLLULL <|> option "" parseNumberSuffix)) <?> "Number"
where
pNumber :: Parser String
pNumber = (++) <$> many1 digit <*> option "" pDecimal
pDecimal :: Parser String
pDecimal = (:) <$> char '.' <*> many digit
pHexDecimal :: Parser String
pHexDecimal = (:) <$> char '.' <*> many pHex
pHexadecimal :: Parser String
pHexadecimal = (++) <$> (try (string "0x") <|> try (string "0X")) <*> ((++) <$> many1 pHex <*> option "" pHexDecimal)
pHex :: Parser Char
pHex = digit <|> oneOf ['a', 'b', 'c', 'd', 'e', 'f', 'A', 'B', 'C', 'D', 'E', 'F']
pBinary :: Parser String
pBinary = (++) <$> (try (string "0b") <|> try (string "0B")) <*> ((++) <$> many1 pBin <*> option "" pDecimal)
pBin :: Parser Char
pBin = digit <|> oneOf ['0', '1']
-- LL/ULL suffix of a number, making it signed/unsigned int64 respectively
-- http://luajit.org/ext_ffi_api.html#literals
pLLULL :: Parser String
pLLULL = pULL <|> pLL
pLL :: Parser String
pLL = "LL" <$ oneOf ['L', 'l'] <* oneOf ['L', 'l']
pULL :: Parser String
pULL = "ULL" <$ oneOf ['U', 'u'] <* pLL
-- Parse the suffix of a number
parseNumberSuffix :: Parser String
parseNumberSuffix = imaginary <|> extension
where
imaginary = (:[]) <$> oneOf ['i', 'I']
extension = (\e s d -> e : s ++ d) <$> oneOf ['e', 'E', 'p', 'P']
<*> option "" (string "+" <|> string "-")
<*> many1 digit
-- | Parse either a keyword or an identifier that starts with that keyword
parseKeyword :: Token -> String -> Parser Token
parseKeyword tok str = try (do
_ <- string str
(\s -> Identifier (str ++ s)) <$> many1 (pIdentifierCharacter) <|>
return tok
<?> "Keyword " ++ str)
-- | Parse just an identifier.
parseIdentifier :: Parser String
parseIdentifier = (:) <$> pNonDigitIdentifierCharacter <*> many pIdentifierCharacter <?> "Identifier"
-- | Parse a label.
parseLabel :: Parser String
parseLabel = string "::" *> (optionMaybe parseWhitespace *> parseIdentifier <* optionMaybe parseWhitespace) <* string "::" <?> "Label"
-- | Parse anything to do with dots. Indexaction (.), concatenation (..), varargs (...) or numbers that start with a dot
parseDots :: Parser Token
parseDots = do
_ <- char '.' -- first .
try (do
_ <- char '.' -- second .
try (VarArg <$ char '.') <|> -- third .
return Concatenate) <|>
-- try to parse a number that starts with a .
try (do
nums <- many1 digit
suffix <- option "" parseNumberSuffix
return $ TNumber ('.' : (nums ++ suffix))
) <|>
return Dot
-- | Parse any kind of token.
parseToken :: Parser Token
parseToken =
Whitespace <$> parseWhitespace <|>
-- Constants
try 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 <$ char ';' <|>
parseDots <|>
try parseComment <|>
-- Operators
Plus <$ string "+" <|>
Minus <$ string "-" <|>
Multiply <$ string "*" <|>
Divide <$ string "/" <|>
Modulus <$ string "%" <|>
Power <$ string "^" <|>
TEq <$ try (string "==") <|>
Equals <$ string "=" <|>
TNEq <$ string "~=" <|>
TCNEq <$ try (string "!=") <|>
CNot <$ string "!" <|>
TLEQ <$ try (string "<=") <|>
TLT <$ string "<" <|>
TGEQ <$ try (string ">=") <|>
TGT <$ string ">" <|>
Label <$> try parseLabel <|>
Colon <$ string ":" <|>
Comma <$ string "," <|>
Hash <$ string "#" <|>
CAnd <$ string "&&" <|>
COr <$ string "||" <|>
LRound <$ char '(' <|>
RRound <$ char ')' <|>
LCurly <$ char '{' <|>
RCurly <$ char '}' <|>
LSquare <$ char '[' <|>
RSquare <$ char ']'
-- | A thing of which the region is to be parsed
annotated :: (Region -> a -> b) -> Parser a -> Parser b
annotated f p = (\s t e -> f (Region s e) t) <$> pPos <*> p <*> pPos
-- | parse located MToken
parseMToken :: Parser MToken
parseMToken =
do
start <- pPos
tok <- parseToken
end <- pPos
return $ MToken (Region start end) tok
-- | Parse a list of tokens and turn them into MTokens.
parseTokens :: Parser [MToken]
parseTokens = many parseMToken
-- | Parse the potential #!comment on the first line
-- Lua ignores the first line if it starts with #
parseHashBang :: Parser String
parseHashBang = option "" (char '#' *> pUntilEnd)
-- | Parse a string into MTokens.
execParseTokens :: String -> Either ParseError [MToken]
execParseTokens = parse (parseHashBang *> parseTokens <* eof) "input"