从“打架”到“和解”:图解LR(0)的移进-归约冲突,以及SLR(1)如何用FOLLOW集摆平它
想象一下你正在指挥一场交响乐演出,乐手们(语法分析器)需要根据乐谱(文法规则)演奏。突然,小提琴手和钢琴手同时要求独奏(移进和归约冲突),整个乐团陷入混乱。这就是LR(0)分析器面临的典型困境——当它在某个状态看到下一个符号时,可能同时存在"继续读取"和"立即化简"两种合法操作,却缺乏判断依据。
1. 冲突现场:LR(0)分析器的"决策瘫痪"
让我们用一个简单的四则运算文法作为案例:
E → E + T | T T → a | ( E )1.1 状态机里的"交通堵塞"
构建LR(0)自动机时,在状态I₁会出现这样的场景:
E' → E • E → E • + T (移进项)此时如果下一个输入符号是+,分析器就陷入两难:
- 移进派:认为应该把
+压入栈中,等待后续的T - 归约派:主张立即将
E归约为开始符号
冲突状态示意图: ┌───────────┐ ┌───────────┐ │ 状态 I₁ │ │ 输入流 │ │ │ │ + 3 ) │ │ E'→E• │ └───────────┘ │ E→E•+T │ └───────────┘1.2 冲突的数学本质
这种冲突本质上是项目集闭包的局限性造成的:
- LR(0)的"0"表示它不预读任何符号
- 项目集中同时存在
A→α•和B→β•aγ时必然冲突 - 在分析表中表现为同一个单元格需要同时填写"s"和"r"
关键观察:冲突的产生是因为分析器在决策时缺乏上下文信息,就像交警没有红绿灯只能靠猜
2. SLR(1)的调解艺术:引入FOLLOW集当"裁判"
SLR(1)的突破在于它聪明地利用了FOLLOW集这个上下文线索。让我们看看这个"调解专家"如何工作:
2.1 FOLLOW集的角色定位
对于产生式A → α,FOLLOW(A)表示所有可能出现在A后面的终结符。在前例中:
FOLLOW(E) = { +, ), # } (#表示输入结束)2.2 冲突解决算法
当状态中出现移进-归约冲突时:
- 计算归约产生式左部非终结符的FOLLOW集
- 检查冲突符号是否属于该FOLLOW集
- 如果不属于:允许移进
- 如果属于:冲突无法解决,文法不是SLR(1)
应用到我们的案例:
冲突符号: '+' 归约项: E'→E• 的 FOLLOW(E') = {#} 交集: {#} ∩ {'+'} = ∅ → 允许移进2.3 分析表升级对比
原始LR(0)分析表的冲突单元格:
| 状态 | + | ... |
|---|---|---|
| I₁ | s/r |
SLR(1)改进后的分析表:
| 状态 | + | ) | # | ... |
|---|---|---|---|---|
| I₁ | s | r |
# FOLLOW集计算伪代码 def compute_follow(grammar): follow = defaultdict(set) follow[start_symbol].add('#') changed = True while changed: changed = False for prod in grammar: A = prod.left for B in prod.right: if is_nonterminal(B): # 规则1:将FIRST(β)-ε加入FOLLOW(B) # 规则2:如果β可推导出ε,将FOLLOW(A)加入FOLLOW(B) # 具体实现略... return follow3. 视觉化解析:从冲突到和谐
3.1 DFA状态图对比
LR(0)的冲突状态:
graph LR I1["I₁: E'→E• E→E•+T"] -->|+| I2 I1 -->|#| ACCEPTSLR(1)的解决方案:
graph LR I1["I₁: E'→E• (FOLLOW={#}) E→E•+T"] -->|+| I2 I1 -->|#| ACCEPT3.2 关键区别总结
| 特性 | LR(0) | SLR(1) |
|---|---|---|
| 前瞻符号 | 无 | 1个 |
| 冲突检测 | 项目集闭包直接暴露冲突 | 通过FOLLOW集过滤假冲突 |
| 分析表大小 | 较小 | 与LR(0)相同 |
| 文法兼容性 | 仅限无冲突文法 | 可处理部分冲突文法 |
4. 实战演练:手把手解决真实冲突
让我们通过一个更复杂的案例巩固理解:
4.1 文法定义
S → L = R | R L → * R | id R → L4.2 冲突识别
在状态I₂会出现:
S → L•=R R → L•当输入符号是=时:
- LR(0)会直接报冲突
- SLR(1)计算:
- FOLLOW(R) = { =, # }
=∈ FOLLOW(R) → 真冲突!
4.3 解决方案
这种情况下SLR(1)也无法解决,需要更强大的LR(1)或LALR(1)分析器。这引出了语法分析技术的演进路径:
- LR(0) → 2. SLR(1) → 3. LR(1) → 4. LALR(1)
每种方法都在前瞻信息和状态数量之间寻找平衡:
- LR(0):简单但能力弱
- SLR(1):用FOLLOW集做简单过滤
- LR(1):精确计算每个项目的前瞻符号
- LALR(1):合并LR(1)的同心状态
在编译器设计实践中,大多数场景下SLR(1)已经能处理80%以上的文法冲突。比如经典的Java语言规范文法,经过适当调整后可以用SLR(1)分析器处理。