复习材料,不建议阅读
确定的自顶向下分析方法,是从文法的开始符号出发,考虑如何根据当前的输入符号(单词符号)唯一地确定选用哪个产生式替换相应非终结符以往下推导,或如何构造一棵相应的语法树。
- 正规情况
S→pA∣qBA→cAd∣aB→dB∣b
文法右部始终由终结符号开始;相同左部时,右部是不同终结符号开始。
就可以直接根据当前输入符号决定产生式。
- 一般情况
S→Ap∣BqA→a∣cAB→dB∣b
右部不全由终结符开始;相同左部时,右部是不同符号开始;没有空产生式。
问题 A:当输入符号是 c 时,从 S 出发选择 Ap 还是 Bq 能推出 c 开头串?
- 舍空产生式
S→aA∣dA→bAS∣ε
有空产生式了。
问题B:如果有一个输入串 W=abd,推导过程:S⇒aA⇒abAS⇒abS⇒abd;在 abAS⇒abS 的时候,A 的产生式右部开始符号集都不包含 d 只有 ε 产生式,就可以认为 d 的匹配实际依赖于 A 后面的符号 S。
FIRST(a) 是符号串 a 可以推导出的所有串的首终结符号集合,称为 a 的开始符号集或首符号集。
FIRST(a)={α∣a⇒∗αβ,α∈VT,β∈V∗}
a 能推导出空的时候,ε∈FIRST(a)。
由此解决问题 A,求 Ap 与 Bq 的 FIRST 集(且它们恰好不相交)就可以得出输入符号选择哪个产生式。
FOLLOW(A) 是在所有句型中紧跟在非终结符 A 后面的终结符号集合。# 是输入串的结束符,也称输入串括号。
FOLLOW(A)={a∣S⇒∗…Aa…,a∈VT}
S 能推导出 …A 的时候,#∈FOLLOW(A)。
结合 SELECT 由此解决问题 B,A、S 不能同时推导出空,替换仍是唯一确定的。
SELECT(A→α) 是产生式 A→α 的选择符号集,表示当遇到这些输入符号时应该选择该产生式进行推导。
- 若 ε∈/FIRST(α),则 SELECT(A→α)=FIRST(α)。
- 若 ε∈FIRST(α),则 SELECT(A→α)=(FIRST(α)−{ε})∪FOLLOW(A)。
简单来说就是,FIRST 不含空选 FIRST,含空就 FIRST 去掉空并 FOLLOW。
一个上下文无关文法是 LL(1) 文法的充分必要条件是,对每个非终结符 A 的两个不同产生式 A→α 和 A→β,满足:SELECT(A→α)∩SELECT(A→β)=∅。(同一左部 SELECT 集不相交)
其中 α 和 β 不同时能推导出 ε。
- 求能推出 ε 的非终结符
建立标志位数组(“未定”/“是”/“否”),扫描产生式:
- 右部含终结符 → 标记”否”
- 右部为 ε → 标记”是”
- 重复扫描直到标志不再变化
- 计算 FIRST 集
对每个符号 X:
- 若 X∈VT,则 FIRST(X)={X}
- 若 X∈VN 且 X→aα,则 a∈FIRST(X)
- 若 X∈VN 且 X→ε,则 ε∈FIRST(X)
- 若 X∈VN 且 X→Y1...Yn,则将 FIRST(Yi)(除 ε)加入 FIRST(X)
- 计算 FOLLOW 集
对每个非终结符 A:
- 开始符号 S:#∈FOLLOW(S)
- 产生式 A→αBβ:FIRST(β)(非空)加入 FOLLOW(B)
- 若 ε∈FIRST(β):FOLLOW(A) 加入 FOLLOW(B)
- 计算 SELECT 集并判别
对每个产生式 A→α:
- 若 ε∈/FIRST(α),则 SELECT(A→α)=FIRST(α)
- 若 ε∈FIRST(α),则 SELECT(A→α)=(FIRST(α)−{ε})∪FOLLOW(A)
LL(1) 条件:对每个非终结符 A 的不同产生式,SELECT 集的交集为空集。
当文法不满足 LL(1) 时,不能用确定的自顶向下分析,但可用不确定的自顶向下分析(带回溯的自顶向下分析)。
引起回溯的原因是:在文法中当关于某个非终结符的产生式有多个候选时,而面临当前的输入符无法确定选用唯一的产生式,从而引起回溯。
- 由于相同左部的产生式的右部 FIRST 集交集不为空而引起回溯
SA→xAy→ab∣a
输入串 xay,先选 A → ab,xa 匹配后当前符 y 与 b 不匹配,回溯改选 A → a,匹配成功。
- 由于相同左部非终结符的右部存在 ε 的产生式,且该非终结符的 FOLLOW 集中含有其他产生式右部 FIRST 集的元素
SA→aA∣b→cA∣ε
输入串 ab#,先选 S → aA,a 匹配后 A 选 A → cA,但 c 与 b 不匹配,回溯改选 A → ε,b 与 S 的 FOLLOW 集中的 b 匹配,匹配成功。
- 由于文法含有左递归而引起回溯
S→Sa∣b
输入串 baa#,先选 S → b,输入串未分析完,回溯改选 S → Sa,再选 S → b,得到 ba,但输入串还有 a#,继续回溯,最终得到 baa,匹配成功。
带回溯分析代价很高,效率很低,在实用编译程序中几乎不用。
文法中含有左递归时不能采用确定的自顶向下分析法。左递归分为直接左递归和间接左递归。
形如 A→Aα 的产生式称为直接左递归。
例:文法 G5 含有直接左递归:
SS→Sa→b
该文法产生的语言 L={ban∣n≥0}。输入串 baaa# 应是该语言的句子,但用自顶向下分析时,当输入符为 b 时,为与 S 匹配则应选用 S→b 推导,但这样就推不出后边部分;而若用 S→Sa 推导则无法确定到什么时候才用 S→b 替换。
消除方法:把直接左递归改写为右递归。对文法 G5 可改写为:
SS′→bS′→aS′∣ε
改写后的文法和原文法产生的语言句子集都为 {ban∣n≥0},且改写后的文法为 LL(1) 文法。
一般情况下,假定关于 A 的全部产生式是:
A→Aα1∣Aα2∣…∣Aαn∣β1∣β2∣…∣βm
其中 βi(1≤i≤m)不以 A 开头,消除直接左递归后改写为:
AA′→β1A′∣β2A′∣…∣βmA′→α1A′∣α2A′∣…∣αnA′∣ε
形如 A→Bα,B→Aβ 等可以形成推导 A⇒+A 的产生式称为间接左递归。
例:文法 G6 含有间接左递归:
AABB→aB→Bb→Ac→d
若有输入串为 adbcbcbc#,当分析过程至 A⇒aB⇒aAc⇒aBbc 时,B 若用产生式 B→d 替换,则分析过程终止,不能推出 adbcbcbc#;而若选用产生式 B→Ac,则会出现无法确定何时终止的情况。
消除方法:先通过产生式非终结符置换,将间接左递归变为直接左递归,然后再按消除直接左递归的方法处理。
以文法 G6 为例,用产生式 A→aB 和 A→Bb 的右部置换产生式 B→Ac 中的非终结符 A,得到左部为 B 的产生式:
BBB→aBc→Bbc→d
消除左递归后得:
BB′→aBcB′∣dB′→bcB′∣ε
再把原来其余的产生式 A→aB 和 A→Bb 加入,最终文法为:
AABB′→aB→Bb→aBcB′∣dB′→bcB′∣ε
该文法与 G6 等价,即它们产生相同的句子集。
对文法中一切左递归的消除要求文法中不含回路,即无 A⇒+A 的推导。满足这个要求的充分条件是,文法中不包含形如 A→A 的有害规则和 A 的空产生式。
算法步骤如下:
- 把文法的所有非终结符按某一顺序排序,例如:A1,A2,…,An
- FOR i = 1 TO n DO
- FOR j = 1 TO i-1 DO
- 若 Ai 的所有产生式为 Ai→δ1∣δ2∣…∣δk
- 将其替换形如 Ai→Ajγ 的产生式得到 Ai→δ1′∣δ2′∣…∣δm′
- 消除 Ai 中的一切直接左递归
- 去掉无用产生式
例:按上述方法消除如下文法的一切左递归:
SQR→Qc→Rb∣b→Sa∣a
若非终结符排序为 S,Q,R:
- 左部为 S 的产生式 S→Qc 无直接左递归
- 左部为 Q 的产生式 Q→Rb∣b 中右部不含 S
- 把产生式 S→Qc 的右部代入产生式 R→Sa 得:R→Qca∣a
- 再将产生式 Q→Rb∣b 的右部代入得:R→Rbca∣bca∣a
- 对产生式消除直接左递归得:
RR′→bcaR′∣aR′→bcaR′∣ε
最终文法变为:
SQRR′→Qc→Rb∣b→bcaR′∣aR′→bcaR′∣ε
若非终结符的排序为 R,Q,S,则把产生式 R→Sa∣a 代入产生式 Q→Rb∣b 得:Q→Sab∣ab∣b,再将此代入产生式 S→Qc 得:S→Sabc∣abc∣bc。消除该产生式的左递归后,文法变为:
SS′→abcS′∣bcS′→abcS′∣ε
由于 Q,R 为不可到达的非终结符,所以以 Q,R 为左部及包含 Q,R 的产生式应删除。
当非终结符的排序不同时,最后结果的产生式形式不同,但它们是等价的。
若文法中含有形如 A→αβ1∣αβ2 的产生式,会导致相同左部产生式的 FIRST 集相交,不满足 LL(1) 条件。
可将产生式等价变换为:
AA′→αA′→β1∣β2
写成一般形式:A→αβ1∣αβ2∣…∣αβn∣γ(其中 γ 不以 α 开头)
提取左公共因子后变为:
AA′→αA′∣γ→β1∣β2∣…∣βn
若 βi 中仍含有左公共因子,可再次提取,直到无左公共因子为止。
例 1:文法 G1 的产生式为
SSS→Sb→Sa→ε
对产生式(1)、(2)提取左公共因子后得:
SS→S(b∣a)→ε
进一步变换为:
SA′A′S→SA′→b→a→ε
例 2:文法 G2 的产生式为
AABB→ad→Bc→aA→b
产生式(2)的右部以非终结符 B 开始,左公共因子可能是隐式的。用产生式(3)、(4)的右部替换产生式(2)中的 B,可得:
AAA→ad→aAc→bc
提取产生式(1)、(2)的左公共因子得:
AA→a(d∣Ac)→bc
引进新非终结符 A′ 后得 G2 为:
AAA′A′→aA′→bc→d→Ac
注意事项:
-
提取左公共因子后,可能使某些产生式变成无用产生式,需要对文法重新压缩。
-
某些文法不能在有限步骤内提取完左公共因子。例如文法 G4:
SAB→Apl∣Ba→aAp∣d→aBq∣e
用产生式(2)、(3)的右部替换产生式(1)中的 A、B,再提取左公共因子,只能使文法的产生式越来越多,无限增加下去,而不能得到提取左公共因子的预期结果。
- 一个文法提取了左公共因子后,只解决了相同左部产生式右部的 FIRST 集不相交的问题。当改写后的文法不含空产生式,且无左递归时,则改写后的文法是 LL(1) 文法;若还有空产生式时,则还需用 LL(1) 文法的判别方式进行判断才能确定是否为 LL(1) 文法。
核心思想:把每个非终结符编写为一个递归函数,右部作为函数体。函数内根据当前输入符号查 SELECT 集选择产生式,递归调用对应函数。
构造方法:
- 为每个非终结符 A 编写函数
A()
- 函数体结构:
- 根据当前输入符号
lookahead 选择产生式
- 对产生式右部的每个符号:
- 终结符:匹配并读入下一符号
- 非终结符:调用对应函数
示例:文法 E→E+T∣T,T→T∗F∣F,F→(E)∣id
消除左递归后:E→TE′,E′→+TE′∣ε,T→FT′,T′→∗FT′∣ε,F→(E)∣id
if lookahead == expected:
lookahead = next_token() # 读入下一符号
优点:代码结构清晰,易于理解和调试,可手工编写。
缺点:每个文法需单独编程,修改文法需重写代码。
核心思想:构建预测分析表 M[A,a] 存储”非终结符 A 遇到输入符号 a 时选择哪个产生式”,用栈模拟推导过程。
分析表构造:对每个产生式 A→α,对 SELECT(A→α) 中的每个终结符 a,令 M[A,a]=A→α。
分析过程:
栈初始化: [#, S] (# 是栈底, S 是开始符号)
else: # M[X, a] = X → Y₁Y₂...Yₖ
将 Yₖ...Y₂Y₁ 逆序压栈 (若 Yᵢ = ε 则不压栈)
示例:文法 E→TE′,E′→+TE′∣ε,T→FT′,T′→∗FT′∣ε,F→(E)∣id
分析表(部分):
| 非终结符 | id | + | * | ( | ) | # |
|---|
| E | E→TE′ | | | E→TE′ | | |
| E’ | | E′→+TE′ | | | E′→ε | E′→ε |
| T | T→FT′ | | | T→FT′ | | |
| T’ | | T′→ε | T′→∗FT′ | | T′→ε | T′→ε |
| F | F→id | | | F→(E) | | |
分析 id+id 的过程:
[#, E] id+id# M[E,id]=E→TE', 弹E压E'T
[#, E', T] id+id# M[T,id]=T→FT', 弹T压T'F
[#, E', T', F] id+id# M[F,id]=F→id, 弹F压id
[#, E', T', id] id+id# 匹配id, 弹栈前移
[#, E', T'] +id# M[T',+]=T'→ε, 弹T'
[#, E'] +id# M[E',+]=E'→+TE', 弹E'压E'T+
[#, E', T, +] +id# 匹配+, 弹栈前移
[#, E', T] id# M[T,id]=T→FT', 弹T压T'F
[#, E', T', F] id# M[F,id]=F→id, 弹F压id
[#, E', T', id] id# 匹配id, 弹栈前移
[#, E', T'] # M[T',#]=T'→ε, 弹T'
[#, E'] # M[E',#]=E'→ε, 弹E'
伪代码实现:
def LL1_parse(input_string, parse_table):
stack = ['#', start_symbol]
a = input_string[input_ptr] # 当前输入
return f"错误: 期望 {X}, 得到 {a}"
elif parse_table[X][a] is None:
return f"错误: 无产生式 M[{X}, {a}]"
production = parse_table[X][a] # X → Y₁Y₂...Yₖ
for symbol in reversed(production):
优点:通用性强,修改文法只需重新生成分析表,适合自动化工具生成。
缺点:需要预先构造分析表,不如递归下降直观。
错误处理包含两个任务:报错(指出错误位置和类型)和错误恢复(使分析继续进行)。
LL(1) 分析中有两种错误情况:
- 栈顶终结符与当前输入不匹配
- 栈顶非终结符 A 面临输入符号 a,但 M[A,a] 为空
核心思想:跳过输入符号直到遇到”同步符号”,使分析能继续。
同步符号选择:将 FIRST(A) 或 FOLLOW(A) 中的符号作为非终结符 A 的同步符号。
恢复策略:
- 遇到 FOLLOW(A) 中的符号:弹出栈顶的 A,继续分析
- 遇到 FIRST(A) 中的符号:保留 A 在栈顶,根据 A 恢复分析
示例:若 E 在栈顶,当前输入是 ),但 M[E,)] 为空:
策略: 跳过输入直到遇到 ) 或 #,弹出 E,继续分析
核心思想:根据当前语法单位的上下文进行更精确的恢复。
流程:
-
进入语法单位时:
- 检查当前符号是否属于 BeginSym(通常取 FIRST 集)
- 若不属于,报错并跳过 BeginSym∪EndSym 之外的符号
- 遇到 BeginSym 中符号:重新分析该单位
- 遇到 EndSym 中符号:退出该单位
-
离开语法单位时:
- 检查当前符号是否属于 EndSym(基于 FOLLOW 集)
- 若不属于,报错并跳过 BeginSym∪EndSym 之外的符号
递归下降中的实现示例:
文法:B→[A]∣(A),A→a
BeginSym = {'[', '('} # FIRST(B)
skip_until(BeginSym | EndSym)
ParseA(EndSym | {']'}) # 传入上下文相关的 EndSym
ParseA(EndSym | {')'}) # 不同上下文使用不同参数
skip_until(BeginSym | EndSym)
关键点:不同上下文传入不同的 EndSym 参数,体现”短语层”的含义。例如方括号内调用 ParseA 时传入 EndSym ∪ {]},圆括号内传入 EndSym ∪ {)}。
| 方法 | 优点 | 缺点 |
|---|
| 应急恢复 | 实现简单,通用 | 恢复不够精确,可能跳过过多符号 |
| 短语层恢复 | 考虑上下文,恢复更精确 | 实现复杂,需为每个语法单位设计 |
实际编译器(如 PL/0)通常采用短语层恢复,在进入和退出语法单位时调用检查函数,根据 FIRST 和 FOLLOW 集合进行错误检测和恢复。
自底向上分析(也称 移进-归约分析)的基本思想是:对输入符号串自左向右扫描,将输入符逐个移入一个后进先出栈中,边移入边分析;一旦栈顶符号串形成某个句型的句柄或其他可归约串(对应某产生式的右部),就用该产生式的左部非终结符代替相应右部的文法符号串,称为一步归约。重复这一过程,直到栈中只剩文法的开始符号时分析成功,确认输入串是文法的句子。
自底向上分析是规范推导(最右推导)的逆过程,因此规范归约也称为最左归约。
设文法 G[S] 为:
(1) (2) (3) (4) S→aAcBeA→bA→AbB→d
对输入串 abbcde# 进行分析:
该串的最右推导为:
S⇒aAcBe⇒aAbcde⇒abbcde
归约过程(构造语法树的逆过程)如下表所示:
| 步骤 | 符号栈 | 输入串 | 动作 |
|---|
| (1) | # | abbcde# | 移进 |
| (2) | #a | bbcde# | 移进 |
| (3) | #ab | bcde# | 归约 (A→b) |
| (4) | #aA | bcde# | 移进 |
| (5) | #aAb | cde# | 归约 (A→Ab) |
| (6) | #aA | cde# | 移进 |
| (7) | #aAc | de# | 移进 |
| (8) | #aAcd | e# | 归约 (B→d) |
| (9) | #aAcB | e# | 移进 |
| (10) | #aAcBe | # | 归约 (S→aAcBe) |
| (11) | #S | # | 接受 |
上述分析过程也是自底向上构造语法树的过程,每步归约对应构造一棵子树,最后当输入串结束时刚好构造出整个语法树(见图 5.1)。
在上述移进-归约过程中,不能简单地在栈顶出现某产生式右部时就立即归约。例如表中第 (5) 步,栈顶符号串 b 和 Ab 分别是产生式 (2)、(3) 的右部,此时必须依据句柄来确定归约。
- 句柄:当前句型的某产生式的右部,且该右部在规范推导中最后被推出。
- 文法无二义性时,一个句子的规范推导唯一,规范归约也唯一。
- 因此,自底向上分析的关键问题是:在分析过程中如何确定句柄(或其他可归约串)。
本章和第 6 章分别介绍优先分析和 LR 类分析。本章介绍的优先分析技术,其可归约串是最左简单短语。
优先分析法可分为简单优先分析法和算符优先分析法。
| 方法 | 基本思想 | 归约性质 | 特点 |
|---|
| 简单优先分析 | 对文法所有符号(终结符 + 非终结符)求优先关系,按关系确定句柄 | 规范归约 | 准确、规范,但效率较低,实际使用价值不大 |
| 算符优先分析 | 仅规定终结符(算符)之间的优先关系,找到可归约串即归约,不关心归约到哪个非终结符 | 非规范归约 | 分析速度快,特别适用于表达式分析,实际中仍有应用 |
简单优先分析法按照文法符号之间的优先关系确定句柄,因此先给出任意两个文法符号之间的优先关系定义。
文法中任意两个文法符号 X 和 Y:
-
相等(X≐Y):X 与 Y 的优先性相等。
X≐Y 当且仅当 G 中存在产生式 A→αXYβ
-
小于(X⋖Y):X 的优先性比 Y 小。
X⋖Y 当且仅当 G 中存在产生式 A→αXBβ 且 B⇒+Yγ
-
大于(X⋗Y):X 的优先性比 Y 大。
X⋗Y 当且仅当 G 中存在产生式 A→αBYβ 且 B⇒+γX
注意:这里的 ⋖、⋗ 与数学中的 <、> 含义不同。
优先关系也可通过语法树的结构直观理解。当某两个符号同时出现在句柄中时,它们具有 ≐ 关系;当左边的符号不在句柄中而右边的在句柄中时,左边 ⋖ 右边;当左边的在句柄中而右边的不在时,左边 ⋗ 右边。
设有文法 G[S]:
SAB→bAb→(B)∣a→Aa
求优先关系:
-
相等关系 (≐):由 S→bAb、A→(B)、B→Aa 可得:
b≐A,A≐b,(≐B,A≐a
-
小于关系 (⋖):
- 由 S→bAb,且 A 可推出以 ( 或 a 开头的串,可得:b⋖(、b⋖a
- 由 A→(B,且 B 可推出以 A 开头的串(B⇒Aa),可得:(⋖A
-
大于关系 (⋗):
- 由 S→bAb,且 A 可推出以 a 结尾的串(A⇒a),可得:a⋗b
- 由 B→Aa,且 A 可推出以 ( 开头的串…(依此类推)
上述关系可用优先关系矩阵表示。矩阵中元素为空时,表示该文法中任何句型都不会出现该符号对的相邻关系,在分析过程中若遇到则为出错,可断定输入串不是该文法的句子。
# 为句子括号符:#⋖X 且 X⋗# 对所有与 # 相邻的符号 X 成立。
例 5.2 文法的简单优先关系矩阵(表 5.2):
| S | A | B | # |
|---|
| S | | | | ⋗ |
| A | | ≐ | ⋖ | ⋗ |
| B | | ⋗ | | ⋗ |
| # | ⋖ | ⋖ | ⋖ | ≐ |
若一个文法是简单优先文法,必须满足以下条件:
- 唯一性:文法符号集中,任意两个符号之间最多只有一种优先关系成立。
- 无二义性:文法中任意两个产生式没有相同的右部。
- 移进:将输入符号串 a1a2…an# 依次存入符号栈,直到栈顶符号的优先性大于下一个待输入符号时为止。
- 找句柄:以当前栈顶符号为句柄尾,由此向左在栈中找句柄的头符号。
- 归约:由句柄在文法的产生式中查找右部为句柄的产生式。若找到,则用相应产生式的左部代替句柄;若找不到,则为出错,断定输入串不是该文法的句子。
- 重复:重复上述 (1)、(2)、(3) 步,直到归约完输入符号串,栈中只剩文法的开始符号为止。
在处理表达式求值过程中,运算次序是先乘除后加减,即乘除运算的优先级高于加减运算的优先级,且同级运算服从左结合(或右结合,如幂运算)。这说明运算的次序只与运算符有关,而与运算对象无关。
因此,直观算符优先分析法的关键只涉及优先级问题和同一优先级的结合性质。算符间的优先关系表示法与简单优先关系类似,规定如下:
- X⋖Y:X 的优先性低于 Y
- X≐Y:X 的优先性等于 Y
- X⋗Y:X 的优先性高于 Y
注意:这三个关系与数学中的 <、=、> 不同,它们是有序的。若有 a⋖b,不一定有 b⋗a 成立;若有 a≐b,也不一定具有对称性。例如,通常表达式中运算符的优先关系有 +⋗+(左结合),但没有 +⋖+;有 (≐),但没有 )≐(。
E→E+E∣E−E∣E∗E∣E/E∣E↑E∣(E)∣i
该文法是二义的,但可以用算符优先分析法进行分析。运算对象(终结符 i)的优先级最高。其他运算符按计算顺序规定优先级和结合性如下:
-
幂运算符 ↑:优先级最高,遵循右结合。相当于 ⋖ 关系。
例如 i↑i↑i 等价于 i↑(i↑i),归约时从右向左进行。
-
乘除运算符 ∗ 和 /:优先级低于 ↑,服从左结合。
优先关系为:∗⋗∗、∗⋗/、/⋗∗、/⋗/、∗⋖↑、/⋖↑。
-
加减运算符 + 和 −:优先级最低,服从左结合。
优先关系为:+⋗+、+⋗−、−⋗+、−⋗−,且 +⋖∗、+⋖/、−⋖∗、−⋖/。
-
括号 ( 和 ):
- 左括号 ( 的优先性小于括号内的运算符,大于括号外的运算符
- 右括号 ) 的优先性大于括号外的运算符,小于括号内的运算符
- 内括号的优先性大于外括号
-
句子括号 #:与它相邻的任何运算符的优先性都比它大,即 #⋖X 且 X⋗# 对所有与 # 相邻的算符 X 成立。
定义 5.1 设有文法 G,如果 G 中没有形如 A→αBCβ 的产生式,其中 B、C 为非终结符,则称 G 为算符文法(Operator Grammar),也称 OG 文法。
算符文法的特点是:任何产生式的右部都不包含两个相邻的非终结符。
性质 1 在算符文法中,任何句型都不包含两个相邻的非终结符。
性质 2 如果 bA(或 Ab)出现在算符文法的句型 γ 中,其中 A∈VN,b∈VT,则任何包含 b(或 A)的短语必含有 A(或 b)。
定义 5.2 设 G 是一个不含 ε 产生式的算符文法,a、b 是任意两个终结符,A、B、C 是非终结符。算符优先关系 ≐、⋖、⋗ 定义如下:
-
相等(a≐b):当且仅当 G 中含有形如 A→αaBbβ 或 A→αabβ 的产生式。
即 a 与 b 在同一句柄中同时归约。
-
小于(a⋖b):当且仅当 G 中含有形如 A→αaBβ 的产生式,且 B⇒+bγ。
即 a 不在句柄中而 b 在句柄中,a 先归约。
-
大于(a⋗b):当且仅当 G 中含有形如 A→αBbβ 的产生式,且 B⇒+γa。
即 a 在句柄中而 b 不在,a 后归约。
这三种优先关系也可由语法树的结构直观说明:
- ≐:如图 5.3(a),a 和 b 在同一句柄中同时归约,优先级相同。
- ⋖:如图 5.3(b),a 不在句柄中而 b 在句柄中,a 的优先级低于 b。
- ⋗:如图 5.3(c),a 在句柄中而 b 不在,a 的优先级高于 b。
注意:算符之间的优先关系是有序的,允许 a⋖b 和 b⋗a 同时存在,但不允许同一对终结符之间同时存在两种不同的关系(如不允许 a⋖b 和 a⋗b 同时成立)。
定义 5.3 设 G 是一个不含 ε 产生式的算符文法。如果对于任意一对终结符 (a,b) 之间至多只有 ≐、⋖、⋗ 三种关系中的一种成立,则称 G 是一个算符优先文法(Operator Precedence Grammar),即 OPG 文法。
算符优先文法一定是算符文法,但算符文法不一定是算符优先文法。
定义:
-
FIRSTVT(B)={a∣B⇒+aα,a∈VT,α∈V∗}(取头)
-
LASTVT(B)={a∣B⇒+αa,a∈VT,α∈V∗}(取尾)
- ≐ 关系:直接查看产生式的右部,对如下形式的产生式,有 a≐b 成立:
- A→αaBbβ
- A→αabβ
-
⋖ 关系:求出每个非终结符 B 的 FIRSTVT(B),观察如下形式的产生式:
- A→αaBβ
对每一 b∈FIRSTVT(B),有 a⋖b 成立。
-
⋗ 关系:求出每个非终结符 B 的 LASTVT(B),观察如下形式的产生式:
- A→αBbβ
对每一 a∈LASTVT(B),有 a⋗b 成立。
表达式文法如下:
(0) (1) (2) (3) (4) (5) (6) (7) (8) E→#E#E→E+TE→TT→T∗FT→FF→P↑FF→PP→(E)P→i
计算 FIRSTVT 和 LASTVT 集合:
| 非终结符 | FIRSTVT | LASTVT |
|---|
| E | {#,+,∗,↑,(,i} | {#,+,∗,↑,),i} |
| T | {∗,↑,(,i} | {∗,↑,),i} |
| F | {↑,(,i} | {↑,),i} |
| P | {(,i} | {),i} |
计算优先关系:
- ≐ 关系:由产生式 (0) 和 (6) 可得 #≐#、i≐i
- ⋖ 关系:
- 由 #E,#⋖FIRSTVT(E)
- 由 E+T,+⋖FIRSTVT(T)
- 由 T∗F,∗⋖FIRSTVT(F)
- 由 F↑F,↑⋖FIRSTVT(F)
- 由 P(E,(⋖FIRSTVT(E)
- ⋗ 关系:
- 由 E#,LASTVT(E)⋗#
- 由 E+,LASTVT(E)⋗+
- 由 T∗,LASTVT(T)⋗∗
- 由 F↑,LASTVT(F)⋗↑
- 由 E),LASTVT(E)⋗)
优先关系矩阵(表 5.5):
| + | − | ∗ | / | ↑ | ( | ) | i | # |
|---|
| + | ⋗ | ⋗ | ⋖ | ⋖ | ⋖ | ⋖ | ⋗ | | ⋗ |
| − | ⋗ | ⋗ | ⋖ | ⋖ | ⋖ | ⋖ | ⋗ | | ⋗ |
| ∗ | ⋗ | ⋗ | ⋗ | ⋗ | ⋖ | ⋖ | ⋗ | | ⋗ |
| / | ⋗ | ⋗ | ⋗ | ⋗ | ⋖ | ⋖ | ⋗ | | ⋗ |
| ↑ | ⋗ | ⋗ | ⋗ | ⋗ | ⋗ | ⋖ | ⋗ | | ⋗ |
| ( | ⋖ | ⋖ | ⋖ | ⋖ | ⋖ | ⋖ | ≐ | ⋖ | |
| ) | ⋗ | ⋗ | ⋗ | ⋗ | ⋗ | | ⋗ | | ⋗ |
| i | ⋗ | ⋗ | ⋗ | ⋗ | ⋗ | | ⋗ | | ⋗ |
| # | ⋖ | ⋖ | ⋖ | ⋖ | ⋖ | ⋖ | | ⋖ | ≐ |
算符优先分析使用一个符号栈,分析过程如下:
- 初始化:将 # 压入栈底,将输入串的第一个符号读入到 a 中
- 循环:
- 设栈顶符号为 X,当前输入符号为 a
- 若 X≐a(或 X、a 都是非终结符且相等):
- 若 X 是终结符,则 X 与 a 匹配,弹栈,a 指向下一符号
- 若 X 是非终结符,则 X 保持不变,a 指向下一符号(a≐b 表示同一优先级的算符,从左向右扫描)
- 若 X⋗a:
- 在栈中自栈顶向栈底方向找出最左的、且满足其右侧符号与 X 是 ≐ 关系的符号 Y(即句柄的左边界)
- 将 Y 与 X 之间的所有符号(包括 Y 和 X)归约为一个非终结符 A
- 若找不到这样的产生式,则报错
- 若 X⋖a:
- 若上述关系都不成立:报错
- 接受:当栈中只剩 # 且输入符号也只剩 # 时,分析成功
对表达式 i+i∗i# 进行分析,使用上述表达式文法:
| 步骤 | 符号栈 | 输入串 | 动作 |
|---|
| (1) | # | i+i∗i# | 移进 |
| (2) | #i | +i∗i# | i⋗+,归约 P→i |
| (3) | #P | +i∗i# | P⋖+,移进 |
| (4) | #P+ | i∗i# | +⋖i,移进 |
| (5) | #P+i | ∗i# | i⋗∗,归约 P→i |
| (6) | #P+P | ∗i# | P⋖∗,移进 |
| (7) | #P+P∗ | i# | ∗⋖i,移进 |
| (8) | #P+P∗i | # | i⋗#,归约 P→i |
| (9) | #P+P∗P | # | P⋗#,归约 F→P |
| (10) | #P+P∗F | # | ∗⋗#,归约 T→F |
| (11) | #P+T | # | T⋗#,归约 E→T |
| (12) | #P+E | # | +⋗#,归约 E→E+T |
| (13) | #E | # | #≐#,接受 |
- 归约的不是句柄,而是最左简单短语:算符优先归约不是规范归约,因为归约过程中不关心归约到哪个非终结符,只关心终结符之间的优先关系。
- 分析速度快:只考虑终结符之间的优先关系,分析表简单。
- 适用范围有限:仅适用于算符优先文法,即不含 ε 产生式且任意两个终结符之间至多只有一种优先关系的文法。
- 不能处理二义性文法:如果表达式的二义性文法(如 E→E+E∣E∗E∣(E)∣i)中同一对终结符之间存在多种优先关系,则不是算符优先文法。
优先矩阵需要 m2 个存储单元(m 为终结符个数),存储开销大。优先函数用两个函数 f、g 代替优先矩阵,只需 2(m+1) 个单元,满足:
- 若 a⋖b,则 f(a)<g(b)
- 若 a≐b,则 f(a)=g(b)
- 若 a⋗b,则 f(a)>g(b)
构造方法:
-
直接构造法:
- 初始化:对所有终结符 a,令 f(a)=g(a)=1
- 迭代修正:逐行扫描优先矩阵,若 a⋖b 但 f(a)≥g(b),则令 f(a)=g(b)−1;若 a⋗b 但 f(a)≤g(b),则令 g(b)=f(a)+1;若 a≐b 但 f(a)=g(b),则调整使两者相等
- 重复直到收敛。若某值超过 2m,则不存在优先函数
-
关系图法:
- 以 fa、ga 为结点名(共 2m 个结点)
- 若 a⋖b,画弧 fa→gb;若 a⋗b,画弧 gb→fa;若 a≐b,画双向弧 fa↔gb
- 每个结点的数值等于从该结点出发可达的结点数(包括自身)
- 若图中存在回路,则不存在优先函数
注意:优先函数不唯一(所有值加同一常数,关系不变);且某些优先矩阵不存在对应的优先函数(如循环依赖 a⋗b,b⋗c,c⋗a)。
缺点:优先函数无法区分”无优先关系”的情况,出错时不能准确定位错误位置。例如,当两个终结符无关系时,优先矩阵可立即报错,而优先函数可能等到归约出两个相邻非终结符时才报错。
-
可能接受非法句子:算符优先分析归约的是最左简单短语而非句柄,因此某些非文法的句子也可能被错误接受。
例 5.6 文法:
ST→aTd→(S)∣b
对输入串 (a) 可完成归约,但该串不是该文法的句子。
-
适用范围窄:通常程序设计语言的文法很难满足算符优先文法的条件(无 ε 产生式、任意终结符对至多一种优先关系),因此算符优先分析法主要适用于表达式分析。
LR 分析是一种自底向上的规范归约分析方法,其核心思想是:根据分析栈的栈顶状态和当前输入符号(有时需要向右查看 k 个符号),唯一确定下一步动作是移进、归约、接受还是报错。
LR(k) 的含义:
- L:从左到右扫描输入串
- R:最右推导的逆过程(规范归约)
- k:向右查看 k 个输入符号
- 总控程序(驱动程序):对所有 LR 分析器通用
- 分析表:分为两部分
- ACTION 表:决定栈顶状态遇到输入符号时应执行的动作
- GOTO 表:决定归约后应转向的状态
- 分析栈:包括状态栈和文法符号栈,均为后进先出
| 方法 | 对文法限制 | 分析能力 | 构造难度 |
|---|
| LR(0) | 严格 | 弱 | 低 |
| SLR(1) | 较松 | 中 | 中 |
| LALR(1) | 更松 | 强 | 中 |
| LR(1) | 最松 | 最强 | 高 |
- 优点:对文法限制少,绝大多数无二义性上下文无关文法都可用;分析速度快;能准确、即时地指出出错位置。
- 缺点:构造分析表的工作量大,手工实现复杂。实用中通常借助工具(如 yacc)自动生成 LALR(1) 分析器。
LR(0) 分析器在分析过程中不向前查看输入符号,仅根据栈顶状态和当前输入符号决定动作。它是构造其他 LR 类分析器的基础。
1. 拓广文法
对原文法 G 增加新产生式 S′→S(S′ 为新的开始符号,S 为原开始符号),得到拓广文法 G′。拓广文法的目的是:在归约过程中能分清是否已归约到文法的初始开始符号,避免与文法右部出现的开始符号混淆。
2. 可归前缀与活前缀
考虑规范推导过程中的一条产生式 A→β,在规范句型中,β 是句柄。
- 可归前缀:规范句型中,句柄及其前面的部分(即句柄左部的前缀,包括句柄本身)
- 活前缀:规范句型中不超过句柄右端的所有前缀(即可归前缀加上形成句柄之前的中间状态)
直观理解:活前缀是”可以安全放在栈中的前缀”,它一定是某个规范句型的前缀,且其右端未超过当前句型的句柄末端。
定义 6.1 设 S′⇒∗γ 是拓广文法的规范推导,符号串 α 是 γ 的前缀,且 α 的右端不超过该句型句柄的末端,则称 α 是 G 的一个活前缀。
在 LR 分析过程中,符号栈中保存的内容始终是活前缀。一旦栈中出现可归前缀(即句柄已形成),就进行归约。
LR 分析的关键是识别活前缀。可以将终结符和非终结符都视为有限自动机的输入符号:
- 每移进一个符号,相当于已识别该符号,状态进行转换
- 当识别到可归前缀时,相当于到达了识别句柄的终态
- 每个终态对应一个句柄识别
- 带
* 的状态既是句柄识别态,又是句子识别态(即已完成整个句子的归约)
- 句子识别态在拓广文法中唯一存在
通过构造识别活前缀的确定有限自动机(DFA),LR 分析器就能根据当前状态和输入符号唯一确定分析动作。
LR(0) 项目:产生式右部加一个圆点 ·,表示已识别部分和待识别部分。
例如,产生式 A→XYZ 对应的项目有:
- A→⋅XYZ(尚未识别任何符号) 即开始形式
- A→X⋅YZ(已识别 X)
- A→XY⋅Z(已识别 XY)
- A→XYZ⋅(已识别全部,可归约) 即归约形式
项目集闭包(CLOSURE):
- 初始时,项目集包含所有项目 S′→⋅S 的闭包
- 若项目集 I 包含项目 A→α⋅Bβ,则对 B 的每个产生式 B→γ,将项目 B→⋅γ 加入 I
GOTO 函数:
- GOTO(I,X) 表示从项目集 I 出发,读入文法符号 X 后到达的新项目集
- 即把 I 中所有形如 A→α⋅Xβ 的项目变为 A→αX⋅β,然后求闭包
步骤:
- 对文法 G 拓广,得到 G′
- 构造识别活前缀的 DFA(即项目集族 C)
- 对每个项目集 Ik(对应状态 k):
- 若项目 A→α⋅aβ 在 Ik 中(a 为终结符),则 ACTION[k,a]=sj(移进到状态 j),其中 j 是 GOTO(Ik,a) 的状态号
- 若项目 A→α⋅ 在 Ik 中,则对所有终结符 a,ACTION[k,a]=rj(归约),其中 j 是产生式 A→α 的编号(注意:S′→S 除外)
- 若项目 S′→S⋅ 在 Ik 中,则 ACTION[k,#]=acc(接受)
- 若 GOTO(Ik,A)=Ij(A 为非终结符),则 GOTO[k,A]=j
冲突:
- 若同一状态对同一终结符,既有移进动作又有归约动作 → 移进-归约冲突
- 若同一状态对同一终结符,有两个以上归约动作 → 归约-归约冲突
- 若分析表存在冲突,则该文法不是 LR(0) 文法
设文法 G[S] 为:
(1) (2) (3) (4) S→aAcBeA→bA→AbB→d
拓广文法 G′:
(0) (1) (2) (3) (4) S′→SS→aAcBeA→bA→AbB→d
构造识别活前缀的 DFA:
I0I1I2I3I4I5I6I7I8I9=CLOSURE({S′→⋅S})={S′→⋅S, S→⋅aAcBe}=GOTO(I0,S)={S′→S⋅}(接受态)=GOTO(I0,a)=CLOSURE({S→a⋅AcBe})={S→a⋅AcBe, A→⋅b, A→⋅Ab}=GOTO(I2,A)=CLOSURE({S→aA⋅cBe, A→A⋅b})={S→aA⋅cBe, A→A⋅b}=GOTO(I2,b)={A→b⋅}(归约 r2)=GOTO(I3,c)=CLOSURE({S→aAc⋅Be})={S→aAc⋅Be, B→⋅d}=GOTO(I3,b)={A→Ab⋅}(归约 r3)=GOTO(I5,B)={S→aAcB⋅e}=GOTO(I5,d)={B→d⋅}(归约 r4)=GOTO(I7,e)={S→aAcBe⋅}(归约 r1)
LR(0) 分析表(表 6.1):
| 状态 | a | c | e | b | d | # | S | A | B |
|---|
| 0 | S2 | | | | | | 1 | | |
| 1 | | | | | | acc | | | |
| 2 | | | | S4 | | | | 3 | |
| 3 | | S5 | | S6 | | | | | |
| 4 | r2 | r2 | r2 | r2 | r2 | r2 | | | |
| 5 | | | | | S8 | | | | 7 |
| 6 | r3 | r3 | r3 | r3 | r3 | r3 | | | |
| 7 | | | S9 | | | | | | |
| 8 | r4 | r4 | r4 | r4 | r4 | r4 | | | |
| 9 | r1 | r1 | r1 | r1 | r1 | r1 | | | |
对输入串 abbcde# 的分析过程(表 6.2):
| 步骤 | 状态栈 | 符号栈 | 输入串 | ACTION | GOTO |
|---|
| (1) | 0 | # | abbcde# | S2 | |
| (2) | 02 | #a | bbcde# | S4 | |
| (3) | 024 | #ab | bcde# | r2 | 3 |
| (4) | 023 | #aA | bcde# | S6 | |
| (5) | 0236 | #aAb | cde# | r3 | 3 |
| (6) | 023 | #aA | cde# | S5 | |
| (7) | 0235 | #aAc | de# | S8 | |
| (8) | 02358 | #aAcd | e# | r4 | 7 |
| (9) | 02357 | #aAcB | e# | S9 | |
| (10) | 023579 | #aAcBe | # | r1 | 1 |
| (11) | 01 | #S | # | acc | |
| 符号 | 含义 | 操作 |
|---|
| Sn | 移进(Shift) | 将当前输入符号压入符号栈,将状态 n 压入状态栈,读头前进一位 |
| rn | 归约(Reduce) | 用第 n 号产生式归约:弹出右部长度个状态和符号,再查 GOTO 表压入新状态 |
| 裸数字 n | GOTO 跳转 | 出现在 GOTO 列,归约后栈顶状态与产生式左部查 GOTO 表得到的状态号 |
| acc | 接受(Accept) | 输入串已完全归约为 S′,语法分析成功 |
理解关键:ACTION 表中所有条目的含义都是”栈顶状态 + 当前输入符号 → 动作”。Sn 中的字母 S 是 Shift 的缩写,数字 n 是目标 DFA 状态号。而 GOTO 列的数字只在归约操作中用到:弹出右部后,露出的栈顶状态与产生式左部(非终结符)交叉查 GOTO 表。
构造 DFA 的核心操作只有两个:
- CLOSURE(I):若项目集中有 A→α⋅Bβ(圆点后是非终结符),则加入 B 的所有产生式(圆点在开头)
- GOTO(I, X):把 I 中所有形如 ⋯⋅X⋯ 的项目圆点右移一位,再求 CLOSURE
1. 求 I0
从 S′→⋅S 出发求闭包。圆点后是 S(非终结符),加入 S 的唯一产生式:
I0=CLOSURE({S′→⋅S})={S′→⋅S, S→⋅aAcBe}
2. 从 I0 出发
I0 中圆点可能读入的符号只有两个:a(终结符,来自 S→⋅aAcBe)和 S(非终结符,来自 S′→⋅S)。
GOTO(I₀, a):S→⋅aAcBe 圆点右移 → S→a⋅AcBe。圆点后是 A(非终结符),闭包:加入 A→⋅b 和 A→⋅Ab。
I2=GOTO(I0,a)={S→a⋅AcBe, A→⋅b, A→⋅Ab}
GOTO(I₀, S):S′→⋅S 圆点右移 → S′→S⋅。圆点在末尾,无需闭包。
I1=GOTO(I0,S)={S′→S⋅}(接受态)
3. 从 I2 出发
I2 中圆点可能读入的符号:A(非终结符,来自 S→a⋅AcBe 和 A→⋅Ab)、b(终结符,来自 A→⋅b)。
GOTO(I₂, b):A→⋅b 圆点右移 → A→b⋅。圆点在末尾,项目集只有一个项目。
I4=GOTO(I2,b)={A→b⋅}(归约 r2)
GOTO(I₂, A):
- S→a⋅AcBe → S→aA⋅cBe(圆点后是 c,终结符)
- A→⋅Ab → A→A⋅b(圆点后是 b,终结符)
两者圆点后都是终结符,无需闭包。
I3=GOTO(I2,A)={S→aA⋅cBe, A→A⋅b}
4. 从 I3 出发
I3 中圆点可能读入:c(S→aA⋅cBe)和 b(A→A⋅b)。
GOTO(I₃, c):
S→aA⋅cBe 圆点右移 → S→aAc⋅Be。圆点后是 B(非终结符),闭包加入 B→⋅d。
I5=GOTO(I3,c)={S→aAc⋅Be, B→⋅d}
GOTO(I₃, b):
A→A⋅b 圆点右移 → A→Ab⋅。
I6=GOTO(I3,b)={A→Ab⋅}(归约 r3)
5. 从 I5 出发
I5 中圆点可能读入:d(B→⋅d)和 B(S→aAc⋅Be)。
GOTO(I₅, d):B→⋅d 圆点右移 → B→d⋅。
I8=GOTO(I5,d)={B→d⋅}(归约 r4)
GOTO(I₅, B):S→aAc⋅Be 圆点右移 → S→aAcB⋅e。圆点后是 e(终结符),无需闭包。
I7=GOTO(I5,B)={S→aAcB⋅e}
6. 从 I7 出发
I7 只有 S→aAcB⋅e,圆点只可能读入 e。
GOTO(I₇, e):S→aAcB⋅e 圆点右移 → S→aAcBe⋅。
I9=GOTO(I7,e)={S→aAcBe⋅}(归约 r1)
DFA 总图:
flowchart LR
I0["I₀"]
I2["I₂"]
I4["I₄ (r₂,归约 A→b)"]
I3["I₃"]
I6["I₆ (r₃,归约 A→Ab)"]
I5["I₅"]
I8["I₈ (r₄,归约 B→d)"]
I7["I₇"]
I9["I₉ (r₁,归约 S→aAcBe)"]
I1["I₁ (acc,接受)"]
I0 -->|a| I2
I2 -->|b| I4
I2 -->|A| I3
I3 -->|b| I6
I3 -->|c| I5
I5 -->|d| I8
I5 -->|B| I7
I7 -->|e| I9
I0 -->|S| I1
逐状态对照 DFA 填写表项:
| 状态 | 项目集关键内容 | 推导出的表项 |
|---|
| 0 | S→⋅aAcBe,GOTO(I₀,a)=I₂ | ACTION[0,a]=S2 |
| 0 | GOTO(I₀,S)=I₁ | GOTO[0,S]=1 |
| 1 | S′→S⋅ | ACTION[1,#]=acc |
| 2 | A→⋅b,GOTO(I₂,b)=I₄ | ACTION[2,b]=S4 |
| 2 | GOTO(I₂,A)=I₃ | GOTO[2,A]=3 |
| 3 | S→aA⋅cBe,圆点在 c 前 | ACTION[3,c]=S5 |
| 3 | A→A⋅b,圆点在 b 前 | ACTION[3,b]=S6 |
| 4 | A→b⋅(圆点在末尾) | 对所有终结符:ACTION[4,*]=r2 |
| 5 | B→⋅d,GOTO(I₅,d)=I₈ | ACTION[5,d]=S8 |
| 5 | GOTO(I₅,B)=I₇ | GOTO[5,B]=7 |
| 6 | A→Ab⋅(圆点在末尾) | 对所有终结符:ACTION[6,*]=r3 |
| 7 | S→aAcB⋅e,GOTO(I₇,e)=I₉ | ACTION[7,e]=S9 |
| 8 | B→d⋅(圆点在末尾) | 对所有终结符:ACTION[8,*]=r4 |
| 9 | S→aAcBe⋅(圆点在末尾) | 对所有终结符:ACTION[9,*]=r1 |
分析过程中维护两个栈:
- 状态栈:记录经过的 DFA 状态序列(决定当前处于哪个项目集)
- 符号栈:记录已读入的文法符号
- 每步操作:查 栈顶状态 + 当前输入符号 在 ACTION 表中的值
约定:步骤编号对应表 6.2。Sn 意为”移进当前输入符号并转到状态 n”;rn 意为”用第 n 号产生式归约:弹出右部长度个状态和符号,再查 GOTO 表压入新状态”。
(1) S2 — 状态 0,输入 a
I0 中 S→⋅aAcBe,圆点在 a 前,期待读到 a。现在输入确实是 a,查表得 S2。
移进 a:符号栈压入 a,状态栈压入 2,读头前进(输入变为 bbcde#)。
(2) S4 — 状态 2,输入 b
I2={A→⋅b, …},圆点在 b 前,期待读到 b。查 ACTION[2,b]=S4。
移进 b:符号栈压入 b,状态栈压入 4。
(3) r2 — 状态 4,输入 b
I4={A→b⋅},圆点在末尾——句柄已形成!句柄是栈顶的 b(长度为 1),无论下一个输入是什么(这里是 b),LR(0) 都执行归约 A→b。
归约操作(三步):
- 弹出右部长度 = 1 个状态(4)和 1 个符号(b)
- 露出栈顶状态 2,产生式左部是 A,查 GOTO[2, A] = 3
- 压入 A(归约结果)和状态 3
归约后:[0, 2, 3] #aA (输入不变:bcde#)
关键:符号栈中的 b 被替换为了 A——这就是”自底向上归约”:已识别的句柄被它的左部非终结符替代。
(4) S6 — 状态 3,输入 b
I3={A→A⋅b, …},圆点在 b 前。查 ACTION[3,b]=S6。
移进 b:符号栈压入 b,状态栈压入 6。
(5) r3 — 状态 6,输入 c
I6={A→Ab⋅},圆点在末尾——句柄已形成!但这次句柄是 Ab(长度 = 2),不是单个 b。查 ACTION[6,c]=r3。
归约 A→Ab:
- 弹出 2 个状态(6、3)和 2 个符号(b、A)——栈顶的 Ab 被整体弹出
- 露出栈顶状态 2,产生式左部是 A,查 GOTO[2, A] = 3
- 压入 A 和状态 3
归约后:[0, 2, 3] #aA (输入不变:cde#)
为什么步骤 (3) 和 (5) 的栈顶都是 b,却执行不同的归约?
步骤 (3) 栈顶状态是 4(I4={A→b⋅})→ 句柄是单个 b → r2
步骤 (5) 栈顶状态是 6(I6={A→Ab⋅})→ 句柄是 Ab → r3
状态本身就记录了历史:状态 4 是从 I2 经 b 到达的(“a 后的第一个 b”),状态 6 是从 I3 经 b 到达的(“A 后面的 b”)。两个不同的路径走到了两个不同的 DFA 状态,尽管当前读到的符号都是 b。这就是 LR 分析器区分不同上下文的机制。
(6) S5 — 状态 3,输入 c
I3={S→aA⋅cBe, …},圆点在 c 前。查 ACTION[3,c]=S5。
移进 c:符号栈压入 c,状态栈压入 5。
(7) S8 — 状态 5,输入 d
I5={B→⋅d, …},圆点在 d 前。查 ACTION[5,d]=S8。
移进 d:符号栈压入 d,状态栈压入 8。
(8) r4 — 状态 8,输入 e
I8={B→d⋅},圆点在末尾,句柄是栈顶的 d(长度 = 1)。
归约 B→d:
- 弹出 1 个状态(8)和 1 个符号(d)
- 露出栈顶状态 5,左部是 B,查 GOTO[5, B] = 7
- 压入 B 和状态 7
归约前:[0, 2, 3, 5, 8] #aAcd
归约后:[0, 2, 3, 5, 7] #aAcB (输入不变:e#)
(9) S9 — 状态 7,输入 e
I7={S→aAcB⋅e},圆点在 e 前。查 ACTION[7,e]=S9。
移进 e:符号栈压入 e,状态栈压入 9。
[0, 2, 3, 5, 7, 9] #aAcBe #
(10) r1 — 状态 9,输入 #
I9={S→aAcBe⋅},圆点在末尾,句柄是 aAcBe(长度 = 5)。
归约 S→aAcBe:
- 弹出 5 个状态(9、7、5、3、2)和 5 个符号(e、B、c、A、a)
- 露出栈顶状态 0,左部是 S,查 GOTO[0, S] = 1
- 压入 S 和状态 1
归约前:[0, 2, 3, 5, 7, 9] #aAcBe
(11) acc — 状态 1,输入 #
I1={S′→S⋅},状态栈顶只剩 S,输入只剩 #——整个输入串被成功归约为开始符号。
接受:语法分析成功完成。
- 拓广文法 → 引入 S′→S
- 构造 DFA → 从 I0=CLOSURE({S′→⋅S}) 出发,反复用 GOTO 函数生成新状态,直到无新状态
- 填写分析表 → 每状态查项目:圆点后是终结符 → Sj;圆点在末尾 → rj;GOTO(I, 非终结符) → GOTO 列
- 执行分析 → 每步查栈顶状态 + 当前输入 → Sj 则移进,rj 则归约(弹出、查 GOTO、压入),直到 acc
LR 分析器的核心优势:DFA 状态编码了完整的分析历史,使得每一步决策都是确定性的(只要文法是无二义的),不需要回溯。
由于大多数实用程序设计语言的文法不满足 LR(0) 条件,SLR(1) 通过向前查看一个输入符号来解决 LR(0) 项目集规范族中的冲突。只对有冲突的状态才需要向前查看,故名”简单” LR(1)。
对 LR(0) 中有冲突的项目集(状态),利用归约项目左部非终结符的 FOLLOW 集来决定是否归约:
- 若当前输入符号 a∈FOLLOW(A),则对归约项目 A→α⋅ 执行归约
- 若 a 是某个移进项目的移进符号,则优先移进
- 若 FOLLOW(A) 与所有移进符号集合不相交,则冲突可解
设 G 是拓广文法,C 是其 LR(0) 项目集规范族。若对 C 中每个有冲突的项目集 I,都能用 FOLLOW 集解决冲突,则称 G 为 SLR(1) 文法,相应的分析表为 SLR(1) 分析表。
假设已构造出 LR(0) 项目集规范族 C={I0,I1,…,In} 和所有非终结符的 FOLLOW 集。
- 移进:若项目 A→α⋅aβ∈Ik,且 GOTO(Ik,a)=Ij,a 为终结符,则 ACTION[k,a]=sj
- 归约:若项目 A→α⋅∈Ik,则对任何终结符 a 和 #,若 a∈FOLLOW(A),则 ACTION[k,a]=rj(j 为产生式编号)
- GOTO:若 GOTO(Ik,A)=Ij(A 为非终结符),则 GOTO[k,A]=j
- 接受:若项目 S′→S⋅∈Ik,则 ACTION[k,#]=acc
- 报错:其余情况填报错标志
改进点:步骤 (2) 中仅当归约项目左部非终结符的 FOLLOW 集包含当前输入符号时才归约。这使得某些在 LR(0) 中会被错误归约的情况能被提前发现。
拓广文法 G:
(0) (1) (2) (3) S′→SS→rDD→D,iD→i
其中 r 表示关键字 real。
表 6.5:G′ 的 LR(0) 项目集规范族
| 状态 | 核集合 | 闭包增加项目 | 项目集 |
|---|
| I0 | S′→⋅S | S→⋅rD | S′→⋅S S→⋅rD |
| I1 | S′→S⋅ | | S′→S⋅ |
| I2 | S→r⋅D | D→⋅D,i D→⋅i | S→r⋅D D→⋅D,i D→⋅i |
| I3 | S→rD⋅ D→D⋅,i | | S→rD⋅ D→D⋅,i |
| I4 | D→i⋅ | | D→i⋅ |
| I5 | D→D,⋅i | | D→D,⋅i |
| I6 | D→D,i⋅ | | D→D,i⋅ |
图 6.9:识别文法 G′ 活前缀的 DFA
flowchart LR
I0["I₀"]
I2["I₂"]
I3["I₃"]
I5["I₅"]
I6["I₆"]
I1["I₁"]
I4["I₄"]
I0 -->|r| I2
I0 -->|S| I1
I2 -->|D| I3
I3 -->|,| I5
I5 -->|i| I6
I3 -->|D| I4
说明:I3 是冲突状态,同时包含归约项目 S→rD⋅ 和移进项目 D→D⋅,i。
表 6.6:实数说明文法的 LR(0) 分析表
| 状态 | r | , | i | # | S | D |
|---|
| 0 | S2 | | | | 1 | |
| 1 | | | | acc | | |
| 2 | | | S4 | | | 3 |
| 3 | r1 | S5 | r1 | r1 | | |
| 4 | r3 | r3 | r3 | r3 | | |
| 5 | | | S6 | | | |
| 6 | r2 | r2 | r2 | r2 | | |
表 6.7:实数说明文法的 SLR(1) 分析表
| 状态 | r | , | i | # | S | D |
|---|
| 0 | S2 | | | | 1 | |
| 1 | | | | acc | | |
| 2 | | | S4 | | | 3 |
| 3 | | S5 | | r1 | | |
| 4 | | r3 | | r3 | | |
| 5 | | | S6 | | | |
| 6 | | r2 | | r2 | | |
该文法的 LR(0) 项目集规范族中,状态 I3 包含:
- S→D⋅ (归约项目)
- D→D⋅,i (移进项目,期待 ’,’)
LR(0) 分析表中,状态 I3 对输入符号 ’,’ 既有移进动作(D→D⋅,i),又有归约动作(S→D⋅ 对所有输入符号归约),存在移进-归约冲突。
SLR(1) 解决:
- FOLLOW(S)={#}
- FOLLOW(D)={#,,}
在状态 I3:
- 归约项目 S→D⋅ 仅当输入符号 ∈FOLLOW(S)={#} 时才归约
- 移进项目期待 ’,’
- 由于 {#}∩{,}=∅,冲突解决:
- 输入 ’,‘:移进
- 输入 ’#‘:归约 S→D
- 其他输入:报错
该文法是 SLR(1) 文法。
拓广文法 G:
(0) (1) (2) (3) (4) (5) (6) S′→EE→E+TE→TT→T∗FT→FF→(E)F→i
图 6.10:识别表达式文法活前缀的 DFA
stateDiagram-v2
I0: I₀
I1: I₁
I2: I₂
I3: I₃
I4: I₄
I5: I₅
I6: I₆
I7: I₇
I8: I₈
I9: I₉
I10: I₁₀
I11: I₁₁
I0 --> I5: i
I0 --> I4: (
I0 --> I1: E
I0 --> I2: T
I0 --> I3: F
I1 --> I6: +
I2 --> I7: *
I4 --> I8: E
I6 --> I9: T
I7 --> I10: F
I7 --> I5: i
I7 --> I4: (
I8 --> I11: )
I8 --> I6: +
I9 --> I6: +
I9 --> I7: *
各状态项目集详情:
| 状态 | 项目集 |
|---|
| I₀ | S′→⋅E, E→⋅E+T, E→⋅T, T→⋅T∗F, T→⋅F, F→⋅(E), F→⋅i |
| I₁ | S′→E⋅, E→E⋅+T |
| I₂ | E→T⋅, T→T⋅∗F |
| I₃ | T→F⋅ |
| I₄ | F→(⋅E), E→⋅E+T, E→⋅T, T→⋅T∗F, T→⋅F, F→⋅(E), F→⋅i |
| I₅ | F→i⋅ |
| I₆ | E→E+⋅T, T→⋅T∗F, T→⋅F, F→⋅(E), F→⋅i |
| I₇ | T→T∗⋅F, F→⋅(E), F→⋅i |
| I₈ | F→(E⋅), E→E⋅+T |
| I₉ | E→E+T⋅, T→T⋅∗F |
| I₁₀ | T→T∗F⋅ |
| I₁₁ | F→(E)⋅ |
说明:该 DFA 是构造 SLR(1) 分析表的基础。其中状态 I1(含 S′→E⋅ 和 E→E⋅+T)、I2(含 E→T⋅ 和 T→T⋅∗F)等存在移进-归约冲突,需用 SLR(1) 方法解决。
该文法的 LR(0) 项目集规范族在多个状态(如包含 E→E⋅+T 与 E→E⋅ 的状态、包含 T→T⋅∗F 与 T→T⋅ 的状态)中存在 移进-归约冲突 ,因此不是 LR(0) 文法。
SLR(1) 冲突分析:
- FOLLOW(E)={+,),#}
- FOLLOW(T)={+,),#}
在包含 E→E⋅+T(移进 ’+‘)和 E→E⋅(归约)的状态中:
- 归约 E→E 仅当输入符号 ∈FOLLOW(E)={+,),#} 时执行
- 移进项目期待 ’+’
- SLR(1) 分析器在输入为 ’+’ 时优先移进(构造 E+T),在输入为 ’)’ 或 ’#’ 时归约
在包含 T→T⋅∗F(移进 ’*‘)和 T→T⋅(归约)的状态中:
- FOLLOW(T)={+,),#}
- 移进符号 ’_’ 不在 FOLLOW(T) 中(FOLLOW(T)∩{}=∅)
- 因此:输入 ’*’ 时移进,输入 ’+’, ’)’, ’#’ 时归约 T→T
综上,该文法是 SLR(1) 文法。
表 6.8:表达式文法的 SLR(1) 分析表
| 状态 | i | + | ∗ | ( | ) | # | E | T | F |
|---|
| 0 | S5 | | | S4 | | | 1 | 2 | 3 |
| 1 | | S6 | | | | acc | | | |
| 2 | | r2 | S7 | | r2 | r2 | | | |
| 3 | | r4 | r4 | | r4 | r4 | | | |
| 4 | S5 | | | S4 | | | 8 | 2 | 3 |
| 5 | | r6 | r6 | | r6 | r6 | | | |
| 6 | S5 | | | S4 | | | | 9 | 3 |
| 7 | S5 | | | S4 | | | | | 10 |
| 8 | | S6 | | | S11 | | | | |
| 9 | | r1 | S7 | | r1 | r1 | | | |
| 10 | | r3 | r3 | | r3 | r3 | | | |
| 11 | | r5 | r5 | | r5 | r5 | | | |
表 6.9:对输入串 i+i∗i# 的 SLR(1) 分析过程
| 步骤 | 状态栈 | 符号栈 | 输入串 | ACTION | GOTO |
|---|
| (1) | 0 | # | i+i∗i# | S5 | |
| (2) | 05 | #i | +i∗i# | r6 | 3 |
| (3) | 03 | #F | +i∗i# | r4 | 2 |
| (4) | 02 | #T | +i∗i# | r2 | 1 |
| (5) | 01 | #E | +i∗i# | S6 | |
| (6) | 016 | #E+ | i∗i# | S5 | |
| (7) | 0165 | #E+i | ∗i# | r6 | 3 |
| (8) | 0163 | #E+F | ∗i# | r4 | 9 |
| (9) | 0169 | #E+T | ∗i# | S7 | |
| (10) | 01697 | #E+T∗ | i# | S5 | |
| (11) | 016975 | #E+T∗i | # | r6 | 10 |
| (12) | 01697(10) | #E+T∗F | # | r3 | 9 |
| (13) | 0169 | #E+T | # | r1 | 1 |
| (14) | 01 | #E | # | acc | |
并非所有非 LR(0) 文法都是 SLR(1) 文法。有些文法的 LR(0) 冲突无法通过 FOLLOW 集解决,这类文法需要用 LR(1) 或 LALR(1) 分析。
反例:文法 G
(0) (1) (2) (3) (4) (5) S′→SS→aAdS→bAcS→aecS→bedA→e
该文法的 LR(0) 项目集规范族在某些状态中存在移进-归约冲突。计算得 FOLLOW(A)={c,d}。虽然在某些状态中 FOLLOW(A) 与移进符号的交集为空,但在另一些状态中,FOLLOW 集无法区分不同的归约情境,导致冲突无法用 SLR(1) 方法解决。该文法是 LR(1) 文法,但不是 SLR(1) 文法。
图 6.11:LR(0) 识别 G′ 活前缀的 DFA(Mermaid)
stateDiagram-v2
I0: I₀: S'→·S<br>S→·aAd<br>S→·bAc<br>S→·aec<br>S→·bed
I1: I₁: S'→S·
I2: I₂: S→a·Ad<br>S→a·ec<br>A→·e
I3: I₃: S→b·Ac<br>S→b·ed<br>A→·e
I4: I₄: S→aA·d
I5: I₅: S→ae·c<br>A→e·
I6: I₆: S→bA·c
I7: I₇: S→be·d<br>A→e·
I8: I₈: S→aAd·
I9: I₉: S→aec·
I10: I₁₀: S→bAc·
I11: I₁₁: S→bed·
I0 --> I1: S
I0 --> I2: a
I0 --> I3: b
I2 --> I4: A
I2 --> I5: e
I4 --> I8: d
I5 --> I9: c
I3 --> I6: A
I3 --> I7: e
I6 --> I10: c
I7 --> I11: d
项目集详情:
| 状态 | 项目集 |
|---|
| I₀ | S′→⋅S, S→⋅aAd, S→⋅bAc, S→⋅aec, S→⋅bed |
| I₁ | S′→S⋅ |
| I₂ | S→a⋅Ad, S→a⋅ec, A→⋅e |
| I₃ | S→b⋅Ac, S→b⋅ed, A→⋅e |
| I₄ | S→aA⋅d |
| I₅ | S→ae⋅c, A→e⋅ |
| I₆ | S→bA⋅c |
| I₇ | S→be⋅d, A→e⋅ |
| I₈ | S→aAd⋅ |
| I₉ | S→aec⋅ |
| I₁₀ | S→bAc⋅ |
| I₁₁ | S→bed⋅ |
冲突分析:
在状态 I5 中:
- S→ae⋅c(移进项目,期待 c)
- A→e⋅(归约项目 r5)
当输入为 c 时,既可以选择将 A→e 归约,也可以选择将 c 作为 S→aec 的一部分移进。此为移进-归约冲突。
FOLLOW(A)={c,d}。由于 c∈FOLLOW(A),FOLLOW(A) 与移进符号 {c} 的交集非空,SLR(1) 无法解决这一冲突。
状态 I7 类似:S→be⋅d(期待 d)与 A→e⋅(归约)冲突,且 d∈FOLLOW(A)。
SLR(1) 失效的根本原因:SLR(1) 对所有出现 A→e⋅ 的状态使用同一个全局 FOLLOW(A)={c,d},但实际在不同上下文中适用的向前看符号是不同的——在 I5(经过 a 的分支)中 A 后面只能跟 d(来自 S→aAd),在 I7(经过 b 的分支)中 A 后面只能跟 c(来自 S→bAc)。LR(1) 正是通过为每个归约项目计算 状态相关的向前看集合 来精确解决这类冲突。
LR(1) 分析表构造精确,对文法限制少,但可能造成状态数急剧增长。LALR(1) 通过合并 LR(1) 项目集规范族中的同心集来压缩状态数,同时尽量保持分析能力。
- 同心集:具有相同核心(核)但向前看符号集合不同的 LR(1) 项目集
- 将同心集合并为一个状态,其向前看符号集合为各同心集的并集
- 若合并后不引入新冲突,则该文法是 LALR(1) 文法
图 6.12:LR(1) 项目集及转换函数(示例)
stateDiagram-v2
I0: I₀
I1: I₁
I2: I₂
I3: I₃
I4: I₄
I5: I₅
I6: I₆
I7: I₇
I8: I₈
I9: I₉
I10: I₁₀
I11: I₁₁
I0 --> I1: S
I0 --> I2: a
I0 --> I3: b
I2 --> I4: A
I2 --> I5: e
I4 --> I8: d
I5 --> I9: c
I3 --> I6: A
I3 --> I7: e
I6 --> I10: c
I7 --> I11: d
- 核心不变:同心集合并后,核心仍相同,仅向前看符号集合扩大为各集合的并集
- 转换封闭:若两个项目集是同心集,则它们的 GOTO 转换后仍为同心集,合并后转换函数自动合并
- 冲突类型限制:若文法是 LR(1) 文法,合并同心集后若产生冲突,只可能是归约-归约冲突,不可能产生新的移进-归约冲突
- 错误发现推迟:合并同心集后,某些错误发现的时间可能推迟,但错误位置仍是准确的
- 构造文法 G 的 LR(1) 项目集族 C={I1,I2,…,In}
- 合并所有同心集,得到 C′={J1,J2,…,Jm}(m<n)
- 基于 C′ 构造 ACTION 表和 GOTO 表,方法与 LR(1) 相同:
- 移进:若 [A→α⋅aβ,b]∈Jk 且 GOTO(Jk,a)=Jl,a 为终结符,则 ACTION[k,a]=sl
- 归约:若 [A→α⋅,a]∈Jk,则 ACTION[k,a]=rj(j 为产生式编号)
- 接受:若 [S′→S⋅,#]∈Jk,则 ACTION[k,#]=acc
- GOTO:若 GOTO(Jk,A)=Jl(A 为非终结符),则 GOTO[k,A]=l
文法 G:
(0) (1) (2) (3) (4) (5) S′→SS→aAdS→bAcS→aeS→bfA→e
该文法是 LR(1) 文法,但不是 SLR(1) 文法(见 6.3 节反例),也不是 LR(0) 文法。
经分析,该文法也是 LALR(1) 文法。其 LR(1) 项目集规范族中存在同心集,合并后得到 LALR(1) 项目集族,分析表与 LR(1) 类似但状态数更少。
文法 G[S]:
(0) (1) (2) (3) (4) (5) (6) S′→SS→aAdS→bBdS→aAeS→bAeA→cB→c
LR(1) 分析:可构造 LR(1) 项目集族,无冲突,因此是 LR(1) 文法。
LALR(1) 分析:LR(1) 项目集族中存在如下同心集:
- I8: [A→c⋅,{d,e}]
- I9: [B→c⋅,{d,e}]
合并后变为:
- J: A→c⋅(向前看符号 {d,e})
- J: B→c⋅(向前看符号 {d,e})
合并后,对于向前看符号 d 和 e,既可以用 A→c 归约,也可以用 B→c 归约,产生归约-归约冲突。
因此该文法是 LR(1) 文法,但不是 LALR(1) 文法。
LR(1) 分析(也称规范 LR 分析)为每个项目精确计算向前看符号集合,因此分析能力最强,能解决 SLR(1) 和 LALR(1) 无法解决的冲突。
LR(1) 项目形如 [A→α⋅β,a],其中 a 是向前看符号(终结符或 #),表示:当用 A→αβ 归约后,合法的后续输入符号应包含 a。
若 [A→α⋅Bβ,a]∈I,则对 B 的每个产生式 B→γ,将 [B→⋅γ,b] 加入 I,其中 b∈FIRST(βa)。
与 LR(0) 类似,但项目是 LR(1) 项目:
- 若 [A→α⋅aβ,b]∈Ik,a 为终结符,且 GOTO(Ik,a)=Il,则 ACTION[k,a]=sl
- 若 [A→α⋅,a]∈Ik,则 ACTION[k,a]=rj(j 为产生式编号)
- 若 [S′→S⋅,#]∈Ik,则 ACTION[k,#]=acc
- GOTO 表同 LR(0)
图 6.13:LR(1) 项目集及转换函数(赋值语句文法)
stateDiagram-v2
I0: I₀: S'→·S,#<br>S→·L=R,#<br>S→·R,#<br>L→·*R,#<br>L→·i,#<br>R→·L,#
I1: I₁: S'→S·,#<br>S→L·=R,#<br>R→L·,#<br>L→*·R,#<br>L→·i,#<br>R→·L,#
I2: I₂: S→R·,#<br>R→L·,#
I3: I₃: S→L·=R,#<br>L→*·R,=<br>L→·i,=<br>R→·L,=
I4: I₄: L→*R·,#<br>R→L·,#
I5: I₅: L→i·,#<br>R→L·,#
I6: I₆: S→L=R·,#<br>R→L·,#
I7: I₇: R→L·,#<br>L→*·R,#<br>L→·i,#
I0 --> I1: S
I0 --> I3: L
I0 --> I2: R
I1 --> I3: =
I3 --> I4: *
I3 --> I5: i
I3 --> I7: L
I4 --> I1: R
I5 --> I1: R
I7 --> I1: L
I1 --> I6: =
文法 G:
(0) (1) (2) (3) (4) (5) S′→SS→L=RS→RL→∗RL→iR→L
LR(0) 分析:存在移进-归约冲突(在 R→L 和 S→L= 等项目中),不是 LR(0) 文法。
SLR(1) 分析:计算 FOLLOW(R)={#,=}。在包含 R→L⋅(归约)和 S→L⋅=R(移进 ’=‘)的状态中,FOLLOW(R)∩{=}={#,=}∩{=}=∅,冲突无法解决。因此不是 SLR(1) 文法。
LR(1) 分析:LR(1) 项目集族中,归约项目 [R→L⋅,#] 的向前看符号只有 #,而移进项目期待 ’=‘,两者不相交,冲突解决。因此是 LR(1) 文法。
LALR(1) 分析:LR(1) 项目集族中存在同心集,合并后向前看符号集合扩大。但由于没有归约-归约冲突,仍是 LALR(1) 文法。
分析能力层次:LR(1) ⊃ LALR(1) ⊃ SLR(1) ⊃ LR(0)
| 分析方法 | 向前看 | 状态数 | 分析能力 | 实用程度 |
|---|
| LR(0) | 0 | 最少 | 最弱 | 理论意义 |
| SLR(1) | 1(FOLLOW 集) | 少 | 中 | 简单文法 |
| LALR(1) | 1(合并同心集) | 中 | 强 | 实用主流 |
| LR(1) | 1(精确向前看) | 最多 | 最强 | 理论/工具生成 |