cimple-0.0.30: src/Language/Cimple/Parser/Error/Pretty.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE Strict #-}
module Language.Cimple.Parser.Error.Pretty
( describeContext
, describeExpected
, formatParseError
) where
import Data.List (intercalate, isPrefixOf, nub,
(\\))
import Data.Maybe (fromMaybe, listToMaybe, mapMaybe)
import Data.Text (Text)
import Language.Cimple.DescribeAst (describeLexeme, describeNode,
getLoc)
import Language.Cimple.Lexer (AlexPosn (..), Context (..),
Lexeme (..), ParseError (..),
lexemePosn)
import Language.Cimple.Tokens (LexemeClass (..))
formatParseError :: ParseError -> String
formatParseError (ParseError p@(AlexPn _ line col) ctx expected l) =
let (AlexPn _ bestLine bestCol) = if line == 0 && col == 0
then fromMaybe p $ listToMaybe $ mapMaybe getContextPosn ctx
else p
getContextPosn (ContextLexeme _ (L p' _ _)) = Just p'
getContextPosn (ContextNode _ n) = Just (lexemePosn (getLoc n))
getContextPosn _ = Nothing
ctxNames = dedupe . filter (not . null) $ map describeContext ctx
contextStr = if null ctxNames then "" else " while parsing " ++ intercalate " > " (reverse ctxNames)
lexemeStr = " near " ++ describeLexeme l
expectedStr = "; expected " ++ describeExpected expected
hint = getHint ctx expected l
in ":" ++ show bestLine ++ ":" ++ show bestCol ++ ": Parse error" ++ contextStr ++ lexemeStr ++ expectedStr ++ hint
dedupe :: Eq a => [a] -> [a]
dedupe [] = []
dedupe (x:xs) = x : dedupe (dropWhile (== x) xs)
getHint :: [Context] -> [String] -> Lexeme Text -> String
getHint ctx expected l
| isTerminator l =
let closers = nub $ filter isCloser expected ++ mapMaybe (closerForCtx . getContextName) ctx
in if null closers
then cimpleHint
else "\nHint: Reached a terminator before finding the expected closing " ++ intercalate " or " closers ++ "."
| isKeyword l && any isIdentifierToken expected =
"\nHint: " ++ describeLexeme l ++ " is a reserved keyword and cannot be used as an identifier."
| isIdentifier l && any isIdentifierToken expected =
let expectedDesc = describeExpected (filter isIdentifierToken expected)
foundDesc = describeLexeme l
in "\nHint: Found " ++ foundDesc ++ ", but here we expected a " ++ expectedDesc ++ "."
| otherwise = cimpleHint
where
cimpleHint = if any ((== "Endif") . getContextName) ctx && any ("'/*'" `isPrefixOf`) expected
then "\nHint: In Cimple, every #endif must be followed by a comment indicating what it closes (e.g. '#endif /* FLAG */')."
else ""
getContextName :: Context -> String
getContextName (Context name) = name
getContextName (ContextLexeme name _) = name
getContextName (ContextNode name _) = name
isIdentifier :: Lexeme Text -> Bool
isIdentifier (L _ c _) = c `elem` [IdVar, IdSueType, IdConst, IdFuncType, IdStdType]
isIdentifierToken :: String -> Bool
isIdentifierToken s = s `elem` ["ID_VAR", "ID_SUE_TYPE", "ID_CONST", "ID_FUNC_TYPE", "ID_STD_TYPE"]
isKeyword :: Lexeme Text -> Bool
isKeyword (L _ c _) = c >= KwBitwise && c <= KwWhile
closerForCtx :: String -> Maybe String
closerForCtx name = case name of
"FunctionCall" -> Just "')'"
"ArgList" -> Just "')'"
"CompoundStmt" -> Just "'}'"
"AggregateDecl" -> Just "'}'"
"MemberDecls" -> Just "'}'"
"EnumDecl" -> Just "'}'"
"PostfixExpr" -> Just "']'" -- Often array access
"PrimaryExpr" -> Just "')'" -- Parenthesized expr
"ParenthesizedExpr" -> Just "')'"
"FunctionPrototype(ID_VAR)" -> Just "')'"
"FunctionPrototype(ID_FUNC_TYPE)" -> Just "')'"
_ -> Nothing
isTerminator :: Lexeme Text -> Bool
isTerminator (L _ c _) = c `elem` [PctSemicolon, Eof, PpNewline]
isCloser :: String -> Bool
isCloser s = s `elem` ["')'", "'}'", "']'", "'*/'", "'@}'"]
describeContext :: Context -> String
describeContext (Context name) = describeContextName name
describeContext (ContextLexeme name l) =
let name' = describeContextName name
lex' = describeLexeme l
in if null name' then lex'
else if isIdentifier l then name' ++ " " ++ lex'
else name'
describeContext (ContextNode name n) =
let name' = describeContextName name
node' = describeNode n
in if null name' then node' else name' ++ " " ++ node'
describeContextName :: String -> String
describeContextName name = case name of
"TranslationUnit" -> ""
"ToplevelDecl" -> "top-level declaration"
"Endif" -> "#endif"
"FunctionDecl" -> "function"
"FunctionDefn" -> "function"
"FunctionDeclarator" -> "function"
"CompoundStmt" -> "block"
"Stmt" -> "statement"
"VarDeclStmt" -> "variable declaration"
"VarDecl" -> "variable declaration"
"MemberDecls" -> "struct/union members"
"MemberDecl" -> "struct/union member"
"IfStmt(CompoundStmt)" -> "if statement"
"IfStmt(ReturnStmt)" -> "if statement"
"ForStmt" -> "for loop"
"WhileStmt" -> "while loop"
"DoWhileStmt" -> "do-while loop"
"SwitchStmt" -> "switch statement"
"AggregateDecl" -> "struct/union definition"
"AggregateType" -> "struct/union definition"
"EnumDecl" -> "enum definition"
"ConstExpr" -> "constant expression"
"Expr" -> "expression"
"PrimaryExpr" -> "expression"
"FunctionCall" -> "function call"
"MacroBody" -> "macro body"
"FunctionPrototype(ID_VAR)" -> "function"
"FunctionPrototype(ID_FUNC_TYPE)" -> "function"
"PreprocIf(Stmts)" -> "#if block"
"PreprocIfdef(Stmts)" -> "#ifdef block"
"PreprocIf(ToplevelDecls)" -> "#if block"
"PreprocIfdef(ToplevelDecls)" -> "#ifdef block"
"Enumerator" -> "enumerator"
"EnumeratorName" -> "enumerator"
"InitialiserList" -> "initializer list"
"QualType(LocalLeafType)" -> "type"
"QualType(GlobalLeafType)" -> "type"
"LocalLeafType" -> "type"
"GlobalLeafType" -> "type"
_ | "FunctionPrototype" `isPrefixOf` name -> "function"
_ | "PreprocIfdef" `isPrefixOf` name -> "#ifdef block"
_ | "PreprocIf" `isPrefixOf` name -> "#if block"
_ | "IfStmt" `isPrefixOf` name -> "if statement"
_ | "push_" `isPrefixOf` name -> ""
_ -> name
describeExpected :: [String] -> String
describeExpected [] = "end of file"
describeExpected [option] = describeTokenName option
describeExpected options
| allComment options = "a comment"
| wants ["break", "const", "continue", "ID_CONST", "VLA"] = "statement or declaration"
| wants ["ID_FUNC_TYPE", "non_null", "static", "'#include'"] = "top-level declaration or definition"
| options == ["ID_FUNC_TYPE", "ID_STD_TYPE", "ID_SUE_TYPE", "struct", "union", "void"] = "top-level type specifier"
| options == ["ID_STD_TYPE", "ID_SUE_TYPE", "struct", "union", "void"] = "type specifier"
| options == ["ID_STD_TYPE", "ID_SUE_TYPE", "bitwise", "const", "force", "struct", "union", "void"] = "type specifier"
| options == ["ID_CONST", "ID_VAR", "LIT_CHAR", "LIT_FLOAT", "LIT_FALSE", "LIT_INTEGER", "'{'"] = "constant or literal"
| ["ID_FUNC_TYPE", "ID_STD_TYPE", "ID_SUE_TYPE", "ID_VAR"] `isPrefixOf` options = "type specifier or variable name"
| ["ID_FUNC_TYPE", "ID_STD_TYPE", "ID_SUE_TYPE", "bitwise", "const"] `isPrefixOf` options = "type specifier"
| ["ID_CONST", "sizeof", "LIT_CHAR", "LIT_FALSE", "LIT_INTEGER"] `isPrefixOf` options = "constant expression"
| ["ID_CONST", "ID_SUE_TYPE", "'/*'" ] `isPrefixOf` options = "enumerator, type name, or comment"
| wants ["'defined'"] = "preprocessor constant expression"
| wants ["ID_CONST", "sizeof", "LIT_CHAR", "LIT_FLOAT", "LIT_FALSE", "LIT_TRUE", "LIT_INTEGER", "LIT_STRING", "'('", "'\\n'"] = "macro body"
| wants ["'&'", "'&&'", "'*'", "'=='", "';'"] = "operator or end of statement"
| wants ["'&'", "'&&'", "'*'", "'^'", "'!='"] = "operator"
| wants ["ID_CONST", "ID_VAR", "LIT_CHAR", "'*'", "'('"] = "expression"
| ["ID_CONST", "ID_STD_TYPE", "ID_SUE_TYPE", "ID_VAR", "const", "sizeof"] `isPrefixOf` options = "expression or type specifier"
| ["ID_CONST", "ID_STD_TYPE", "ID_SUE_TYPE", "const", "sizeof"] `isPrefixOf` options = "constant expression or type specifier"
| ["'&='", "'->'", "'*='"] `isPrefixOf` options = "assignment or member/array access"
| wants ["'='", "'*='", "'/='", "'+='", "'-='"] = "assignment operator"
| wants ["CMT_WORD"] = "comment contents"
| wants ["','", "'}'", "'/*'"] = "',' or closing brace (or a comment)"
| length options == 2 = commaOr options
| otherwise = "one of " <> commaOr options
where
allComment opts = not (null opts) && all (`elem` ["'/*'", "'/**'", "'/***'", "'/** @{'", "'/** @} */'", "IGN_START"]) opts
wants xs = null (xs \\ options)
describeTokenName :: String -> String
describeTokenName name = case name of
"ID_VAR" -> "variable name"
"ID_SUE_TYPE" -> "type name"
"ID_CONST" -> "constant name"
"ID_FUNC_TYPE" -> "function type name"
"ID_STD_TYPE" -> "standard type name"
_ -> name
commaOr :: [String] -> String
commaOr = go . reverse
where
go [] = ""
go (x:xs) = intercalate ", " (reverse xs) <> " or " <> x