第57集:LR(1)和LALR(1)分析器
学习目标
- 掌握LR(1)分析器的工作原理
- 理解LR(1)项和项集的构建方法
- 掌握LALR(1)分析器的基本思想
- 了解LR(1)和LALR(1)分析表的生成过程
- 理解LALR(1)分析器的优势和局限性
1. LR(1)分析器详解
1.1 基本思想
LR(1)分析器是对SLR(1)分析器的进一步改进,它的基本思想是:在每个LR(0)项中添加一个展望符(lookahead),用于更精确地决定何时进行归约操作。展望符表示在归约后可能出现的下一个终结符。
1.2 LR(1)项
LR(1)项是一个LR(0)项加上一个展望符集合,表示为 [A → α·β, a],其中:
A → α·β是一个LR(0)项a是一个终结符或 $,表示展望符
1.3 LR(1)闭包运算
LR(1)闭包运算与LR(0)类似,但需要考虑展望符的传递:
算法:
function CLOSURE(I) {
J = I
repeat {
for each item [A → α·Bβ, a] in J {
for each production B → γ in G {
for each b in FIRST(βa) {
if item [B → ·γ, b] not in J {
add [B → ·γ, b] to J
}
}
}
}
} until J doesn't change
return J
}1.4 LR(1) GOTO运算
LR(1) GOTO运算与LR(0)类似,但操作的是LR(1)项:
GOTO(I, X) = CLOSURE({ [A → αX·β, a] | [A → α·Xβ, a] ∈ I })
1.5 LR(1)项集规范族的构建
LR(1)项集规范族的构建与LR(0)类似,但使用LR(1)闭包和GOTO运算:
function ITEM_SETS(G') {
C = { CLOSURE({ [S' → ·S, $] }) }
repeat {
for each item set I in C {
for each grammar symbol X {
if GOTO(I, X) is not empty and not in C {
add GOTO(I, X) to C
}
}
}
} until C doesn't change
return C
}1.6 LR(1)分析表的生成
LR(1)分析表的生成与SLR(1)类似,但使用LR(1)项的展望符:
ACTION表生成规则:
- 对于项集 I 中的每个项
[A → α·aβ, b],如果 GOTO(I, a) = J,则 ACTION[I, a] = sJ(移进) - 对于项集 I 中的每个项
[A → α·, a],如果 A 不是开始符号,则 ACTION[I, a] = rk(归约,k是产生式编号) - 对于项集 I 中的项
[S' → S·, $],则 ACTION[I, $] = accept(接受)
GOTO表生成规则:
- 对于每个项集 I 和非终结符 A,如果 GOTO(I, A) = J,则 GOTO[I, A] = J
1.7 LR(1)分析器的优势
LR(1)分析器的主要优势是它可以处理SLR(1)无法处理的一些冲突情况。例如,对于以下文法:
S → A | B
A → a
B → aSLR(1)分析器会在状态中产生归约-归约冲突,因为 FOLLOW(A) = FOLLOW(B) = { $ },当输入符号是 $ 时,无法决定是归约到 A 还是 B。而LR(1)分析器通过展望符可以区分这种情况。
1.8 示例:LR(1)项集构建
考虑以下文法:
G: S → A | B
A → a
B → a扩展文法:
G': S' → S
S → A | B
A → a
B → aLR(1)项集构建:
初始项集 I0:
I0 = CLOSURE({ [S' → ·S, $] })应用闭包运算:
I0: [S' → ·S, $] [S → ·A, $] [S → ·B, $] [A → ·a, $] // 因为 FIRST(A$) = {a} [B → ·a, $] // 因为 FIRST(B$) = {a}**计算 GOTO(I0, S)**:
I1 = GOTO(I0, S) = CLOSURE({ [S' → S·, $] })I1: [S' → S·, $]**计算 GOTO(I0, A)**:
I2 = GOTO(I0, A) = CLOSURE({ [S → A·, $] })I2: [S → A·, $]**计算 GOTO(I0, B)**:
I3 = GOTO(I0, B) = CLOSURE({ [S → B·, $] })I3: [S → B·, $]**计算 GOTO(I0, a)**:
I4 = GOTO(I0, a) = CLOSURE({ [A → a·, $], [B → a·, $] })I4: [A → a·, $] [B → a·, $]
1.9 LR(1)分析表的生成
对于上面的例子,LR(1)分析表如下:
| 状态 | ACTION | GOTO | ||
|---|---|---|---|---|
| a | $ | S | A | |
| 0 | s4 | 1 | 2 | |
| 1 | acc | |||
| 2 | r2 | |||
| 3 | r3 | |||
| 4 | r4/r5 |
注意: 在状态4中,当输入符号是 $ 时,既可以归约 A→a,又可以归约 B→a,产生归约-归约冲突。这是因为这两个产生式的展望符都是 $。
2. LALR(1)分析器详解
2.1 基本思想
LALR(1)分析器是对LR(1)分析器的优化,它的基本思想是:将LR(1)项集中具有相同核心(即去掉展望符后的LR(0)项集)的项集合并,以减少分析表的大小,同时保持LR(1)分析器的大部分能力。
2.2 核心的概念
LR(1)项集的核心是指去掉展望符后的LR(0)项集。例如,对于LR(1)项集 { [A → α·β, a], [B → γ·δ, b] },其核心是 { A → α·β, B → γ·δ }。
2.3 LALR(1)项集的构建
LALR(1)项集的构建步骤:
- 构建LR(1)项集规范族
- 合并具有相同核心的LR(1)项集:将所有具有相同核心的LR(1)项集合并为一个LALR(1)项集,合并后的项集的展望符是各个LR(1)项集展望符的并集
- 构建LALR(1)分析表:使用合并后的项集生成分析表
2.4 LALR(1)分析表的生成
LALR(1)分析表的生成与LR(1)类似,但使用合并后的项集:
ACTION表生成规则:
- 对于项集 I 中的每个项
[A → α·aβ, b],如果 GOTO(I, a) = J,则 ACTION[I, a] = sJ(移进) - 对于项集 I 中的每个项
[A → α·, a],如果 A 不是开始符号,则 ACTION[I, a] = rk(归约,k是产生式编号) - 对于项集 I 中的项
[S' → S·, $],则 ACTION[I, $] = accept(接受)
GOTO表生成规则:
- 对于每个项集 I 和非终结符 A,如果 GOTO(I, A) = J,则 GOTO[I, A] = J
2.5 LALR(1)分析器的优势
LALR(1)分析器的主要优势包括:
- 分析表大小适中:与SLR(1)分析表大小相当,远小于LR(1)分析表
- 分析能力强:可以处理大多数实用的上下文无关文法
- 实现相对简单:相比LR(1)分析器,实现复杂度较低
- 错误检测及时:与LR(1)分析器一样,可以在错误发生后的第一个符号处检测到错误
2.6 LALR(1)分析器的局限性
LALR(1)分析器的主要局限性是:在合并具有相同核心的LR(1)项集时,可能会引入新的归约-归约冲突,即使原来的LR(1)项集没有冲突。
2.7 示例:LALR(1)项集合并
考虑以下文法:
G: S → A
A → aB | bC
B → c | a
C → c | bLR(1)项集:
- 项集 I1:
{ [A → a·B, $], [B → ·c, $], [B → ·a, $] } - 项集 I2:
{ [A → b·C, $], [C → ·c, $], [C → ·b, $] }
这两个项集的核心不同,所以不会合并。
另一个例子:
G: S → A | B
A → a
B → bLR(1)项集:
- 项集 I1:
{ [A → a·, $] } - 项集 I2:
{ [B → b·, $] }
这两个项集的核心不同,所以不会合并。
3. LR(1)与LALR(1)的比较
3.1 分析能力
- LR(1)分析器:能力最强,可以处理几乎所有上下文无关文法
- LALR(1)分析器:能力略弱于LR(1),但可以处理大多数实用的上下文无关文法
- SLR(1)分析器:能力较弱,只能处理部分上下文无关文法
- LR(0)分析器:能力最弱,只能处理极少数上下文无关文法
3.2 分析表大小
- LR(1)分析器:分析表最大,可能是LALR(1)的数倍
- LALR(1)分析器:分析表大小与SLR(1)相当
- SLR(1)分析器:分析表较小
- LR(0)分析器:分析表最小
3.3 实现复杂度
- LR(1)分析器:实现最复杂
- LALR(1)分析器:实现复杂度适中
- SLR(1)分析器:实现相对简单
- LR(0)分析器:实现最简单
3.4 实际应用
- LR(1)分析器:由于分析表过大,实际应用较少
- LALR(1)分析器:是实际应用的首选,被广泛用于现代编译器
- SLR(1)分析器:适用于简单语言,实现简单
- LR(0)分析器:主要用于教学,实际应用很少
4. 实战案例:构建LALR(1)分析器
4.1 文法定义
S → E
E → E + T | T
T → T * F | F
F → ( E ) | id4.2 扩展文法
S' → S
S → E
E → E + T | T
T → T * F | F
F → ( E ) | id4.3 构建LR(1)项集规范族
由于篇幅限制,这里省略LR(1)项集的详细构建过程。构建完成后,我们会得到多个LR(1)项集,其中一些具有相同的核心。
4.4 合并具有相同核心的项集
例如,以下两个LR(1)项集具有相同的核心:
项集 I5:
[F → id·, +]
[F → id·, *]
[F → id·, )]
[F → id·, $]**项集 I5'**:
[F → id·, +]
[F → id·, *]
[F → id·, )]
[F → id·, $]它们的核心都是 { F → id· },所以可以合并为一个LALR(1)项集。
4.5 构建LALR(1)分析表
合并后的LALR(1)分析表与SLR(1)分析表类似,但具有更精确的归约条件。对于上面的表达式文法,LALR(1)分析表与SLR(1)分析表相同,因为该文法是SLR(1)的。
4.6 分析示例
以输入串 id + id * id 为例,LALR(1)分析器的分析过程与SLR(1)分析器相同:
步骤:
- 初始状态:状态栈 [0],符号栈 [$],输入
id + id * id $ - ACTION[0, id] = s5(移进),状态栈 [0, 5],符号栈 [$id],输入
+ id * id $ - ACTION[5, +] = r6(归约 F→id),弹出状态 5,状态栈 [0],GOTO[0, F] = 3,状态栈 [0, 3],符号栈 [$F],输入
+ id * id $ - ACTION[3, +] = r4(归约 T→F),弹出状态 3,状态栈 [0],GOTO[0, T] = 2,状态栈 [0, 2],符号栈 [$T],输入
+ id * id $ - ACTION[2, +] = r2(归约 E→T),弹出状态 2,状态栈 [0],GOTO[0, E] = 1,状态栈 [0, 1],符号栈 [$E],输入
+ id * id $ - ACTION[1, +] = s6(移进),状态栈 [0, 1, 6],符号栈 [$E+],输入
id * id $ - ACTION[6, id] = s5(移进),状态栈 [0, 1, 6, 5],符号栈 [$E+id],输入
* id $ - ACTION[5, *] = r6(归约 F→id),弹出状态 5,状态栈 [0, 1, 6],GOTO[6, F] = 3,状态栈 [0, 1, 6, 3],符号栈 [$E+F],输入
* id $ - ACTION[3, *] = r4(归约 T→F),弹出状态 3,状态栈 [0, 1, 6],GOTO[6, T] = 9,状态栈 [0, 1, 6, 9],符号栈 [$E+T],输入
* id $ - ACTION[9, ] = s7(移进),状态栈 [0, 1, 6, 9, 7],符号栈 [$E+T],输入
id $ - ACTION[7, id] = s5(移进),状态栈 [0, 1, 6, 9, 7, 5],符号栈 [$E+T*id],输入
$ - ACTION[5, $] = r6(归约 F→id),弹出状态 5,状态栈 [0, 1, 6, 9, 7],GOTO[7, F] = 10,状态栈 [0, 1, 6, 9, 7, 10],符号栈 [$E+T*F],输入
$ - ACTION[10, $] = r5(归约 T→T*F),弹出状态 7, 10,状态栈 [0, 1, 6, 9],GOTO[6, T] = 9,状态栈 [0, 1, 6, 9],符号栈 [$E+T],输入
$ - ACTION[9, $] = r3(归约 E→E+T),弹出状态 6, 9,状态栈 [0, 1],GOTO[0, E] = 1,状态栈 [0, 1],符号栈 [$E],输入
$ - ACTION[1, $] = acc(接受),分析成功
5. LALR(1)分析器的实现工具
5.1 Yacc/Bison
Yacc(Yet Another Compiler-Compiler)是一个经典的LALR(1)分析器生成工具,由Stephen C. Johnson在1975年开发。Bison是Yacc的GNU实现,是目前最常用的LALR(1)分析器生成工具之一。
特点:
- 生成LALR(1)分析器
- 支持文法规则和动作代码
- 提供错误处理机制
- 与词法分析器Flex配合使用
5.2 其他工具
- ANTLR:生成LL(*)分析器,也支持LALR(1)
- JavaCC:生成LL(k)分析器
- Happy:Haskell的LALR(1)分析器生成工具
- Ply:Python的lex/yacc实现
6. 自测题
LR(1)分析器与SLR(1)分析器的主要区别是什么?
什么是LR(1)项?它与LR(0)项有什么不同?
LALR(1)分析器的基本思想是什么?它如何减少分析表的大小?
比较LR(1)、LALR(1)、SLR(1)和LR(0)分析器的分析能力和分析表大小。
为什么LALR(1)分析器在实际应用中被广泛使用?
7. 小结
在本集中,我们详细介绍了LR(1)和LALR(1)分析器的工作原理、项集构建和分析表生成方法。LR(1)分析器通过在每个项中添加展望符,实现了更精确的归约决策,能够处理SLR(1)无法处理的冲突情况。LALR(1)分析器通过合并具有相同核心的LR(1)项集,在保持LR(1)分析器大部分能力的同时,显著减少了分析表的大小,成为实际应用中的首选分析方法。
8. 下集预告
下一集将介绍 语法分析器的错误处理,详细讲解语法分析过程中的错误检测、错误恢复策略和错误信息生成方法,帮助我们构建更加健壮的编译器前端。
9. 参考资料
- 《编译原理》(龙书),Alfred V. Aho 等著
- 《编译原理教程》,蒋立源等著
- 《编译器设计》,Alexander A. Stepanov 等著
- LR parser - Wikipedia:https://en.wikipedia.org/wiki/LR_parser
- Lookahead LR parser - Wikipedia:https://en.wikipedia.org/wiki/Lookahead_LR_parser
- Yacc - Wikipedia:https://en.wikipedia.org/wiki/Yacc
- Bison - GNU Project:https://www.gnu.org/software/bison/