{-# 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 -> do v <- integer return $ Integer v 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