packages feed

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