packages feed

glualint-1.26.0: src/GLua/PSLexer.hs

{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}

module GLua.PSLexer where

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

import Data.Char (ord)
import Text.Parsec (
  ParseError,
  SourcePos,
  anyChar,
  char,
  digit,
  endOfLine,
  eof,
  getPosition,
  lookAhead,
  many,
  many1,
  manyTill,
  noneOf,
  oneOf,
  option,
  optionMaybe,
  parse,
  satisfy,
  sourceColumn,
  sourceLine,
  space,
  string,
  try,
  (<?>),
  (<|>),
 )
import Text.Parsec.Pos (newPos)
import Text.Parsec.String (Parser)
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 Token
parseLabel =
  Label
    <$ string "::"
    <*> parseOptionalWhitespace
    <*> parseIdentifier
    <*> parseOptionalWhitespace
    <* 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 '.')
          <|> return Concatenate -- third .
    )
    <|>
    -- 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 ">"
    <|> 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"