【编译原理】简明自顶向下分析算法总结:递归下降,LL(1)分析算法

本文深入讲解了编译器前端的语法分析过程,包括上下文无关语法的概念、推导方法、分析树及二义性文法的处理。并详细介绍了自顶向下分析方法,如递归下降分析与LL(1)分析算法,以及如何通过消除左递归和提取左公因子来解决冲突。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

语法分析概念

从编译器前端的流程上说,语法分析对词法分析得到的记号流进行分析,识别其中的语法错误,并将正确的记号流转化为语法树,交给编译器的后续步骤进行进一步处理。

上下文无关语法

上下文无关语法是一个四元组:G=(T,N,P,S)G=(T,N,P,S)G=(T,N,P,S),其中

  • TTT是终结符集合
  • NNN是非终结符集合
  • PPP是一组产生式规则:
    • 形式:X→β1,β2,...,βnX\rightarrow \beta _1,\beta _2,...,\beta _nXβ1,β2,...,βn
    • 其中X∈N,βi∈(T∪N)X\in N,\beta _i\in (T\cup N)XN,βi(TN)
  • SSS是唯一的开始符号,S∈NS\in NSN

推导

给定文法GGG,从SSS开始,用产生式的右部替换左部的非终结符(把非终结符“展开”),直到不出现非终结符为止。推导结果称为句子

  • 最左推导:每次总选择最左侧符号替换
  • 最右推导:每次总选择最右侧符号替换
  • 语法分析的任务:给定GGG和句子sss,回答是否存在对句子sss的推导

分析树和二义性文法

分析树

推导可以表示成树状结构,树中的每个内部结点代表非终结符,每个叶子结点代表终结符,每一步推导代表如何从双亲结点生成直接孩子结点。

  • 分析树的含义取决于树的后序遍历。

二义性文法

给定文法GGG,如果存在句子sss对应不止一颗分析树,称GGG是二义性文法。

  • 解决方案:文法重写

语法分析算法

显然,语法分析从两个思路去做:

  • 思路1:根据文法GGG,从唯一的开始符号SSS开始,对非终结符,用产生式的右部替换左部(“展开”),观察是否能产生对应的句子sss
  • 思路2:根据句子sss,对其不断归约(“合并”),看是否能归约成开始符号SSS的形态。

思路1称为自顶向下分析,对应分析树的自顶向下的构造顺序;思路2称为自底向上分析

自顶向下分析

从开始符号SSS出发去推导句子称为自顶向下分析。后文将从最原始的回溯展开动机出发,逐步讨论如何优化算法(然后更加秃头)。

朴素的回溯思路

最朴素的自顶向下分析思想是:从开始符号SSS出发,随意地推导出句子ttt,比较tttsss
以下是华保健老师里的网课伪代码:

tokens[]; // holding all tokens
i = 0; // 指向第i个token
stack = [S] // S是开始符号
while (stack != [])
	if (stack[top] is a terminal t)
		if (t == tokens[i++]) // 如果匹配成功
			pop();
		else
			backtrack();
	else if (stack[top] is a nonterminal T)
		pop();
		push(the next right hand side of T) // 不符合,尝试下一个右部式

简单地说,就是不断地去试探展开式如何匹配每个句子,如果匹配不成功就回溯,试探下一种可能,由此引出递归下降分析算法和LL(1)分析算法。

递归下降分析算法

递归下降分析算法(需要回溯的方法)的基本思想如下:

  • 每个非终结符构造一个分析函数
  • 用前看符号指导产生式规则的选择

示例,对如下产生式规则:

S -> N V N
N -> s
   | t
   | g
   | w
V -> e
   | d

构造每条规则的算法伪代码:

parse_S()
	parse(N)
	parse(V)
	parse(N)

parse_N()
	token = token[i++]
	if (token == s || token == t || token == g || token == w)
		return;
	error("...")

parse_V()
	// TODO

一般的递归下降分析算法框架如下:

parse_X()
	token = nextToken()
	switch(token)
	case ...:
	case ...:
	......

可以看出,通用的递归下降分析技术仍然可能需要回溯。据此进一步讨论LL(1)分析算法。

LL(1)分析

  • 概念:从左(L)到右推导符号,最左(L)推导,采用一个(1)前看符号
  • 基本思想:表驱动的分析方法

