swarm-0.7.0.0: src/swarm-lang/Swarm/Language/Parser/Lex.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE ViewPatterns #-}
-- |
-- SPDX-License-Identifier: BSD-3-Clause
--
-- Token lexing and comment preservation for the Swarm language.
module Swarm.Language.Parser.Lex (
-- * Parsing with source locations
parseLoc,
parseLocG,
-- * Whitespace + comments
getCommentSituation,
lineComment,
blockComment,
sc,
-- * Tokens
-- ** Lexemes
lexeme,
-- ** Specific token types
symbol,
operator,
reservedWords,
reservedCS,
reserved,
IdentifierType (..),
locIdentifier,
locTmVar,
locTyName,
identifier,
tyVar,
tyName,
tmVar,
textLiteral,
integer,
-- ** Combinators
braces,
parens,
brackets,
) where
import Control.Lens (use, (%=), (.=))
import Control.Monad (void)
import Data.Char (isLower, isUpper)
import Data.Containers.ListUtils (nubOrd)
import Data.List.Extra (enumerate)
import Data.Sequence qualified as Seq
import Data.Set (Set)
import Data.Set qualified as S
import Data.Text (Text)
import Data.Text qualified as T
import Swarm.Language.Parser.Core
import Swarm.Language.Syntax
import Swarm.Language.Syntax.Direction
import Swarm.Language.TDVar (TDVar, mkTDVar)
import Swarm.Language.Types (baseTyName)
import Swarm.Util (failT, squote)
import Text.Megaparsec
import Text.Megaparsec.Char
import Text.Megaparsec.Char.Lexer qualified as L
import Witch (from, into)
------------------------------------------------------------
-- Parsing with source locations
-- | Add 'SrcLoc' to a parser
parseLocG :: Parser a -> Parser (SrcLoc, a)
parseLocG pa = do
-- Remember the start location.
start <- getOffset
a <- pa
-- Instead of using 'getOffset' to get the end location, which would
-- include any whitespace consumed at the end of @pa@, get the
-- @preWSLoc@ which was set by 'sc' at the /beginning/ of the
-- consumed whitespace.
end <- use preWSLoc
pure (SrcLoc start end, a)
-- | Add 'SrcLoc' to a 'Term' parser
parseLoc :: Parser Term -> Parser Syntax
parseLoc pterm = uncurry Syntax <$> parseLocG pterm
------------------------------------------------------------
-- Whitespace
-- Approach for preserving comments taken from https://www.reddit.com/r/haskell/comments/ni4gpm/comment/gz0ipmp/
-- | If we see a comment starting now, is it the first non-whitespace
-- thing on the current line so far, or were there other
-- non-whitespace tokens previously?
getCommentSituation :: Parser CommentSituation
getCommentSituation = do
fl <- use freshLine
return $ if fl then StandaloneComment else SuffixComment
-- | Parse a line comment, while appending it out-of-band to the list of
-- comments saved in the custom state.
lineComment :: Text -> Parser ()
lineComment start = do
cs <- getCommentSituation
-- Note we must manually get the start and end offset rather than
-- using parseLocG, since parseLocG explicitly does not include the
-- source span of trailing whitespace.
s <- getOffset
t <- string start *> takeWhileP (Just "character") (/= '\n')
e <- getOffset
comments %= (Seq.|> Comment (SrcLoc s e) LineComment cs t)
-- | Parse a block comment, while appending it out-of-band to the list of
-- comments saved in the custom state.
blockComment :: Text -> Text -> Parser ()
blockComment start end = do
cs <- getCommentSituation
-- Note we must manually get the start and end offset rather than
-- using parseLocG, since parseLocG explicitly does not include the
-- source span of trailing whitespace.
s <- getOffset
void $ string start
t <- manyTill anySingle (string end)
e <- getOffset
comments %= (Seq.|> Comment (SrcLoc s e) BlockComment cs (into @Text t))
-- | Skip spaces and comments.
sc :: Parser ()
sc = do
-- Remember where we were before we started consuming whitespace.
l <- getOffset
preWSLoc .= l
-- Typically we would use L.space here, but we have to inline its
-- definition and use our own slight variant, since we need to treat
-- end-of-line specially.
skipMany . choice . map hidden $
[ hspace1
, eol *> (freshLine .= True) -- If we see a newline, reset freshLine to True.
, lineComment "//"
, blockComment "/*" "*/"
]
------------------------------------------------------------
-- Tokens
-- | In general, we follow the convention that every token parser
-- assumes no leading whitespace and consumes all trailing
-- whitespace. Concretely, we achieve this by wrapping every token
-- parser using 'lexeme'.
--
-- Also sets freshLine to False every time we see a non-whitespace
-- token.
lexeme :: Parser a -> Parser a
lexeme p = (freshLine .= False) *> L.lexeme sc p
-- | A lexeme consisting of a literal string.
symbol :: Text -> Parser Text
symbol s = (freshLine .= False) *> L.symbol sc s
-- | A lexeme consisting of a specific string, not followed by any other
-- operator character.
operator :: Text -> Parser Text
operator n = (lexeme . try) (string n <* notFollowedBy operatorChar)
-- | Recognize a single character which is one of the characters used
-- by a built-in operator.
operatorChar :: Parser Text
operatorChar = T.singleton <$> oneOf opChars
where
isOp = \case { ConstMFunc {} -> False; _ -> True } . constMeta
opChars = nubOrd . concatMap (from . syntax) . filter isOp $ map constInfo allConst
-- | Names of base types built into the language.
baseTypeNames :: [Text]
baseTypeNames = map baseTyName enumerate
-- | Names of types built into the language.
primitiveTypeNames :: [Text]
primitiveTypeNames = "Cmd" : baseTypeNames
-- | List of keywords built into the language.
keywords :: [Text]
keywords = T.words "let in def tydef end true false forall require stock requirements rec"
-- | List of reserved words that cannot be used as variable names.
reservedWords :: Set Text
reservedWords =
S.fromList $
map (syntax . constInfo) (filter isUserFunc allConst)
++ map directionSyntax allDirs
++ primitiveTypeNames
++ keywords
-- | Parse a reserved word, given a string recognizer (which can
-- /e.g./ be case sensitive or not), making sure it is not a prefix
-- of a longer variable name, and allowing the parser to backtrack
-- if it fails.
reservedGen :: (Text -> Parser a) -> Text -> Parser ()
reservedGen str w = (lexeme . try) $ str w *> notFollowedBy (alphaNumChar <|> char '_')
-- | Parse a case-sensitive reserved word.
reservedCS :: Text -> Parser ()
reservedCS = reservedGen string
-- | Parse a case-insensitive reserved word.
reserved :: Text -> Parser ()
reserved = reservedGen string'
-- | What kind of identifier are we parsing?
data IdentifierType = IDTyVar | IDTyName | IDTmVar
deriving (Eq, Ord, Show)
-- | Parse an identifier together with its source location info.
locIdentifier :: IdentifierType -> Parser LocVar
locIdentifier idTy =
uncurry LV <$> parseLocG ((lexeme . try) (p >>= check) <?> "variable name")
where
p = (:) <$> (letterChar <|> char '_') <*> many (alphaNumChar <|> char '_' <|> char '\'')
check (into @Text -> t)
| IDTyVar <- idTy
, T.toTitle t `S.member` reservedWords =
failT ["Reserved type name", squote t, "cannot be used as a type variable name; perhaps you meant", squote (T.toTitle t) <> "?"]
| IDTyName <- idTy
, t `S.member` reservedWords =
failT ["Reserved type name", squote t, "cannot be redefined."]
| t `S.member` reservedWords || T.toLower t `S.member` reservedWords =
failT ["Reserved word", squote t, "cannot be used as a variable name"]
| IDTyName <- idTy
, isLower (T.head t) =
failT ["Type synonym names must start with an uppercase letter"]
| IDTyVar <- idTy
, isUpper (T.head t) =
failT ["Type variable names must start with a lowercase letter"]
| otherwise = pure t
-- | Parse a term variable together with its source location info.
locTmVar :: Parser LocVar
locTmVar = locIdentifier IDTmVar
-- | Parse a user-defined type name together with its source location
-- info.
locTyName :: Parser (Located TDVar)
locTyName = (fmap . fmap) mkTDVar (locIdentifier IDTyName)
-- | Parse an identifier, i.e. any non-reserved string containing
-- alphanumeric characters and underscores, not starting with a
-- digit. The Bool indicates whether we are parsing a type variable.
identifier :: IdentifierType -> Parser Var
identifier = fmap lvVar . locIdentifier
-- | Parse a type variable, which must start with an underscore or
-- lowercase letter and cannot be the lowercase version of a type
-- name.
tyVar :: Parser Var
tyVar = identifier IDTyVar
-- | Parse a (user-defined) type constructor name, which must start
-- with an uppercase letter.
--
-- Note that this always produces a variable with version number 0,
-- via 'mkTDVar'. We cannot properly version variables at parsing
-- time since we don't know what else is in scope. There is a
-- separate name resolution pass later that assigns correct version
-- numbers to user type names.
tyName :: Parser TDVar
tyName = mkTDVar <$> identifier IDTyName
-- | Parse a term variable, which can start in any case and just
-- cannot be the same (case-insensitively) as a lowercase reserved
-- word.
tmVar :: Parser Var
tmVar = identifier IDTmVar
-- | Parse a text literal (including escape sequences) in double quotes.
textLiteral :: Parser Text
textLiteral = into <$> lexeme (char '"' >> manyTill L.charLiteral (char '"'))
-- | Parse a positive integer literal token, in decimal, binary,
-- octal, or hexadecimal notation. Note that negation is handled as
-- a separate operator.
integer :: Parser Integer
integer =
label "integer literal" $
lexeme $ do
n <-
string "0b"
*> L.binary
<|> string "0o"
*> L.octal
<|> string "0x"
*> L.hexadecimal
<|> L.decimal
notFollowedBy alphaNumChar
return n
------------------------------------------------------------
-- Combinators
braces :: Parser a -> Parser a
braces = between (symbol "{") (symbol "}")
parens :: Parser a -> Parser a
parens = between (symbol "(") (symbol ")")
brackets :: Parser a -> Parser a
brackets = between (symbol "[") (symbol "]")