nirum-0.4.2: src/Nirum/Parser.hs
{-# LANGUAGE ScopedTypeVariables #-}
{-# OPTIONS_GHC -fno-warn-unused-do-bind #-}
module Nirum.Parser ( Parser
, ParseError
, aliasTypeDeclaration
, annotation
, annotationSet
, docs
, enumTypeDeclaration
, file
, handleNameDuplication
, handleNameDuplicationError
, identifier
, importName
, imports
, listModifier
, mapModifier
, method
, module'
, modulePath
, name
, optionModifier
, parse
, parseFile
, recordTypeDeclaration
, serviceDeclaration
, setModifier
, typeDeclaration
, typeExpression
, typeExpressionWithoutOptionModifier
, typeIdentifier
, unboxedTypeDeclaration
, unionTypeDeclaration
) where
import Control.Monad (unless, void, when)
import Data.Void
import qualified System.IO as SIO
import qualified Data.List as L
import Data.Map.Strict as Map hiding (foldl, toList)
import Data.Set hiding (foldl)
import qualified Data.Text as T
import qualified Data.Text.IO as TIO
import Text.Megaparsec hiding (ParseError, parse)
import Text.Megaparsec.Char ( char
, digitChar
, eol
, noneOf
, spaceChar
, string
, string'
)
import qualified Text.Megaparsec.Error as E
import Text.Megaparsec.Char.Lexer (charLiteral)
import Text.Read hiding (choice)
import qualified Nirum.Constructs.Annotation as A
import Nirum.Constructs.Annotation.Internal hiding ( Text
, annotations
, name
)
import qualified Nirum.Constructs.Annotation.Internal as AI
import Nirum.Constructs.Declaration (Declaration)
import qualified Nirum.Constructs.Declaration as D
import Nirum.Constructs.Docs (Docs (Docs))
import Nirum.Constructs.DeclarationSet as DeclarationSet hiding (toList)
import Nirum.Constructs.Identifier ( Identifier
, identifierRule
, reservedKeywords
, toString
)
import Nirum.Constructs.Module (Module (Module))
import Nirum.Constructs.ModulePath (ModulePath (ModulePath, ModuleName))
import Nirum.Constructs.Name (Name (Name, facialName))
import Nirum.Constructs.Service ( Method (Method)
, Parameter (Parameter)
, Service (Service)
)
import Nirum.Constructs.TypeDeclaration as TD hiding ( fields
, modulePath
, importName
)
import Nirum.Constructs.TypeExpression ( TypeExpression ( ListModifier
, MapModifier
, OptionModifier
, SetModifier
, TypeIdentifier
)
)
type Parser = Parsec Void T.Text
type ParseError = E.ParseError Char Void
-- | State-tracking 'many'.
many' :: [a] -> ([a] -> Parser a) -> Parser [a]
many' i p = do
r <- optional (p i)
case r of
Nothing -> return i
Just v -> many' (i ++ [v]) p
-- FIXME: i ++ [v] is not efficient
-- | Get the facial name of a declaration.
declFacialName :: Declaration d => d -> Identifier
declFacialName = facialName . D.name
-- CHECK: If a new reserved keyword is introduced, it has to be also
-- added to `reservedKeywords` set in the `Nirum.Constructs.Identifier`
-- module.
comment :: Parser ()
comment = string "//" >> void (many $ noneOf ("\n" :: String)) <?> "comment"
spaces :: Parser ()
spaces = skipMany $ void spaceChar <|> comment
spaces1 :: Parser ()
spaces1 = skipSome $ void spaceChar <|> comment
identifier :: Parser Identifier
identifier =
quotedIdentifier <|> bareIdentifier <?> "identifier"
where
bareIdentifier :: Parser Identifier
bareIdentifier = try $ do
ident <- lookAhead identifierRule
if ident `Data.Set.member` reservedKeywords
then fail $ "\"" ++ toString ident ++ "\" is a reserved keyword; "
++ "wrap it with backquotes to use it as a normal "
++ "identifier (i.e. \"`" ++ toString ident ++ "`\")"
else identifierRule
quotedIdentifier :: Parser Identifier
quotedIdentifier = do
char '`'
identifier' <- identifierRule
char '`'
return identifier'
name :: Parser Name
name = do
facialName' <- identifier <?> "facial name"
behindName <- option facialName' $ try $ do
spaces
char '/'
spaces
identifier <?> "behind name"
return $ Name facialName' behindName
uniqueIdentifier :: [Identifier] -> String -> Parser Identifier
uniqueIdentifier forwardNames label' = try $ do
ident <- lookAhead identP
when (ident `elem` forwardNames)
(fail $ "the " ++ label' ++ " `" ++ toString ident ++
"` is duplicated")
identP
where
identP :: Parser Identifier
identP = identifier <?> label'
uniqueName :: [Identifier] -> String -> Parser Name
uniqueName forwardNames label' = try $ do
Name fName _ <- lookAhead nameP
when (fName `elem` forwardNames)
(fail $ "the " ++ label' ++ " `" ++ toString fName ++
"` is duplicated")
nameP
where
nameP :: Parser Name
nameP = name <?> label'
integer :: Parser Integer
integer = do
v <- many digitChar
case readMaybe v of
Just i -> return i
Nothing -> fail "digit expected." -- never happened
annotationArgumentValue :: Parser AnnotationArgument
annotationArgumentValue = do
startQuote <- optional $ try $ char '"'
case startQuote of
Just _ -> do
v <- manyTill charLiteral (char '"')
return $ AI.Text $ T.pack v
Nothing ->
Integer <$> integer
annotationArgument :: Parser (Identifier, AnnotationArgument)
annotationArgument = do
arg <- identifier <?> "annotation parameter"
spaces
char '='
spaces
value <- annotationArgumentValue <?> "annotation argument value"
return (arg, value)
annotation :: Parser A.Annotation
annotation = do
char '@'
spaces
name' <- identifier
spaces
args' <- option Map.empty $ do
char '('
spaces
args <- (`sepEndBy` char ',') $ do
spaces
a <- annotationArgument
spaces
return a
spaces
char ')'
return $ Map.fromList args
return $ A.Annotation name' args'
annotationSet :: Parser A.AnnotationSet
annotationSet = do
annotations <- many $ do
spaces
a <- annotation
spaces
return a
case A.fromList annotations of
Right annotations' -> return annotations'
Left (A.AnnotationNameDuplication _) -> fail "annotation name duplicate"
typeExpression :: Parser TypeExpression
typeExpression =
try optionModifier <|> typeExpressionWithoutOptionModifier
<?> "type expression"
typeExpressionWithoutOptionModifier :: Parser TypeExpression
typeExpressionWithoutOptionModifier =
try setModifier <|> listModifier <|> mapModifier <|> typeIdentifier
typeIdentifier :: Parser TypeExpression
typeIdentifier = do
typeIdentifier' <- identifier <?> "type identifier"
return $ TypeIdentifier typeIdentifier'
optionModifier :: Parser TypeExpression
optionModifier = do
expr <- typeExpressionWithoutOptionModifier
spaces
char '?'
return $ OptionModifier expr
setModifier :: Parser TypeExpression
setModifier = do
char '{'
spaces
expr <- typeExpression <?> "element type of set type"
spaces
char '}'
return $ SetModifier expr
listModifier :: Parser TypeExpression
listModifier = do
char '['
spaces
expr <- typeExpression <?> "element type of list type"
spaces
char ']'
return $ ListModifier expr
mapModifier :: Parser TypeExpression
mapModifier = do
char '{'
spaces
key <- typeExpression <?> "key type of map type"
spaces
char ':'
spaces
value <- typeExpression <?> "value type of map type"
spaces
char '}'
return $ MapModifier key value
docs :: Parser Docs
docs = do
comments <- sepEndBy1 (do
char '#'
void $ optional $ char ' '
line <- many $ noneOf ("\r\n" :: String)
return $ T.pack line
) (eol >> spaces) <?> "comments"
return $ Docs $ T.unlines comments
annotationsWithDocs :: Monad m
=> A.AnnotationSet
-> Maybe Docs
-> m A.AnnotationSet
annotationsWithDocs set' (Just docs') = A.insertDocs docs' set'
annotationsWithDocs set' Nothing = return set'
aliasTypeDeclaration :: [Identifier] -> Parser TypeDeclaration
aliasTypeDeclaration forwardNames = do
annotationSet' <- annotationSet <?> "type alias annotations"
string' "type" <?> "type alias keyword"
spaces
typeName <- uniqueIdentifier forwardNames "alias type name"
let name' = Name typeName typeName
spaces
char '='
spaces
canonicalType' <- typeExpression <?> "canonical type of alias"
spaces
char ';'
docs' <- optional $ try $ spaces >> (docs <?> "type alias docs")
annotationSet'' <- annotationsWithDocs annotationSet' docs'
return $ TypeDeclaration name' (Alias canonicalType') annotationSet''
unboxedTypeDeclaration :: [Identifier] -> Parser TypeDeclaration
unboxedTypeDeclaration forwardNames = do
annotationSet' <- annotationSet <?> "unboxed type annotations"
string' "unboxed" <?> "unboxed type keyword"
spaces
typeName <- uniqueIdentifier forwardNames "unboxed type name"
let name' = Name typeName typeName
spaces
char '('
spaces
innerType' <- typeExpression <?> "inner type of unboxed type"
spaces
char ')'
spaces
char ';'
docs' <- optional $ try $ spaces >> (docs <?> "unboxed type docs")
annotationSet'' <- annotationsWithDocs annotationSet' docs'
return $ TypeDeclaration name' (UnboxedType innerType') annotationSet''
enumMember :: [Identifier] -> Parser EnumMember
enumMember forwardNames = do
annotationSet' <- annotationSet <?> "enum member annotations"
spaces
memberName <- uniqueName forwardNames "enum member name"
spaces
docs' <- optional $ do
d <- docs <?> "enum member docs"
spaces
return d
annotationSet'' <- annotationsWithDocs annotationSet' docs'
return $ EnumMember memberName annotationSet''
handleNameDuplication :: Declaration a
=> String
-> [a]
-> (DeclarationSet a -> Parser b)
-> Parser b
handleNameDuplication label' declarations cont = do
set <- handleNameDuplicationError label' $
DeclarationSet.fromList declarations
cont set
handleNameDuplicationError :: String -> Either NameDuplication a -> Parser a
handleNameDuplicationError _ (Right v) = return v
handleNameDuplicationError label' (Left dup) =
fail ("the " ++ nameType ++ " " ++ label' ++ " name `" ++
toString name' ++ "` is duplicated")
where
(nameType, name') = case dup of
BehindNameDuplication (Name _ bname) -> ("behind", bname)
FacialNameDuplication (Name fname _) -> ("facial", fname)
enumTypeDeclaration :: [Identifier] -> Parser TypeDeclaration
enumTypeDeclaration forwardNames = do
annotationSet' <- annotationSet <?> "enum type annotations"
string "enum" <?> "enum keyword"
spaces
typeName@(Name typeFName _) <- uniqueName forwardNames "enum type name"
spaces
frontDocs <- optional $ do
d <- docs <?> "enum type docs"
spaces
return d
char '='
spaces
docs' <- case frontDocs of
d@(Just _) -> return d
Nothing -> optional $ do
d <- docs <?> "enum type docs"
spaces
return d
annotationSet'' <- annotationsWithDocs annotationSet' docs'
members' <- sepBy1
(enumMember (typeFName : forwardNames))
(spaces >> char '|' >> spaces)
<?> "enum members"
case DeclarationSet.fromList members' of
Left (BehindNameDuplication (Name _ bname)) ->
fail ("the behind member name `" ++ toString bname ++
"` is duplicated")
Left (FacialNameDuplication (Name fname _)) ->
fail ("the facial member name `" ++ toString fname ++
"` is duplicated")
Right memberSet -> do
spaces
char ';'
return $ TypeDeclaration typeName (EnumType memberSet)
annotationSet''
fieldsOrParameters :: forall a . (String, String)
-> (Name -> TypeExpression -> A.AnnotationSet -> a)
-> Parser [a]
fieldsOrParameters (label', pluralLabel) make = do
annotationSet' <- annotationSet <?> (label' ++ " annotations")
spaces
typeExpr <- typeExpression <?> (label' ++ " type")
spaces1
name' <- name <?> (label' ++ " name")
spaces
let makeWithDocs = make name' typeExpr . A.union annotationSet'
. annotationsFromDocs
followedByComma makeWithDocs <|> do
d <- optional docs' <?> (label' ++ " docs")
return [makeWithDocs d]
where
recur :: Parser [a]
recur = fieldsOrParameters (label', pluralLabel) make
followedByComma :: (Maybe Docs -> a) -> Parser [a]
followedByComma makeWithDocs = do
char ','
spaces
d <- optional docs' <?> (label' ++ " docs")
rest <- option [] recur <?> ("rest of " ++ pluralLabel)
return $ makeWithDocs d : rest
docs' :: Parser Docs
docs' = do
d <- docs <?> (label' ++ " docs")
spaces
return d
annotationsFromDocs :: Maybe Docs -> A.AnnotationSet
annotationsFromDocs Nothing = A.empty
annotationsFromDocs (Just d) = A.singleton $ A.docs d
fields :: Parser [Field]
fields = fieldsOrParameters ("label", "labels") Field
fieldSet :: Parser (DeclarationSet Field)
fieldSet = do
fields' <- fields <?> "fields"
handleNameDuplication "field" fields' return
recordTypeDeclaration :: [Identifier] -> Parser TypeDeclaration
recordTypeDeclaration forwardNames = do
annotationSet' <- annotationSet <?> "record type annotations"
string "record" <?> "record keyword"
spaces
typeName <- uniqueName forwardNames "record type name"
spaces
char '('
spaces
docs' <- optional $ do
d <- docs <?> "record type docs"
spaces
return d
fields' <- fieldSet <?> "record fields"
spaces
char ')'
spaces
char ';'
annotationSet'' <- annotationsWithDocs annotationSet' docs'
return $ TypeDeclaration typeName (RecordType fields') annotationSet''
tag :: [Identifier] -> Parser (Tag, Bool)
tag forwardNames = do
annotationSet' <- annotationSet <?> "union tag annotations"
spaces
default' <- optional (string "default" <?> "default tag")
spaces
tagName' <- uniqueName forwardNames "union tag name"
spaces
paren <- optional $ char '('
spaces
frontDocs <- optional $ do
d <- docs <?> "union tag docs"
spaces
return d
fields' <- case paren of
Just _ -> do
spaces
f <- fieldSet <?> "union tag fields"
spaces
char ')'
return f
Nothing -> return DeclarationSet.empty
spaces
docs' <- case frontDocs of
d@(Just _) -> return d
Nothing -> optional $ do
d <- docs <?> "union tag docs"
spaces
return d
annotationSet'' <- annotationsWithDocs annotationSet' docs'
return ( Tag tagName' fields' annotationSet''
, case default' of
Just _ -> True
Nothing -> False
)
unionTypeDeclaration :: [Identifier] -> Parser TypeDeclaration
unionTypeDeclaration forwardNames = do
annotationSet' <- annotationSet <?> "union type annotations"
string "union" <?> "union keyword"
spaces
typeName <- uniqueName forwardNames "union type name"
spaces
docs' <- optional $ do
d <- docs <?> "union type docs"
spaces
return d
char '='
spaces
tags' <- sepBy1
(tag forwardNames)
(try (spaces >> char '|' >> spaces))
<?> "union tags"
let tags'' = [t | (t, _) <- tags']
let defaultTag' = do
(t''', _) <- L.find snd tags'
return t'''
spaces
char ';'
annotationSet'' <- annotationsWithDocs annotationSet' docs'
if length (L.filter snd tags') > 1
then fail "A union type cannot have more than a default tag."
else do
ut <- handleNameDuplicationError "tag" $
unionType tags'' defaultTag'
return $ TypeDeclaration typeName ut annotationSet''
typeDeclaration :: [Identifier] -> Parser TypeDeclaration
typeDeclaration forwardNames = do
-- Preconsume the common prefix (annotations) to disambiguate
-- the continued branches of parsers.
spaces
annotationSet' <- annotationSet <?> "type annotations"
spaces
typeDecl <- choice
[ unless' ["union", "record", "enum", "unboxed"]
(aliasTypeDeclaration forwardNames)
, unless' ["union", "record", "enum"]
(unboxedTypeDeclaration forwardNames)
, unless' ["union", "record"]
(enumTypeDeclaration forwardNames)
, unless' ["union"]
(recordTypeDeclaration forwardNames)
, unionTypeDeclaration forwardNames
] <?> "type declaration (e.g. enum, record, unboxed, union)"
-- In theory, though it preconsumes annotationSet' before parsing typeDecl
-- so that typeDecl itself has no annotations, to prepare for an
-- unlikely situation (that I bet it'll never happen)
-- unite the preconsumed annotationSet' with typeDecl's annotations
-- (that must be empty).
let annotations = A.union annotationSet' $ typeAnnotations typeDecl
return $ typeDecl { typeAnnotations = annotations }
where
unless' :: [T.Text] -> Parser a -> Parser a
unless' [] _ = fail "no candidates" -- Must never happen
unless' [s] p = notFollowedBy (string s) >> p
unless' (x : xs) p = notFollowedBy (string x) >> unless' xs p
parameters :: Parser [Parameter]
parameters = fieldsOrParameters ("parameter", "parameters") Parameter
parameterSet :: Parser (DeclarationSet Parameter)
parameterSet = option DeclarationSet.empty $ try $ do
params <- parameters <?> "method parameters"
handleNameDuplication "parameter" params return
method :: Parser Method
method = do
annotationSet' <- annotationSet <?> "service method annotation"
returnType <- optional $ try $ do
rt <- typeExpression <?> "method return type"
spaces1
notFollowedBy $ char '('
return rt
methodName <- name <?> "method name"
spaces
char '('
spaces
docs' <- optional $ do
d <- docs <?> "method docs"
spaces
return d
params <- parameterSet
spaces
char ')'
spaces
errorType <- optional $ do
string "throws" <?> "throws keyword"
spaces
e <- typeExpression <?> "method error type"
spaces
return e
annotationSet'' <- annotationsWithDocs annotationSet' docs'
return $ Method methodName params returnType errorType annotationSet''
methods :: Parser [Method]
methods = method `sepEndBy` try (spaces >> char ',' >> spaces)
methodSet :: Parser (DeclarationSet Method)
methodSet = do
methods' <- methods <?> "service methods"
handleNameDuplication "method" methods' return
serviceDeclaration :: [Identifier] -> Parser TypeDeclaration
serviceDeclaration forwardNames = do
annotationSet' <- annotationSet <?> "service annotation"
string "service" <?> "service keyword"
spaces
serviceName' <- uniqueName forwardNames "service name"
spaces
char '('
spaces
docs' <- optional $ do
d <- docs <?> "service docs"
spaces
return d
methods' <- methodSet <?> "service methods"
spaces
char ')'
spaces
char ';'
annotationSet'' <- annotationsWithDocs annotationSet' docs'
return $ ServiceDeclaration serviceName' (Service methods') annotationSet''
modulePath :: Parser ModulePath
modulePath = do
idents <- sepBy1 (identifier <?> "module identifier")
(try (spaces >> char '.' >> spaces))
<?> "module path"
case makePath idents of
Nothing -> fail "module path cannot be empty"
Just path -> return path
where
makePath :: [Identifier] -> Maybe ModulePath
makePath = foldl f Nothing
f :: Maybe ModulePath -> Identifier -> Maybe ModulePath
f Nothing i = Just $ ModuleName i
f (Just p) i = Just $ ModulePath p i
importName :: [Identifier]
-> Parser (Identifier, Identifier, A.AnnotationSet)
importName forwardNames = do
aSet <- annotationSet <?> "import annotations"
spaces
iName <- identifier <?> "name to import"
hasAlias <- optional $ try $ do
spaces
string' "as"
aName <- case hasAlias of
Just _ -> do
spaces
uniqueIdentifier forwardNames "alias name to import"
Nothing ->
return iName
return (aName, iName, aSet)
imports :: [Identifier] -> Parser [TypeDeclaration]
imports forwardNames = do
string' "import" <?> "import keyword"
spaces
path <- modulePath <?> "module path"
spaces
char '('
spaces
idents <- many' [] $ \ importNames' -> do
notFollowedBy $ choice [char ')', char ',' >> spaces >> char ')']
let forwardNames' = [i | (i, _, _) <- importNames'] ++ forwardNames
unless (L.null importNames') $ do
string' ","
spaces
n <- importName forwardNames'
spaces
return n
when (L.null idents) $ fail "parentheses cannot be empty"
void $ optional $ string' ","
spaces
char ')'
spaces
char ';'
return [ Import path imp source aSet
| (imp, source, aSet) <- idents
]
module' :: Parser Module
module' = do
spaces
docs' <- optional $ do
d <- docs <?> "module docs"
spaces
return d
spaces
importLists <- many $ do
importList <- imports []
spaces
return importList
let imports' = [i | l <- importLists, i <- l]
types <- many' imports' $ \ tds -> do
typeDecl <- do
-- Preconsume the common prefix (annotations) to disambiguate
-- the continued branches of parsers.
spaces
annotationSet' <- annotationSet <?> "annotations"
spaces
let forwardNames =
[ n
| td <- tds
, n <- declFacialName td : toList (D.extraPublicNames td)
]
decl <- choice
[ notFollowedBy (string "service")
>> typeDeclaration forwardNames
, serviceDeclaration forwardNames <?> "service declaration"
]
-- In theory, though it preconsumes annotationSet' before parsing
-- decl so that decl itself has no annotations, to prepare for an
-- unlikely situation (that I bet it'll never happen)
-- unite the preconsumed annotationSet' with decl's annotations
-- (that must be empty).
return $ case decl of
TypeDeclaration { typeAnnotations = set } ->
decl { typeAnnotations = A.union annotationSet' set }
ServiceDeclaration { serviceAnnotations = set } ->
decl { serviceAnnotations = A.union annotationSet' set }
_ -> decl -- Never happen!
spaces
return typeDecl
handleNameDuplication "type" types $
\ typeSet -> return $ Module typeSet docs'
file :: Parser Module
file = do
mod' <- module'
eof
return mod'
parse :: FilePath -- ^ Source path (although it's only used for error message)
-> T.Text -- ^ Input source code
-> Either ParseError Module
parse = runParser file
parseFile :: FilePath -- ^ Source path
-> IO (Either ParseError Module)
parseFile path = do
code <- SIO.withFile path SIO.ReadMode $ \ h -> do
SIO.hSetEncoding h SIO.utf8_bom
TIO.hGetContents h
return $ runParser file path code