# 编译原理——语法分析

84 min read
Table of Contents

复习材料,不建议阅读

语法分析

确定的自顶向下分析思想

确定的自顶向下分析方法,是从文法的开始符号出发,考虑如何根据当前的输入符号(单词符号)唯一地确定选用哪个产生式替换相应非终结符以往下推导,或如何构造一棵相应的语法树。

  1. 正规情况
SpAqBAcAdaBdBbS → pA | qB \\ A → cAd | a \\ B → dB | b

文法右部始终由终结符号开始;相同左部时,右部是不同终结符号开始。

就可以直接根据当前输入符号决定产生式。

  1. 一般情况
SApBqAacABdBbS → Ap | Bq \\ A → a | cA \\ B → dB | b

右部不全由终结符开始;相同左部时,右部是不同符号开始;没有空产生式。

问题 A:当输入符号是 cc 时,从 SS 出发选择 ApAp 还是 BqBq 能推出 cc 开头串?

  1. 舍空产生式
SaAdAbASεS → aA | d \\ A → bAS | ε

有空产生式了。

问题B:如果有一个输入串 W=abdW=abd,推导过程:SaAabASabSabdS \Rightarrow aA \Rightarrow abAS \Rightarrow abS \Rightarrow abd;在 abASabSabAS \Rightarrow abS 的时候,AA 的产生式右部开始符号集都不包含 dd 只有 εε 产生式,就可以认为 dd 的匹配实际依赖于 AA 后面的符号 SS

FIRST 集

FIRST(a) 是符号串 aa 可以推导出的所有串的首终结符号集合,称为 aa 的开始符号集或首符号集。

FIRST(a)={αaαβ,αVT,βV}FIRST(a)=\{\alpha |a \overset{*}{\Rightarrow} \alpha \beta,\alpha \in V_T ,\beta \in V^∗ \}

aa 能推导出空的时候,εFIRST(a)ε \in FIRST(a)

由此解决问题 A,求 ApApBqBqFIRSTFIRST 集(且它们恰好不相交)就可以得出输入符号选择哪个产生式。

FOLLOW 集

FOLLOW(A)FOLLOW(A) 是在所有句型中紧跟在非终结符 AA 后面的终结符号集合。# 是输入串的结束符,也称输入串括号。

FOLLOW(A)={aSAa,aVT}FOLLOW(A)=\{a|S \overset{∗}{\Rightarrow} \dots Aa \dots,a \in V_T\}

SS 能推导出 A\dots A 的时候,#FOLLOW(A)\# \in FOLLOW(A)

结合 SELECTSELECT 由此解决问题 B,ASA、S 不能同时推导出空,替换仍是唯一确定的。

SELECT 集

SELECT(Aα)SELECT(A \to \alpha) 是产生式 AαA \to \alpha 的选择符号集,表示当遇到这些输入符号时应该选择该产生式进行推导。

  • εFIRST(α)ε \notin FIRST(\alpha),则 SELECT(Aα)=FIRST(α)SELECT(A \to \alpha) = FIRST(\alpha)
  • εFIRST(α)ε \in FIRST(\alpha),则 SELECT(Aα)=(FIRST(α){ε})FOLLOW(A)SELECT(A \to \alpha) = (FIRST(\alpha) - \{ε\}) \cup FOLLOW(A)

简单来说就是,FIRSTFIRST 不含空选 FIRSTFIRST,含空就 FIRSTFIRST 去掉空并 FOLLOWFOLLOW

LL(1)LL(1) 文法

概念

一个上下文无关文法是 LL(1)LL(1) 文法的充分必要条件是,对每个非终结符 AA 的两个不同产生式 AαA \to \alphaAβA \to \beta,满足:SELECT(Aα)SELECT(Aβ)=SELECT(A \to \alpha) ∩ SELECT(A \to \beta) = ∅。(同一左部 SELECT 集不相交)

其中 α\alphaβ\beta 不同时能推导出 εε

判别

  1. 求能推出 ε 的非终结符

建立标志位数组(“未定”/“是”/“否”),扫描产生式:

  • 右部含终结符 → 标记”否”
  • 右部为 ε → 标记”是”
  • 重复扫描直到标志不再变化
  1. 计算 FIRST 集

对每个符号 X:

  • XVTX \in V_T,则 FIRST(X)={X}FIRST(X) = \{X\}
  • XVNX \in V_NXaαX \to a\alpha,则 aFIRST(X)a \in FIRST(X)
  • XVNX \in V_NXεX \to \varepsilon,则 εFIRST(X)\varepsilon \in FIRST(X)
  • XVNX \in V_NXY1...YnX \to Y_1...Y_n,则将 FIRST(Yi)FIRST(Y_i)(除 ε\varepsilon)加入 FIRST(X)FIRST(X)
  1. 计算 FOLLOW 集

对每个非终结符 A:

  • 开始符号 S:#FOLLOW(S)\# \in FOLLOW(S)
  • 产生式 AαBβA \to \alpha B\betaFIRST(β)FIRST(\beta)(非空)加入 FOLLOW(B)FOLLOW(B)
  • εFIRST(β)\varepsilon \in FIRST(\beta)FOLLOW(A)FOLLOW(A) 加入 FOLLOW(B)FOLLOW(B)
  1. 计算 SELECT 集并判别

对每个产生式 AαA \to \alpha

  • εFIRST(α)\varepsilon \notin FIRST(\alpha),则 SELECT(Aα)=FIRST(α)SELECT(A \to \alpha) = FIRST(\alpha)
  • εFIRST(α)\varepsilon \in FIRST(\alpha),则 SELECT(Aα)=(FIRST(α){ε})FOLLOW(A)SELECT(A \to \alpha) = (FIRST(\alpha) - \{\varepsilon\}) \cup FOLLOW(A)

LL(1) 条件:对每个非终结符 A 的不同产生式,SELECT 集的交集为空集。

不确定的自顶向下分析思想

当文法不满足 LL(1) 时,不能用确定的自顶向下分析,但可用不确定的自顶向下分析(带回溯的自顶向下分析)。

引起回溯的原因是:在文法中当关于某个非终结符的产生式有多个候选时,而面临当前的输入符无法确定选用唯一的产生式,从而引起回溯。

回溯分析

  1. 由于相同左部的产生式的右部 FIRST 集交集不为空而引起回溯
SxAyAaba\begin{aligned} S &\to xAy \\ A &\to ab \mid a \end{aligned}

输入串 xay,先选 A → abxa 匹配后当前符 yb 不匹配,回溯改选 A → a,匹配成功。

  1. 由于相同左部非终结符的右部存在 ε 的产生式,且该非终结符的 FOLLOW 集中含有其他产生式右部 FIRST 集的元素
SaAbAcAε\begin{aligned} S &\to aA \mid b \\ A &\to cA \mid \varepsilon \end{aligned}

输入串 ab#,先选 S → aAa 匹配后 AA → cA,但 cb 不匹配,回溯改选 A → εbS 的 FOLLOW 集中的 b 匹配,匹配成功。

  1. 由于文法含有左递归而引起回溯
SSab\begin{aligned} S &\to Sa \mid b \end{aligned}

输入串 baa#,先选 S → b,输入串未分析完,回溯改选 S → Sa,再选 S → b,得到 ba,但输入串还有 a#,继续回溯,最终得到 baa,匹配成功。

带回溯分析代价很高,效率很低,在实用编译程序中几乎不用。

某些非 LL(1)文法到 LL(1)文法的等价变换

左递归消除

文法中含有左递归时不能采用确定的自顶向下分析法。左递归分为直接左递归和间接左递归。

直接左递归消除

形如 AAαA \to A\alpha 的产生式称为直接左递归。

:文法 G5G_5 含有直接左递归:

SSaSb\begin{aligned} S &\to Sa \\ S &\to b \end{aligned}

该文法产生的语言 L={bann0}L = \{ba^n | n \geq 0\}。输入串 baaa# 应是该语言的句子,但用自顶向下分析时,当输入符为 b 时,为与 S 匹配则应选用 SbS \to b 推导,但这样就推不出后边部分;而若用 SSaS \to Sa 推导则无法确定到什么时候才用 SbS \to b 替换。

消除方法:把直接左递归改写为右递归。对文法 G5G_5 可改写为:

SbSSaSε\begin{aligned} S &\to bS' \\ S' &\to aS' | \varepsilon \end{aligned}

改写后的文法和原文法产生的语言句子集都为 {bann0}\{ba^n | n \geq 0\},且改写后的文法为 LL(1) 文法。

一般情况下,假定关于 AA 的全部产生式是:

AAα1Aα2Aαnβ1β2βmA \to A\alpha_1 | A\alpha_2 | \dots | A\alpha_n | \beta_1 | \beta_2 | \dots | \beta_m

其中 βi\beta_i1im1 \leq i \leq m)不以 AA 开头,消除直接左递归后改写为:

Aβ1Aβ2AβmAAα1Aα2AαnAε\begin{aligned} A &\to \beta_1 A' | \beta_2 A' | \dots | \beta_m A' \\ A' &\to \alpha_1 A' | \alpha_2 A' | \dots | \alpha_n A' | \varepsilon \end{aligned}

间接左递归消除

形如 ABαA \to B\alphaBAβB \to A\beta 等可以形成推导 A+AA \Rightarrow^+ A 的产生式称为间接左递归。

:文法 G6G_6 含有间接左递归:

AaBABbBAcBd\begin{aligned} A &\to aB \\ A &\to Bb \\ B &\to Ac \\ B &\to d \end{aligned}

若有输入串为 adbcbcbc#,当分析过程至 AaBaAcaBbcA \Rightarrow aB \Rightarrow aAc \Rightarrow aBbc 时,BB 若用产生式 BdB \to d 替换,则分析过程终止,不能推出 adbcbcbc#;而若选用产生式 BAcB \to Ac,则会出现无法确定何时终止的情况。

消除方法:先通过产生式非终结符置换,将间接左递归变为直接左递归,然后再按消除直接左递归的方法处理。

以文法 G6G_6 为例,用产生式 AaBA \to aBABbA \to Bb 的右部置换产生式 BAcB \to Ac 中的非终结符 AA,得到左部为 BB 的产生式:

BaBcBBbcBd\begin{aligned} B &\to aBc \\ B &\to Bbc \\ B &\to d \end{aligned}

消除左递归后得:

BaBcBdBBbcBε\begin{aligned} B &\to aBcB' | dB' \\ B' &\to bcB' | \varepsilon \end{aligned}

再把原来其余的产生式 AaBA \to aBABbA \to Bb 加入,最终文法为:

AaBABbBaBcBdBBbcBε\begin{aligned} A &\to aB \\ A &\to Bb \\ B &\to aBcB' | dB' \\ B' &\to bcB' | \varepsilon \end{aligned}

该文法与 G6G_6 等价,即它们产生相同的句子集。

消除一切左递归的算法

对文法中一切左递归的消除要求文法中不含回路,即无 A+AA \overset{+}{\Rightarrow} A 的推导。满足这个要求的充分条件是,文法中不包含形如 AAA \to A 的有害规则和 AA 的空产生式。

算法步骤如下:

  1. 把文法的所有非终结符按某一顺序排序,例如:A1,A2,,AnA_1, A_2, \dots, A_n
  2. FOR i = 1 TO n DO
    • FOR j = 1 TO i-1 DO
      • AiA_i 的所有产生式为 Aiδ1δ2δkA_i \to \delta_1 | \delta_2 | \dots | \delta_k
      • 将其替换形如 AiAjγA_i \to A_j\gamma 的产生式得到 Aiδ1δ2δmA_i \to \delta_1' | \delta_2' | \dots | \delta_m'
    • 消除 AiA_i 中的一切直接左递归
  3. 去掉无用产生式

