合肥工业大学编译原理实验三 LR(1)分析法

前言

刚开始的时候觉得这个东西不好写,估计花的时间比前一个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就会报异常,就能让我们知道哪里出现了这个错误。

LR(1)项目

项目的概念这里就不说了,书上有。
用一个元组来表示,(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)

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

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

求GO函数

有了上一步求J函数,就可以求GO函数了。令I是一个项目集,X是一个文法符号,函数GO(I,X)定义为:GO(I,X)=CLOSURE(J)

求分析表ACTION和GOTO

  1. 若项目[A→·a, b]属于Ik且GO(Ik, a)=Ij, a为终结符,则置ACTION[k, a]为 “sj”。
  2. 若项目[A→α·,a]属于Ik,则置ACTION[k, a]为 “rj”;其中假定A→α为文法G’的第j个产生式。
  3. 若项目[S’→S·, #]属于Ik,则置ACTION[k, #]为 “acc”。
  4. 若GO(Ik,A)=Ij,则置GOTO[k, A]=j。
  5. 分析表中凡不能用规则1至4填入信息的空白栏均填上“出错标志”。

算法中先提取所有项目,放到一个单独的列表中,再执行判断。先判断第三个规则,再判断第二个,第一个,第四个,原因详见下面代码。
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 ''

LR1分析器

求完上述的所有东西之后,如果分析表中没有一个位置有两个值(即多重定义的入口),那么就能用这张表做一个LR(1)分析器了。

全代码

全部代码放到了我的GitHub上,传送门完整代码

总结

如果这篇文章对你有帮助的话,动动小手点个赞吧。

猜你喜欢

转载自blog.csdn.net/weixin_44801799/article/details/102903641