仍然是很直观的思路。对于需要回溯的情况,我们可以根据已有的条件和规律对其剪枝,从而避免无谓的搜索。而在语法分析当中,显然token与token之间的相对位置是存在关系的。据此,我们可以利用这些相对位置关系去构造分析表,记录遇到下一个符号时应该跳转到哪个状态,从而避免回溯。
具体地说,我们需要构造两个集合:FIRSTFIRSTFIRST集和FOLLOWFOLLOWFOLLOW集。FIRSTFIRSTFIRST集的动机是引导表达式展开的跳转方向,FOLLOWFOLLOWFOLLOW集的动机是避免空符ϵ\epsilonϵ带来的错误。当FIRSTFIRSTFIRST可能为空时,就应当加入FOLLOWFOLLOWFOLLOW集,后文算法将会体现这一点。
注:此处约定英文大写符号默认为非终结符,英文小写符号默认为终结符,希腊字母暗示为一般文法符号,无法确认是终结符还是非终结符。

  • FIRST(α)FIRST(\alpha)FIRST(α):从任意文法符号串α\alphaα开始推导得出的所有可能的终结符集合。通俗地说,就是α\alphaα可能以什么开头。
  • FOLLOW(A)FOLLOW(A)FOLLOW(A):可能在某些句型中紧跟AAA后边的终结符号的集合。

FIRSTFIRSTFIRST集可以采用不动点算法计算。计算各文法符号的FIRST(α)FIRST(\alpha)FIRST(α)时,不断应用下列规则刷表,直到所有FIRST()都不再更新:

  1. 如果α=a\alpha=aα=a是终结符,则FIRST(a)={a}FIRST(a)=\{a\}FIRST(a)={a}
  2. 如果α=N\alpha=Nα=N不是终结符,则对NNN的每个产生式β1β2...βn\beta_1\beta_2...\beta_nβ1β2...βn,对其中的βi\beta_iβi,若β1β2...βi−1\beta_1\beta_2...\beta_{i-1}β1β2...βi1FIRSTFIRSTFIRST集中都有ϵ\epsilonϵ空符,则FIRST(N)∪=FIRST(βi)FIRST(N) \cup = FIRST(\beta_i)FIRST(N)=FIRST(βi)。通俗地说,对于产生式的第iii个符号,如果它前面的符号都可能取空,那么它的FIRSTFIRSTFIRST元素当然也可能是NNNFIRSTFIRSTFIRST元素,因此要将它的集合加入到NNN的集合中去。当然,对于i=1i=1i=1的情况,总是会FIRST(N)∪FIRST(β1)FIRST(N)\cup FIRST(\beta_1)FIRST(N)FIRST(β1)

给出伪代码如下:

while (some set is changing)
	for (symbol alpha: symbols)
		if (alpha is terminal)
			FIRST[alpha] = {alpha}
		else if (alpha is nonterminal)
			for (production p: alpha->beta_1,...beta_n)
				for (i = 1; i <= n; i++)
					if (beta_i is terminal a)
						FIRST[alpha] += {a}
						break
					else if (beta_i is nonterminal M)
						FIRST[N] += FIRST[M]
						if (M cannot be null)
							break

类似地,可以给出FOLLOWFOLLOWFOLLOW集的不动点算法:
对所有非终结符AAA的FOLLOW(A)集合时,不断应用以下规则刷表,不再有集合被更新:
3. 如果存在产生式A→αBβA\rightarrow \alpha B\betaAαBβ,则β\betaβ除空符ϵ\epsilonϵ以外的所有符号都在FOLLOW(B)FOLLOW(B)FOLLOW(B)中;
4. 如果存在产生式A→αBA\rightarrow \alpha BAαB,或A→αBβA\rightarrow \alpha B\betaAαBβFIRST(β)FIRST(\beta)FIRST(β)包含空符ϵ\epsilonϵ,则FOLLOW(B)∪=FOLLOW(A)FOLLOW(B) \cup=FOLLOW(A)FOLLOW(B)=FOLLOW(A)

通俗地说,就是找到每个“紧跟其后”的终结符号。类比FIRSTFIRSTFIRST集的求法,把“紧跟其后”的符号的FIRST集加入所求的FOLLOW集中。如果“紧跟其后”的FIRSTFIRSTFIRST集有机会取空符,那么下一个集合的FIRSTFIRSTFIRST集合也有机会补上来“紧跟其后”,以此类推。
伪代码如下:

while (some set is changing)
	for (nonterminal A : nonterminals)
		for (production p: A->beta_1,...,beta_n)
			tmp = FOLLOW[N]
			for (i = n; i > 0; i--)
				if (beta_i == terminal a)
					tmp = {a}
				else if (beta_i == nonterminal M)
					FOLLOW[M] += tmp
					if (M cannot be null)
						tmp = FIRST[M]
					else
						tmp += FIRST[M]

已有FIRST集FIRST集FIRSTFOLLOWFOLLOWFOLLOW集,可按如下规则构造二维预测分析表M[][]M[][]M[][],该预测分析表的行头是非终结符号,列头是输入的下一个符号(终结符号)。对文法GGG的每个产生式A→αA\rightarrow\alphaAα,进行如下处理:

  1. 对于FIRST(α)FIRST(\alpha)FIRST(α)的每个终结符号α\alphaα,将A→αA\rightarrow\alphaAα加入到M[A,a]M[A,a]M[A,a]
  2. 如果ϵ∈FIRST(α)\epsilon\in FIRST(\alpha)ϵFIRST(α),则对于FOLLOW(A)FOLLOW(A)FOLLOW(A)的每个终结符号bbb,也将A→αA\rightarrow\alphaAα加入到M[A,b]M[A,b]M[A,b]中。

由上可以很清楚地看出FOLLOWFOLLOWFOLLOW集的作用:前一个FIRSTFIRSTFIRST为空的时候的“替补”。通过同时求解FIRSTFIRSTFIRST集和FOLLOWFOLLOWFOLLOW集,可以保证LL(1)分析总能前看到“正确”的符号。
由上,可以书写LL(1)分析伪代码:

tokens[]; // all tokens
i = 0;
stack = [S] // S是开始符号
while (stack != [])
	if (stack[top] is a terminal t)
		if (t == token[i++])
			pop();
		else
			error(...); // 朴素自顶向下的回溯改成直接报错
	else if (stack[top] is a nonterminal T)
		pop();
		push(table[T, tokens[i]]); // 朴素自顶向下的尝试展开任意表达式改成按表展开

尽管LL(1)对朴素自顶向下做了优化,但LL(1)在语法上仍然存在发生冲突的可能。下面简单讨论两种解决冲突的方法:消除左递归和提取左公因子。

消除左递归

有例子如下:

E -> E + T
   | T

此时由于LL算法总是从左到右读入,从左到右展开,那么计算的时候将会无限展开E->E+T而进入死循环。对于左递归的情况,有一般解法可以转换成右递归,如上式可改写如下:

E -> T E'
E'-> + T E'
提取左公因子

有例子如下:

X -> a Y
   | a Z

显然,此时同样的a可能会导向不同的表达式,存在冲突,直观的解决方法时将公共的a提取出来,如下所示:

X -> a X'
X'-> Y
   | Z

先整理自顶向下到这里,有空再整理自底向上……

参考资料:
《编译原理》课程,华保健,中国科学院大学(网易云课堂或b站免费观看)
《编译原理》(紫龙书),机械工业出版社
《编译原理》,清华大学出版社

递归下降分析法 一、实验目的: 根据某一文法编制调试递归下降分析程序,以便对任意输入的符号串进行分析。本次实验的目的主要是加深对递归下降分析法的理解。 二、实验说明 1递归下降分析法的功能 词法分析器的功能是利用函数之间的递归调用模拟语法树自上而下的构造过程。 2、递归下降分析法的前提 改造文法:消除二义性、消除左递归、提取左因子,判断是否为LL1)文法, 3、递归下降分析法实验设计思想及算法 为G的每个非终结符号U构造一个递归过程,不妨命名为U。 U的产生式的右边指出这个过程的代码结构: (1)若是终结符号,则向前看符号对照, 若匹配则向前进一个符号;否则出错。 (2)若是非终结符号,则调用与此非终结符对应的过程。当A的右部有多个产生式时,可用选择结构实现。 三、实验要求 (一)准备: 1.阅读课本有关章节, 2.考虑好设计方案; 3.设计出模块结构、测试数据,初步编制好程序。 (二)上课上机: 将源代码拷贝到机上调试,发现错误,再修改完善。第二次上机调试通过。 (三)程序要求: 程序输入/输出示例: 对下列文法,用递归下降分析法对任意输入的符号串进行分析: (1)E->eBaA (2)A->a|bAcB (3)B->dEd|aC (4)C->e|dc 输出的格式如下: (1)递归下降分析程序,编制人:姓名,学号,班级 (2)输入一以#结束的符号串:在此位置输入符号串例如:eadeaa# (3)输出结果:eadeaa#为合法符号串 注意: 1.如果遇到错误的表达式,应输出错误提示信息(该信息越详细越好); 2.对学有余力的同学,可以详细的输出推导的过程,即详细列出每一步使用的产生式。 (四)程序思路 0.定义部分:定义常量、变量、数据结构。 1.初始化:从文件将输入符号串输入到字符缓冲区中。 2.利用递归下降分析分析,对每个非终结符编写函数,在主函数中调用文法开始符号的函数。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值