:按上述方法消除如下文法的一切左递归:

SQcQRbbRSaa\begin{aligned} S &\to Qc \\ Q &\to Rb | b \\ R &\to Sa | a \end{aligned}

若非终结符排序为 S,Q,RS, Q, R

  • 左部为 SS 的产生式 SQcS \to Qc 无直接左递归
  • 左部为 QQ 的产生式 QRbbQ \to Rb | b 中右部不含 SS
  • 把产生式 SQcS \to Qc 的右部代入产生式 RSaR \to Sa 得:RQcaaR \to Qca | a
  • 再将产生式 QRbbQ \to Rb | b 的右部代入得:RRbcabcaaR \to Rbca | bca | a
  • 对产生式消除直接左递归得:
RbcaRaRRbcaRε\begin{aligned} R &\to bcaR' | aR' \\ R' &\to bcaR' | \varepsilon \end{aligned}

最终文法变为:

SQcQRbbRbcaRaRRbcaRε\begin{aligned} S &\to Qc \\ Q &\to Rb | b \\ R &\to bcaR' | aR' \\ R' &\to bcaR' | \varepsilon \end{aligned}

若非终结符的排序为 R,Q,SR, Q, S,则把产生式 RSaaR \to Sa | a 代入产生式 QRbbQ \to Rb | b 得:QSababbQ \to Sab | ab | b,再将此代入产生式 SQcS \to Qc 得:SSabcabcbcS \to Sabc | abc | bc。消除该产生式的左递归后,文法变为:

SabcSbcSSabcSε\begin{aligned} S &\to abcS' | bcS' \\ S' &\to abcS' | \varepsilon \end{aligned}

由于 Q,RQ, R 为不可到达的非终结符,所以以 Q,RQ, R 为左部及包含 Q,RQ, R 的产生式应删除。

当非终结符的排序不同时,最后结果的产生式形式不同,但它们是等价的。

提取左公因子

若文法中含有形如 Aαβ1αβ2A \to \alpha\beta_1 | \alpha\beta_2 的产生式,会导致相同左部产生式的 FIRST 集相交,不满足 LL(1) 条件。

可将产生式等价变换为:

AαAAβ1β2\begin{aligned} A &\to \alpha A' \\ A' &\to \beta_1 | \beta_2 \end{aligned}

写成一般形式:Aαβ1αβ2αβnγA \to \alpha\beta_1 | \alpha\beta_2 | \dots | \alpha\beta_n | \gamma(其中 γ\gamma 不以 α\alpha 开头)

提取左公共因子后变为:

AαAγAβ1β2βn\begin{aligned} A &\to \alpha A' | \gamma \\ A' &\to \beta_1 | \beta_2 | \dots | \beta_n \end{aligned}

βi\beta_i 中仍含有左公共因子,可再次提取,直到无左公共因子为止。

例 1:文法 G1G_1 的产生式为

SSbSSaSε\begin{aligned} S &\to Sb \\ S &\to Sa \\ S &\to \varepsilon \end{aligned}

对产生式(1)、(2)提取左公共因子后得:

SS(ba)Sε\begin{aligned} S &\to S(b | a) \\ S &\to \varepsilon \end{aligned}

进一步变换为:

SSAAbAaSε\begin{aligned} S &\to SA' \\ A' &\to b \\ A' &\to a \\ S &\to \varepsilon \end{aligned}

例 2:文法 G2G_2 的产生式为

AadABcBaABb\begin{aligned} A &\to ad \\ A &\to Bc \\ B &\to aA \\ B &\to b \end{aligned}

产生式(2)的右部以非终结符 BB 开始,左公共因子可能是隐式的。用产生式(3)、(4)的右部替换产生式(2)中的 BB,可得:

AadAaAcAbc\begin{aligned} A &\to ad \\ A &\to aAc \\ A &\to bc \end{aligned}

提取产生式(1)、(2)的左公共因子得:

Aa(dAc)Abc\begin{aligned} A &\to a(d | Ac) \\ A &\to bc \end{aligned}

引进新非终结符 AA' 后得 G2G_2 为:

AaAAbcAdAAc\begin{aligned} A &\to aA' \\ A &\to bc \\ A' &\to d \\ A' &\to Ac \end{aligned}

注意事项

  1. 提取左公共因子后,可能使某些产生式变成无用产生式,需要对文法重新压缩。

  2. 某些文法不能在有限步骤内提取完左公共因子。例如文法 G4G_4

SAplBaAaApdBaBqe\begin{aligned} S &\to Apl | Ba \\ A &\to aAp | d \\ B &\to aBq | e \end{aligned}

用产生式(2)、(3)的右部替换产生式(1)中的 AABB,再提取左公共因子,只能使文法的产生式越来越多,无限增加下去,而不能得到提取左公共因子的预期结果。

  1. 一个文法提取了左公共因子后,只解决了相同左部产生式右部的 FIRST 集不相交的问题。当改写后的文法不含空产生式,且无左递归时,则改写后的文法是 LL(1) 文法;若还有空产生式时,则还需用 LL(1) 文法的判别方式进行判断才能确定是否为 LL(1) 文法。

LL(1) 分析的实现

递归下降分析

核心思想:把每个非终结符编写为一个递归函数,右部作为函数体。函数内根据当前输入符号查 SELECT 集选择产生式,递归调用对应函数。

构造方法

  1. 为每个非终结符 AA 编写函数 A()
  2. 函数体结构:
    • 根据当前输入符号 lookahead 选择产生式
    • 对产生式右部的每个符号:
      • 终结符:匹配并读入下一符号
      • 非终结符:调用对应函数

示例:文法 EE+TTE \to E+T \mid TTTFFT \to T*F \mid FF(E)idF \to (E) \mid \text{id}

消除左递归后:ETEE \to TE'E+TEεE' \to +TE' \mid \varepsilonTFTT \to FT'TFTεT' \to *FT' \mid \varepsilonF(E)idF \to (E) \mid \text{id}

def E():
T()
E_prime()
def E_prime():
if lookahead == '+':
match('+')
T()
E_prime()
# else: ε 产生式,什么都不做
def T():
F()
T_prime()
def T_prime():
if lookahead == '*':
match('*')
F()
T_prime()
def F():
if lookahead == '(':
match('(')
E()
match(')')
elif lookahead == 'id':
match('id')
else:
error()
def match(expected):
global lookahead
if lookahead == expected:
lookahead = next_token() # 读入下一符号
else:
error()

优点:代码结构清晰,易于理解和调试,可手工编写。

缺点:每个文法需单独编程,修改文法需重写代码。

表驱动分析(预测分析)

核心思想:构建预测分析表 M[A,a]M[A, a] 存储”非终结符 AA 遇到输入符号 aa 时选择哪个产生式”,用栈模拟推导过程。

分析表构造:对每个产生式 AαA \to \alpha,对 SELECT(Aα)SELECT(A \to \alpha) 中的每个终结符 aa,令 M[A,a]=AαM[A, a] = A \to \alpha

分析过程

栈初始化: [#, S] (# 是栈底, S 是开始符号)
输入指针: 指向输入串首符号
循环:
X = 栈顶符号
a = 当前输入符号
if X == a == #:
分析成功
elif X == a:
弹栈, 输入指针前移
elif X 是终结符:
报错 (栈顶终结符与输入不匹配)
elif M[X, a] 为空:
报错 (无对应产生式)
else: # M[X, a] = X → Y₁Y₂...Yₖ
弹出 X
将 Yₖ...Y₂Y₁ 逆序压栈 (若 Yᵢ = ε 则不压栈)

示例:文法 ETEE \to TE'E+TEεE' \to +TE' \mid \varepsilonTFTT \to FT'TFTεT' \to *FT' \mid \varepsilonF(E)idF \to (E) \mid \text{id}

分析表(部分):

非终结符id+*()#
EETEE \to TE'ETEE \to TE'
E’E+TEE' \to +TE'EεE' \to \varepsilonEεE' \to \varepsilon
TTFTT \to FT'TFTT \to FT'
T’TεT' \to \varepsilonTFTT' \to *FT'TεT' \to \varepsilonTεT' \to \varepsilon
FFidF \to \text{id}F(E)F \to (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]
input_ptr = 0
input_string += '#'
while True:
X = stack[-1] # 栈顶
a = input_string[input_ptr] # 当前输入
if X == a == '#':
return "接受"
elif X == a:
stack.pop()
input_ptr += 1
elif X in terminals:
return f"错误: 期望 {X}, 得到 {a}"
elif parse_table[X][a] is None:
return f"错误: 无产生式 M[{X}, {a}]"
else:
production = parse_table[X][a] # X → Y₁Y₂...Yₖ
stack.pop()
if production != 'ε':
for symbol in reversed(production):
stack.append(symbol)

优点:通用性强,修改文法只需重新生成分析表,适合自动化工具生成。

缺点:需要预先构造分析表,不如递归下降直观。

LL(1) 分析中的错误处理

错误处理包含两个任务:报错(指出错误位置和类型)和错误恢复(使分析继续进行)。

错误发生的情况

LL(1) 分析中有两种错误情况:

  1. 栈顶终结符与当前输入不匹配
  2. 栈顶非终结符 AA 面临输入符号 aa,但 M[A,a]M[A, a] 为空

应急恢复 (Panic Mode)

核心思想:跳过输入符号直到遇到”同步符号”,使分析能继续。

同步符号选择:将 FIRST(A)FIRST(A)FOLLOW(A)FOLLOW(A) 中的符号作为非终结符 AA 的同步符号。

恢复策略

  • 遇到 FOLLOW(A)FOLLOW(A) 中的符号:弹出栈顶的 AA,继续分析
  • 遇到 FIRST(A)FIRST(A) 中的符号:保留 AA 在栈顶,根据 AA 恢复分析

示例:若 EE 在栈顶,当前输入是 ),但 M[E,)]M[E, )] 为空:

FOLLOW(E) = {), #}
策略: 跳过输入直到遇到 ) 或 #,弹出 E,继续分析

短语层恢复 (Phrase-Level)

核心思想:根据当前语法单位的上下文进行更精确的恢复。

流程

  1. 进入语法单位时

    • 检查当前符号是否属于 BeginSymBeginSym(通常取 FIRSTFIRST 集)
    • 若不属于,报错并跳过 BeginSymEndSymBeginSym \cup EndSym 之外的符号
    • 遇到 BeginSymBeginSym 中符号:重新分析该单位
    • 遇到 EndSymEndSym 中符号:退出该单位
  2. 离开语法单位时

    • 检查当前符号是否属于 EndSymEndSym(基于 FOLLOWFOLLOW 集)
    • 若不属于,报错并跳过 BeginSymEndSymBeginSym \cup EndSym 之外的符号

递归下降中的实现示例

文法:B[A](A)B \to [A] \mid (A)AaA \to a

def ParseB(EndSym):
BeginSym = {'[', '('} # FIRST(B)
# 进入时检查
if sym not in BeginSym:
error("期望 [ 或 (")
skip_until(BeginSym | EndSym)
if sym == '[':
match('[')
ParseA(EndSym | {']'}) # 传入上下文相关的 EndSym
match(']')
else:
match('(')
ParseA(EndSym | {')'}) # 不同上下文使用不同参数
match(')')
# 离开时检查
if sym not in EndSym:
error("意外的符号")
skip_until(BeginSym | EndSym)

