編譯原理實驗4——SLR(1)分析器的生成


本篇博客用來記錄完成的編譯原理實驗4的學習過程以及最終成果

實驗要求

1.必做功能:
  (1)要提供一個源程序編輯界面,讓用戶輸入文法規則(可保存、打開存有文法規則的文件)
  (2)檢查該文法是否需要進行文法的擴充。
  (3)求出該文法各非終結符號的first集合與follow集合,並提供窗口以便用戶可以查看這些集合結果。
  (4)需要提供窗口以便用戶可以查看文法對應的LR(0)DFA圖。(可以用畫圖的方式呈現,也可用表格方式呈現該圖點與邊的關系數據)
  (5)需要提供窗口以便用戶可以查看該文法是否為SLR(1)文法。(如果非SLR(1)文法,可查看其原因)
  (6)需要提供窗口以便用戶可以查看文法對應的SLR(1)分析表。(如果該文法為SLR(1)文法時)
  (7)應該書寫完善的軟件文檔

2.選做功能。如果是組隊完成實驗,則是必做功能,即必須把下面的兩個功能也要實現。
  (1)需要提供窗口以便用戶輸入需要分析的句子。
  (2)需要提供窗口以便用戶查看使用SLR(1)分析該句子的過程。【可以使用表格的形式逐行顯示分析過程】

學習

問題:LR(0) 和 SLR(1)是什么
LR(0) 和 SLR(1)都是自底向下的語法分析方法的一個算法
項目
假設有文法

S'->S
S->(S)S|ε

那么下面列舉出LR(0)的8個項目(可以把每一個項目看成是旅游地圖的一部分,而其中的點是游客當前所處的位置)

S'->.S
S'->S.
S->.(S)
S->(.S)S
S->(S.)S
S->(S).S
S->(S)S.
S->.

每個項目可以看成一個狀態。如果某個狀態可以通過輸入文法中的終結符、非終結符得到新狀態,例如

S'->.S ——S——> S'->S.

則稱該狀態為移進狀態;如果某個狀態點已經到了末尾,需要原路返回,則稱該狀態為歸約狀態,例如

S->S. 為歸約項

最終形成一個NFA表,NFA表通過ε閉包的改寫則可成為DFA表(一個狀態可能有多個項目),通過DFA表就可以對句子進行LR(0)分析(移進歸約)。
LR(0)文法:如果一個文法產生的DFA表中的每一個狀態包含的都是移進項目或者只有一個歸約項,那么該文法成為LR(0)文法。
SLR(1)文法:如果一個文法產生的DFA表中的每一個狀態包含的歸約項目的Follow集合交集為空和移進的符號不屬於任何一個歸約項目,則稱該文法為SLR(1)文法

實踐

項目基於C++實現,可視化是使用QT。所有用到的編程知識點都在此鏈接
項目地址

文法規則的存儲結構

對於下面的文法規則,我們要如何存儲呢?

E' -> E
E -> E + n | n

首先給出定義如下:
(1)產生式: 若干條產生式構成一個文法規則。例如E -> E + n | n為一條產生式
(2)key,values: 對於一條產生式來說,->左邊的符號我們稱作key,右邊的符號序列我們稱作values。例如,對於產生式E -> E + n | n來說,E為key,E + n | n為values
(3)value: 若干個value構成一個values。例如對於valuesE + n | n來說,|符號分割出了兩個value,分別為E + nn


給出定義之后,接下來就來分析一下具體的存儲結構?
(1)對於每個符號(不包括->|),使用string對象來存儲,因為一個符號可以為多個字符。
(2)對於一個value,使用vector< string>對象來存儲,因為它本質為string的序列。之所以用vector對象而不用數組,是因為value的大小不確定。
(3)對於一個values,使用vector<vector< string>>來存儲,本質上就是value序列。
(4)對於一個文法, 使用map<string, vector<vector< string>>>對象來存儲,因為產生式本質為一個鍵值對,多個鍵值對就構成一個文法。


既然已經知道了文法的存儲結構,下面就需要解決如何把用戶輸入的文法字符串(轉換前需要先進行文法擴充)轉換為上面的存儲結構。首先我們必須給用戶輸入文法字符串給予一定的約束,因為不同的輸入形式會對應不同的轉換算法。我的字符串約束為:

E' -> E\nE -> (E) + n | n

