1. 程式人生 > >可配置語法分析器開發紀事(五)——構造一個真正能用的狀態機(中)

可配置語法分析器開發紀事(五)——構造一個真正能用的狀態機(中)

上一篇部落格寫到了如何給一個非終結符的文法規則構造出一個壓縮過的下推狀態機,那麼今天說的就是如何把所有的文法都連線起來。其實主要的idea在(三)和他的勘誤(三點五)裡面已經說得差不多了。但是今天我們要處理的是帶資訊的transition,所以還有一些地方要注意一下。

所以在這裡我們先把幾條文法的最後的狀態機都列出來(大圖):

image

接下來的這一步,就是要對所有靠非終結符(Exp啊Term這些)進行跳轉的transition都執行上一篇文章所說的傳說中的交叉連結。在產生連結的時候,我們給shift和reduce的邊分別加上shift和reduce。而shift和reduce是有引數的——就是被shift走的狀態的id。這樣可以在parse的時候匹配和處理狀態堆疊。在這裡我門對e3->e1這一步做一下操作做為例子。紅色的邊是被刪掉的,而粗壯的綠色邊是被新加進去的:

image

紅色的邊變成了兩條綠色的邊,紅色的邊附帶的資訊則被複制到了綠色的reduce邊上。當我們使用這個狀態機的時候,shift(s3)就表示往堆疊裡面壓入s3,reduce(s3)就表示從堆疊裡面彈出(s3)。當然彈出不一定會成功,所以如果不成功的話,這條邊就不能在當時使用。因此這也就是為什麼在e3跳轉到t0之後,t1知道往回跳的是e1而不是別的什麼地方——就如同為什麼C++的函式執行完之後總是知道如何跳轉回呼叫它的地方一樣——因為把資訊推入了堆疊。

那現在我們就來看一下,當所有的非終結符跳轉都處理掉之後,會變成什麼樣子吧(這個圖真是複雜和亂到我不想畫啊),為了讓圖變得不那麼糟糕,我把shift都變成紫色,reduce都變成綠色:

image

在新增shift和reduce邊之前,每一條邊都是有輸入token的。但是我們剛剛新增上去的shift和reduce邊卻是不輸入token的,所以他們是epsilon邊,下一步就是要消除他們。上面這個圖消除了epsilon邊之後,會變成一個狀態很少,但是每一條邊附帶的資訊都會非常多,而且像n1這種經常到達的狀態(因為四則運算裡面有很多數字)將恢復射出無數條邊。到了這裡這個狀態機已經再也畫不出來了。所以我下面就只拿兩個例子來畫。下面要展示的是用Exp來parse單獨的一個數字會走的邊,當然就是Exp –> Term –> Factor –> Number了:

image

就會被處理成:

image

注意邊上面的資訊是要按順序重新疊加在一起的。當所有的epsilon邊都去掉了之後,我們就得到了最終的一個狀態機。最重要的一件事情出現了。我們知道,發明LR和LALR這種東西就基本上是為了處理左遞迴的,所以這種圖就可以在去除epsilon邊的過程中自動發現左遞迴。這是怎麼做到的呢?只要在去除epsilon邊的時候,發現了一條完全由shift這種epsilon邊組成的環,那麼左遞迴就發現了。為了方便,我們可以只處理直接左遞迴——就是這種環的長度是1的。不包含間接左遞迴的問法是很容易寫出來的。當然這種環並不一定是首尾相接的,譬如說我們在處理e0的時候就會發現e0->t0->t0這種環(當然嚴格來說環只有最後一截的這個部分)。我們的程式要很好地應對這種情況。因為我們只接受直接左遞迴,所以類似這種結構的epsilon路徑可以直接的拋棄他,因為t0->t0會被t0狀態單獨處理掉。因此這樣做並不會漏掉什麼。

細心的朋友可能會發現,這個結構的圖是不能直接處理右遞迴的(總之左遞迴和右遞迴總要有一個會讓你的狀態機傻逼就是了!)。關於如何處理有遞迴(其實內容也不復雜)地方法會在“下篇”描述出來。那處理左遞迴有什麼用呢?舉個例子,我們的e0->e2就是一個左遞迴,而他會在之前的步驟被處理成shift(e0->e0)和reduce(e1->e2)。我們要記下shift和reduce的對應關係,那麼當我們找到一個左遞迴的shift之後,我們就可以把對應的reduce給標記成“left-recursive-reduce”。這是一個在構造語法樹的時候,非常關鍵的一種構造指令。

處理完這些之後,我們可以把左遞迴的shift邊全部刪掉,最後把token和state都統統處理成連續的數字,做成一張[state, token] –> [transitions]的二維表,每一個表的元素是transition的列表。為什麼是這樣呢?因為我們對一個state輸入一個token之後,由於儲存著state的堆疊(你還記得嗎?shift==push,reduce==pop)的棧頂若干個元素的不同,可能會走不通的路線。於是最後我們就得到了這麼一張圖。

