1. 理解SLR(1)分析表的核心挑战
第一次接触编译原理中的语法分析时,很多同学都会被各种分析表搞得晕头转向。我自己当年学习SLR(1)时,最困惑的就是为什么有些状态会同时出现"移进"和"归约"两种动作,这就像开车时导航突然告诉你"既可以左转也可以直行"一样让人抓狂。
SLR(1)分析表的核心价值就在于它能帮我们解决这类冲突。与LR(0)分析器相比,SLR(1)多了一个关键武器:Follow集合。这个小小的改进让它的分析能力大幅提升,可以处理更多类型的文法。举个例子,假设我们有个简单的算术表达式文法:
(0) S' → E (1) E → E + T (2) E → T (3) T → T * F (4) T → F (5) F → ( E ) (6) F → id这个文法看起来简单,但在构造分析表时就会遇到典型的移进-归约冲突。SLR(1)的聪明之处在于,它会检查下一个输入符号是否在归约产生式左部非终结符的Follow集中,只有符合条件时才执行归约,否则就选择移进。
2. 从文法到项目集规范族
2.1 文法的拓广与项目表示
构造SLR(1)分析表的第一步是对原文法进行拓广。这就像给房子打地基,虽然看起来多此一举,但能确保分析过程有个明确的起点。拓广方法很简单:添加一个新的开始符号S'和产生式S'→S。对于上面的例子,我们已经有了(0) S'→E。
接下来要把每个产生式转换成多个"项目"。项目就是在产生式右部某个位置加个点,表示分析进度。比如E→E+T会产生三个项目:
- E→·E+T
- E→E·+T
- E→E+·T
- E→E+T·
点在最后的就是归约项目,表示这个产生式已经完整识别,可以归约了。我第一次做这个练习时,经常漏掉某些项目,后来发现一个技巧:对于右部有n个符号的产生式,它会产生n+1个项目。
2.2 构造项目集规范族
项目集规范族(Canonical Collection)是SLR(1)分析的核心数据结构,可以看作是一个DFA的状态集合。构造过程采用闭包(closure)和转移(goto)两个操作:
闭包操作就像打开一个俄罗斯套娃。从一个核心项目出发,如果点后面是非终结符,就要把这个非终结符的所有产生式也加进来(点在开头)。比如对于项目E→·E+T,因为E后面有点,所以要把E的所有产生式(E→·E+T和E→·T)都加入闭包。
转移操作则是在看到某个符号X后,把项目中所有点在X前面的项目收集起来,然后对它们求闭包。这就像在迷宫中沿着标记前进,每个符号都指引我们前往新的状态。
3. 构建活前缀DFA
3.1 识别活前缀
活前缀(Living Prefix)是指规范句型的一个前缀,它不会越过该句型的最右句柄。简单说,就是分析过程中栈里可能出现的符号串。构造DFA的过程实际上就是在识别所有可能的活前缀。
以我们的表达式文法为例,从初始项目集I0(包含S'→·E和E的闭包)开始,逐步计算所有可能的转移。这个过程需要耐心,我建议在纸上画出每个状态和转移,就像这样:
I0: S'→·E E→·E+T E→·T T→·T*F T→·F F→·(E) F→·id从I0出发,看到E就转移到I1,看到T到I2,看到F到I3,看到(到I4,看到id到I5。每个新状态都要计算其闭包。
3.2 处理冲突状态
在构造DFA时,最需要关注的就是包含冲突的项目集。典型的冲突有两种:
- 移进-归约冲突:同一个符号下既可以移进又可以归约
- 归约-归约冲突:同一个符号下可以用不同产生式归约
比如在状态I2中:
- E→T·
- T→T·*F
当下一符号是*时,既可以按E→T·归约,也可以按T→T·F移进。这就是典型的移进-归约冲突。SLR(1)的解决之道是检查是否在Follow(E)中。如果不在,就选择移进;如果在,那就真的冲突了,文法就不是SLR(1)的。
4. 计算关键Follow集合
4.1 Follow集合的定义与计算
Follow(A)是所有句型中可能出现在非终结符A后面的终结符集合。计算Follow集的规则有三条:
- 对于开始符号S,把$(输入结束标记)加入Follow(S)
- 如果存在产生式A→αBβ,把First(β)的非ε元素加入Follow(B)
- 如果存在产生式A→αB或A→αBβ且ε∈First(β),把Follow(A)加入Follow(B)
对于我们的表达式文法,Follow集计算如下:
- Follow(S') = {$}
- Follow(E) = {$, +, )}
- Follow(T) = {$, +, ), *}
- Follow(F) = {$, +, ), *}
4.2 用Follow集解决冲突
回到状态I2的冲突,Follow(E)是{$, +, )},而不在其中。因此当下一符号是时,我们选择移进;如果是$, +或),就按E→T·归约。这样冲突就完美解决了。
这个判断过程就是SLR(1)比LR(0)强大的地方。LR(0)遇到这种情况只能报错,而SLR(1)通过Follow集的额外信息做出了合理选择。不过要注意,如果*也在Follow(E)中,那冲突就无法用SLR(1)解决,需要考虑更强的LR(1)或LALR(1)方法。
5. 填充分析表
5.1 分析表的结构
SLR(1)分析表有两个部分:动作表(action)和转移表(goto)。动作表处理终结符,包含四种操作:
- 移进(s):将当前符号和转移状态压栈
- 归约(r):按指定产生式归约
- 接受(acc):成功接受输入
- 空白:错误情况
转移表则处理非终结符,指示goto操作后的新状态。
5.2 填表步骤详解
填表过程就是遍历所有状态和符号:
- 对于每个移进动作,比如在状态Ii看到符号a转移到Ij,就在action[i,a]填"s j"
- 对于归约项目A→α·在状态Ii,对每个a∈Follow(A),在action[i,a]填"r k",k是产生式编号
- 对于接受项目S'→S·在状态Ii,在action[i,$]填"acc"
- 对于goto转移,比如在状态Ii看到A转移到Ij,就在goto[i,A]填"j"
以状态I2为例:
- 包含E→T·和T→T·*F
- 对于Follow(E)中的$,+,),填r2(用产生式E→T归约)
- 对于*,填s7(移进到I7)
- goto部分:没有新的非终结符转移
5.3 验证分析表
填完表后要仔细检查是否有冲突(同一格子有多个动作)。没有冲突才是有效的SLR(1)分析表。对于我们的表达式文法,最终的分析表如下:
| 状态 | id | + | * | ( | ) | $ | E | T | F |
|---|---|---|---|---|---|---|---|---|---|
| 0 | s5 | s4 | 1 | 2 | 3 | ||||
| 1 | s6 | acc | |||||||
| 2 | r2 | s7 | r2 | r2 | |||||
| 3 | r4 | r4 | r4 | r4 | |||||
| ... | ... |
6. 实战调试技巧
在实际构造SLR(1)分析表时,有几个容易出错的地方需要特别注意:
首先,项目集闭包的计算要完整。我经常犯的错误是漏掉间接的非终结符。比如A→·B的闭包不仅要加B的产生式,如果B→·C,还要加C的产生式。
其次,Follow集的计算要准确。特别是第三条规则(A→αB的情况),需要反复传递Follow集,直到不再变化为止。建议用不同颜色标注已经处理过的非终结符。
最后,填表时要仔细核对每个动作的来源。一个好的做法是为每个填写的动作标注理由,比如:
- action[2,*]=s7:因为I2有T→T·F,看到移进到I7
- action[2,+]=r2:因为I2有E→T·,+∈Follow(E)
当遇到真正的冲突(无法用SLR(1)解决)时,可能需要考虑修改文法或者使用更强大的分析器生成工具。在实践中,很多编程语言的文法都经过特别设计,使其能被SLR(1)或LALR(1)分析器处理。