关键点:不同上下文传入不同的 EndSymEndSym 参数,体现”短语层”的含义。例如方括号内调用 ParseA 时传入 EndSym ∪ {]},圆括号内传入 EndSym ∪ {)}

两种方法对比

方法优点缺点
应急恢复实现简单,通用恢复不够精确,可能跳过过多符号
短语层恢复考虑上下文,恢复更精确实现复杂,需为每个语法单位设计

实际编译器(如 PL/0)通常采用短语层恢复,在进入和退出语法单位时调用检查函数,根据 FIRSTFIRSTFOLLOWFOLLOW 集合进行错误检测和恢复。

自底向上优先分析

自底向上分析(也称 移进-归约分析)的基本思想是:对输入符号串自左向右扫描,将输入符逐个移入一个后进先出栈中,边移入边分析;一旦栈顶符号串形成某个句型的句柄或其他可归约串(对应某产生式的右部),就用该产生式的左部非终结符代替相应右部的文法符号串,称为一步归约。重复这一过程,直到栈中只剩文法的开始符号时分析成功,确认输入串是文法的句子。

自底向上分析是规范推导(最右推导)的逆过程,因此规范归约也称为最左归约

例 5.1

设文法 G[S]G[S] 为:

(1) SaAcBe(2) Ab(3) AAb(4) Bd\begin{aligned} (1)\ &S \to aAcBe \\ (2)\ &A \to b \\ (3)\ &A \to Ab \\ (4)\ &B \to d \end{aligned}

对输入串 abbcde#abbcde\# 进行分析:

该串的最右推导为:

SaAcBeaAbcdeabbcdeS \Rightarrow aAcBe \Rightarrow aAbcde \Rightarrow abbcde

归约过程(构造语法树的逆过程)如下表所示:

步骤符号栈输入串动作
(1)#abbcde#移进
(2)#abbcde#移进
(3)#abbcde#归约 (AbA \to b)
(4)#aAbcde#移进
(5)#aAbcde#归约 (AAbA \to Ab)
(6)#aAcde#移进
(7)#aAcde#移进
(8)#aAcde#归约 (BdB \to d)
(9)#aAcBe#移进
(10)#aAcBe#归约 (SaAcBeS \to aAcBe)
(11)#S#接受

上述分析过程也是自底向上构造语法树的过程,每步归约对应构造一棵子树,最后当输入串结束时刚好构造出整个语法树(见图 5.1)。

关键问题

在上述移进-归约过程中,不能简单地在栈顶出现某产生式右部时就立即归约。例如表中第 (5) 步,栈顶符号串 bbAbAb 分别是产生式 (2)、(3) 的右部,此时必须依据句柄来确定归约。

  • 句柄:当前句型的某产生式的右部,且该右部在规范推导中最后被推出。
  • 文法无二义性时,一个句子的规范推导唯一,规范归约也唯一。
  • 因此,自底向上分析的关键问题是:在分析过程中如何确定句柄(或其他可归约串)。

本章和第 6 章分别介绍优先分析LR 类分析。本章介绍的优先分析技术,其可归约串是最左简单短语

优先分析分类

优先分析法可分为简单优先分析法算符优先分析法

方法基本思想归约性质特点
简单优先分析对文法所有符号(终结符 + 非终结符)求优先关系,按关系确定句柄规范归约准确、规范,但效率较低,实际使用价值不大
算符优先分析仅规定终结符(算符)之间的优先关系,找到可归约串即归约,不关心归约到哪个非终结符非规范归约分析速度快,特别适用于表达式分析,实际中仍有应用

简单优先分析法

简单优先分析法按照文法符号之间的优先关系确定句柄,因此先给出任意两个文法符号之间的优先关系定义。

优先关系定义

文法中任意两个文法符号 XXYY

  1. 相等XYX \doteq Y):XXYY 的优先性相等。

    XY 当且仅当 G 中存在产生式 AαXYβX \doteq Y \text{ 当且仅当 } G \text{ 中存在产生式 } A \to \alpha X Y \beta
  2. 小于XYX \lessdot Y):XX 的优先性比 YY 小。

    XY 当且仅当 G 中存在产生式 AαXBβ 且 B+YγX \lessdot Y \text{ 当且仅当 } G \text{ 中存在产生式 } A \to \alpha X B \beta \text{ 且 } B \Rightarrow^+ Y \gamma
  3. 大于XYX \gtrdot Y):XX 的优先性比 YY 大。

    XY 当且仅当 G 中存在产生式 AαBYβ 且 B+γXX \gtrdot Y \text{ 当且仅当 } G \text{ 中存在产生式 } A \to \alpha B Y \beta \text{ 且 } B \Rightarrow^+ \gamma X

注意:这里的 \lessdot\gtrdot 与数学中的 <<>> 含义不同。

优先关系也可通过语法树的结构直观理解。当某两个符号同时出现在句柄中时,它们具有 \doteq 关系;当左边的符号不在句柄中而右边的在句柄中时,左边 \lessdot 右边;当左边的在句柄中而右边的不在时,左边 \gtrdot 右边。

例 5.2

设有文法 G[S]G[S]

SbAbA(B)aBAa\begin{aligned} S &\to bAb \\ A &\to (B) \mid a \\ B &\to Aa \end{aligned}

