clang編譯器的錯誤提示的精準程度是如何做到的?
比如,下面的一段代碼:
int[]* fun()
{
...
}
聲明函數fun的返回值是int[]*,這個並不符合C語言的語法,使用gcc編譯的錯誤提示為:
error: expected identifier or 『(』 before 『[』 token
int[]* fun()
clang編譯的錯誤提示是:
它可以精確的提示某一行代碼的某個單詞處發生了錯誤,而且還能給出準確的修改建議,這是如何做到的?
error: brackets go after the identifier
int[]* fun()
~~ ^
( )[]
謝邀。
一個parser遇到錯誤應該怎麼處理?無非兩種策略。一種就是遇到錯誤就停止(我現在比較喜歡這種,錯了報告完行號就掛。當然實際運用中的parser幾乎都不是這樣的),另外一種就是嘗試恢復到一個正確的狀態繼續解析(比如發現少分號的時候,嘗試補充一個分號再繼續解析,或是跳到下一個分號繼續解析。當然根據不同情況有很多不同的策略。)。clang就是遇到錯誤的時候不斷捨棄token,直到找到下一個正確的狀態。舉個例子,這是clang解析expression的函數片段。 // expression[opt] ";"
ExprResult Expr(ParseExpression());
if (Expr.isInvalid()) {
// If the expression is invalid, skip ahead to the next semicolon or "}".
// Not doing this opens us up to the possibility of infinite loops if
// ParseExpression does not consume any tokens.
SkipUntil(tok::r_brace, StopAtSemi | StopBeforeMatch);
if (Tok.is(tok::semi))
ConsumeToken();
return Actions.ActOnExprStmtError();
}
出錯了就直接跳過這個expression的解析,跳到下個分號。
還有就是預先想好大家會錯什麼地方,然後寫好錯誤錯誤代碼的解析,解析到了就報錯然後掛。把常見錯誤收集下,人肉寫出來,無非就是費點時間。具體例子看R大的那個答案。
你看人家Roslyn的錯誤信息為什麼這麼好看?跟clang一樣當然都是人力堆出來的啊。所以編譯器前端真的沒有什麼可以搞的了,就是堆人力啊。運用上述提到的方法,你也可以堆一個intellisense出來(這是Clang手寫的parser的錯誤恢復機製做得好。官方文檔也把這個當作賣點:Clang - Expressive Diagnostics
本質上說,這就是把一些常見的錯誤的語法也放進parser的語法匹配規則中,只不過正常的語法會正常的parse下去,而這些用於表示錯誤的語法用於錯誤檢查與恢復用。
可能有些人會覺得寫編譯器寫個正確的parser就挺困難的了。但其實把常見錯誤也歸整進去的話,這些處理「錯誤」的部分的工作量更加大…
一個parser的報錯信息不友善說到底還是因為懶啊。
請從這裡開始看:clang/ParseDecl.cpp at 34a3e1fd8ec31d535cf6967d9769546403ab75d7 · llvm-mirror/clang · GitHub/// Diagnose brackets before an identifier.
void Parser::ParseMisplacedBracketDeclarator(Declarator D)
在Parser::ParseDirectDeclarator(Declarator D)里,當它發現在identifier前有方括弧時,就會調用這個錯誤檢查/錯誤恢復用的parser函數來記錄錯誤並且提示parser要如何「恢復」到一個正常的狀態繼續parse下去。
這些診斷信息的ID可以參考:clang/DiagnosticParseKinds.td at e72003e211c5541371e6ea378da8d8a4ef2a2e19 · llvm-mirror/clang · GitHubdef err_brackets_go_after_unqualified_id : Error&<
"brackets are not allowed here; to declare an array, "
"place the brackets after the %select{identifier|name}0">;
可以看到還有很多錯誤診斷信息的ID,它們背後都關聯著「檢查錯誤的語法」。
===================================
備份信息:
Clang的代碼風格挺有趣的。看下面clang/ParseDecl.cpp at 34a3e1fd8ec31d535cf6967d9769546403ab75d7 · llvm-mirror/clang · GitHub/// isValidAfterIdentifierInDeclaratorAfterDeclSpec - Return true if the
/// specified token is valid after the identifier in a declarator which
/// immediately follows the declspec. For example, these things are valid:
///
/// int x [ 4]; // direct-declarator
/// int x ( int y); // direct-declarator
/// int(int x ) // direct-declarator
/// int x ; // simple-declaration
/// int x = 17; // init-declarator-list
/// int x , y; // init-declarator-list
/// int x __asm__ ("foo"); // init-declarator-list
/// int x : 4; // struct-declarator
/// int x { 5}; // C++"0x unified initializers
///
/// This is not, because "x" does not immediately follow the declspec (though
/// ")" happens to be valid anyway).
/// int (x)
///
static bool isValidAfterIdentifierInDeclarator(const Token T) {
return T.isOneOf(tok::l_square, tok::l_paren, tok::r_paren, tok::semi,
tok::comma, tok::equal, tok::kw_asm, tok::l_brace,
tok::colon);
}
// Since we know that this either implicit int (which is rare) or an
// error, do lookahead to try to do better recovery. This never applies
// within a type specifier. Outside of C++, we allow this even if the
// language doesn"t "officially" support implicit int -- we support
// implicit int as an extension in C99 and C11.
if (!isTypeSpecifier(DSC) !getLangOpts().CPlusPlus
isValidAfterIdentifierInDeclarator(NextToken())) {
// If this token is valid for implicit int, e.g. "static x = 4", then
// we just avoid eating the identifier, so it will be parsed as the
// identifier in the declarator.
return false;
}
寫得真清晰。
手寫的parser,只要有無限多的時間,都可以提供無限精準的錯誤信息和無限好看的錯誤信息。大部分編譯器的錯誤信息很難看(譬如SQLServer),是因為parser是生成的。並沒有什麼演算法可以從一個上下文無關文法推導出一個錯誤信息在各種語言裡面應該怎麼寫。
而且給出題目這種錯誤信息的方法其實超級簡單。你可以在parser裡面支持 &<完整類型&> &<名字&> &<參數表&>這種語法(所以可以寫出int[]* fun()),然後通過看類型是不是複合類型,然後給出錯誤信息。這一切都是事先想好了然後hardcode進去的。所以說都是苦力活,完全靠情懷。寫parser的一個重要技巧就是,在文法裡面寫很多你不想支持的、但是用戶可能會莫名其妙寫出來的語法,然後parse到那裡唯一的action就是返回錯誤信息。我也總是這麼做。
所以錯誤信息好看才是正常的。使用yacc進行解析(parsing with yacc)
"Yacc"就是我用過yacc(1)之後想喊的。——匿名"YACC"是再一個編譯編譯器的編譯器(Yet Another Compiler Compiler)的意思。它接受與上下文無關(context-free)的語法,構造用於解析的下推自動機(pushdown automaton)。運行這個自動機,就得到了一個特定語言的解析器。這一理論是很成熟的,因為以前計算機科學的一個重要課題就是如何減少編寫編譯器的時間。這個方法有個小問題:許多語言的語法不是與上下文無關的。這樣yacc的使用者不得不在每一個狀態轉換點上加上相關代碼,以處理和上下文有關的部分(類型檢查一般就是這麼處理的)。許多C編譯器使用的都是yacc生成的解析器;GCC 2.1的yacc語法有1650行之多 (如果不用yacc,GCC應該能成為自由軟體基金會不錯的作品)。由yacc生成的代碼就更多了。……………………
…………當然,編譯器會幫你的。如果你把倚賴關係搞錯了,編譯器會毫不留情地指出語法錯誤。記住,編譯器是個很忙很有身份的程序,它沒時間去區分未定義的數據結構和輸入錯誤的區別。事實上,即使你只是忘了敲個分號,C編譯器也會惱羞成怒,立馬撂挑子不幹了。在編譯器社區,這一現象被稱為「錯誤雪崩」,或者按照編譯器自己的說法:「我完蛋了,起不來了。」 缺個分號會把解析器徹底搞暈,狂吐不止。這個解析器很可能是用yacc寫成的,yacc對語法正確的程序(很少見的一種情況)處理得很好,但要讓它生成健壯容錯自動恢復的解析器,這就有點兒勉為其難了。有經驗的C程序員都知道只有第一條解析錯誤才是有意義的。
以上摘自「UNIX痛恨者手冊」。
解析程序遇到語法錯誤時如果表現低劣,不能給出準確的錯誤反饋,那很可能是用某種東西生成的而不是手寫的。clang則反之,應該是手寫的。根據c11的語法定義,int [12] func(void)的定義似乎是錯誤的,如果編譯器支持返回數組,正確的表達式應該是 int func(void)[12]。具體原因是,int [] 的數組部分 [] 屬於聲明符 (declarator),而根據語法 []應該在標識符(identifier)的後面 .不過目前clang和gcc都不支持返回值是數組的形式,對於形式為數組的函數參數,編譯器將他們轉為指針。原因大概是大數組不僅佔用寶貴的堆棧資源,而且調用函數時拷貝函數參數或返回值時可能效率比直接傳指針要低。至於為什麼不夠精細,我覺得大概是gcc的開發人員沒有clang的開發人員那麼有錢有閑吧,真要做肯定是可以做的。
其實你貼的gcc錯誤和clang錯誤,在我看來是相同的錯誤提示,即[]應該在identifier之後。
編譯器面對此類初級的問題檢測起來還是比較容易的,比如未初始化,定義,命名規範之類,在後端一般使用模式匹配演算法對所有代碼執行一次檢測就好--簡單來說用Python 就可以做到,基本上都是規則確認好的,只是語言各有不同,嗯,類似於gcc面向古板的理工男,clang有些偏小資^_^但是編譯器的核心任務和功能可不是這個,而是編譯鏈接。這類問題在Coverity中有一個統一的分類 叫做Parse Warnings-用過的同學們應該很熟悉。
推薦閱讀: