编译原理学习笔记(五)——LL(1)算法学习

编译原理笔记(五)——语法分析LL(1)算法

从基本自顶向下到LL(1)

自顶向下的基本算法思想中,从一个文法推导出一个表达式,其伪代码为

# 文法规则1
# S -> N V N
# N -> s
#    | t
#    | g
#    | w
# V -> e
#    | d

# 一个例子: g d w
# what is 右部, 比如 S -> N V N,这是S的一个生成式,那么N V N就是S的这一条生成式的右部
tokens[];
i = 0;
stack = [S]

while(stack not empty){
	if(stack[top] is terminal char T)
		if(T == tokens[i++])
			pop();
		else
			backtrace();
	else if(stack[top] is nonterminal char T)
		pop()
		push(the 'next' right hand side of T)  # 将非终结符的下一个生成式的右部压入栈中
}

其算法的基本思想为:从开始状态S开始,首先将S入栈,进行初始化,然后进入循环,栈非空就一直在循环中,对于每一个循环,判断栈顶元素的类型

  • 如果栈顶元素为非终结符,将其出栈,然后将这个非终结符的下一条生成式的右部压入栈中,这里注意,就是将没有被压入过的生成式依次按顺序压入,一次只压入一个生成式
  • 如果栈顶元素为终结符,判断token[i]是否与之相等,如果相等,就说明这一小部分推导完毕,就将其出栈,如果不相等,说明这个生成式可能不满足token[i],需要对比其它生成式、就需要进行回溯,直到所有的生成式对比过,都没有的话就报错,这就是,如果句子t==s,则说明s是能从文法推导出来的,而如果t!=s,则说明的是这个文法下的这一条推导不能推出s,但是不能说明其它的推导不能推出s,所以需要回溯

而对于上述的自顶向下的基本方法,其时间复杂度很大的关键在于这种下一个压入栈的右部是按顺序来的,但是按顺寻来的并不一定是我们想要的,所以如果我们想要的在这个最后的一条生成式,实际上需要的是最后一条,但是偏要从头到尾都检查一遍,十分的耗费时间,性能也十分低下,所以如果将next换成correct,这样就不会死板的按照顺序去扫描所有生成式,而是直接将依据需要将对应的生成式压入栈中,具体实现先不用管,只是有着这样的一种思想,那么其伪代码可以这样写:

tokens[];
i = 0;
stack = [S];

while(stack not empty)
	if(stack[top] is terminal char T)
		if(T == stack[i++])
			pop();
		else
			error("....")  # 这里因为是遇到非终结符压入需要的的右部,所以当遇到终结符但与token不相同的时候就可以判断推导失败了,直接报error即可
	else if(stack[top] is nonterminal char T)
		pop();
		push(the 'correct' right hand side of T)

LL(1)算法

初识

image-20211121221623782

LL(1)分析算法官方介绍是:从左L向右读入程序,最左(L)推导,采用一个前看符号(forward char)来分析的算法,其特点是

  • 分析高效,能做到线性时间
  • 错误定位和诊断信息准确
  • 很多开源和商业的生成工具都会用(ANTLR、 Yacc、SML、bison

我的理解就是:LL(1)算法是带有一个forword char(前看符号)的分析算法,它不需要回溯,性能叫基本的自顶向下分析算法高出许多,最常见的是一种基于分析表驱动的算法,而这个问题集中的关键就在于遇到非终结符的时候如何将正确的右部压入栈中,而这个操作正是需要借助这个forward char (即token),以及这个构建好的分析表去找到对应的右部压入栈中,从而避免回溯,提高性能,其架构图为:

image-20211121222444714

依照架构图而言,语法分析器自动生成器根据文法规则来生成这样一个分析表,然后驱动代码依据分析表对读取token然后对分析栈进行操作,分析正确则将token进行一定的转化传入后序处理工序,不正确则给出错误信息

分析表就是类似于这样的一张表,如下:

以上述文法规则1为例,每一条产生式都有一个特定的编号,如下

# 0:S -> N V N
# 1:N -> s
# 2:   | t
# 3:   | g
# 4:   | w
# 5:V -> e
# 6:   | d

其分析表table为:

非终结符\终结符stgwed
S0000
N1234
V56
  • 横列代表非终结符、竖列代表终结符、内部的值代表选取哪一条生成式,没有定义的代表error

这样有了这个分析表,其在遇到非终结符找到这个正确的下一个状态的操作即为

......
pop()
push(table[T][tokens[i]])  # T是一个非终结符,tokens[i]是前看符号,这里不会消耗字符,因为i没有++
......

有了这个分析表,其分析的性能就能得到大幅的提升,而下一步的关键就是怎么构造分析表

分析表怎么来?

对于这个分析表的构造,其是由文法的FIRST集的基础上而推导而来的,对于上面的文法,其各自生成式对应的FIRST集为如下,暂且先不管FIRST集是什么

# 0:S -> N V N {s, t, g, w}
# 1:N -> s {s}
# 2:   | t {t}
# 3:   | g {g}
# 4:   | w {w}
# 5:V -> e {e}
# 6:   | d {d}

# (S s => 0)  (S t => 0)  (S g => 0)  (S w => 0)
# (N s => 1)  (N t => 2)  (N g => 3)  (N w => 4)
# (V e => 5)  (V d => 6)

# 根据这样的推导,就可以将这样的一张分析表推导出来

非终结符的FIRST集

非终结符的FIRST集: 从非终结符N开始推导得出的句子开头的所有可能的终结符的集合

计算公式为:

1、对 N => a ....
	FIRST(N) U= {a}

2、对 N => M ....
	FIRST(N) U= FIRST(M)

FIRST集的不动点算法

foreach(nonterminal N)
	FIRST(N) = {}   # init FIRST(N) = {}

while(some set is changing)
	foreach(nonterminal N in Grammer)
		foreach(production p: N -> b1, b2 ..... bn)  # 对于N的每一个生成式
			if(b1 == a....)
				FIRST(N) U= {a}
			else if(b1 == M....)
				FIRST(N) U= FIRST(M)

而对于上述的信息,去推导上述文法的非终结符

N\迭代次数0123
S{}{}{s, t , g, w}{s, t, g, w}
N{}{s, t, g, w}{s, t, g, w}{s, t, g, w}
V{}{e, d}{e, d}{e, d}

FIRST集推广到任意位置

之前的FIRST集只是针对于非终结符,那么把这个FIRST集推广到任意串上面,针对每个产生式右部,就会有下面的式子:

FIRST_S(b1,b2,b3.....bn)
	FIRST(N)	IF b1 == N
	{a}         IF b1 == a

而对于上面例子的每一个生成式

# 0:S -> N V N {s, t, g, w}
# 1:N -> s {s}
# 2:   | t {t}
# 3:   | g {g}
# 4:   | w {w}
# 5:V -> e {e}
# 6:   | d {d}

NULLABLE集

考虑这样的一种情况,如果有一组规则

# Z -> d
# 	 | X Y Z
# Y -> c
# 	 | e   # e为空串
# X -> Y
# 	 | a

考虑上述语法规则的FIRST集,对于Z的FIRST集,它不仅仅是X的FIRTS集和d,因为X和Y可能会存在退出空字符串的情况,故Z也可能为开头,所以这样的考虑是不一样的,在这基础上,引入了一个NULLABLE集,其定义为

非终结符能推出一个空串,那么这个非终结符就属于NULLABLE集里面

引入NULLABLE集的概念是为了更准确的计算FIRST集,根据归纳定义的方法,对于一个非终结符是否属于NULLABLE集,当且仅当

# 基本情况
# X -> e    # e为空串

# 归纳情况
# X -> b1 b2 ... bn
	当且仅当b1 b2 ... bn都属于NULLABLE时,X才为NULLABLE

其算法伪代码为

NULLABEL = {}   # init nullable

while(nullable is still change)
	foreach(production p: X -> b)
		if(b == e)
			NULLABLE U= {X}
		else if (b == Y1, Y2, .... Yn)
			if(Y1 属于 NULLABLE && Y2 属于 NULLABLE && .....)
				NULLABLE U= {X}

对于上述文法,求其NULLABLE集的迭代过程为

012
NULLABLE{}{Y, X}{Y, X}

考虑NULLABLE集的完整计算公式

对于一个非终结符X,其FIRST计算规则为

# 基本情况:
	X -> a
		FIRST(X) U= {a}
# 归纳情况:
	X -> Y1,Y2,Y3...Yn
		FIRST(X) U= {Y1}
		if Y1 属于 NULLABLE
			FIRST(X) U= {Y2}
		if Y1, Y2 属于 NULLABLE
			FIRST(X) U= {Y3}
		.......
		

其算法伪代码为:

foreach(nonterminal N)
	FIRST(N) = {}
	
while(some set is changing)
	foreach(prduction p: N->b1,b2,b3...bn)
		foreach(bi from b1 to bn)
			if (bi == a...)
				FIRST(N) U= {a}
				break
			if (bi == M...)
				FIRST(N) U= FIRST(M)
				if(M not in NULLABLE)
					break

对于上面的文法,求取其FIRST集的迭代过程为

注意:NULLABLE = {X, Y} 之前算出来了

N\FIRST0123
Z{}{d}{a,c,d}{a,c,d}
Y{}{c}{c}{c}
X{}{a,c}{a.c}{a,c}

FOLLOW集的不动点算法

FOLLOW集,某些非终结符后面跟着什么符号。有哪些句子能跟在后面,其准确定义A为,对于非终结符号A,FOLLOW(A)被定义为可能在某些句型中紧跟在A右边的终结符号的集合。

foreach( nonterminal N)
	FOLLOW(N) = {} 			# init nonterminal N
	
while(some set is changing)
	foreach(production p: N -> b1, b2, b3.... bn)
		temp = FOLLOW(N) 
		foreach(bi from bn to b1)  # 逆序!
			if(bi == a ...)
				temp = {a}
			if(bi == M ...)
				FOLLOW(M) U= temp
				if(M is not NULLABLE)
					temp = FIRST(M)
				else
					temp U= FIRST(M)

针对上面给的例子,给出其FOLLOW集计算的迭代过程

N\FOLLOW012
X{}{}{}
Y{}{a, c, d}{a, c, d}
Z{}{a, c, d}{a, c, d}

FIRST_S集的计算

foreach(production p)
	FIRST_S(p) = {}
	
calculte_FIRST_S(production p: N -> b1, b2, ... bn)
	foreach(bi from b1 to bn)
		if(bi == a ...)
			FIRST_S(p) U= {a}
			return
		if(bi == M ...)
			FIRST_S(p) U= FIRST(M)
				if(M is not NULLABLE)
					return
	FIRST_S(p) U= FOLLOW(N)

此时,对于每一条产生式,其FIRST_S集

012345
FIRST_S{d}{a, c, d}{c}{a, c, d}{a, c, d}{a}

然后根据FIRST_S集,在根据文法的生成式规则,依照FIRST_S集,对照每一条生成式的内容,就可以构造非终结符与终结符对应的分析表

# 0: Z -> d
# 1:    | X Y Z
# 2: Y -> c
# 3:	| e    这里e为空串
# 4: X -> Y
# 5:    | a
acd
Z110,1
Y32,33
X4,544

然后最终根据这个分析表,其语法分析的驱动代码可以这样写

tokens[]   // all tokens
i = 0
stack = [S]

while(stack is not empty)
	if(stack[top] is terminal char T)
		if(T == tokens[i++])
			pop()
		else
			error("...")
	else if(stack[top] is nonterminal char T)
		pop()
		push(table[T, tokens[i]])

LL(1)算法中的冲突处理

**LL(1)算法中的冲突:**当分析表中特定位置对应的产生式的只并不是唯一的一条产生式,仍然还是会遇到不好选择的情况

对于LL(1)算法的冲突检测有下面的定义:对于N的两条产生式规则 N->a和N->b,如果不存在冲突,则要求FIRST_S(a)和FIRST_S(b)的交集为空集,并且如果LL(1)分析表存在冲突,那么就不能说明这是一个LL(1)文法关系,比如下面的一个文法

#0:E -> E + T
#1:   | T
#2:T -> T * F
#3:   | F 
#4:F -> n

其FIRST_S集为

01234
FIRST_S{n}{n}{n}{n}{n}

构造出来的分析表为:

n+*
E0,1
T2,3
F4

这种分析表还是存在冲突的,就算到了这个状态还是不知道要去选择哪一个生成式,但是解决方法

解决左递归问题

上述文法可以转换成

#0: E -> TE'
#1: E'-> +TE‘
#2:    | 
#3: T -> FT'
#4: T'-> *FT'
#5:    |
#6: F -> n

其分析表就可以变为

n+*
E0
E’1
T3
T’54
F6
提取左公因子
# 0: X -> aY
# 1:    | aZ

# 可以通过提取公因子的方法,比如对上述文法提取一个{a}.就可变为

# 0: X -> aY'
# 1: Y'-> Y
# 2:    | Z
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值