python | 編譯原理,語法分析——LL(1)文法實現 上

前置文法處理

我們在做語法分析的時候會可以利用自上而下的分析方法,那麼其中LL(1)文法就是其中一個分析方法。
在實現之前我們需要知道LL(1)文法有以下侷限性:

  1. 不能處理左遞歸。
  2. 產生式集合中不能有公共左因子。

這兩個地方我就不在代碼中實現了,給出公式,大家自己先把自己的文法改成LL(1)文法然後再進行下面的python實現過程。

  1. 消除左遞歸:
    AAα{AβAAαAϵA \rightarrow A\alpha \Rightarrow \left\{ \begin{aligned} A\rightarrow \beta|A' \\ A' \rightarrow \alpha A' | \epsilon\\ \end{aligned} \right.
  2. 提取公共左因子:
    {Aαβ1Aαβ2{AαAAβ1β2\left\{ \begin{aligned} A \rightarrow \alpha \beta_1 \\ A \rightarrow \alpha \beta_2\\ \end{aligned} \right. \Rightarrow\left\{ \begin{aligned} A \rightarrow \alpha A' \\ A' \rightarrow \beta_1 | \beta_2\\ \end{aligned} \right.

下面開始python代碼實現過程

上下文無關文法由四元組(Vt,Vn,S,P)表示,其中:
Vt表示終結符集合,Vn表示非終結符集合,S表示開始符號,P表示產生是集合。
我們將這個四個元素保存到三個文件gramma.txt、vt.txt、vn.txt中,爲了下文的實現方便,我們規定這些文本文件中的書寫格式。

  1. vt.txt文件中保存文法中的終結符。每兩個終結符之間用空格隔開。特定符號ϵ\epsilon用epsilon表示,$表示結束符。例如:
id ( ) + * epsilon $
  1. vn.txt文件中保存文法中的的非終結符。每兩個終結符之間用空格隔開,因爲開始符號爲非終結符中的一個,我們將開始符號放在第一位就能區分開始符號了。例如:
E T E' T' F
  1. gramma.txt文件中保存文法的產生式集合。每個產生式佔一行,產生式中每個文法符號之間都用空格隔開,這樣做的目的是爲了劃分的時候方便。例如:
E -> T E'
E' -> + T E' | epsilon
T -> F T'
T' -> * F T' | epsilon
F -> ( E ) | id

下面就可以開始代碼編寫了:

  1. 首先打開文件:
# 讀入文法
gramma = open("5_gramma.txt",'r')

# vt:終結符
# vn:非終結符
vt = open("5_vt.txt",'r').readline().split(' ')
vn = open("5_vn.txt",'r').readline().split(' ')
startCode = vn[0]
# vt = ['id','(',')','+','*','epsilon','$']
# vn = ['E','T',"E'","T'",'F']
  1. 我們文件打開gramma.txt文件,一行行地每一個讀入產生式,我們要先把文法中A->B|C 切分爲A->B和A->C。
# 把文法中A->B|C 切分爲A->B和A->C
def splitOr(gramma):
    stack = []
    for i in gramma:
        i = i.split(' ')
        ss = i[0]
        j = 1
        while j<len(i):
            if i[j] == "->":
                break
            j+=1
        j+=1
        while j<len(i):
            if i[j][-1] == '\n':
                i[j] = i[j][0:-1]
            if i[j] != '|':
                ss+=" "+i[j]
            else:
                stack.append(ss.split(' '))
                ss = i[0]
            j+=1
        stack.append(ss.split(' '))
    return stack
productions = splitOr(gramma)
print(productions)

這是我們用一個大list存放所有的產生式,大list的每個元素是一個小list,小list中存放這個產生式所用到的所有文法符號,list[0]是產生式左側符號,其餘元素是產生式右側符號。此步驟結果如下:

[['E', 'T', "E'"], ["E'", '+', 'T', "E'"], ["E'", 'epsilon'], ['T', 'F', "T'"],\
 ["T'", '*', 'F', "T'"], ["T'", 'epsilon'], ['F', '(', 'E', ')'], ['F', 'id']]

與預期結果相同。

  1. LL(1)文法中需要我們求每個非終結符的first集和follow集。
其中,查找非終結符的First集步驟爲:
Step 1. 按產生式順序來,從開始符找起;
Step 2. 如果右部的串首爲終結符,則直接將該終結符填入左部非終結符的First集中;
Step 3. 如果右部的串首爲非終結符,則左部非終結符的First集等價於串首非終結符的First集。
		因而,需要利用Step 2和Step 3繼續尋找串首非終結符的First集。

查找非終結符的Follow集步驟爲:
Step 1. 按產生式順序來,從開始符找起(開始符的Follow集必定包含`$`);
Step 2. 從所有產生式右部尋找目標非終結符,若其後緊跟終結符,則將終結符填入目標非終結符的
		Follow集。特別地,若其後緊跟`$`,則目標非終結符的Follow集等價於產生式左部非終結符
		的Follow集。
Step 3. 從所有產生式右部尋找目標非終結符,若其後緊跟非終結符,則將該非終結符的First集元素
		填入目標非終結符的Follow集。特別地,若該非終結符的First集元素中包含$\epsilon$,
		則需針對$\epsilon$情況時做特殊處理,即目標非終結符的Follow集等價於產生式左部
		非終結符的Follow集。

得知了以上的步驟我們就能寫python代碼來求解了,我們這裏用default(set)來存放每個終結符和非終結符的first和follow集。代碼還不算很冗長,大家細細看總能看明白的。

first = defaultdict(set)
follow = defaultdict(set)

def getFirst(curVn):
	# 這裏是爲了下面遞歸搜索的時候以免已經確定的first再進行一次計算,下面的getFollow函數同。
	# 只要集合不爲空說明已經求過則可以直接返回值即可。
    if first[curVn] != set():
        return first[curVn]
    #print(curVi)
    # 遍歷每個產生式,找到左邊是我們需要求的非終結符的產生式。
    for curPro in productions:
        if curPro[0] == curVn:
            #print(" ",curPro)
            # 判斷右側首字符是否爲終結符。
            if curPro[1] in vt:
                first[curVn].add(curPro[1])
            else:
            	# 加入最開始的遞歸直接返回判斷後,這裏其實不需要if語句。
            	# 唯一這樣做可能會省去部分遞歸過程,節省一點點系統棧空間。
                if first[curPro[1]] == set():
                    first[curVn] = set(getFirst(curPro[1]))
                else:
                    first[curVn] = set(first[curPro[1]])
    return first[curVn]

def getFollow(curVn):
	# 同上
    if curVn != 'E' and follow[curVn] != set():
        # print(1)
        return follow[curVn]
    # print(curVn)
    # 這兩層循環是爲了在產生式右側找到正在求的非終結符
    for curPro in productions:
        l = len(curPro)
        for i in range(1,l):
            if curPro[i] != curVn:
                continue
            # print(curPro)
            # 如果當前非終結符是列表最後一個元素那麼他的下一個元素一定是結束符$
            if i == l-1:
                # print(2)
                follow[curVn] = set(getFollow(curPro[0]))
                follow[curVn].add('$')
                continue
            # 判斷後面的是終結符還是非終結符
            if curPro[i+1] in vt:
                # print(3)
                follow[curVn].add(curPro[i+1])
            elif curPro[i+1] in vn:
            	# 下面的這個if-eles其實能簡化,需要的自己簡化去吧。
                if 'epsilon' in first[curPro[i+1]]:
                    # print(4)
                    follow[curVn] = follow[curVn] | first[curPro[i+1]]
                    follow[curVn] = follow[curVn] | set(getFollow(curPro[0]))
                    follow[curVn].discard('epsilon')
                else:
                    # print(5)
                    follow[curVn] = follow[curVn] | first[curPro[i+1]]
    return follow[curVn]

# 對於每一個非終結符求first集合和follow集合
for curVn in vn:
   getFirst(curVn)
print(first)
follow[startCode].add('$')
for curVn in vn:
    getFollow(curVn)
print(follow)

運行結果如下:

defaultdict(<class 'set'>, {'E': {'id', '('}, 'T': {'id', '('}, 'F': {'id', '('}, "E'": {'epsilon', '+'}, "T'": {'*', 'epsilon'}})
defaultdict(<class 'set'>, {'E': {')', '$'}, 'T': {'+', '$', ')'}, "E'": {')', '$'}, "T'": {')', '+', '$'}, 'F': {'*', '+', '$', ')'}})

在這裏插入圖片描述
至此,我們的first和follow集合就求出來了。LL(1)算法後續實現方法見我的下一個文章吧。再熬夜就要掉頭髮了,睡啦睡啦。
代碼我放到我在csdn的上傳文件中了,需要的自取。

By Brosea

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章