(1)每個產生式用'\n'分割
(2)每一個value用'|'分割
(3)符號之間用' '分割,但是左右括號除外
(4)key為開始符號的產生式必須置於最前面(開始符號會在轉換的過程中通過string對象保存起來)


得到了文法的存儲結構后,我們可以很容易求得文法規則的非終結符集,通過set< string>對象來存儲

所有非終結符的first集合

首先說明一下用原來自頂向下的方法來求first集合是行不通的,因為自底向上的文法規則可以有左遞歸,該算法無法解決有左遞歸的情況甚至一些不是左遞歸的情況。比如下面的文法。

A -> A s | ε
A -> B A | ε
B -> b | ε

所有非終結符first集合的存儲結構

(1)一個非終結符的first集合用set< string>來存儲
(2)所有非終結符的first集合用map<string, set< string>>來存儲,因為通過映射我們可以直接找到某個非終結符的first集合

求解所有非終結符first集合的算法

算法描述如下:

初始化所有非終結符對應的first集合(置為空)
while 存在非終結符的first集合發生改變:
    for 產生式 in 文法規則:
        定義產生式對應的key, values
        for value in values:
            for value[k] in value: //遍歷value中每一個符號,value[k]中為value的第k個符號(編號從0開始)
                定義符號value[k]的first集合為k_first_set
                if value[k]為終結符:
                    k_first_set = {value[k]}
                else:
                    k_first_set = 非終結符value[k]對應的first集合
                add k_first_set - {"ε"} to 非終結符key對應的first集合
                if "ε" not in k_first_set:
                    break;
           if k == value.size: //value元素的大小
                add {"ε"} to 非終結符key對應的first集合

所有非終結符的follow集合

下面算法求解必須在first集合求解完畢后進行

求解所有非終結符follow集合的存儲結構

(1)一個非終結符的follow集合用set< string>來存儲
(2)所有非終結符的follow集合用map<string, set< string>>來存儲,因為通過映射我們可以直接找到某個非終結符的follow集合

求解所有非終結符follow集合的算法

我們需要先定義求解一個符號序列first集合的算法,因為求解所有非終結符的follow集合需要用到
first(符號序列)算法描述如下:

定義空集合set
for s[k] in 符號序列: //遍歷符號序列的每一個符號,其中s[k]為符號序列第k個符號(編號從0開始)
    求解s[k]的first集合k_set //s[k]如果為終結符就是{s[k]}, 如果為非終結符就在原來的存儲first集合的地方獲取
    add k_set - {"ε"} to set
    if "ε" not in k_set:
        break;
if k == 符號序列.size:
    add {"ε"} to set
return set

求解所有非終結符的follow集合的算法描述如下:

初始化所有非終結符的follow集合(置為空)
while 存在一個非終結符的follow集合發生改變:
    for 產生式 in 文法:
        定義產生式對應的key, values
        for value[k] in values:
            if value[k]為非終結符:
                add first(value[k+1, ]) to 非終結符value[k]對應的follow集合 //value[k+1, ]是value1的子序列
                if "ε" in first(value[k+1, ]):
                    add 非終結符key對應的follow集合 to 非終結符value[k]對應的follow集合

DFA圖、SLR(1)分析表

項目的存儲結構

首先給出項目的定義:
項目就是key -> 被加上了'.'的value
例如,對於文法規則

E' -> E
E -> E + n | n

來說,對應的8個項目為

E' -> .E
E' -> E.
E -> .E + n
E -> E. + n
E -> E +. n
E -> E + n.
E -> .n
E -> n.

項目還有不同的類型
形如E -> n.的項目稱為歸約項, 其'.'在項目最后
形如E -> .n的項目稱為移進項, 其'.'不在項目最后

那么,該如何存儲一個項目呢?
首先我們發現一個項目由key,value,'.'的位置唯一確定,為了節省存儲空間,value可以用在key對應的values下的編號表示
所以我們定義一個數據結構Project來存儲項目

Class Project{
    string key;
    int value_num; //在key對應的values下value的編號
    int index;  //'.'的位置編號,表示'.'在value中第index個符號前面
    int type; //項目的類型: 1為移進項 2為歸約項
}

結點的存儲結構

首先給出結點的定義:
一個結點如圖所示:若個項目的集合為結點
(1)單個結點的存儲結構: 因為結點是若干個項目的集合,所以我們使用vector< Project>對象來存儲

(2)所有結點的存儲結構: 用vector<vector< Project>>來存儲,這樣我們也可以通過vector的下標得到結點編號