下面這張圖可以通過執行gac.codeplex.com上面的Common\UnitTest\UnitTest.sln(VS2012限定)之後,在Common\UnitTest\TestFiles\Parsing.Calculator.Table.txt裡面找到。這一組檔案都是我在測試狀態機的時候log下來的。

image

如果大家有VS2012的話,通過執行我準備的幾個輸入,譬如說“1*2+3*4”,就可以在Parsing.Calculator.[2].txt裡面找到所有狀態跳轉的軌跡。因為我們總是需要parse一個Exp,所以我們從22: Exp.RootStart開始。我們假設token stream的第一個和最後一個分別是$TokenBegin和$TokenFinish。上圖的$TryReduce是為了應對右遞迴而設計出來的一種特殊輸入。由於四則運算裡面並沒有右遞迴,所以這一列就是空的:

StartState: 22[Exp.RootStart]
$TokenBegin => 23[Exp.Start]
    State Stack:
NUMBER[1] => 2[Number.1]
    State Stack: 23[Exp.Start], 21[Term.Start], 19[Factor.Start]
    Shift 23[Exp]
    Shift 21[Term]
    Shift 19[Factor]
    Assign value
    Create NumberExpression
MUL[*] => 5[Term.3]
    State Stack: 23[Exp.Start]
    Reduce 19[Factor]
    Using
    Reduce 21[Term]
    Using
    LR-Reduce 21[Term]
    Assign firstOperand
    Setter binaryOperator = Mul
    Create BinaryExpression
NUMBER[2] => 2[Number.1]
    State Stack: 23[Exp.Start], 5[Term.3], 19[Factor.Start]
    Shift 5[Term]
    Shift 19[Factor]
    Assign value
    Create NumberExpression
ADD[+] => 10[Exp.3]
    State Stack:
    Reduce 19[Factor]
    Using
    Reduce 5[Term]
    Assign secondOperand
    Reduce 23[Exp]
    Using
    LR-Reduce 23[Exp]
    Assign firstOperand
    Setter binaryOperator = Add
    Create BinaryExpression
NUMBER[3] => 2[Number.1]
    State Stack: 10[Exp.3], 21[Term.Start], 19[Factor.Start]
    Shift 10[Exp]
    Shift 21[Term]
    Shift 19[Factor]
    Assign value
    Create NumberExpression
MUL[*] => 5[Term.3]
    State Stack: 10[Exp.3]
    Reduce 19[Factor]
    Using
    Reduce 21[Term]
    Using
    LR-Reduce 21[Term]
    Assign firstOperand
    Setter binaryOperator = Mul
    Create BinaryExpression
NUMBER[4] => 2[Number.1]
    State Stack: 10[Exp.3], 5[Term.3], 19[Factor.Start]
    Shift 5[Term]
    Shift 19[Factor]
    Assign value
    Create NumberExpression
$TokenFinish => 11[Exp.RootEnd]
    State Stack:
    Reduce 19[Factor]
    Using
    Reduce 5[Term]
    Assign secondOperand
    Reduce 10[Exp]
    Assign secondOperand

我們把所有跳轉過的transition的資訊都記錄下來,就可以構造語法蘇了。我們想象一下,在執行這些指令的時候,遇到NUMBER[4]就等於獲得了一個內容為4的token,shift的話就是往堆疊裡面push進一個狀態的名字,而reduce則是彈出。

相對應的,因為每一個文法都會建立一個物件,所以我們在初始化的時候,要先放一個空物件在堆疊上。shift一次就再建立一個空的物件push進去,reduce的時候就把棧頂的物件彈出來作為“待處理物件”,using了就把待處理物件和棧頂物件合併,left-reduce就是把棧頂物件彈出來作為待處理物件的同時,push一個空物件進去。assign fieldName就是把“待處理物件”儲存到棧頂物件的叫做fieldName的成員變數裡面去。如果棧頂物件為空,那麼被儲存的物件就是剛剛輸入的那個token了。因此我們從頭到尾執行一遍之後,就可以得到下面的一顆語法樹:

BinaryExpression {
    binaryOperator = [Add]
    firstOperand = BinaryExpression {
        binaryOperator = [Mul]
        firstOperand = NumberExpression {
            value = [1]
        }
        secondOperand = NumberExpression {
            value = [2]
        }
    }
    secondOperand = BinaryExpression {
        binaryOperator = [Mul]
        firstOperand = NumberExpression {
            value = [3]
        }
        secondOperand = NumberExpression {
            value = [4]
        }
    }
}

基本上parsing的過程就結束了。在“下篇”——也就是(六)——裡面,我會講述如何處理右遞迴,然後這個系列基本上就要完結了。

posted on 2013-01-01 15:52 陳梓瀚(vczh) 閱讀(3994) 評論(10)  編輯 收藏 引用 所屬分類: C++