思维导图:
3.5 LR分析器
LR分析器是一种强大的语法分析工具,用于自下而上的语法分析。它能够处理一大类上下文无关文法,适用于多数编程语言的语法分析。LR分析器的核心思想是从左到右读入输入(L),构造一个最右推导的逆(R),同时根据需要向前看k个符号来决定分析动作(k)。当k的值省略时,默认为1,即LR(1)分析器。
LR分析器的组成
LR分析器主要由以下几部分组成:
- 输入缓冲区:存放待分析的输入串。
- 栈:用于存储分析过程中的状态和文法符号。
- 分析表:包含动作(action)和转移(goto)函数,用于指导分析过程。
- 驱动程序:根据分析表和当前输入符号以及栈顶状态来决定下一步动作。
分析表的构成
分析表由动作函数(action)和转移函数(goto)组成:
- 动作函数(action):根据当前状态和输入符号决定是移进(shift)、归约(reduce)、接受(accept)还是报错(error)。
- 转移函数(goto):在归约操作后,根据当前状态和归约后的非终结符来决定下一个状态。
LR分析过程
LR分析过程包括移进、归约、接受和报错四种基本动作:
- 移进(shift):将当前输入符号和一个新状态一起压入栈中。
- 归约(reduce):根据某个产生式将栈顶的几个符号(与产生式右部匹配的)替换为产生式的左部,并根据goto表进入新状态。
- 接受(accept):当分析表指示当前状态和输入符号对应的动作为接受时,分析成功完成。
- 报错(error):遇到无法处理的状态和输入符号组合时,调用错误恢复例程。
构造分析表的方法
构造LR分析表有三种主要方法:
- 简单LR(SLR)方法:最易于实现,但适用范围最小。
- 规范LR(Canonical LR)方法:功能最强,但实现成本最高。
- 查前看LR(LALR)方法:功能和成本介于SLR和规范LR之间,适用于大多数编程语言的文法。
示例:LR分析器分析过程
考虑输入串id * id + id
的分析过程,LR分析器通过移进和归约操作,逐步分析输入串。例如,在处理id * id + id
时,分析器首先执行移进操作,将id
读入栈中;然后根据分析表执行归约操作,将符合产生式的符号串归约为对应的非终结符;最终,通过一系列移进和归约操作,如果整个输入串能被归约为起始符号,则分析成功。
LR分析器通过精确控制分析过程中的状态转换和符号处理,能够有效地解析复杂的文法结构,是编译器设计中常用的语法分析技术之一。
3.5.2 LR文法和LR分析方法的特点
LR文法和LR分析方法为编译器设计中的语法分析提供了一个强大而灵活的框架。它们能处理一大类上下文无关文法,支持几乎所有编程语言的语法结构。以下是LR文法和LR分析方法的一些关键特点:
LR文法的定义
一个文法被称为LR文法,如果能为其构造一个所有条目都唯一的LR分析表。这意味着,当句柄出现在栈顶时,LR分析器可以凭借栈顶的状态符号及向前看的k个符号(通常是0或1)来及时识别句柄,无需扫描整个栈。这种能力使LR分析器在自左向右扫描输入时非常高效。
LR(k)文法
LR(k)文法指的是那些使用LR分析器分析时,最多需要向前看k个符号来决定分析动作的文法。这里的k值越小,所需的向前看信息越少,分析器的设计和实现也就越简单。在实践中,k的值通常为1,即LR(1)分析器。
LR分析方法的优点
- 广泛的适用性:LR分析器可以识别所有能够用上下文无关文法表示的编程语言构造。
- 无回溯性:LR方法是已知最一般的无回溯移进-归约方法,意味着它在分析过程中不需要回退到之前的状态,从而保证了高效率。
- 超越LL方法:LR分析法能分析的文法类是LL分析法所能分析的文法类的真超集,这意味着LR方法能处理更加复杂的语言结构。
- 快速错误发现:在自左向右扫描输入的过程中,LR分析器能尽可能早地发现语法错误,这对于编译器的错误诊断和修复非常有用。
LR方法与LL方法的区别
与LL(k)方法相比,LR(k)分析器在决定使用哪个产生式进行归约之前,需要看到产生式右部的全部内容加上k个向前看符号。这个要求使得LR方法比LL方法能够处理更广泛的语言结构,因为LR方法在做决策时拥有更多的上下文信息。
缺点及解决方案
尽管LR方法非常强大,但手工构造LR分析表对于复杂的编程语言文法来说是不现实的。因此,开发了自动化工具,如Yacc,来生成LR分析器。这些工具能够自动产生分析器,对于含有二义性或难以从左到右扫描分析的文法结构提供诊断信息,大大简化了编译器开发过程。
总的来说,LR分析方法通过其强大的分析能力和自动化工具的支持,在现代编译器构建中扮演着关键角色。
3.5.3 构造SLR分析表
简介
构造SLR分析表是理解LR分析方法的起点,提供了一种相对简单的方式来构建LR分析器。SLR分析器使用的SLR分析表基于SLR方法,该方法通过识别文法的活前缀来指导分析器的移进-归约决策。活前缀是文法右句型的前缀,不超过该右句型的最右句柄的右端。
SLR方法的基本概念
- 项目:文法产生式在其右部某处加点的形式。点的位置表示分析过程中已经识别的产生式部分(点左侧)和期望看到的部分(点右侧)。
- LR(0)项目集的规范族:所有项目按一定方法组成的集合,这些集合对应到SLR分析器的状态,也是构造SLR分析表的基础。
- 拓广文法:为了方便分析,给原文法增加一个新的开始符号和产生式,以指示分析器何时停止分析并宣布接受输入。
构造SLR分析表的步骤
- 拓广文法:给定文法G,增加新的开始符号和产生式形成拓广文法G'。
- 构造LR(0)项目集规范族:从拓广文法G'出发,定义闭包函数
closure
和转移函数goto
来构造项目集的规范族。 - 定义动作函数和转移函数:
- 动作函数(action)根据当前状态和输入符号决定移进、归约、接受或报错动作。
- 转移函数(goto)根据当前状态和非终结符决定下一个状态。
- 填充分析表:
- 对每个状态,根据其项目集中的项目和文法的FOLLOW集来确定动作函数的条目。
- 使用
goto
函数填充转移函数的条目。
SLR分析器的特点和局限性
- SLR分析器是基于较为简单的SLR方法构造的,适用于一类特定的SLR文法。
- SLR方法利用文法的FOLLOW集来决定归约操作,这使得SLR分析器相对简单,但也限制了其适用范围。某些文法可能因为FOLLOW集的交集不为空而导致归约-归约冲突或移进-归约冲突,使得文法不是SLR(1)的。
示例和应用
构造SLR分析表的过程涉及识别文法的活前缀、定义闭包和转移函数,以及填充动作和转移表。通过这个过程,可以为一大类文法自动构造分析器,尽管SLR方法可能不适用于所有文法。对于一些复杂的文法,可能需要使用更先进的LR分析方法,如规范LR(Canonical LR)或查前看LR(LALR)方法,这些方法通过提供更精确的向前看信息或通过状态合并来增强分析能力,同时控制状态数量和分析表的大小。
3.5.4 构造规范的LR分析表
规范的LR(1)分析表构造过程,相比于SLR分析,引入了LR(1)项目,增加了每个项目中的向前看符号(即搜索符),以提供更精确的归约条件。这种方法允许构造出能够识别更广泛语言范围的LR分析器。
规范LR(1)项目和项目集
- LR(1)项目:形式为A→α⋅β,a的项目,其中A → αβ是产生式,a是终结符号或$,α·β表示产生式的已见和未见部分,a是项目的搜索符,指出在哪些向前看符号下可以进行归约。
- 项目集:包含一组LR(1)项目,每个项目集对应LR分析器中的一个状态。
项目集规范族的构造
- 使用闭包(closure)和转移(goto)函数构造LR(1)项目集的规范族。与LR(0)相比,LR(1)考虑了向前看符号,使得闭包和转移函数能够根据产生式和向前看符号的不同生成更多的项目集,从而细化状态的划分。
构造规范LR(1)分析表的步骤
- 拓广文法:给定文法G,增加一个新的开始符号和产生式形成拓广文法G'。
- 构造项目集规范族:利用修改后的closure和goto函数,针对G'的每个LR(1)项目构造项目集规范族。
- 填充分析表:
- 动作函数(action):根据每个状态的项目集,结合项目中的搜索符,确定在特定输入符号下应执行的动作(移进、归约、接受或报错)。
- 转移函数(goto):基于非终结符的转移,确定状态转移。
规范LR(1)分析的优点与挑战
- 优点:相比SLR和LALR方法,规范LR(1)分析提供了最精确的归约条件,允许处理更复杂的文法,包括那些SLR或LALR方法无法处理的文法。
- 挑战:构造规范LR(1)分析表比SLR或LALR更为复杂,因为它可能产生更多的状态和更大的分析表,这导致分析器的实现和执行可能更为复杂和耗时。
规范LR(1)分析器的应用
尽管规范LR(1)分析器的构造和实现较为复杂,但它能处理的文法范围最广,为语法分析提供了强大的能力。当语言的文法特别复杂,或需要精确控制归约时机时,使用规范LR(1)分析是最合适的选择。然而,由于其高复杂度,在实践中,人们常常寻求在分析能力和资源消耗之间平衡的方法,如LALR分析。
3.5.5 构造LALR分析表
LALR(Look-Ahead LR)方法结合了LR(1)的精确性和SLR的简洁性,生成大小适中且能力强大的分析表。它特别适合用于实际编译器构建,因为LALR分析表既不如SLR方法限制性那么强,也没有规范LR方法那么复杂和庞大。
LALR分析表的构造
LALR方法通过合并具有相同核心(即忽略搜索符后项目集相同)的LR(1)项目集来生成。这种合并减少了状态的数量,同时保留了必要的向前看信息以避免移进-归约冲突和归约-归约冲突,尽管某些情况下可能导致新的归约-归约冲突。
- 构造LR(1)项目集规范族:首先,为拓广文法构造完整的LR(1)项目集规范族。
- 合并同心项目集:查找所有LR(1)项目集,将具有相同核心(产生式和点的位置)但搜索符不同的项目集合并。合并后的项目集将具有相同的核心项目,但它们的搜索符是原始项目集搜索符的并集。
- 构造LALR分析表:基于合并后的项目集构造LALR分析表,包括动作(action)和转移(goto)函数。合并过程可能会导致某些新的归约-归约冲突,需要额外处理。
LALR与LR(1)和SLR的比较
- 与LR(1)比较:LALR分析表比规范LR(1)的小得多,因为它通过合并具有相同核心的项目集来减少状态的数量。这使得LALR更适合实际应用,尽管它可能牺牲了一些分析的精确性。
- 与SLR比较:LALR分析器能处理一些SLR方法无法处理的文法。LALR方法通过保留LR(1)方法中的部分向前看信息来避免SLR方法中的一些冲突。
示例和应用
文法(3.13)提供了构造LALR分析表的一个实例。通过合并具有相同核心的LR(1)项目集,LALR方法生成了一个既不过于庞大也不失精确性的分析表,适用于许多编程语言的文法分析。LALR分析器在实践中广泛用于各种编译器的构建,如著名的Yacc(Yet Another Compiler Compiler)工具。
总结
LALR分析方法是编译器构建中非常重要的工具,它提供了一种平衡分析表大小和分析能力的有效途径。通过合并LR(1)项目集,LALR方法能够以较小的性能代价处理复杂的文法,使其成为编译器设计者的首选技术之一。
非二义且非LR的上下文无关文法
存在非二义且非LR的上下文无关文法,这表明LR分析方法的限制并非仅与文法的二义性有关。即使一个文法是非二义的,它仍可能因为无法在自左向右扫描时及时识别句柄而不是LR文法。
示例分析
例3.36
文法 S→aSa∣bSb∣ϵ 定义了语言 L={wwR∣w∈(a∣b)∗},即所有由字符 a
和 b
组成、且为其自身逆序的字符串。直观上,分析这类字符串时,前一半应压栈,而后一半则与栈顶元素进行匹配。问题在于,无法仅通过向前看固定数量的字符判断是否已到达字符串中点,导致无法决定何时进行空归约。因此,该文法对于任何固定的k值,都不是LR(k)的。
例3.37
这个例子介绍了语言 L={anbm∣n<m} 的三种不同文法:一种是LR(1)的,一种是二义的,另一种是非二义且非LR(1)的。该语言包含的字符串由多个字符 a
后跟更多字符 b
组成。
- LR(1)文法:通过将每个
a
与一个b
匹配,并允许额外的b
存在,可以构造出LR(1)文法。示例文法通过分别处理与a
直接匹配的b
和额外的b
,避免了分析过程中的冲突。 - 非二义且非LR(1)文法:尝试在分析过程中先处理所有的
a
,然后归约中间的一些b
,以确保剩余的b
数量与a
相匹配,会遇到决定何时停止归约的问题。向前看一个符号无法准确判断,导致这种文法非LR(1)。 - 二义文法:允许
a
和b
以多种方式配对的文法是二义的。二义性来源于a
和b
的配对方式不唯一。
结论
这些示例说明了非二义文法仍可能因为分析过程中的技术限制而非LR,特别是在需要精确控制归约时刻但无法通过有限的向前看信息做到这一点的场景中。LR分析方法虽强大,但在处理某些特定类型的语言结构时存在局限,特别是当这些结构要求在分析过程中有明确的分界点判断时。这些限制强调了在编译器设计中选择合适语法和分析策略的重要性。