求优先关系:

  1. 相等关系 (\doteq):由 SbAbS \to bAbA(B)A \to (B)BAaB \to Aa 可得:

    bA,Ab,(B,Aab \doteq A,\quad A \doteq b,\quad ( \doteq B,\quad A \doteq a
  2. 小于关系 (\lessdot)

    • SbAbS \to bAb,且 AA 可推出以 (( aa 开头的串,可得:b(b \lessdot (bab \lessdot a
    • A(BA \to (B,且 BB 可推出以 AA 开头的串(BAaB \Rightarrow Aa),可得:(A( \lessdot A
  3. 大于关系 (\gtrdot)

    • SbAbS \to bAb,且 AA 可推出以 aa 结尾的串(AaA \Rightarrow a),可得:aba \gtrdot b
    • BAaB \to Aa,且 AA 可推出以 (( 开头的串…(依此类推)

上述关系可用优先关系矩阵表示。矩阵中元素为空时,表示该文法中任何句型都不会出现该符号对的相邻关系,在分析过程中若遇到则为出错,可断定输入串不是该文法的句子。

#\# 为句子括号符:#X\# \lessdot XX#X \gtrdot \# 对所有与 #\# 相邻的符号 XX 成立。

例 5.2 文法的简单优先关系矩阵(表 5.2):

SSAABB#\#
SS\gtrdot
AA\doteq\lessdot\gtrdot
BB\gtrdot\gtrdot
#\#\lessdot\lessdot\lessdot\doteq

简单优先文法的定义

若一个文法是简单优先文法,必须满足以下条件:

  1. 唯一性:文法符号集中,任意两个符号之间最多只有一种优先关系成立。
  2. 无二义性:文法中任意两个产生式没有相同的右部。

简单优先分析法的操作步骤

  1. 移进:将输入符号串 a1a2an#a_1 a_2 \dots a_n \# 依次存入符号栈,直到栈顶符号的优先性大于下一个待输入符号时为止。
  2. 找句柄:以当前栈顶符号为句柄尾,由此向左在栈中找句柄的头符号。
  3. 归约:由句柄在文法的产生式中查找右部为句柄的产生式。若找到,则用相应产生式的左部代替句柄;若找不到,则为出错,断定输入串不是该文法的句子。
  4. 重复:重复上述 (1)、(2)、(3) 步,直到归约完输入符号串,栈中只剩文法的开始符号为止。

直观算符优先分析法

在处理表达式求值过程中,运算次序是先乘除后加减,即乘除运算的优先级高于加减运算的优先级,且同级运算服从左结合(或右结合,如幂运算)。这说明运算的次序只与运算符有关,而与运算对象无关。

因此,直观算符优先分析法的关键只涉及优先级问题同一优先级的结合性质。算符间的优先关系表示法与简单优先关系类似,规定如下:

  • XYX \lessdot YXX 的优先性低于 YY
  • XYX \doteq YXX 的优先性等于 YY
  • XYX \gtrdot YXX 的优先性高于 YY

注意:这三个关系与数学中的 <<==>> 不同,它们是有序的。若有 aba \lessdot b,不一定有 bab \gtrdot a 成立;若有 aba \doteq b,也不一定具有对称性。例如,通常表达式中运算符的优先关系有 +++ \gtrdot +(左结合),但没有 +++ \lessdot +;有 ()( \doteq ),但没有 )() \doteq (

表达式的二义性文法

EE+EEEEEE/EEE(E)iE \to E + E \mid E - E \mid E * E \mid E / E \mid E \uparrow E \mid (E) \mid i

该文法是二义的,但可以用算符优先分析法进行分析。运算对象(终结符 ii)的优先级最高。其他运算符按计算顺序规定优先级和结合性如下:

  1. 幂运算符 \uparrow:优先级最高,遵循右结合。相当于 \lessdot 关系。 例如 iiii \uparrow i \uparrow i 等价于 i(ii)i \uparrow (i \uparrow i),归约时从右向左进行。

  2. 乘除运算符 *//:优先级低于 \uparrow,服从左结合。 优先关系为:* \gtrdot */* \gtrdot /// \gtrdot */// \gtrdot /* \lessdot \uparrow// \lessdot \uparrow

  3. 加减运算符 ++-:优先级最低,服从左结合。 优先关系为:+++ \gtrdot +++ \gtrdot -+- \gtrdot +- \gtrdot -,且 ++ \lessdot *+/+ \lessdot /- \lessdot */- \lessdot /

  4. 括号 (( ))

    • 左括号 (( 的优先性小于括号内的运算符,大于括号外的运算符
    • 右括号 )) 的优先性大于括号外的运算符,小于括号内的运算符
    • 内括号的优先性大于外括号
  5. 句子括号 #\#:与它相邻的任何运算符的优先性都比它大,即 #X\# \lessdot XX#X \gtrdot \# 对所有与 #\# 相邻的算符 XX 成立。


算符优先分析法的理论基础

算符文法

定义 5.1 设有文法 GG,如果 GG 中没有形如 AαBCβA \to \alpha B C \beta 的产生式,其中 BBCC 为非终结符,则称 GG算符文法(Operator Grammar),也称 OG 文法。

算符文法的特点是:任何产生式的右部都不包含两个相邻的非终结符。

性质 1 在算符文法中,任何句型都不包含两个相邻的非终结符。

性质 2 如果 bAbA(或 AbAb)出现在算符文法的句型 γ\gamma 中,其中 AVNA \in V_NbVTb \in V_T,则任何包含 bb(或 AA)的短语必含有 AA(或 bb)。

算符优先关系定义

定义 5.2GG 是一个不含 ε\varepsilon 产生式的算符文法,aabb 是任意两个终结符,AABBCC 是非终结符。算符优先关系 \doteq\lessdot\gtrdot 定义如下:

  1. 相等aba \doteq b):当且仅当 GG 中含有形如 AαaBbβA \to \alpha a B b \betaAαabβA \to \alpha a b \beta 的产生式。

    aabb 在同一句柄中同时归约。

  2. 小于aba \lessdot b):当且仅当 GG 中含有形如 AαaBβA \to \alpha a B \beta 的产生式,且 B+bγB \Rightarrow^+ b \gamma

    aa 不在句柄中而 bb 在句柄中,aa 先归约。

  3. 大于aba \gtrdot b):当且仅当 GG 中含有形如 AαBbβA \to \alpha B b \beta 的产生式,且 B+γaB \Rightarrow^+ \gamma a

    aa 在句柄中而 bb 不在,aa 后归约。

这三种优先关系也可由语法树的结构直观说明:

  • \doteq:如图 5.3(a),aabb 在同一句柄中同时归约,优先级相同。
  • \lessdot:如图 5.3(b),aa 不在句柄中而 bb 在句柄中,aa 的优先级低于 bb
  • \gtrdot:如图 5.3(c),aa 在句柄中而 bb 不在,aa 的优先级高于 bb

注意:算符之间的优先关系是有序的,允许 aba \lessdot bbab \gtrdot a 同时存在,但不允许同一对终结符之间同时存在两种不同的关系(如不允许 aba \lessdot baba \gtrdot b 同时成立)。

算符优先文法的定义

定义 5.3GG 是一个不含 ε\varepsilon 产生式的算符文法。如果对于任意一对终结符 (a,b)(a, b) 之间至多只有 \doteq\lessdot\gtrdot 三种关系中的一种成立,则称 GG 是一个算符优先文法(Operator Precedence Grammar),即 OPG 文法。

算符优先文法一定是算符文法,但算符文法不一定是算符优先文法。


算符优先关系表的构造

FIRSTVT 和 LASTVT 集合

定义

  • FIRSTVT(B)={aB+aα,aVT,αV}FIRSTVT(B) = \{a \mid B \Rightarrow^+ a\alpha, a \in V_T, \alpha \in V^*\}(取头)

  • LASTVT(B)={aB+αa,aVT,αV}LASTVT(B) = \{a \mid B \Rightarrow^+ \alpha a, a \in V_T, \alpha \in V^*\}(取尾)

优先关系的计算方法

  1. \doteq 关系:直接查看产生式的右部,对如下形式的产生式,有 aba \doteq b 成立:
    • AαaBbβA \to \alpha a B b \beta
    • AαabβA \to \alpha a b \beta
  1. \lessdot 关系:求出每个非终结符 BBFIRSTVT(B)FIRSTVT(B),观察如下形式的产生式:

    • AαaBβA \to \alpha a B \beta

    对每一 bFIRSTVT(B)b \in FIRSTVT(B),有 aba \lessdot b 成立。

  1. \gtrdot 关系:求出每个非终结符 BBLASTVT(B)LASTVT(B),观察如下形式的产生式:

    • AαBbβA \to \alpha B b \beta

    对每一 aLASTVT(B)a \in LASTVT(B),有 aba \gtrdot b 成立。

例 5.3

表达式文法如下:

(0) E#E#(1) EE+T(2) ET(3) TTF(4) TF(5) FPF(6) FP(7) P(E)(8) Pi\begin{aligned} (0)\ &E \to \# E \# \\ (1)\ &E \to E + T \\ (2)\ &E \to T \\ (3)\ &T \to T * F \\ (4)\ &T \to F \\ (5)\ &F \to P \uparrow F \\ (6)\ &F \to P \\ (7)\ &P \to (E) \\ (8)\ &P \to i \end{aligned}

计算 FIRSTVT 和 LASTVT 集合

非终结符FIRSTVTLASTVT
EE{#,+,,,(,i}\{\#, +, *, \uparrow, (, i\}{#,+,,,),i}\{\#, +, *, \uparrow, ), i\}
TT{,,(,i}\{*, \uparrow, (, i\}{,,),i}\{*, \uparrow, ), i\}
FF{,(,i}\{\uparrow, (, i\}{,),i}\{\uparrow, ), i\}
PP{(,i}\{(, i\}{),i}\{), i\}

计算优先关系

  1. \doteq 关系:由产生式 (0) 和 (6) 可得 ##\# \doteq \#iii \doteq i
  2. \lessdot 关系
    • #E\# E#FIRSTVT(E)\# \lessdot FIRSTVT(E)
    • E+TE + T+FIRSTVT(T)+ \lessdot FIRSTVT(T)
    • TFT * FFIRSTVT(F)* \lessdot FIRSTVT(F)
    • FFF \uparrow FFIRSTVT(F)\uparrow \lessdot FIRSTVT(F)
    • P(EP (E(FIRSTVT(E)( \lessdot FIRSTVT(E)
  3. \gtrdot 关系
    • E#E \#LASTVT(E)#LASTVT(E) \gtrdot \#
    • E+E +LASTVT(E)+LASTVT(E) \gtrdot +
    • TT *LASTVT(T)LASTVT(T) \gtrdot *
    • FF \uparrowLASTVT(F)LASTVT(F) \gtrdot \uparrow
    • E)E)LASTVT(E))LASTVT(E) \gtrdot )

优先关系矩阵(表 5.5)

++-*//\uparrow(())ii#\#
++\gtrdot\gtrdot\lessdot\lessdot\lessdot\lessdot\gtrdot\gtrdot
-\gtrdot\gtrdot\lessdot\lessdot\lessdot\lessdot\gtrdot\gtrdot
*\gtrdot\gtrdot\gtrdot\gtrdot\lessdot\lessdot\gtrdot\gtrdot
//\gtrdot\gtrdot\gtrdot\gtrdot\lessdot\lessdot\gtrdot\gtrdot
\uparrow\gtrdot\gtrdot\gtrdot\gtrdot\gtrdot\lessdot\gtrdot\gtrdot
((\lessdot\lessdot\lessdot\lessdot\lessdot\lessdot\doteq\lessdot
))\gtrdot\gtrdot\gtrdot\gtrdot\gtrdot\gtrdot\gtrdot
ii\gtrdot\gtrdot\gtrdot\gtrdot\gtrdot\gtrdot\gtrdot
#\#\lessdot\lessdot\lessdot\lessdot\lessdot\lessdot\lessdot\doteq

算符优先分析算法

分析过程

算符优先分析使用一个符号栈,分析过程如下:

  1. 初始化:将 #\# 压入栈底,将输入串的第一个符号读入到 aa
  2. 循环
    • 设栈顶符号为 XX,当前输入符号为 aa
    • XaX \doteq a(或 XXaa 都是非终结符且相等):
      • XX 是终结符,则 XXaa 匹配,弹栈,aa 指向下一符号
      • XX 是非终结符,则 XX 保持不变,aa 指向下一符号(aba \doteq b 表示同一优先级的算符,从左向右扫描)
    • XaX \gtrdot a
      • 在栈中自栈顶向栈底方向找出最左的、且满足其右侧符号与 XX\doteq 关系的符号 YY(即句柄的左边界)
      • YYXX 之间的所有符号(包括 YYXX)归约为一个非终结符 AA
      • 若找不到这样的产生式,则报错
    • XaX \lessdot a
      • aa 压入栈中,aa 指向下一符号
    • 若上述关系都不成立:报错
  3. 接受:当栈中只剩 #\# 且输入符号也只剩 #\# 时,分析成功

例 5.4

对表达式 i+ii#i + i * i \# 进行分析,使用上述表达式文法:

步骤符号栈输入串动作
(1)#\#i+ii#i+i*i\#移进
(2)#i\#i+ii#+i*i\#i+i \gtrdot +,归约 PiP \to i
(3)#P\#P+ii#+i*i\#P+P \lessdot +,移进
(4)#P+\#P+ii#i*i\#+i+ \lessdot i,移进
(5)#P+i\#P+ii#*i\#ii \gtrdot *,归约 PiP \to i
(6)#P+P\#P+Pi#*i\#PP \lessdot *,移进
(7)#P+P\#P+P*i#i\#i* \lessdot i,移进
(8)#P+Pi\#P+P*i#\#i#i \gtrdot \#,归约 PiP \to i
(9)#P+PP\#P+P*P#\#P#P \gtrdot \#,归约 FPF \to P
(10)#P+PF\#P+P*F#\##* \gtrdot \#,归约 TFT \to F
(11)#P+T\#P+T#\#T#T \gtrdot \#,归约 ETE \to T
(12)#P+E\#P+E#\#+#+ \gtrdot \#,归约 EE+TE \to E+T
(13)#E\#E#\###\# \doteq \#,接受

算符优先分析的特点

  1. 归约的不是句柄,而是最左简单短语:算符优先归约不是规范归约,因为归约过程中不关心归约到哪个非终结符,只关心终结符之间的优先关系。
  2. 分析速度快:只考虑终结符之间的优先关系,分析表简单。
  3. 适用范围有限:仅适用于算符优先文法,即不含 ε\varepsilon 产生式且任意两个终结符之间至多只有一种优先关系的文法。
  4. 不能处理二义性文法:如果表达式的二义性文法(如 EE+EEE(E)iE \to E+E \mid E*E \mid (E) \mid i)中同一对终结符之间存在多种优先关系,则不是算符优先文法。

5.3.5 优先函数

优先矩阵需要 m2m^2 个存储单元(mm 为终结符个数),存储开销大。优先函数用两个函数 ffgg 代替优先矩阵,只需 2(m+1)2(m+1) 个单元,满足:

  • aba \lessdot b,则 f(a)<g(b)f(a) < g(b)
  • aba \doteq b,则 f(a)=g(b)f(a) = g(b)
  • aba \gtrdot b,则 f(a)>g(b)f(a) > g(b)

构造方法

  1. 直接构造法

    • 初始化:对所有终结符 aa,令 f(a)=g(a)=1f(a) = g(a) = 1
    • 迭代修正:逐行扫描优先矩阵,若 aba \lessdot bf(a)g(b)f(a) \geq g(b),则令 f(a)=g(b)1f(a) = g(b) - 1;若 aba \gtrdot bf(a)g(b)f(a) \leq g(b),则令 g(b)=f(a)+1g(b) = f(a) + 1;若 aba \doteq bf(a)g(b)f(a) \neq g(b),则调整使两者相等
    • 重复直到收敛。若某值超过 2m2m,则不存在优先函数
  2. 关系图法

    • faf_agag_a 为结点名(共 2m2m 个结点)
    • aba \lessdot b,画弧 fagbf_a \to g_b;若 aba \gtrdot b,画弧 gbfag_b \to f_a;若 aba \doteq b,画双向弧 fagbf_a \leftrightarrow g_b
    • 每个结点的数值等于从该结点出发可达的结点数(包括自身)
    • 若图中存在回路,则不存在优先函数

注意:优先函数不唯一(所有值加同一常数,关系不变);且某些优先矩阵不存在对应的优先函数(如循环依赖 ab,bc,caa \gtrdot b, b \gtrdot c, c \gtrdot a)。

缺点:优先函数无法区分”无优先关系”的情况,出错时不能准确定位错误位置。例如,当两个终结符无关系时,优先矩阵可立即报错,而优先函数可能等到归约出两个相邻非终结符时才报错。

5.3.6 算符优先分析法的局限性

  1. 可能接受非法句子:算符优先分析归约的是最左简单短语而非句柄,因此某些非文法的句子也可能被错误接受。

    例 5.6 文法:

    SaTdT(S)b\begin{aligned} S &\to aTd \\ T &\to (S) \mid b \end{aligned}

    对输入串 (a)(a) 可完成归约,但该串不是该文法的句子。

  2. 适用范围窄:通常程序设计语言的文法很难满足算符优先文法的条件(无 ε\varepsilon 产生式、任意终结符对至多一种优先关系),因此算符优先分析法主要适用于表达式分析


LR 分析

LR 分析概述

LR 分析是一种自底向上的规范归约分析方法,其核心思想是:根据分析栈的栈顶状态和当前输入符号(有时需要向右查看 k 个符号),唯一确定下一步动作是移进、归约、接受还是报错。

LR(k) 的含义:

  • L:从左到右扫描输入串
  • R:最右推导的逆过程(规范归约)
  • k:向右查看 k 个输入符号

LR 分析器的组成

  1. 总控程序(驱动程序):对所有 LR 分析器通用
  2. 分析表:分为两部分
    • ACTION 表:决定栈顶状态遇到输入符号时应执行的动作
    • GOTO 表:决定归约后应转向的状态
  3. 分析栈:包括状态栈文法符号栈,均为后进先出

LR 分析的特点

方法对文法限制分析能力构造难度
LR(0)严格
SLR(1)较松
LALR(1)更松
LR(1)最松最强
  • 优点:对文法限制少,绝大多数无二义性上下文无关文法都可用;分析速度快;能准确、即时地指出出错位置。
  • 缺点:构造分析表的工作量大,手工实现复杂。实用中通常借助工具(如 yacc)自动生成 LALR(1) 分析器。

LR(0) 分析

LR(0) 分析器在分析过程中不向前查看输入符号,仅根据栈顶状态和当前输入符号决定动作。它是构造其他 LR 类分析器的基础。

核心概念

1. 拓广文法

对原文法 GG 增加新产生式 SSS' \to SSS' 为新的开始符号,SS 为原开始符号),得到拓广文法 GG'。拓广文法的目的是:在归约过程中能分清是否已归约到文法的初始开始符号,避免与文法右部出现的开始符号混淆。

2. 可归前缀与活前缀

考虑规范推导过程中的一条产生式 AβA \to \beta,在规范句型中,β\beta句柄

  • 可归前缀:规范句型中,句柄及其前面的部分(即句柄左部的前缀,包括句柄本身)
  • 活前缀:规范句型中不超过句柄右端的所有前缀(即可归前缀加上形成句柄之前的中间状态)

直观理解:活前缀是”可以安全放在栈中的前缀”,它一定是某个规范句型的前缀,且其右端未超过当前句型的句柄末端。

定义 6.1SγS' \Rightarrow^* \gamma 是拓广文法的规范推导,符号串 α\alphaγ\gamma 的前缀,且 α\alpha 的右端不超过该句型句柄的末端,则称 α\alphaGG 的一个活前缀

在 LR 分析过程中,符号栈中保存的内容始终是活前缀。一旦栈中出现可归前缀(即句柄已形成),就进行归约。

6.2.2 识别活前缀的有限自动机

LR 分析的关键是识别活前缀。可以将终结符和非终结符都视为有限自动机的输入符号:

  • 每移进一个符号,相当于已识别该符号,状态进行转换
  • 当识别到可归前缀时,相当于到达了识别句柄的终态
  • 每个终态对应一个句柄识别
  • * 的状态既是句柄识别态,又是句子识别态(即已完成整个句子的归约)
  • 句子识别态在拓广文法中唯一存在

通过构造识别活前缀的确定有限自动机(DFA),LR 分析器就能根据当前状态和输入符号唯一确定分析动作。

6.2.3 LR(0) 项目与项目集

LR(0) 项目:产生式右部加一个圆点 ·,表示已识别部分和待识别部分。

例如,产生式 AXYZA \to X Y Z 对应的项目有:

  • AXYZA \to \cdot X Y Z(尚未识别任何符号) 即开始形式
  • AXYZA \to X \cdot Y Z(已识别 XX
  • AXYZA \to X Y \cdot Z(已识别 XYX Y
  • AXYZA \to X Y Z \cdot(已识别全部,可归约) 即归约形式

项目集闭包(CLOSURE)

  • 初始时,项目集包含所有项目 SSS' \to \cdot S 的闭包
  • 若项目集 II 包含项目 AαBβA \to \alpha \cdot B \beta,则对 BB 的每个产生式 BγB \to \gamma,将项目 BγB \to \cdot \gamma 加入 II

GOTO 函数

  • GOTO(I,X)GOTO(I, X) 表示从项目集 II 出发,读入文法符号 XX 后到达的新项目集
  • 即把 II 中所有形如 AαXβA \to \alpha \cdot X \beta 的项目变为 AαXβA \to \alpha X \cdot \beta,然后求闭包

6.2.4 LR(0) 分析表构造

步骤

  1. 对文法 GG 拓广,得到 GG'
  2. 构造识别活前缀的 DFA(即项目集族 CC
  3. 对每个项目集 IkI_k(对应状态 kk):
    • 若项目 AαaβA \to \alpha \cdot a \betaIkI_k 中(aa 为终结符),则 ACTION[k,a]=sjACTION[k, a] = s_j(移进到状态 jj),其中 jjGOTO(Ik,a)GOTO(I_k, a) 的状态号
    • 若项目 AαA \to \alpha \cdotIkI_k 中,则对所有终结符 aaACTION[k,a]=rjACTION[k, a] = r_j(归约),其中 jj 是产生式 AαA \to \alpha 的编号(注意:SSS' \to S 除外)
    • 若项目 SSS' \to S \cdotIkI_k 中,则 ACTION[k,#]=accACTION[k, \#] = acc(接受)
    • GOTO(Ik,A)=IjGOTO(I_k, A) = I_jAA 为非终结符),则 GOTO[k,A]=jGOTO[k, A] = j

冲突

  • 若同一状态对同一终结符,既有移进动作又有归约动作 → 移进-归约冲突
  • 若同一状态对同一终结符,有两个以上归约动作 → 归约-归约冲突
  • 若分析表存在冲突,则该文法不是 LR(0) 文法

6.2.5 例 5.1

设文法 G[S]G[S] 为:

(1) SaAcBe(2) Ab(3) AAb(4) Bd\begin{aligned} (1)\ &S \to a A c B e \\ (2)\ &A \to b \\ (3)\ &A \to A b \\ (4)\ &B \to d \end{aligned}

拓广文法 GG'

(0) SS(1) SaAcBe(2) Ab(3) AAb(4) Bd\begin{aligned} (0)\ &S' \to S \\ (1)\ &S \to a A c B e \\ (2)\ &A \to b \\ (3)\ &A \to A b \\ (4)\ &B \to d \end{aligned}

构造识别活前缀的 DFA

I0=CLOSURE({SS})={SS, SaAcBe}I1=GOTO(I0,S)={SS}(接受态)I2=GOTO(I0,a)=CLOSURE({SaAcBe})={SaAcBe, Ab, AAb}I3=GOTO(I2,A)=CLOSURE({SaAcBe, AAb})={SaAcBe, AAb}I4=GOTO(I2,b)={Ab}(归约 r2I5=GOTO(I3,c)=CLOSURE({SaAcBe})={SaAcBe, Bd}I6=GOTO(I3,b)={AAb}(归约 r3I7=GOTO(I5,B)={SaAcBe}I8=GOTO(I5,d)={Bd}(归约 r4I9=GOTO(I7,e)={SaAcBe}(归约 r1\begin{aligned} I_0 &= \mathrm{CLOSURE}(\{S' \to \cdot S\}) = \{S' \to \cdot S,\ S \to \cdot a A c B e\} \\ I_1 &= \mathrm{GOTO}(I_0, S) = \{S' \to S \cdot\} \quad\text{(接受态)} \\ I_2 &= \mathrm{GOTO}(I_0, a) = \mathrm{CLOSURE}(\{S \to a \cdot A c B e\}) = \{S \to a \cdot A c B e,\ A \to \cdot b,\ A \to \cdot A b\} \\ I_3 &= \mathrm{GOTO}(I_2, A) = \mathrm{CLOSURE}(\{S \to a A \cdot c B e,\ A \to A \cdot b\}) = \{S \to a A \cdot c B e,\ A \to A \cdot b\} \\ I_4 &= \mathrm{GOTO}(I_2, b) = \{A \to b \cdot\} \quad\text{(归约 } r_2 \text{)} \\ I_5 &= \mathrm{GOTO}(I_3, c) = \mathrm{CLOSURE}(\{S \to a A c \cdot B e\}) = \{S \to a A c \cdot B e,\ B \to \cdot d\} \\ I_6 &= \mathrm{GOTO}(I_3, b) = \{A \to A b \cdot\} \quad\text{(归约 } r_3 \text{)} \\ I_7 &= \mathrm{GOTO}(I_5, B) = \{S \to a A c B \cdot e\} \\ I_8 &= \mathrm{GOTO}(I_5, d) = \{B \to d \cdot\} \quad\text{(归约 } r_4 \text{)} \\ I_9 &= \mathrm{GOTO}(I_7, e) = \{S \to a A c B e \cdot\} \quad\text{(归约 } r_1 \text{)} \end{aligned}

LR(0) 分析表(表 6.1)

状态aacceebbdd#\#SSAABB
0S2S_21
1acc
2S4S_43
3S5S_5S6S_6
4r2r_2r2r_2r2r_2r2r_2r2r_2r2r_2
5S8S_87
6r3r_3r3r_3r3r_3r3r_3r3r_3r3r_3
7S9S_9
8r4r_4r4r_4r4r_4r4r_4r4r_4r4r_4
9r1r_1r1r_1r1r_1r1r_1r1r_1r1r_1

对输入串 abbcde#abbcde\# 的分析过程(表 6.2)

步骤状态栈符号栈输入串ACTIONGOTO
(1)0#\#abbcde#abbcde\#S2S_2
(2)02#a\#abbcde#bbcde\#S4S_4
(3)024#ab\#abbcde#bcde\#r2r_23
(4)023#aA\#aAbcde#bcde\#S6S_6
(5)0236#aAb\#aAbcde#cde\#r3r_33
(6)023#aA\#aAcde#cde\#S5S_5
(7)0235#aAc\#aAcde#de\#S8S_8
(8)02358#aAcd\#aAcde#e\#r4r_47
(9)02357#aAcB\#aAcBe#e\#S9S_9
(10)023579#aAcBe\#aAcBe#\#r1r_11
(11)01#S\#S#\#acc

解题过程详解

分析表符号说明

符号含义操作
SnS_n移进(Shift)将当前输入符号压入符号栈,将状态 nn 压入状态栈,读头前进一位
rnr_n归约(Reduce)用第 nn 号产生式归约:弹出右部长度个状态和符号,再查 GOTO 表压入新状态
裸数字 nnGOTO 跳转出现在 GOTO 列,归约后栈顶状态与产生式左部查 GOTO 表得到的状态号
acc接受(Accept)输入串已完全归约为 SS',语法分析成功

理解关键:ACTION 表中所有条目的含义都是”栈顶状态 + 当前输入符号 → 动作”。SnS_n 中的字母 S 是 Shift 的缩写,数字 n 是目标 DFA 状态号。而 GOTO 列的数字只在归约操作中用到:弹出右部后,露出的栈顶状态与产生式左部(非终结符)交叉查 GOTO 表。


第一步:构造识别活前缀的 DFA

构造 DFA 的核心操作只有两个:

  • CLOSURE(I):若项目集中有 AαBβA \to \alpha \cdot B \beta(圆点后是非终结符),则加入 BB 的所有产生式(圆点在开头)
  • GOTO(I, X):把 II 中所有形如 X\cdots \cdot X \cdots 的项目圆点右移一位,再求 CLOSURE

1. 求 I0I_0

SSS' \to \cdot S 出发求闭包。圆点后是 SS(非终结符),加入 SS 的唯一产生式:

I0=CLOSURE({SS})={SS, SaAcBe}I_0 = \mathrm{CLOSURE}(\{S' \to \cdot S\}) = \{S' \to \cdot S,\ S \to \cdot a A c B e\}

2. 从 I0I_0 出发

I0I_0 中圆点可能读入的符号只有两个:aa(终结符,来自 SaAcBeS \to \cdot a A c B e)和 SS(非终结符,来自 SSS' \to \cdot S)。

GOTO(I₀, a)SaAcBeS \to \cdot a A c B e 圆点右移 → SaAcBeS \to a \cdot A c B e。圆点后是 AA(非终结符),闭包:加入 AbA \to \cdot bAAbA \to \cdot A b

I2=GOTO(I0,a)={SaAcBe, Ab, AAb}I_2 = \mathrm{GOTO}(I_0, a) = \{S \to a \cdot A c B e,\ A \to \cdot b,\ A \to \cdot A b\}

GOTO(I₀, S)SSS' \to \cdot S 圆点右移 → SSS' \to S \cdot。圆点在末尾,无需闭包。

I1=GOTO(I0,S)={SS}(接受态)I_1 = \mathrm{GOTO}(I_0, S) = \{S' \to S \cdot\} \quad\text{(接受态)}

3. 从 I2I_2 出发

I2I_2 中圆点可能读入的符号:AA(非终结符,来自 SaAcBeS \to a \cdot A c B eAAbA \to \cdot A b)、bb(终结符,来自 AbA \to \cdot b)。

GOTO(I₂, b)AbA \to \cdot b 圆点右移 → AbA \to b \cdot。圆点在末尾,项目集只有一个项目。

I4=GOTO(I2,b)={Ab}(归约 r2I_4 = \mathrm{GOTO}(I_2, b) = \{A \to b \cdot\} \quad\text{(归约 } r_2 \text{)}

GOTO(I₂, A)

  • SaAcBeS \to a \cdot A c B eSaAcBeS \to a A \cdot c B e(圆点后是 cc,终结符)
  • AAbA \to \cdot A bAAbA \to A \cdot b(圆点后是 bb,终结符) 两者圆点后都是终结符,无需闭包。
I3=GOTO(I2,A)={SaAcBe, AAb}I_3 = \mathrm{GOTO}(I_2, A) = \{S \to a A \cdot c B e,\ A \to A \cdot b\}

4. 从 I3I_3 出发

I3I_3 中圆点可能读入:ccSaAcBeS \to a A \cdot c B e)和 bbAAbA \to A \cdot b)。

GOTO(I₃, c)SaAcBeS \to a A \cdot c B e 圆点右移 → SaAcBeS \to a A c \cdot B e。圆点后是 BB(非终结符),闭包加入 BdB \to \cdot d

I5=GOTO(I3,c)={SaAcBe, Bd}I_5 = \mathrm{GOTO}(I_3, c) = \{S \to a A c \cdot B e,\ B \to \cdot d\}

GOTO(I₃, b)AAbA \to A \cdot b 圆点右移 → AAbA \to A b \cdot

I6=GOTO(I3,b)={AAb}(归约 r3I_6 = \mathrm{GOTO}(I_3, b) = \{A \to A b \cdot\} \quad\text{(归约 } r_3 \text{)}

5. 从 I5I_5 出发

I5I_5 中圆点可能读入:ddBdB \to \cdot d)和 BBSaAcBeS \to a A c \cdot B e)。

GOTO(I₅, d)BdB \to \cdot d 圆点右移 → BdB \to d \cdot

I8=GOTO(I5,d)={Bd}(归约 r4I_8 = \mathrm{GOTO}(I_5, d) = \{B \to d \cdot\} \quad\text{(归约 } r_4 \text{)}

GOTO(I₅, B)SaAcBeS \to a A c \cdot B e 圆点右移 → SaAcBeS \to a A c B \cdot e。圆点后是 ee(终结符),无需闭包。

I7=GOTO(I5,B)={SaAcBe}I_7 = \mathrm{GOTO}(I_5, B) = \{S \to a A c B \cdot e\}

6. 从 I7I_7 出发

I7I_7 只有 SaAcBeS \to a A c B \cdot e,圆点只可能读入 ee

GOTO(I₇, e)SaAcBeS \to a A c B \cdot e 圆点右移 → SaAcBeS \to a A c B e \cdot

I9=GOTO(I7,e)={SaAcBe}(归约 r1I_9 = \mathrm{GOTO}(I_7, e) = \{S \to a A c B e \cdot\} \quad\text{(归约 } r_1 \text{)}

DFA 总图


第二步:由 DFA 推导分析表

逐状态对照 DFA 填写表项:

状态项目集关键内容推导出的表项
0SaAcBeS \to \cdot a A c B e,GOTO(I₀,a)=I₂ACTION[0,a]=S2S_2
0GOTO(I₀,S)=I₁GOTO[0,S]=1
1SSS' \to S \cdotACTION[1,#]=acc
2AbA \to \cdot b,GOTO(I₂,b)=I₄ACTION[2,b]=S4S_4
2GOTO(I₂,A)=I₃GOTO[2,A]=3
3SaAcBeS \to a A \cdot c B e,圆点在 c 前ACTION[3,c]=S5S_5
3AAbA \to A \cdot b,圆点在 b 前ACTION[3,b]=S6S_6
4AbA \to b \cdot(圆点在末尾)对所有终结符:ACTION[4,*]=r2r_2
5BdB \to \cdot d,GOTO(I₅,d)=I₈ACTION[5,d]=S8S_8
5GOTO(I₅,B)=I₇GOTO[5,B]=7
6AAbA \to A b \cdot(圆点在末尾)对所有终结符:ACTION[6,*]=r3r_3
7SaAcBeS \to a A c B \cdot e,GOTO(I₇,e)=I₉ACTION[7,e]=S9S_9
8BdB \to d \cdot(圆点在末尾)对所有终结符:ACTION[8,*]=r4r_4
9SaAcBeS \to a A c B e \cdot(圆点在末尾)对所有终结符:ACTION[9,*]=r1r_1

第三步:逐步分析输入串 abbcde#abbcde\#

分析过程中维护两个栈

  • 状态栈:记录经过的 DFA 状态序列(决定当前处于哪个项目集)
  • 符号栈:记录已读入的文法符号
  • 每步操作:查 栈顶状态 + 当前输入符号 在 ACTION 表中的值

约定:步骤编号对应表 6.2。SnS_n 意为”移进当前输入符号并转到状态 nn”;rnr_n 意为”用第 nn 号产生式归约:弹出右部长度个状态和符号,再查 GOTO 表压入新状态”。

(1) S2S_2 — 状态 0,输入 a

I0I_0SaAcBeS \to \cdot a A c B e,圆点在 a 前,期待读到 a。现在输入确实是 a,查表得 S2S_2

移进 a:符号栈压入 a,状态栈压入 2,读头前进(输入变为 bbcde#)。

[0, 2] #a bbcde#

(2) S4S_4 — 状态 2,输入 b

I2={Ab, }I_2 = \{A \to \cdot b,\ \dots\},圆点在 b 前,期待读到 b。查 ACTION[2,b]=S4S_4

移进 b:符号栈压入 b,状态栈压入 4。

[0, 2, 4] #ab bcde#

(3) r2r_2 — 状态 4,输入 b

I4={Ab}I_4 = \{A \to b \cdot\},圆点在末尾——句柄已形成!句柄是栈顶的 b(长度为 1),无论下一个输入是什么(这里是 b),LR(0) 都执行归约 AbA \to b

归约操作(三步)

  1. 弹出右部长度 = 1 个状态(4)和 1 个符号(b)
  2. 露出栈顶状态 2,产生式左部是 AA,查 GOTO[2, A] = 3
  3. 压入 AA(归约结果)和状态 3
归约前:[0, 2, 4] #ab
归约后:[0, 2, 3] #aA (输入不变:bcde#)

关键:符号栈中的 b 被替换为了 A——这就是”自底向上归约”:已识别的句柄被它的左部非终结符替代。

(4) S6S_6 — 状态 3,输入 b

I3={AAb, }I_3 = \{A \to A \cdot b,\ \dots\},圆点在 b 前。查 ACTION[3,b]=S6S_6

移进 b:符号栈压入 b,状态栈压入 6。

[0, 2, 3, 6] #aAb cde#

(5) r3r_3 — 状态 6,输入 c

I6={AAb}I_6 = \{A \to A b \cdot\},圆点在末尾——句柄已形成!但这次句柄是 Ab(长度 = 2),不是单个 b。查 ACTION[6,c]=r3r_3

归约 AAbA \to Ab

  1. 弹出 2 个状态(6、3)和 2 个符号(b、A)——栈顶的 Ab 被整体弹出
  2. 露出栈顶状态 2,产生式左部是 AA,查 GOTO[2, A] = 3
  3. 压入 AA 和状态 3
归约前:[0, 2, 3, 6] #aAb
归约后:[0, 2, 3] #aA (输入不变:cde#)

为什么步骤 (3) 和 (5) 的栈顶都是 b,却执行不同的归约?

步骤 (3) 栈顶状态是 4I4={Ab}I_4 = \{A \to b \cdot\})→ 句柄是单个 b → r2r_2 步骤 (5) 栈顶状态是 6I6={AAb}I_6 = \{A \to A b \cdot\})→ 句柄是 Ab → r3r_3

状态本身就记录了历史:状态 4 是从 I2I_2 经 b 到达的(“a 后的第一个 b”),状态 6 是从 I3I_3 经 b 到达的(“A 后面的 b”)。两个不同的路径走到了两个不同的 DFA 状态,尽管当前读到的符号都是 b。这就是 LR 分析器区分不同上下文的机制。

(6) S5S_5 — 状态 3,输入 c

I3={SaAcBe, }I_3 = \{S \to a A \cdot c B e,\ \dots\},圆点在 c 前。查 ACTION[3,c]=S5S_5

移进 c:符号栈压入 c,状态栈压入 5。

[0, 2, 3, 5] #aAc de#

(7) S8S_8 — 状态 5,输入 d

I5={Bd, }I_5 = \{B \to \cdot d,\ \dots\},圆点在 d 前。查 ACTION[5,d]=S8S_8

移进 d:符号栈压入 d,状态栈压入 8。

[0, 2, 3, 5, 8] #aAcd e#

(8) r4r_4 — 状态 8,输入 e

I8={Bd}I_8 = \{B \to d \cdot\},圆点在末尾,句柄是栈顶的 d(长度 = 1)。

归约 BdB \to d

  1. 弹出 1 个状态(8)和 1 个符号(d)
  2. 露出栈顶状态 5,左部是 BB,查 GOTO[5, B] = 7
  3. 压入 BB 和状态 7
归约前:[0, 2, 3, 5, 8] #aAcd
归约后:[0, 2, 3, 5, 7] #aAcB (输入不变:e#)

(9) S9S_9 — 状态 7,输入 e

I7={SaAcBe}I_7 = \{S \to a A c B \cdot e\},圆点在 e 前。查 ACTION[7,e]=S9S_9

移进 e:符号栈压入 e,状态栈压入 9。

[0, 2, 3, 5, 7, 9] #aAcBe #

(10) r1r_1 — 状态 9,输入 #

I9={SaAcBe}I_9 = \{S \to a A c B e \cdot\},圆点在末尾,句柄是 aAcBe(长度 = 5)。

归约 SaAcBeS \to a A c B e

  1. 弹出 5 个状态(9、7、5、3、2)和 5 个符号(e、B、c、A、a)
  2. 露出栈顶状态 0,左部是 SS,查 GOTO[0, S] = 1
  3. 压入 SS 和状态 1
归约前:[0, 2, 3, 5, 7, 9] #aAcBe
归约后:[0, 1] #S (输入不变:#)

(11) acc — 状态 1,输入 #

I1={SS}I_1 = \{S' \to S \cdot\},状态栈顶只剩 SS,输入只剩 #——整个输入串被成功归约为开始符号。

接受:语法分析成功完成。


总结:LR(0) 分析的完整解题流程

  1. 拓广文法 → 引入 SSS' \to S
  2. 构造 DFA → 从 I0=CLOSURE({SS})I_0 = \mathrm{CLOSURE}(\{S' \to \cdot S\}) 出发,反复用 GOTO 函数生成新状态,直到无新状态
  3. 填写分析表 → 每状态查项目:圆点后是终结符 → SjS_j;圆点在末尾 → rjr_j;GOTO(I, 非终结符) → GOTO 列
  4. 执行分析 → 每步查栈顶状态 + 当前输入 → SjS_j 则移进,rjr_j 则归约(弹出、查 GOTO、压入),直到 acc

LR 分析器的核心优势:DFA 状态编码了完整的分析历史,使得每一步决策都是确定性的(只要文法是无二义的),不需要回溯。


6.3 SLR(1) 分析法

由于大多数实用程序设计语言的文法不满足 LR(0) 条件,SLR(1) 通过向前查看一个输入符号来解决 LR(0) 项目集规范族中的冲突。只对有冲突的状态才需要向前查看,故名”简单” LR(1)。

核心思想

对 LR(0) 中有冲突的项目集(状态),利用归约项目左部非终结符的 FOLLOW 集来决定是否归约:

  • 若当前输入符号 aFOLLOW(A)a \in FOLLOW(A),则对归约项目 AαA \to \alpha \cdot 执行归约
  • aa 是某个移进项目的移进符号,则优先移进
  • FOLLOW(A)FOLLOW(A) 与所有移进符号集合不相交,则冲突可解

定义

GG 是拓广文法,CC 是其 LR(0) 项目集规范族。若对 CC 中每个有冲突的项目集 II,都能用 FOLLOW 集解决冲突,则称 GGSLR(1) 文法,相应的分析表为 SLR(1) 分析表

改进的 SLR(1) 分析表构造算法

假设已构造出 LR(0) 项目集规范族 C={I0,I1,,In}C = \{I_0, I_1, \dots, I_n\} 和所有非终结符的 FOLLOW 集。

  1. 移进:若项目 AαaβIkA \to \alpha \cdot a \beta \in I_k,且 GOTO(Ik,a)=IjGOTO(I_k, a) = I_jaa 为终结符,则 ACTION[k,a]=sjACTION[k, a] = s_j
  2. 归约:若项目 AαIkA \to \alpha \cdot \in I_k,则对任何终结符 aa#\#,若 aFOLLOW(A)a \in FOLLOW(A),则 ACTION[k,a]=rjACTION[k, a] = r_jjj 为产生式编号)
  3. GOTO:若 GOTO(Ik,A)=IjGOTO(I_k, A) = I_jAA 为非终结符),则 GOTO[k,A]=jGOTO[k, A] = j
  4. 接受:若项目 SSIkS' \to S \cdot \in I_k,则 ACTION[k,#]=accACTION[k, \#] = \text{acc}
  5. 报错:其余情况填报错标志

改进点:步骤 (2) 中仅当归约项目左部非终结符的 FOLLOW 集包含当前输入符号时才归约。这使得某些在 LR(0) 中会被错误归约的情况能被提前发现。


示例 1:实型变量说明文法

拓广文法 GG

(0) SS(1) SrD(2) DD,i(3) Di\begin{aligned} (0)\ &S' \to S \\ (1)\ &S \to r D \\ (2)\ &D \to D, i \\ (3)\ &D \to i \end{aligned}

其中 rr 表示关键字 real

表 6.5:GG' 的 LR(0) 项目集规范族

状态核集合闭包增加项目项目集
I0I_0SSS' \to \cdot SSrDS \to \cdot r DSSS' \to \cdot S
SrDS \to \cdot r D
I1I_1SSS' \to S \cdotSSS' \to S \cdot
I2I_2SrDS \to r \cdot DDD,iD \to \cdot D, i
DiD \to \cdot i
SrDS \to r \cdot D
DD,iD \to \cdot D, i
DiD \to \cdot i
I3I_3SrDS \to r D \cdot
DD,iD \to D \cdot, i
SrDS \to r D \cdot
DD,iD \to D \cdot, i
I4I_4DiD \to i \cdotDiD \to i \cdot
I5I_5DD,iD \to D, \cdot iDD,iD \to D, \cdot i
I6I_6DD,iD \to D, i \cdotDD,iD \to D, i \cdot

图 6.9:识别文法 GG' 活前缀的 DFA

说明I3I_3 是冲突状态,同时包含归约项目 SrDS \to r D \cdot 和移进项目 DD,iD \to D \cdot, i

表 6.6:实数说明文法的 LR(0) 分析表

状态rr,,ii#\#SSDD
0S2S_21
1acc
2S4S_43
3r1r_1S5S_5r1r_1r1r_1
4r3r_3r3r_3r3r_3r3r_3
5S6S_6
6r2r_2r2r_2r2r_2r2r_2

表 6.7:实数说明文法的 SLR(1) 分析表

状态rr,,ii#\#SSDD
0S2S_21
1acc
2S4S_43
3S5S_5r1r_1
4r3r_3r3r_3
5S6S_6
6r2r_2r2r_2

该文法的 LR(0) 项目集规范族中,状态 I3I_3 包含:

  • SDS \to D \cdot (归约项目)
  • DD,iD \to D \cdot, i (移进项目,期待 ’,’)

LR(0) 分析表中,状态 I3I_3 对输入符号 ’,’ 既有移进动作(DD,iD \to D \cdot, i),又有归约动作(SDS \to D \cdot 对所有输入符号归约),存在移进-归约冲突

SLR(1) 解决

  • FOLLOW(S)={#}FOLLOW(S) = \{\#\}
  • FOLLOW(D)={#,,}FOLLOW(D) = \{\#, ,\}

在状态 I3I_3

  • 归约项目 SDS \to D \cdot 仅当输入符号 FOLLOW(S)={#}\in FOLLOW(S) = \{\#\} 时才归约
  • 移进项目期待 ’,’
  • 由于 {#}{,}=\{\#\} \cap \{,\} = \emptyset,冲突解决:
    • 输入 ’,‘:移进
    • 输入 ’#‘:归约 SDS \to D
    • 其他输入:报错

该文法是 SLR(1) 文法。


示例 2:表达式文法

拓广文法 GG

(0) SE(1) EE+T(2) ET(3) TTF(4) TF(5) F(E)(6) Fi\begin{aligned} (0)\ &S' \to E \\ (1)\ &E \to E + T \\ (2)\ &E \to T \\ (3)\ &T \to T * F \\ (4)\ &T \to F \\ (5)\ &F \to (E) \\ (6)\ &F \to i \end{aligned}

图 6.10:识别表达式文法活前缀的 DFA

各状态项目集详情

状态项目集
I₀SES' \to \cdot E, EE+TE \to \cdot E+T, ETE \to \cdot T, TTFT \to \cdot T*F, TFT \to \cdot F, F(E)F \to \cdot (E), FiF \to \cdot i
I₁SES' \to E \cdot, EE+TE \to E \cdot +T
I₂ETE \to T \cdot, TTFT \to T \cdot *F
I₃TFT \to F \cdot
I₄F(E)F \to (\cdot E), EE+TE \to \cdot E+T, ETE \to \cdot T, TTFT \to \cdot T*F, TFT \to \cdot F, F(E)F \to \cdot (E), FiF \to \cdot i
I₅FiF \to i \cdot
I₆EE+TE \to E+ \cdot T, TTFT \to \cdot T*F, TFT \to \cdot F, F(E)F \to \cdot (E), FiF \to \cdot i
I₇TTFT \to T* \cdot F, F(E)F \to \cdot (E), FiF \to \cdot i
I₈F(E)F \to (E \cdot), EE+TE \to E \cdot +T
I₉EE+TE \to E+T \cdot, TTFT \to T \cdot *F
I₁₀TTFT \to T*F \cdot
I₁₁F(E)F \to (E) \cdot

说明:该 DFA 是构造 SLR(1) 分析表的基础。其中状态 I1I_1(含 SES' \to E \cdotEE+TE \to E \cdot +T)、I2I_2(含 ETE \to T \cdotTTFT \to T \cdot *F)等存在移进-归约冲突,需用 SLR(1) 方法解决。


该文法的 LR(0) 项目集规范族在多个状态(如包含 EE+TE \to E \cdot + TEEE \to E \cdot 的状态、包含 TTFT \to T \cdot * FTTT \to T \cdot 的状态)中存在 移进-归约冲突 ,因此不是 LR(0) 文法。

SLR(1) 冲突分析

  • FOLLOW(E)={+,),#}FOLLOW(E) = \{+, ), \#\}
  • FOLLOW(T)={+,),#}FOLLOW(T) = \{+, ), \#\}

在包含 EE+TE \to E \cdot + T(移进 ’+‘)和 EEE \to E \cdot(归约)的状态中:

  • 归约 EEE \to E 仅当输入符号 FOLLOW(E)={+,),#}\in FOLLOW(E) = \{+, ), \#\} 时执行
  • 移进项目期待 ’+’
  • SLR(1) 分析器在输入为 ’+’ 时优先移进(构造 E+TE+T),在输入为 ’)’ 或 ’#’ 时归约

在包含 TTFT \to T \cdot * F(移进 ’*‘)和 TTT \to T \cdot(归约)的状态中:

  • FOLLOW(T)={+,),#}FOLLOW(T) = \{+, ), \#\}
  • 移进符号 ’_’ 不在 FOLLOW(T)FOLLOW(T) 中(FOLLOW(T){}=FOLLOW(T) \cap \{_\} = \emptyset
  • 因此:输入 ’*’ 时移进,输入 ’+’, ’)’, ’#’ 时归约 TTT \to T

综上,该文法是 SLR(1) 文法。

表 6.8:表达式文法的 SLR(1) 分析表

状态ii++*(())#\#EETTFF
0S5S_5S4S_4123
1S6S_6acc
2r2r_2S7S_7r2r_2r2r_2
3r4r_4r4r_4r4r_4r4r_4
4S5S_5S4S_4823
5r6r_6r6r_6r6r_6r6r_6
6S5S_5S4S_493
7S5S_5S4S_410
8S6S_6S11S_{11}
9r1r_1S7S_7r1r_1r1r_1
10r3r_3r3r_3r3r_3r3r_3
11r5r_5r5r_5r5r_5r5r_5

表 6.9:对输入串 i+ii#i+i*i\# 的 SLR(1) 分析过程

步骤状态栈符号栈输入串ACTIONGOTO
(1)0#\#i+ii#i+i*i\#S5S_5
(2)05#i\#i+ii#+i*i\#r6r_63
(3)03#F\#F+ii#+i*i\#r4r_42
(4)02#T\#T+ii#+i*i\#r2r_21
(5)01#E\#E+ii#+i*i\#S6S_6
(6)016#E+\#E+ii#i*i\#S5S_5
(7)0165#E+i\#E+ii#*i\#r6r_63
(8)0163#E+F\#E+Fi#*i\#r4r_49
(9)0169#E+T\#E+Ti#*i\#S7S_7
(10)01697#E+T\#E+T*i#i\#S5S_5
(11)016975#E+Ti\#E+T*i#\#r6r_610
(12)01697(10)#E+TF\#E+T*F#\#r3r_39
(13)0169#E+T\#E+T#\#r1r_11
(14)01#E\#E#\#acc

SLR(1) 的局限性

并非所有非 LR(0) 文法都是 SLR(1) 文法。有些文法的 LR(0) 冲突无法通过 FOLLOW 集解决,这类文法需要用 LR(1) 或 LALR(1) 分析。

反例:文法 GG

(0) SS(1) SaAd(2) SbAc(3) Saec(4) Sbed(5) Ae\begin{aligned} (0)\ &S' \to S \\ (1)\ &S \to a A d \\ (2)\ &S \to b A c \\ (3)\ &S \to a e c \\ (4)\ &S \to b e d \\ (5)\ &A \to e \end{aligned}

该文法的 LR(0) 项目集规范族在某些状态中存在移进-归约冲突。计算得 FOLLOW(A)={c,d}FOLLOW(A) = \{c, d\}。虽然在某些状态中 FOLLOW(A)FOLLOW(A) 与移进符号的交集为空,但在另一些状态中,FOLLOW 集无法区分不同的归约情境,导致冲突无法用 SLR(1) 方法解决。该文法是 LR(1) 文法,但不是 SLR(1) 文法。

图 6.11:LR(0) 识别 GG' 活前缀的 DFA(Mermaid)

项目集详情

状态项目集
I₀SSS' \to \cdot S, SaAdS \to \cdot aAd, SbAcS \to \cdot bAc, SaecS \to \cdot aec, SbedS \to \cdot bed
I₁SSS' \to S \cdot
I₂SaAdS \to a \cdot Ad, SaecS \to a \cdot ec, AeA \to \cdot e
I₃SbAcS \to b \cdot Ac, SbedS \to b \cdot ed, AeA \to \cdot e
I₄SaAdS \to aA \cdot d
I₅SaecS \to ae \cdot c, AeA \to e \cdot
I₆SbAcS \to bA \cdot c
I₇SbedS \to be \cdot d, AeA \to e \cdot
I₈SaAdS \to aAd \cdot
I₉SaecS \to aec \cdot
I₁₀SbAcS \to bAc \cdot
I₁₁SbedS \to bed \cdot

冲突分析

在状态 I5I_5 中:

  • SaecS \to a e \cdot c(移进项目,期待 cc
  • AeA \to e \cdot(归约项目 r5r_5

当输入为 cc 时,既可以选择将 AeA \to e 归约,也可以选择将 cc 作为 SaecS \to aec 的一部分移进。此为移进-归约冲突

FOLLOW(A)={c,d}FOLLOW(A) = \{c, d\}。由于 cFOLLOW(A)c \in FOLLOW(A)FOLLOW(A)FOLLOW(A) 与移进符号 {c}\{c\} 的交集非空,SLR(1) 无法解决这一冲突。

状态 I7I_7 类似:SbedS \to b e \cdot d(期待 dd)与 AeA \to e \cdot(归约)冲突,且 dFOLLOW(A)d \in FOLLOW(A)

SLR(1) 失效的根本原因:SLR(1) 对所有出现 AeA \to e \cdot 的状态使用同一个全局 FOLLOW(A)={c,d}FOLLOW(A) = \{c, d\},但实际在不同上下文中适用的向前看符号是不同的——在 I5I_5(经过 aa 的分支)中 AA 后面只能跟 dd(来自 SaAdS \to a A d),在 I7I_7(经过 bb 的分支)中 AA 后面只能跟 cc(来自 SbAcS \to b A c)。LR(1) 正是通过为每个归约项目计算 状态相关的向前看集合 来精确解决这类冲突。


6.4 LALR(1) 分析

LR(1) 分析表构造精确,对文法限制少,但可能造成状态数急剧增长。LALR(1) 通过合并 LR(1) 项目集规范族中的同心集来压缩状态数,同时尽量保持分析能力。

核心思想

  • 同心集:具有相同核心(核)但向前看符号集合不同的 LR(1) 项目集
  • 将同心集合并为一个状态,其向前看符号集合为各同心集的并集
  • 若合并后不引入新冲突,则该文法是 LALR(1) 文法

图 6.12:LR(1) 项目集及转换函数(示例)

同心集合并的性质

  1. 核心不变:同心集合并后,核心仍相同,仅向前看符号集合扩大为各集合的并集
  2. 转换封闭:若两个项目集是同心集,则它们的 GOTO 转换后仍为同心集,合并后转换函数自动合并
  3. 冲突类型限制:若文法是 LR(1) 文法,合并同心集后若产生冲突,只可能是归约-归约冲突,不可能产生新的移进-归约冲突
  4. 错误发现推迟:合并同心集后,某些错误发现的时间可能推迟,但错误位置仍是准确的

LALR(1) 分析表构造算法

  1. 构造文法 GGLR(1) 项目集族 C={I1,I2,,In}C = \{I_1, I_2, \dots, I_n\}
  2. 合并所有同心集,得到 C={J1,J2,,Jm}C' = \{J_1, J_2, \dots, J_m\}m<nm < n
  3. 基于 CC' 构造 ACTION 表和 GOTO 表,方法与 LR(1) 相同:
    • 移进:若 [Aαaβ,b]Jk[A \to \alpha \cdot a \beta, b] \in J_kGOTO(Jk,a)=JlGOTO(J_k, a) = J_laa 为终结符,则 ACTION[k,a]=slACTION[k, a] = s_l
    • 归约:若 [Aα,a]Jk[A \to \alpha \cdot, a] \in J_k,则 ACTION[k,a]=rjACTION[k, a] = r_jjj 为产生式编号)
    • 接受:若 [SS,#]Jk[S' \to S \cdot, \#] \in J_k,则 ACTION[k,#]=accACTION[k, \#] = \text{acc}
    • GOTO:若 GOTO(Jk,A)=JlGOTO(J_k, A) = J_lAA 为非终结符),则 GOTO[k,A]=lGOTO[k, A] = l

示例 1:LR(1) 但非 SLR(1) 文法

文法 GG

(0) SS(1) SaAd(2) SbAc(3) Sae(4) Sbf(5) Ae\begin{aligned} (0)\ &S' \to S \\ (1)\ &S \to a A d \\ (2)\ &S \to b A c \\ (3)\ &S \to a e \\ (4)\ &S \to b f \\ (5)\ &A \to e \end{aligned}

该文法是 LR(1) 文法,但不是 SLR(1) 文法(见 6.3 节反例),也不是 LR(0) 文法

经分析,该文法也是 LALR(1) 文法。其 LR(1) 项目集规范族中存在同心集,合并后得到 LALR(1) 项目集族,分析表与 LR(1) 类似但状态数更少。

示例 2:LR(1) 但非 LALR(1) 文法

文法 G[S]G[S]

(0) SS(1) SaAd(2) SbBd(3) SaAe(4) SbAe(5) Ac(6) Bc\begin{aligned} (0)\ &S' \to S \\ (1)\ &S \to a A d \\ (2)\ &S \to b B d \\ (3)\ &S \to a A e \\ (4)\ &S \to b A e \\ (5)\ &A \to c \\ (6)\ &B \to c \end{aligned}

LR(1) 分析:可构造 LR(1) 项目集族,无冲突,因此是 LR(1) 文法。

LALR(1) 分析:LR(1) 项目集族中存在如下同心集:

  • I8I_8: [Ac,{d,e}][A \to c \cdot, \{d, e\}]
  • I9I_9: [Bc,{d,e}][B \to c \cdot, \{d, e\}]

合并后变为:

  • JJ: AcA \to c \cdot(向前看符号 {d,e}\{d, e\}
  • JJ: BcB \to c \cdot(向前看符号 {d,e}\{d, e\}

合并后,对于向前看符号 ddee,既可以用 AcA \to c 归约,也可以用 BcB \to c 归约,产生归约-归约冲突

因此该文法是 LR(1) 文法,但不是 LALR(1) 文法


6.5 LR(1) 分析

LR(1) 分析(也称规范 LR 分析)为每个项目精确计算向前看符号集合,因此分析能力最强,能解决 SLR(1) 和 LALR(1) 无法解决的冲突。

LR(1) 项目

LR(1) 项目形如 [Aαβ,a][A \to \alpha \cdot \beta, a],其中 aa 是向前看符号(终结符或 #\#),表示:当用 AαβA \to \alpha \beta 归约后,合法的后续输入符号应包含 aa

LR(1) 项目集闭包

[AαBβ,a]I[A \to \alpha \cdot B \beta, a] \in I,则对 BB 的每个产生式 BγB \to \gamma,将 [Bγ,b][B \to \cdot \gamma, b] 加入 II,其中 bFIRST(βa)b \in FIRST(\beta a)

LR(1) 分析表构造

与 LR(0) 类似,但项目是 LR(1) 项目:

  1. [Aαaβ,b]Ik[A \to \alpha \cdot a \beta, b] \in I_kaa 为终结符,且 GOTO(Ik,a)=IlGOTO(I_k, a) = I_l,则 ACTION[k,a]=slACTION[k, a] = s_l
  2. [Aα,a]Ik[A \to \alpha \cdot, a] \in I_k,则 ACTION[k,a]=rjACTION[k, a] = r_jjj 为产生式编号)
  3. [SS,#]Ik[S' \to S \cdot, \#] \in I_k,则 ACTION[k,#]=accACTION[k, \#] = \text{acc}
  4. GOTO 表同 LR(0)

图 6.13:LR(1) 项目集及转换函数(赋值语句文法)

示例:LR(1) vs LALR(1) vs SLR(1) 比较

文法 GG

(0) SS(1) SL=R(2) SR(3) LR(4) Li(5) RL\begin{aligned} (0)\ &S' \to S \\ (1)\ &S \to L = R \\ (2)\ &S \to R \\ (3)\ &L \to * R \\ (4)\ &L \to i \\ (5)\ &R \to L \end{aligned}

LR(0) 分析:存在移进-归约冲突(在 RLR \to LSL=S \to L= 等项目中),不是 LR(0) 文法。

SLR(1) 分析:计算 FOLLOW(R)={#,=}FOLLOW(R) = \{\#, =\}。在包含 RLR \to L \cdot(归约)和 SL=RS \to L \cdot = R(移进 ’=‘)的状态中,FOLLOW(R){=}={#,=}{=}FOLLOW(R) \cap \{=\} = \{\#, =\} \cap \{=\} \neq \emptyset,冲突无法解决。因此不是 SLR(1) 文法。

LR(1) 分析:LR(1) 项目集族中,归约项目 [RL,#][R \to L \cdot, \#] 的向前看符号只有 #\#,而移进项目期待 ’=‘,两者不相交,冲突解决。因此是 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(精确向前看)最多最强理论/工具生成
My avatar

Thanks for reading my blog post! Feel free to check out my other posts or contact me via the social links in the footer.


More Posts

Comments