上面例子中,對表達式的文法描述用一種特別的形式規避了二義文法。然而,在很多情況下,這樣的特殊文法很難寫,或者很別扭。一個更為自然和舒服的語法表達應該是這樣的:
~~~
expression : expression PLUS expression
| expression MINUS expression
| expression TIMES expression
| expression DIVIDE expression
| LPAREN expression RPAREN
| NUMBER
~~~
不幸的是,這樣的文法是存在二義性的。舉個例子,如果你要解析字符串”3 * 4 + 5”,操作符如何分組并沒有指明,究竟是表示”(3 * 4) + 5”還是”3 * (4 + 5)”呢?
如果在yacc.py中存在二義文法,會輸出”移進歸約沖突”或者”歸約歸約沖突”。在分析器無法確定是將下一個符號移進棧還是將當前棧中的符號歸約時會產生移進歸約沖突。例如,對于”3 * 4 + 5”,分析器內部棧是這樣工作的:
~~~
Step Symbol Stack Input Tokens Action
---- --------------------- --------------------- -------------------------------
1 $ 3 * 4 + 5$ Shift 3
2 $ 3 * 4 + 5$ Reduce : expression : NUMBER
3 $ expr * 4 + 5$ Shift *
4 $ expr * 4 + 5$ Shift 4
5 $ expr * 4 + 5$ Reduce: expression : NUMBER
6 $ expr * expr + 5$ SHIFT/REDUCE CONFLICT ????
~~~
在這個例子中,當分析器來到第6步的時候,有兩種選擇:一是按照expr : expr * expr歸約,一是將標記’+’繼續移進棧。兩種選擇對于上面的上下文無關文法而言都是合法的。
默認情況下,所有的移進歸約沖突會傾向于使用移進來處理。因此,對于上面的例子,分析器總是會將’+’進棧,而不是做歸約。雖然在很多情況下,這個策略是合適的(像”if-then”和”if-then-else”),但這對于算術表達式是不夠的。事實上,對于上面的例子,將’+’進棧是完全錯誤的,應當先將expr * expr歸約,因為乘法的優先級要高于加法。
為了解決二義文法,尤其是對表達式文法,yacc.py允許為標記單獨指定優先級和結合性。需要像下面這樣增加一個precedence變量:
~~~
precedence = (
('left', 'PLUS', 'MINUS'),
('left', 'TIMES', 'DIVIDE'),
)
~~~
這樣的定義說明PLUS/MINUS標記具有相同的優先級和左結合性,TIMES/DIVIDE具有相同的優先級和左結合性。在precedence聲明中,標記的優先級從低到高。因此,這個聲明表明TIMES/DIVIDE(他們較晚加入precedence)的優先級高于PLUS/MINUS。
由于為標記添加了數字表示的優先級和結合性的屬性,所以,對于上面的例子,將會得到:
~~~
PLUS : level = 1, assoc = 'left'
MINUS : level = 1, assoc = 'left'
TIMES : level = 2, assoc = 'left'
DIVIDE : level = 2, assoc = 'left'
~~~
隨后這些值被附加到語法規則的優先級和結合性屬性上,這些值由最右邊的終結符的優先級和結合性決定:
~~~
expression : expression PLUS expression # level = 1, left
| expression MINUS expression # level = 1, left
| expression TIMES expression # level = 2, left
| expression DIVIDE expression # level = 2, left
| LPAREN expression RPAREN # level = None (not specified)
| NUMBER # level = None (not specified)
~~~
當出現移進歸約沖突時,分析器生成器根據下面的規則解決二義文法:
1. 如果當前的標記的優先級高于棧頂規則的優先級,移進當前標記
2. 如果棧頂規則的優先級更高,進行歸約
3. 如果當前的標記與棧頂規則的優先級相同,如果標記是左結合的,則歸約,否則,如果是右結合的則移進
4. 如果沒有優先級可以參考,默認對于移進歸約沖突執行移進
比如,當解析到”expression PLUS expression”這個語法時,下一個標記是TIMES,此時將執行移進,因為TIMES具有比PLUS更高的優先級;當解析到”expression TIMES expression”,下一個標記是PLUS,此時將執行歸約,因為PLUS的優先級低于TIMES。
如果在使用前三種技術解決已經歸約沖突后,yacc.py將不會報告語法中的沖突或者錯誤(不過,會在parser.out這個調試文件中輸出一些信息)
使用precedence指定優先級的技術會帶來一個問題,有時運算符的優先級需要基于上下文。例如,考慮”3 + 4 * -5”中的一元的’-‘。數學上講,一元運算符應當擁有較高的優先級。然而,在我們的precedence定義中,MINUS的優先級卻低于TIMES。為了解決這個問題,precedene規則中可以包含”虛擬標記”:
~~~
precedence = (
('left', 'PLUS', 'MINUS'),
('left', 'TIMES', 'DIVIDE'),
('right', 'UMINUS'), # Unary minus operator
)
~~~
在語法文件中,我們可以這么表示一元算符:
~~~
def p_expr_uminus(p):
'expression : MINUS expression %prec UMINUS'
p[0] = -p[2]
~~~
在這個例子中,%prec UMINUS覆蓋了默認的優先級(MINUS的優先級),將UMINUS指代的優先級應用在該語法規則上。
起初,UMINUS標記的例子會讓人感到困惑。UMINUS既不是輸入的標記也不是語法規則,你應當將其看成precedence表中的特殊的占位符。當你使用%prec宏時,你是在告訴yacc,你希望表達式使用這個占位符所表示的優先級,而不是正常的優先級。
還可以在precedence表中指定”非關聯”。這表明你不希望鏈式運算符。比如,假如你希望支持比較運算符’<’和’>’,但是你不希望支持 `a < b < c`,只要簡單指定規則如下:
~~~
precedence = (
('nonassoc', 'LESSTHAN', 'GREATERTHAN'), # Nonassociative operators
('left', 'PLUS', 'MINUS'),
('left', 'TIMES', 'DIVIDE'),
('right', 'UMINUS'), # Unary minus operator
)
~~~
此時,當輸入形如 a < b < c時,將產生語法錯誤,卻不影響形如 a < b 的表達式。
對于給定的符號集,存在多種語法規則可以匹配時會產生歸約/歸約沖突。這樣的沖突往往很嚴重,而且總是通過匹配最早出現的語法規則來解決。歸約/歸約沖突幾乎總是相同的符號集合具有不同的規則可以匹配,而在這一點上無法抉擇,比如:
~~~
assignment : ID EQUALS NUMBER
| ID EQUALS expression
expression : expression PLUS expression
| expression MINUS expression
| expression TIMES expression
| expression DIVIDE expression
| LPAREN expression RPAREN
| NUMBER
~~~
這個例子中,對于下面這兩條規則將產生歸約/歸約沖突:
~~~
assignment : ID EQUALS NUMBER
expression : NUMBER
~~~
比如,對于”a = 5”,分析器不知道應當按照assignment : ID EQUALS NUMBER歸約,還是先將5歸約成expression,再歸約成assignment : ID EQUALS expression。
應當指出的是,只是簡單的查看語法規則是很難減少歸約/歸約沖突。如果出現歸約/歸約沖突,yacc()會幫助打印出警告信息:
~~~
WARNING: 1 reduce/reduce conflict
WARNING: reduce/reduce conflict in state 15 resolved using rule (assignment -> ID EQUALS NUMBER)
WARNING: rejected rule (expression -> NUMBER)
~~~
上面的信息標識出了沖突的兩條規則,但是,并無法指出究竟在什么情況下會出現這樣的狀態。想要發現問題,你可能需要結合語法規則和`parser.out`調試文件的內容。
- 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 如何繼續