注意:結點的存儲結構應該使用排序樹會更好,因為后面算法有搜索步驟。

移進關系(DFA的圖邊關系)的存儲結構

移進關系就是結點間的轉移關系,就是上圖中的那些邊。存儲結構為:
(1)某個結點(作為起點)的轉移關系用map<string, int>對象存儲,其中string存儲移進符號,int存儲轉移后的結點編號
(2)所有結點(作為起點)的轉移關系用map<int, map<string, int>對象來存儲,其中int是起點的結點編號,map<string, int>存儲着對應結點的轉移關系

歸約關系的存儲結構

歸約關系是某個結點的回退操作(與移進相對)。存儲結構為:
(1)某個結點(作為回退起點)的歸約關系用map<string, int>對象存儲,其中string存儲導致歸約發生的下一個符號(follow),int存儲歸約項在結點中的編號。
(2)所有結點(作為回退起點)的歸約關系用map<int, map<string, int>>對象存儲

結點、移進關系、歸約關系、判斷是否為SLR(1)文法的求解算法

首先通過舉例的方式定義擴展這個概念:對於文法規則

E' -> E
E -> E + n | n

項目E' -> .E擴展得到

E' -> .E
E -> .E + n
E -> n

就是把'.'后面的非終結符用其對應的項目('.'在value最前面)來具體化。就像你走到門口推開大門看到里面具體的風景一樣。

然后描述算法:

初始化結點序列(只有一個結點, 並且結點只有一個項目,項目=Project(開始符號,0,0,1))  
for 結點 in 結點序列:
    初始化該結點對應的移進關系 //置為空字典
    初始化該結點對應的歸約關系 //置為空字典
    擴展該結點(中的每一個項目)
    for 項目j in 結點:
        if 項目為移進項:
            定義該項目的轉換符號為t
            定義移進后的新項目為p
            if t未在移進關系的關鍵字集合里面:
                if p in 結點k:
                    將t:k鍵值對加入移進關系中
                else:
                    向結點集合插入一個含有p的新結點k
                    將t:k鍵值對加入移進關系中
            else: //t已經在移進關系的關鍵字集合里面
                獲取轉移到的結點k
                if p not in 結點k:
                    p加入結點k中
        else:  //項目為歸約項
            獲取項目j.key對應的follow集合follow
            for t in follow:
                if t:j 已經存在:
                    說明文法不是SLR(1)文法   //因為不滿足SLR(1)結點中歸約項.key的follow集合交集為空的原則
                將t:j加入移進關系
for 結點 in 結點序列:
    if 結點的移進符號集和歸約符號集有交集:
        說明文法不是SLR(1)文法  //因為不滿足SLR(1)結點中移進符號不存在於該結點中任何一個歸約項key的follow集合

用結點、移進關系生成LR(0)DFA圖

有了結點序列和所有結點的移進關系,就已經包含了LR(0)DFA圖的所有信息,自己想怎么生成就怎么生成

用結點、移進關系、歸約關系生成SLR(1)表

有了結點序列和所有結點的移進關系、歸約關系,就已經包含了SLR(1)表的所有信息,自己想怎么生成就怎么生成

分析句子

實現句子的分析需要一個分析棧和輸入隊列,存儲結構我選擇vector< string>,之所以不用stack和queue,是因為stack和queue沒有迭代器,沒有辦法遍歷,而vector只要在使用時給一定約束就可以當作stack或queue使用。
算法描述如下:

定義分析棧為stack,用戶隊列為queue
初始化stack(0壓入棧頂)
初始化queue(用戶輸入的句子分割成一個個符號后加入queue,最后再加入"$")
for i in range(0, 無窮大):
    輸出步驟i
    輸出分析棧
    輸出輸入隊列
    獲取棧頂元素f
    獲取隊列頭元素s
    if s為結點f的移進字符:
       彈出隊首元素
       t = 結點k由s轉換后的結點編號
       s、t分別入棧
       輸出"移進s"
    else:
       t = 結點f的歸約項目編號
       得到歸約項p
       定義p對應的key -> value
       if key為開始符號:
           輸出"接受"
           break
       if value[0] != "ε":
           彈出stack的2*value.size個元素
       f = 棧頂元素
       s = key
       t = 結點k由s轉換后的結點編號
       s、t入棧
       輸出"歸約key -> value"     

運行效果


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM