刚开始的时候觉得这个东西不好写,估计花的时间比前一个LL1更长,但后来转念一想,UI可以挪用上次的,改个标题,换点控件,换个总控程序不就可以了吗。剩下的问题就是用python的哪个数据结构来表示和存储LR文法的一些东西,比如项目怎么表示、项目集怎么表示、文法怎么表示、哪个数据结构能最大程度方便编程。
把这些问题都解决了之后就好办了。也许是我太菜了,光是想这些都用了一个晚上,第二天才开始写代码。差不多第二天花了9个小时左右把代码写完了,剩下的就是debug了,然鹅debug就又花了我一天的时间,有两个逻辑错误藏得是真滴深。接下来正文开始。
下面是各种东西的数据结构。以文法:
(0)S->E
(1)E->E+T
(2)E->T
(3)T->T*F
(4)T->F
(5)F->(E)
(6)F->i
为例。
这里我在实验二的文法表示基础上又加了一种表示法,因为如果用实验二中那样表示的话,后面写求first集啊,求项目集那些算法的时候就不好写了。另一种表示就是不在乎产生式左边非终结符重叠问题,一个产生式用一个二元组来表示,这个元组第一个元素是产生式左边非终结符,第二个元素是右边推导式,比如第(1)个产生式 E->E+T 就表示成(‘E’, ‘E+T’),然后,再用一个列表把它们装起来,这样就能通过列表索引来得到文法,这也是为什么编号从0开始,因为刚好和列表索引一一对应。
这样的表示方法还有一个好处,因为python元组是不可变对象(还不知道的小伙伴先去补一补这方面的知识啦),后续算法如果出了问题要修改这个文法时python就会报异常,就能让我们知道哪里出现了这个错误。
项目的概念这里就不说了,书上有。
用一个元组来表示,(T, n,p),T表示产生式编号,如E->E+T的编号就是1;n表示产生式右边圆点的位置,比如E->·E+T,n就等于1,这里我用1来表示最左边那里而不是0,这样的话后边编程会有一点麻烦,不过这也是我全写完后才发现的问题,看这篇文章的你如果想改的话,也可以改成从0开始;p表示产生式的展望符,何为展望符的概念也不说了,自行查阅。
这样一来,拿1号产生式来说,项目[E->·E+T, ‘i’]就能表示成(1, 1, ‘i’)。
有了项目集的表示,接下来的项目集就更好说了。项目集族不就是很多个项目组成一个集合吗,那正好就可以用python的万金油–列表来存储和表示,可能你会问‘那字典或者集合不也行吗’,这里还真不行,这些项目之间都是相对独立的,不存在谁索引谁的问题,所以字典不行,而集合是无序的,要用的时候也不能索引,后面的算法要索引某个项目,所以最好还是用列表来表示。
举个栗子,项目[E->T·, ‘#’]的项目集就表示为[(2, 2, ‘#’), (2, 2, ‘+’), (3, 2, ‘#’), (3, 2, ‘+’), (3, 2, ‘*’)],至于怎么求,在后面介绍。
项目集都有了,每个项目集对应文法一个状态,也可以说是一个文法的DFA图中的一个节点。DFA是一个图,但我在这里不考虑怎么存这个图,也没必要存这个图,项目集之间的状态转换用后面要求的GOTO表就能表示,这里只把所有项目集放到一个列表里就行了,于是一个项目集族就是一个二维列表。第一个文法S->E的项目(0, 1, ‘#’)的项目集的索引同样也从0开始。
python代码:
def calProjectSetFamily(self):
# 元组(T,n,p)
T = 0 # 第一个产生式标号为0
n = 1 # 第一个项目右边圆点位置是第一个
p = '#' # 第一个项目的展望符肯定是#
I0 = self.__closure([(T, n, p)]) # 先求第一个项目的闭包
self.projectSetFamily.append(I0) # 加入项目集族
allChar = self.NonTerminater + self.Terminater # 文法的所有符号
allChar.remove(self.NonTerminater[0]) #去掉拓广文法的最开始符
for projectset in self.projectSetFamily: # 对每个项目集和每个文法,求
for char in allChar:
J = self.__J(projectset, char)
if not J:
continue
tmp = self.__closure(J)
if tmp not in self.projectSetFamily:
self.projectSetFamily.append(tmp)
I的任何项目都属于CLOSURE(I)。
2. 若项目[A→α·Bβ, a]属于CLOSURE(I),B→ξ 是一个产生式,那么,对于FIRST(βa) 中的每个终结符b,如果[B→·ξ, b]原来不在CLOSURE(I)中,则把它加进去。
3. 重复执行步骤2,直至CLOSURE(I)不再增大为止。
python代码:
def __closure(self, project):
# project是一个项目,是一个三个元素的元组
res = project
for item in project:
T = item[0] # 产生式编号
n = item[1] # 右边圆点位置,用来索引圆点右边那个符号
p = item[2] # 当前项目item的展望符
sizeOfProduct = len(self.grammar[T][1])
if n == sizeOfProduct + 1: # 如果圆点的位置在产生式的最后,那么就跳过当前这个产生式,看下一个
continue
X = self.grammar[T][1][n - 1] # 索引圆点右边那个符号
if X in self.NonTerminater: # 如果X是非终结符
# 先求这个X后面的符号连接上展望符的first集
if n == sizeOfProduct:
first = p
else:
# 再求展望符
first = self.first(self.grammar[T][1][n] + p)
prods = [] # 求X作为产生式左边的推导的编号
for i in range(len(self.grammar)):
if self.grammar[i][0] == X:
prods.append(i)
# 把不在原项目集中的项目加到当前项目集中
for prod in prods:
for f in first:
if (prod, 1, f) not in res:
res.append((prod, 1, f))
# else就是终结符,就不管
return res
J={任何形如[A→αX·β, a]的项目| [A→α·Xβ, a]∈I}
那么久遍历项目集I中的每个项目,看是否符合上述规则,符合就添加到项目集J里面,最后返回这个项目集J。
python代码:
def __J(self, I, X):
res = []
for project in I:
T = project[0] # 项目的产生式的标号
n = project[1] # 右边圆点位置
p = project[2] # 项目的展望符
product = self.grammar[T][1] # 产生式右边的字符串
# 遍历这个推导,由于字符串的特性,因此这里用下标的方式来遍历
for i in range(len(product)):
if product[i] == X: # 第i个字符是X,
if i == n - 1: # 如果它在圆点右边,就把它加入res
res.append((T, n + 1, p))
return res
有了上一步求J函数,就可以求GO函数了。令I是一个项目集,X是一个文法符号,函数GO(I,X)定义为:GO(I,X)=CLOSURE(J)
算法中先提取所有项目,放到一个单独的列表中,再执行判断。先判断第三个规则,再判断第二个,第一个,第四个,原因详见下面代码。
python代码:
def calActionAndGOTOTable(self):
statusNum = len(self.projectSetFamily) # 状态数
Terminater = self.Terminater.copy()
Terminater.append('#')
# 先把所有项目集放到一个列表里
allProject = []
for projectSet in self.projectSetFamily:
allProject += [x for x in projectSet if x not in allProject]
for k in range(statusNum): # 遍历每个项目集
self.actionTable[k] = {}# 初始,给每个状态一个空字典,这样就能通过双重字典来实现两个字符索引
self.GOTOtable[k] = {}
for T in self.Terminater:
self.actionTable[k][T] = '' # 先给表中每个元素赋值空字
self.actionTable[k]['#'] = '' # 把’#‘也给加上
for NT in self.NonTerminater:
self.GOTOtable[k][NT] = ''
for project in allProject: # 遍历每个项目
T = project[0] # 项目的产生式的标号
n = project[1] # 右边圆点位置
p = project[2] # 项目的展望符
sizeOfProduct = len(self.grammar[T][1])
for k in range(statusNum):
if project not in self.projectSetFamily[k]: # 某项目不在某项目集,continue
continue
# 执行到这说明该项目在某项目集中
if (0, 2, '#') == project: # 先判断第三个规则,符合的话直接退出当前循环
self.actionTable[k]['#'] = 'acc'
else:
if n == sizeOfProduct + 1: # 第二个规则,因为如果先判断第一个的话要用到n-1,而n-1可能会越界,所以先判第二
self.actionTable[k][p] = 'r' + str(T)
else:
a = self.grammar[T][1][n - 1] # 索引圆点右边那个符号,判断第一个规则
if a in Terminater:
j = self.__GO(k, a)
self.actionTable[k][a] = 's' + str(j) if j != -1 else ''
A = self.grammar[T][0] # 第四个规则
j = self.__GO(k, A)
self.GOTOtable[k][A] = j if j != -1 else ''
求完上述的所有东西之后,如果分析表中没有一个位置有两个值(即多重定义的入口),那么就能用这张表做一个LR(1)分析器了。
全部代码放到了我的GitHub上,传送门完整代码
如果这篇文章对你有帮助的话,动动小手点个赞吧。