如果你創建的分析器用于產品,處理語法錯誤是很重要的。一般而言,你不希望分析器在遇到錯誤的時候就拋出異常并終止,相反,你需要它報告錯誤,盡可能的恢復并繼續分析,一次性的將輸入中所有的錯誤報告給用戶。這是一些已知語言編譯器的標準行為,例如C,C++,Java。在PLY中,在語法分析過程中出現錯誤,錯誤會被立即檢測到(分析器不會繼續讀取源文件中錯誤點后面的標記)。然而,這時,分析器會進入恢復模式,這個模式能夠用來嘗試繼續向下分析。LR分析器的錯誤恢復是個理論與技巧兼備的問題,yacc.py提供的錯誤機制與Unix下的yacc類似,所以你可以從諸如O’Reilly出版的《Lex and yacc》的書中找到更多的細節。
當錯誤發生時,yacc.py按照如下步驟進行:
1. 第一次錯誤產生時,用戶定義的p_error()方法會被調用,出錯的標記會作為參數傳入;如果錯誤是因為到達文件結尾造成的,傳入的參數將為None。隨后,分析器進入到“錯誤恢復”模式,該模式下不會在產生`p_error()`調用,直到它成功的移進3個標記,然后回歸到正常模式。
2. 如果在p_error()中沒有指定恢復動作的話,這個導致錯誤的標記會被替換成一個特殊的error標記。
3. 如果導致錯誤的標記已經是error的話,原先的棧頂的標記將被移除。
4. 如果整個分析棧被放棄,分析器會進入重置狀態,并從他的初始狀態開始分析。
5. 如果此時的語法規則接受error標記,error標記會移進棧。
6. 如果當前棧頂是error標記,之后的標記將被忽略,直到有標記能夠導致error的歸約。
### 6.8.1 根據error規則恢復和再同步
最佳的處理語法錯誤的做法是在語法規則中包含error標記。例如,假設你的語言有一個關于print的語句的語法規則:
~~~
def p_statement_print(p):
'statement : PRINT expr SEMI'
...
~~~
為了處理可能的錯誤表達式,你可以添加一條額外的語法規則:
~~~
def p_statement_print_error(p):
'statement : PRINT error SEMI'
print "Syntax error in print statement. Bad expression"
~~~
這樣(expr錯誤時),error標記會匹配任意多個分號之前的標記(分號是`SEMI`指代的字符)。一旦找到分號,規則將被匹配,這樣error標記就被歸約了。
這種類型的恢復有時稱為”分析器再同步”。error標記扮演了表示所有錯誤標記的通配符的角色,而緊隨其后的標記扮演了同步標記的角色。
重要的一個說明是,通常error不會作為語法規則的最后一個標記,像這樣:
~~~
def p_statement_print_error(p):
'statement : PRINT error'
print "Syntax error in print statement. Bad expression"
~~~
這是因為,第一個導致錯誤的標記會使得該規則立刻歸約,進而使得在后面還有錯誤標記的情況下,恢復變得困難。
### 6.8.2 悲觀恢復模式
另一個錯誤恢復方法是采用“悲觀模式”:該模式下,開始放棄剩余的標記,直到能夠達到一個合適的恢復機會。
悲觀恢復模式都是在p_error()方法中做到的。例如,這個方法在開始丟棄標記后,直到找到閉合的’}’,才重置分析器到初始化狀態:
~~~
def p_error(p):
print "Whoa. You are seriously hosed."
# Read ahead looking for a closing '}'
while 1:
tok = yacc.token() # Get the next token
if not tok or tok.type == 'RBRACE': break
yacc.restart()
~~~
下面這個方法簡單的拋棄錯誤的標記,并告知分析器錯誤被接受了:
~~~
def p_error(p):
print "Syntax error at token", p.type
# Just discard the token and tell the parser it's okay.
yacc.errok()
~~~
在`p_error()`方法中,有三個可用的方法來控制分析器的行為:
* `yacc.errok()`?這個方法將分析器從恢復模式切換回正常模式。這會使得不會產生error標記,并重置內部的error計數器,而且下一個語法錯誤會再次產生p_error()調用
* `yacc.token()`?這個方法用于得到下一個標記
* `yacc.restart()`?這個方法拋棄當前整個分析棧,并重置分析器為起始狀態
注意:這三個方法只能在`p_error()`中使用,不能用在其他任何地方。
p_error()方法也可以返回標記,這樣能夠控制將哪個標記作為下一個標記返回給分析器。這對于需要同步一些特殊標記的時候有用,比如:
~~~
def p_error(p):
# Read ahead looking for a terminating ";"
while 1:
tok = yacc.token() # Get the next token
if not tok or tok.type == 'SEMI': break
yacc.errok()
# Return SEMI to the parser as the next lookahead token
return tok
~~~
### 6.8.3 從產生式中拋出錯誤
如果有需要的話,產生式規則可以主動的使分析器進入恢復模式。這是通過拋出`SyntacError`異常做到的:
~~~
def p_production(p):
'production : some production ...'
raise SyntaxError
~~~
raise SyntaxError錯誤的效果就如同當前的標記是錯誤標記一樣。因此,當你這么做的話,最后一個標記將被彈出棧,當前的下一個標記將是error標記,分析器進入恢復模式,試圖歸約滿足error標記的規則。此后的步驟與檢測到語法錯誤的情況是完全一樣的,p_error()也會被調用。
手動設置錯誤有個重要的方面,就是p_error()方法在這種情況下不會調用。如果你希望記錄錯誤,確保在拋出SyntaxError錯誤的產生式中實現。
注:這個功能是為了模仿yacc中的`YYERROR`宏的行為
### 6.8.4 錯誤恢復總結
對于通常的語言,使用error規則和再同步標記可能是最合理的手段。這是因為你可以將語法設計成在一個相對容易恢復和繼續分析的點捕獲錯誤。悲觀恢復模式只在一些十分特殊的應用中有用,這些應用往往需要丟棄掉大量輸入,再尋找合理的同步點。
- 0 一些翻譯約定
- 1 前言和預備
- 2 介紹
- 3 PLY概要
- 4 Lex
- 4.1 Lex的例子
- 4.2 標記列表
- 4.3 標記的規則
- 4.4 標記的值
- 4.5 丟棄標記
- 4.6 行號和位置信息
- 4.7 忽略字符
- 4.8 字面字符
- 4.9 錯誤處理
- 4.10 構建和使用lexer
- 4.11 @TOKEN裝飾器
- 4.12 優化模式
- 4.13 調試
- 4.14 其他方式定義詞法規則
- 4.15 額外狀態維護
- 4.16 Lexer克隆
- 4.17 Lexer的內部狀態
- 4.18 基于條件的掃描和啟動條件
- 4.19 其他問題
- 5 語法分析基礎
- 6 Yacc
- 6.1 一個例子
- 6.2 將語法規則合并
- 6.3 字面字符
- 6.4 空產生式
- 6.5 改變起始符號
- 6.6 處理二義文法
- 6.7 parser.out調試文件
- 6.8 處理語法錯誤
- 6.9 行號和位置的跟蹤
- 6.10 構造抽象語法樹
- 6.11 嵌入式動作
- 6.12 Yacc的其他
- 7 多個語法和詞法分析器
- 8 使用Python的優化模式
- 9 高級調試
- 9.1 調試lex()和yacc()命令
- 9.2 運行時調試
- 10 如何繼續