盡管使用 Flex 和 Bison 生成程序非常簡單,但是要讓這些程序產生用戶友好的語法和語義錯誤消息卻很困難。本文將介紹 Flex 和 Bison 的錯誤處理特性,并展示如何使用它們,然后詳細介紹它們的一些缺陷。
正如 UNIX® 開發人員所了解的那樣,Flex 和 Bison 的功能非常強大,非常適合開發詞法和語法解析器,尤其是語言編譯器和解釋器。如果我們不熟悉它們所實現的工具 —— 分別是 Lex 和 Yacc —— 可以參考一下本文 參考資料 一節中有關 Flex 和 Bison 文檔的鏈接,以及其他介紹這兩個程序的文章。
本文介紹了更高級的一些主題:用來在編譯器和解釋器中更好地實現錯誤處理能力的特性和技術。為了展示這些技術,我使用了一個示例程序 ccalc,它基于 Bison 手冊中的計算機實現了一個增強的計算器。我們可以從本文后面 下載 一節下載 ccalc 和相關文件。
增強包括使用了很多變量。在 ccalc 中,變量是通過在初始化中首次使用時定義的,例如 a = 3
。如果變量是在初始化之前使用的,那就會產生語義錯誤,使用值為 0 來創建這個變量,并打印一條消息。
示例源代碼中包括 7 個文件:
這個程序接收兩個參數:
-debug
:產生調試輸出 filename
:輸入文件名;默認值為 defs.txt
為了處理變量名和實際值,Bison 的語義類型必須進行增強:
/* generate include-file with symbols and types */ %defines /* a more advanced semantic type */ %union { double value; char *string; } |
有些文法規則可以產生特定的語義類型,這需要像清單 2 中一樣對 Bison 進行聲明。要獲得一個可移植性更好的 Bison 文法版本,我們需要重新定義 +-*/()
符號。下面這個例子沒有使用左括號 (
,而是使用了結束符符號 LBRACE
,這是由詞法分析提供的。另外,操作符的優先順序也必須進行聲明。
對于 Flex 來說,所生成的代碼通常都依賴于平臺所使用的代碼頁(codepage)。盡管我們可以使用其他代碼頁,但是必須要對輸入進行轉換。因此與 Bison 代碼不同,Flex 代碼尚不能進行移植。
/* terminal symbols */ %token <string> IDENTIFIER %token <value> VALUE %type <value> expression /* operator-precedence * top-0: - * 1: * / * 2: + - */ %left ADD SUB %left MULT DIV %left NEG %start program |
這段文法與 Bison 手冊非常類似,不同之處在于它使用了名字作為終端符號和標識符的簡寫形式。標識符是在賦值語句中進行定義和初始化的,并且可以在任何允許使用的地方使用。清單 3 給出了一個示例文法:
program : statement SEMICOLON program | statement SEMICOLON | statement error SEMICOLON program ; statement : IDENTIFIER ASSIGN expression | expression ; expression : LBRACE expression RBRACE | SUB expression %prec NEG | expression ADD expression | expression SUB expression | expression MULT expression | expression DIV expression | VALUE | IDENTIFIER ; |
program
的第三個輸出讓這個分析程序可以獲得錯誤,從中搜索分號,然后繼續執行(通常錯誤對于解析器來說都是非常嚴重的)。
為了讓這個例子更加有趣,規則體中的真正數學函數都是以單獨函數的形式實現的。在進行高級文法分析時,我們要盡量保證規則簡短,并使用函數來實現一些不會直接處理解析的過程:
| expression DIV expression { $$ = ReduceDiv($1, $3); } |
最后,函數 yyerror()
必須要進行定義。這個函數是在所生成的解析器檢測到語法錯誤時調用的,它又會調用一個小函數 PrintError()
,后者會打印增強的錯誤消息。詳細內容請參看源代碼。
Flex 所生成的詞法分析器必須要根據語義類型提供終止符號。清單 5 定義了空格、實際值、標識符和符號所使用的語法。
[ \t\r\n]+ { /* eat up whitespace */ } {DIGIT}+ { yylval.value = atof(yytext); return VALUE; } {DIGIT}+"."{DIGIT}* { yylval.value = atof(yytext); return VALUE; } {DIGIT}+[eE]["+""-"]?{DIGIT}* { yylval.value = atof(yytext); return VALUE; } {DIGIT}+"."{DIGIT}*[eE]["+""-"]?{DIGIT}* { yylval.value = atof(yytext); return VALUE; } {ID} { yylval.string = malloc(strlen(yytext)+1); strcpy(yylval.string, yytext); return IDENTIFIER; } "+" { return ADD; } "-" { return SUB; } "*" { return MULT; } "/" { return DIV; } "(" { return LBRACE; } ")" { return RBRACE; } ";" { return SEMICOLON; } "=" { return ASSIGN; } |
為了幫助調試,我們在程序運行的末尾把所有已知的變量及其當前內容都打印了出來。
使用下面的輸入(其中稍微進行了排版)來編譯并運行這個示例解析器程序 ccalc
:
a = 3; 3 aa = a * 4; b = aa / ( a - 3 ); |
輸出結果如下所示:
Error 'syntax error' Error: reference to unknown variable 'aa' division by zero! final content of variables Name------------------ Value---------- 'a ' 3 'b ' 3 'aa ' 0 |
這個輸出結果并非非常有用,因為它并沒有顯示問題到底在什么地方。這在下一節中會進行介紹。
Bison 的最主要的特性在 Bison 手冊中隱藏的很深,就是它可以通過使用 YYERROR_VERBOSE
宏在產生語法錯誤的情況下生成更有意義的錯誤消息。
普通的 'syntax error'
消息如下:
Error 'syntax error, unexpected IDENTIFIER, expecting SEMICOLON'
這條消息對于調試更為合適。
使用原來的錯誤消息,很難判斷語義的錯誤。當然,這個例子非常容易修復,因為我們立即就可以找出有錯誤的那一行。在更加復雜的語法和對應輸入中,這可能并不簡單。讓我們編寫一個輸入函數來從文件中讀取相應的行。
Flex 具有一個非常有用的宏 YY_INPUT
,它負責為符號解釋讀入數據。我們可以在 YY_INPUT
宏中添加一個對 GetNextChar()
函數的調用,后者從文件中讀取數據,并保留了下一個要讀取的字符的位置信息。GetNextChar()
使用了一個緩沖區來存放一行輸入。這兩個變量保存了當前行號和該行中下一個字符的位置:
#define YY_INPUT(buf,result,max_size) { result = GetNextChar(buf, max_size); if ( result <= 0 ) result = YY_NULL; } |
使用這個增強的錯誤打印函數 PrintError()
(在前面討論過,它可以很好地顯示有問題的輸入行,完整的 PrintError()
源代碼請參看 示例源代碼),我們就具有了一個用戶友好的消息,它顯示了下一個字符的位置:
|....+....:....+....:....+....:....+....:....+....:....+ 1 |a = 3; 2 |3 aa = a * 4; ...... !.....^ Error: syntax error, unexpected IDENTIFIER, expecting SEMICOLON 3 |b = aa / ( a - 3 ); ...... !.......^ Error: reference to unknown variable 'aa' ...... !.................^ Error: division by zero! |
這個示例函數可以從其他函數(例如 ReduceDiv()
)中進行調用,從而打印語義錯誤,例如 division by zero 或 unknown identifiers。
如果我們希望標記一下最后使用的符號,就可以對 Flex 規則進行擴展,并修改錯誤的打印。函數 BeginToken()
和 PrintError()
(二者都可以在示例源代碼中找到)是關鍵:BeginToken()
是由每條規則進行調用的,這樣它就可以記住每個符號的開始和結束,每次打印錯誤時都會調用 PrintError()
。這樣,我們就可以生成一條有用的消息了,例如:
2 |3 aa = a * 4; ...... !..^^............ Error: syntax error, unexpected IDENTIFIER, expecting SEMICOLON |
所生成的詞法解析器可能會在檢測到某個符號之前讀入多個字符。因此,這個過程不可能精確地顯示確切的位置。它最終取決于為 Flex 所提供的規則。規則越復雜,位置的精確程度就越低。這個例子中的規則可以由 Flex 通過提前查找一個字符來進行處理,這會讓位置的預測更加精確。
下面讓我們來看一下 division by zero 這個錯誤。最后一次符號讀?。ńY束括號)并不是這個錯誤的根源。表達式 (a-3)
的值就是 0。對于更好的錯誤消息來說,我們需要知道表達式的位置。要實現這種功能,我們可以在 YYLTYPE
類型的全局變量 yylloc
中提供這個符號的確切位置。使用宏 YYLLOC_DEFAULT(請參看 Bison 文檔 中默認的定義),Bison 可以計算出某個表達式的位置。
記住,只有當您在文法中使用位置時才會定義類型。這是一個常見的錯誤。
默認的位置類型 YYLTYPE
如清單 11 所示。我們可以對這個類型重新進行定義,使其包括更多信息,例如 Flex 所讀取的文件名。
typedef struct YYLTYPE { int first_line; int first_column; int last_line; int last_column; } YYLTYPE; |
在上一節中,我們看到了 BeginToken()
函數,它是在新符號開始時調用的。此時就應該存儲這個位置了。在我們的例子中,一個符號不能跨越多行,因此 first_line
和 last_line
是相同的,它們都保存了當前的行號。其他屬性有符號的起點(first_column
)和終點(last_column
),這是通過符號的起點和長度計算出來的。
要使用這個位置,我們必須對規則處理函數進行處理,如清單 12 所示。符號 $3
的位置是通過 @3
進行引用的。為了防止拷貝這個規則中的整個結構,我們生成了一個指針 &@3
。這看起來可能有點奇怪,但卻是正確的。
| expression DIV expression { $$ = ReduceDiv($1, $3, &@3); } |
在處理函數中,我們獲得了一個指向保存了位置信息的 YYLTYPE
結構的指針,這樣可以生成一條很好的錯誤消息。
extern double ReduceDiv(double a, double b, YYLTYPE *bloc) { if ( b == 0 ) { PrintError("division by zero! Line %d:c%d to %d:c%d", bloc->first_line, bloc->first_column, bloc->last_line, bloc->last_column); return MAXFLOAT; } return a / b; } |
現在錯誤消息可以幫助我們來定位問題了。除零操作錯誤在第 3 行的第 10 列到 18 列之間。
|....+....:....+....:....+....:....+....:....+....:....+ 1 |a = 3; 2 |3 aa = a * 4; ...... !..^^........... Error: syntax error, unexpected IDENTIFIER, expecting SEMICOLON 3 |b = aa / ( a - 3 ); ...... !....^^............... Error: reference to unknown variable 'aa' ...... !.................^.. Error: division by zero! Line 3:10 to 3:18 final content of variables Name------------------ Value---------- 'a ' 3 'b ' 3.40282e+38 'aa ' 0 |
Flex 和 Bison 是用來解析文法的一對功能強大的組合。通過使用本文中介紹的技巧,我們可以構建更好的解釋器,它們可以生成像您自己喜歡的編譯器中一樣的有用的、容易理解的錯誤消息。