[英]Translating precedence table into grammar appropriate for recursive descent?
如果我們有一種只包含原子元素和一元和二元運算符的語言:
atomic elements: a b c
unary operators: ! ~ + -
binary operators: + - / *
然后我們可以定義一個語法:
ATOM := a | b | c
UNOP := ! | ~ | + | -
BINOP := + | - | / | *
EXPR := ATOM | UNOP EXPR | EXPR BINOP EXPR
然而,這種語法導致一個模糊的解析樹(由於左遞歸而在遞歸下降解析器中產生無限循環)。
所以我們添加一個優先表:
Precendence 1: unary+ unary- ~ ! (Right to Left)
Precendence 2: * / (Left to Right)
Precendence 3: binary+ binary- (Left to Right)
我的問題是我們可以采用什么算法或過程來獲取優先級表,並為遞歸下降解析器(不是左遞歸)生成適當的語法。
優先級表是操作員組和相關方向的有序列表(L-> R或R <-L)。 答案是將此作為輸入並將語法作為輸出。
將運算符優先級語法轉換為LR(1)
語法[1]很容易,但結果語法將使用左遞歸來解析左關聯運算符。 消除左遞歸很容易 - 例如,使所有運算符都正確關聯 - 但是當結果語法識別相同的語言時,解析樹是不同的。
事實證明,稍微修改遞歸下降解析器以便能夠處理優先級關系並不困難。 該技術是由Vaughan Pratt發明的,並且基本上使用調用堆棧來代替經典的分流碼算法中的顯式堆棧。
Pratt解析似乎正在經歷某種復興,你可以找到很多關於它的博客文章; 一個相當不錯的是Eli Bendersky 。 普拉特在20世紀70年代早期設計了這個程序,同時Frank deRemer證明LR(1)
解析是實用的。 普拉特對正式解析的實用性和不靈活性持懷疑態度。 從那以后,我認為這場辯論一直在醞釀着。 Pratt解析器確實簡單而靈活,但另一方面,很難證明它們是正確的(或者它們解析特定的正式描述的語法)。 另一方面,盡管bison
最近獲得了對GLR解析的支持,但使用它可能不那么煩躁,盡管bison
生成的解析器實際上解析了他們聲稱解析的語法,但仍有許多人會同意在Pratt的聲明(從1973年開始)中,正式的解析方法“不易使用且使用起來不太令人愉快”。
[1]在實踐中,所有yacc衍生物和許多其他LR解析器生成器都將接受優先關系以消除歧義; 生成的語法表較小,涉及的單位減少量較少,因此如果您要使用解析器生成器,則沒有特別好的理由不使用此技術。
描述任意優先級的一般語法可以使用基於表的LALR解析器進行解析,並且可以使用yacc生成。 但是,這並不意味着當您希望使用遞歸下降解析器時,所有內容都會丟失。
遞歸下降解析器只能驗證語法是否正確。 構建語法樹是另一回事,應該在樹構建級別上處理優先級。
因此,請考慮以下語法,而不使用可以解析中綴表達式的左遞歸。 沒有什么特別沒有優先權的跡象:
Expr := Term (InfixOp Term)*
InfixOp := '+' | '-' | '*' | '/'
Term := '(' Expr ')'
Term := identifier
(在右側使用的符號是正則表達式,使用大型駝峰案例編寫替換的規則,使用小駱駝案例引用或編寫令牌)。
構建語法樹時,您有一個當前節點 ,您可以向其添加新節點。
通常,在解析規則時,您在當前節點上創建一個新的子節點,並使該子節點成為當前節點。 完成解析后,您將升級到父節點。
現在,在解析InfixOp
規則時,應該采用不同的InfixOp
。 您應該為相關節點分配優先級。 Expr
節點具有最弱的優先級,而所有其他運算符具有更強的優先級。
解析InfixOp
規則時,請執行以下操作:
雖然當前節點的優先級高於傳入節點的優先級,但仍保持上升一級(使父節點成為當前節點)。
然后插入傳入節點的節點作為當前節點的最后一個子節點的父節點並使其成為當前節點。
由於Expr
節點被聲明具有最弱的優先級,它將最終停止攀爬。
我們來看一個例子: A+B*C
那里的當前節點總是標有!
消耗當前令牌后。
Parsed: none
Expr!
Parsed: A
Expr!
|
A
Parsed: A+
Expr
|
+!
|
A
Parsed: A+B
Expr
|
+!
/ \
A B
Parsed: A+B*
Expr
|
+
/ \
A *!
/
B
Parsed: A+B*C
Expr
|
+
/ \
A *!
/ \
B C
如果以后序方式遍歷此方法,您將獲得可用於評估它的表達式的反向拋光表示法。
或者反過來一個例子: A*B+C
Parsed: none
Expr!
Parsed: A
Expr!
|
A
Parsed: A*
Expr
|
*!
|
A
Parsed: A*B
Expr
|
*!
/ \
A B
Parsed: A*B+
Expr
|
+!
|
*
/ \
A B
Parsed: A*B+C
Expr
|
+!
/ \
* C
/ \
A B
有些運算符是左關聯的,而其他運算符是右關聯的。 例如,在C語言族中, +
是左關聯的,而=
是右關聯的。
實際上整個關聯性事物歸結為在相同優先級上處理運算符。 對於左關聯運算符,當您在相同的優先級別遇到運算符時,攀爬會繼續上升。 對於右關聯運算符,遇到相同的運算符時停止。
(展示所有技術需要太多空間,我建議在一張紙上試一試。)
在這種情況下,您需要稍微修改語法:
Expr := PrefixOp* Term PostfixOp* (InfixOp PrefixOp* Term PostfixOp*)*
InfixOp := '+' | '-' | '*' | '/'
Term := '(' Expr ')'
Term := identifier
當您遇到前綴運算符時,只需將其作為新子項添加到當前節點並將新子項作為當前節點,無論優先級如何,即使它是強運算符或弱運算符也是正確的,優先級上升規則為中綴運營商確保正確性。
對於后綴運算符,您可以使用我在中綴運算符中描述的相同優先級攀升,唯一的區別是我們沒有右側的后綴運算符,因此它只有1個子節點。
C語言系列有?:
三元運算符。 關於語法樹構建,你可以處理?
和:
作為單獨的中綴運算符。 但有一個技巧。 您為?
創建的節點?
應該是一個不完整的三元節點,這意味着你進行通常的優先級攀登並放置它,但是這個不完整的節點將具有最低的優先級,這可以防止甚至更弱的運算符如逗號運算符爬過它。 當你到達:
你必須爬到第一個不完整的三元節點(如果你沒有找到一個,然后報告語法錯誤),然后將它改為一個具有正常優先級的完整節點,並使其成為當前節點。 如果當前分支上存在不完整的三元節點時意外到達表達式的末尾,則再次報告語法錯誤。
那么a , b ? c : d
a , b ? c : d
被解釋為a , (b ? c : d)
。
但是a ? c , d : e
a ? c , d : e
會被解釋為a ? (c , d) : e
a ? (c , d) : e
,因為我們阻止了逗號爬過?
盡管有后綴外觀,但它們是中綴操作符,右側是語法強制括號術語,對於數組索引和函數調用也是如此。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.