浅谈编译原理——语法分析篇

7605 字
38 分钟
浅谈编译原理——语法分析篇

1. 语法分析简介#

语法分析程序的地位:

语法分析程序的地位
语法分析程序的地位

词法分析程序接受字符流(即源程序),输出记号流,作为语法分析程序的输入(按照自左向右的顺序扫描输入的记号序列),语法分析程序会根据语法规则,判断输入的记号流是否合法,并输出分析树。

分析方法包括:自顶向下分析和自底向上分析。

2. 自顶向下分析方法#

2.1 递归下降分析#

核心思想是:

  • 对每一个非终结符构造一个分析函数
  • 用前看符号指导产生式规则的选择

由于上下文无关文法(CFG)中所有的产生式左边只有一个非终结符,所以我们在调用产生式规则的函数后,就分为两种情况:

  1. 遇到终结符,则直接把这个终结符和句子中对应位置的 token 进行比较,判断是否符合即可;符合就继续,不符合就返回(回溯);
  2. 遇到非终结符,则调用这个非终结符对应的函数(可能会递归的调用,所以叫递归下降文法);

以一个简单的文法为例,分析 aab 是否为该文法的句子:

S –> AB
A –> aA | ε
B –> b | bB
  1. 从起始状态 SS 开始,调用 SS 的函数,得到非终结符 A,BA,B;
  2. 调用 AA 的函数,得到 a,Aa,A,由于 aa 为终结符,则取句子中的 token aa 进行比较,符合,继续;
  3. 继续递归调用 AA 的函数,得到 a,Aa,A,取句子中的 token bb 进行比较,不符合,回溯;
  4. 调用 AA 的另一个函数,得到 ε\varepsilon,符合,继续;
  5. 调用 BB 的函数,由于前面都是终结符 bb,但产生式 1 已经到达结尾,所以选择第二个产生式并调用,得到 b,Bb,B,继续递归调用 BB 的函数,后略;

显然,这种方法不好:

  1. 如果语法存在左递归(形如 A⇒+Aβ,A∈NA\Rightarrow^+ A\beta,A\in N 的推导),则会导致递归调用无限进行下去,使得分析过程陷入死循环;
  2. 通过回溯不断试探,效率低;

2.2 递归调用预测分析#

从递归下降分析方法的问题来看,优化的点就在于解决回溯,实现一种确定的、不带回溯的方法 —— 递归调用预测分析。

如何克服回溯?能够根据所面临的输入符号准确地指派一个候选式去执行任务。因此通常需要对文法进行改造,以满足自顶向下的分析方法的要求,具体包括:

  • 消除文法的二义性
  • 消除左递归
  • 提取左公共因子
  • 非终结符号 AA 的所有候选式的开头终结符号集两两互不相交;
    • 即 FIRST(αi)∪FIRST(αj)=∅(i≠j)FIRST(\alpha_i)\cup FIRST(\alpha_j)=\emptyset (i\ne j)

注:一般情况下,消除左递归和提取左公共因子后的文法无二义性。

Note

FIRST(αi)FIRST(\alpha_i): 由 αi\alpha_i 开始推导,可以推导出的所有开头终结符号的集合(得出的句子开头的所有可能终结符的集合)。

求解 First 集合的方法:对于产生式 A→β1β2⋯βnA\rightarrow \beta_1\beta_2\cdots\beta_n,根据开头的 β1\beta_1 分情况讨论:

  1. β1\beta_1 是终结符,则 β1∈FIRST(A)\beta_1\in FIRST(A);
  2. β1\beta_1 是非终结符,则 FIRST(β1)⊆FIRST(A)FIRST(\beta_1)\subseteq FIRST(A)

注:针对考虑 ε\varepsilon 产生式的 FIRST 集合,还需要处理:

  • 如果 A→εA\rightarrow\varepsilon 也是产生式,则 ε∈FIRST(A)\varepsilon\in FIRST(A);
  • 如果 A→β1⋯βi−1βi⋯βnA\rightarrow\beta_1\cdots\beta_{i-1}\beta_i\cdots\beta_n,其中 FIRST(β1)−FIRST(βi−1)FIRST(\beta_1)- FIRST(\beta_{i-1}) 均含有 ε\varepsilon,则将 FIRST(βi)⊆FIRST(A)FIRST(\beta_i)\subseteq FIRST(A);
  • 同理如果 FIRST(β1)−FIRST(βn)FIRST(\beta_1)-FIRST(\beta_n) 均含有 ε\varepsilon,则 ε∈FIRST(A)\varepsilon\in FIRST(A);

这里的额外处理事实上是有一个专门的术语:NULLABLE 集合,即所有可直接或间接推导出空串的集合:

NULLABLE 集合的构造方法:对于非终结符 XX,若满足 X⇒∗εX\Rightarrow^*\varepsilon,则 X∈NULLABLEX\in NULLABLE。

上述对 ε\varepsilon 产生式的处理可以简化为:如果 A→XYA\rightarrow XY 且 X∈NULLABLEX\in NULLABLE,则 FIRST(A)=FIRST(X)∪FIRST(Y)FIRST(A)=FIRST(X)\cup FIRST(Y);

显然,这个过程是一个当任何集合有变化就要检查其他集合的动态更新过程。

对于 FIRST 集合的理解?FIRST 集合是后续用来构造分析表的,这里构建一个个人的理解视角,给定输入的记号流 S=abcS = abc:

假如推导的第一步是 S→ABCS\rightarrow ABC,而 FIRST 集合的作用就是用于判断该推导能否满足文法要求,判断方式就是输入的字符 aa 是否在 FIRST(A)FIRST(A) 中,那么这一步推导是可以接受的,推导变为 S→aBCS\rightarrow aBC,之后同理。

并且,由于前面对于文法的要求,非终结符号 AA 的所有候选式的开头终结符号集两两互不相交,所以有且只有这种选择,避免了回溯。

预测分析程序的转换图构造方法#

1. 消除左递归

这里主要以消除直接左递归为主,实际题目中很少出现间接左递归。

消除直接左递归:假如有 Ai→Aiα∣βA_i\rightarrow A_i\alpha\mid \beta,则引入一个新的非终结符 Ai′A^\prime_i,并添加产生式:

Ai→βAi′Ai′→αAi′∣ε\begin{aligned} A_i&\rightarrow \beta A^\prime_i\\ A^\prime_i&\rightarrow \alpha A^\prime_i\mid\varepsilon \end{aligned}

消除间接左递归:在消除 AiA_i 后面的 AjA_j 的直接左递归之前,发现有 Aj→AiγA_j\rightarrow A_i\gamma,则用所有已经在新文法产生式中的 Ai→δA_i\rightarrow \delta 替换 AiA_i,即将产生式 Aj→AiγA_j\rightarrow A_i\gamma 替换为:

Aj→δγA_j\rightarrow \delta\gamma

直至 δ\delta 最前面为终结符或大于等于 AjA_j 的非终结符。

2. 提取左公因子

如果有产生式形如:A→αβ1∣αβ2A\rightarrow\alpha\beta_1\mid\alpha\beta_2,则提取左公因子 α\alpha,将产生式替换为:

A→αA′A′→β1∣β2\begin{aligned} A&\rightarrow\alpha A^\prime\\ A^\prime&\rightarrow\beta_1\mid\beta_2 \end{aligned}

3. 绘制转换图

对于每一个非终结符号 AA,创建一个初态和终态,对于每个产生式 A→X1X2⋯XnA\rightarrow X_1X_2\cdots X_n 构造一条创建一条从初态到终态的路径,有向边依次为 Xi,i=1,2,⋯ ,nX_i,i=1,2,\cdots,n。

4. 化简状态转换图

总之就是反复代入化简。

5. 预测分析程序的实现

以下图为例:

void procE(void) {
procT();
if (char == '+') {
forward pointer;
procE();
}
}

简单来说:

  • 对于 ε\varepsilon 的有向边一般不做任何处理;
  • 遇到终结符则移动记号流的指针指向后一个待读如的字符;
  • 遇到非终结符则调用它的对应函数;

2.3 非递归预测分析/LL(1) 分析算法#

分析过程#

上面的方法中,虽然不存在回溯和死循环,但函数的调用还是存在嵌套的,所以引入非递归预测分析方法,它通过分析表和分析栈联合控制,消除递归。

按照网上的说法,似乎基本鲜有关于递归调用预测分析的介绍,而是直接介绍 LL(1) 分析算法,对应我们教材上的非递归预测分析。

预测分析程序模型
预测分析程序模型

按照个人理解,分析表是用来解决回溯的,而符号栈则是用来解决函数的递归调用的。

输入:符号串 ω\omega(词法分析输出的记号流);文法的预测分析表 MM(手动构造,方法见下/通过语法分析器自动生成);

初始化:

  1. 将符号串 ω\omega 放入输入缓冲区,注意后面要添加一个 $;
  2. 将 $ 放入符号栈,再将文法的起始符号 SS 入栈,此时栈顶元素 X=SX=S;
  3. pointer 指向输入缓冲区的第一个符号,代表当前输入符号 aa;

执行分析:

根据符号栈栈顶元素 XX 和输入符号 aa 执行如下循环过程:

  1. 若 X=a=X=a= $,则分析成功,停止;
  2. 若 X=a≠X=a\ne $,说明当前栈顶的终结符命中输入,匹配成功,将 XX 弹出,同时 pointer 前移指向下一个输入符号;
  3. 若 X∈VT,X≠aX\in V_T,X\ne a,说明发现错误;
  4. 若 X∈VNX\in V_N,则访问分析表 M[X,a]M[X,a],即找到对应的需要被调用的产生式,根据产生式分情况执行:
    1. 若 M[X,a]=X→Y1Y2⋯YnM[X,a]=X\rightarrow Y_1Y_2\cdots Y_n,则将 XX 弹栈,然后按照 Yn,⋯ ,Y2,Y1Y_n,\cdots,Y_2,Y_1 的顺序压入栈(YnY_n 显然是被最后调用的,所以先入栈,放在底下),
    2. 若 M[X,a]=X→εM[X,a]=X\rightarrow \varepsilon,则将 XX 弹栈;
    3. 若 M[X,a]=X→errorM[X,a]=X\rightarrow error,即分析表中没有任何内容,出错;

输出:每次命中分析表对应的产生式时,输出产生式,最终结果为依次调用的产生式序列,相当于一个最左推导的过程。

预测分析表的构造#

1. 改写文法:消除左递归 + 消除左公因子;(见前面)

2. 构造 FIRST 集合:使用前面提到的方法;

3. 构造 FOLLOW 集合:

Note

FOLLOW 集合的定义:假定 SS 是文法 GG 的开始符号,对于 GG 的任何非终结符号 AA,集合 FOLLOW(A)FOLLOW(A) 是在所有句型中,紧跟 AA 之后出现的终结符号或 $ 组成的集合,即所有可以合法的站在此非终结符后面的终结符(可以包括结束符 $ ,但不包括 ε\varepsilon )的集合。

FOLLOW(A)={a∣S⇒∗⋯Aa⋯ ,a∈VT}FOLLOW(A)=\{a\mid S\Rightarrow^* \cdots Aa\cdots,a\in V_T\}

特别地,若有 S⇒∗⋯AS\Rightarrow^*\cdots A,则规定 $ ∈FOLLOW(A)\in FOLLOW(A)。

FOLLOW 集合的构造方法:

  1. 对于起始符号 SS,$ ∈FOLLOW(S)\in FOLLOW(S);
  2. 若有 A→αBβA\rightarrow\alpha B\beta,则 FIRST(β)⊆FOLLOW(B)FIRST(\beta)\subseteq FOLLOW(B),注意除去 ε\varepsilon;
  3. 若有 A→αBA\rightarrow\alpha B 或 A→αBβ,β⇒∗εA\rightarrow\alpha B\beta,\beta\Rightarrow^*\varepsilon,则 FOLLOW(A)⊆FOLLOW(B)FOLLOW(A)\subseteq FOLLOW(B);

FOLLOW 集合的意义?确定某个非终结符可能出现在产生式右侧的上下文环境。举个例子,当栈顶为 XX ,读入的符号为 aa,但 aa 不在任何 FIRST 集合中,如果有 X→εX \rightarrow\varepsilon,那么 aa 必须是 XX 的后继字符才能保证最终句子是一个符合语法的句子,即此时调用 FOLLOW 集合。

4. 预测分析表的构造:对于每条产生式 A→αA\rightarrow \alpha:

  • 对所有终结符 a∈FIRST(α)a\in FIRST(\alpha),{A→α}⊆M[A,a]\{A\rightarrow\alpha\}\subseteq M[A,a];
  • 对 ε∈FIRST(α)\varepsilon\in FIRST(\alpha),则对所有 b∈FOLLOW(A)b\in FOLLOW(A),{A→α}⊆M[A,b]\{A\rightarrow\alpha\}\subseteq M[A,b];

最终留空处均为 error。

LL(1) 文法#

可以用上述方法解析的文法称之为 LL(1) 文法,即从左 (L) 向右读入一个程序,最左 (L) 推导,采用一个 (1) 前看符号。要求对应的预测分析表不含多重表项,也就是要求:对所有产生式 A→u1∣u2∣⋯∣unA\rightarrow u_1\mid u_2\mid \cdots\mid u_n,有:

FIRST(u1)∩⋯∩FIRST(un)=∅,即 FIRST 集合互不相交ui⇒∗ε,FIRST(ui)∩FOLLOW(A)=∅,i=1,2,⋯ ,n,即 FOLLOW(A) 与所有 FIRST 集合互不相交\begin{aligned} &FIRST(u_1)\cap \cdots \cap FIRST(u_n)=\emptyset,\text{即 FIRST 集合互不相交}\\ u_i\Rightarrow^*\varepsilon,&FIRST(u_i)\cap FOLLOW(A)=\emptyset,i=1,2,\cdots,n,\text{即 FOLLOW(A) 与所有 FIRST 集合互不相交} \end{aligned}

3. 自底向上分析方法#

LL(1) 分析法的优点是不需要回溯,构造方法较简单,且分析速度非常快,每读到第一个符号就可以预测出整个产生式来。缺点是对语法的限制太强,能满足此要求的语法相当少,而将一个不满足此要求的语法改写到满足要求也相当不容易。因此, LL(1) 分析法目前已经应用的比较少了。基于此,我们引入现在被广泛使用的自底向上分析方法。

  • 对输入串的扫描:自左向右;
  • 分析树的构造:自底向上;
  • 分析过程:
    • 从输入符号串开始分析
    • 查找当前句型的“可归约串”
    • 使用规则,把它归约成相应的非终结符号
    • 重复

以下列文法为例,输入串为 id + id * id:

E → E + T | T
T → T * F | F
F → (E) | id

一种自底向上的过程如下:

步骤当前句型可归约串使用的产生式归约结果
1id + id * ididF → idF
2F + id * idFT → FT
3T + id * ididF → idF
4T + F * idFT → FT
5T + T * ididF → idF
6T + T * FT * FT → T * FT
7T + TTE → TE
8E + TE + TE → E + TE
9E--结束

从上述例子可见,分析过程的关键在于如何找出“可归约串”。

3.1 “移进-归约”分析方法#

“移进-归约”分析过程#

  1. 把输入符号逐个地移进符号栈中;(即“移进”,把下一个输入符号移入栈顶)
  2. 当栈顶的符号串形成某个产生式的一个候选式(右部)时,在一定条件下, 把该符号串替换(归约)为该产生式的左部符号;(即“归约”)
  3. 重复步骤 2 直到栈顶符号串不再是“可归约串”为止;
  4. 重复步骤 1 ~ 3 直到归约出文法起始符号 SS;
  5. 接收:宣布分析成功,停止分析;
  6. 错误处理:调用错误处理程序进行诊断和恢复;

简单来说,“移进”相当于“往栈里装符号,等待形成句法单位”,“归约”相当于“识别出一个语法成分,用它的非终结符替代”。通过不断循环“读符号 + 识别结构”,最终构建整个语法树。

规范归约#

假定 α\alpha 为文法 GG 的一个句子,若右句型序列 αn,αn−1,⋯ ,α1,α0\alpha_n,\alpha_{n-1},\cdots,\alpha_1,\alpha_0 满足:

  1. αn=α\alpha_n=\alpha,α0=S\alpha_0=S;
  2. ∀0<i≤n\forall 0<i\le n,αi−1\alpha_{i-1} 是经过把 αi\alpha_i 的句柄替换为相应产生式的左部符号而得到的;

规范归约相当于最右推导的逆过程,每次只归约当前句型中的句柄(handle),也就是恰好能还原出上一步最右推导的右部的那一部分。

3.2 LR 分析方法#

LR 分析是移进-归约分析的一种规范化、自动化的实现方法。即从左 (L) 向右扫描输入符号串,为输入符号串构造一个最右 (R) 推导的逆过程,采用 k 个前看符号。

LR 分析法的基本思想是在归约的过程中,一方面记住移入和归约的整个符号串(历史信息),另一方面通过产生式推测未来可能碰到的输入符号(预测信息)。在分析的每一步,只须根据分析栈当前已移进和归约出的全部文法符号,并至多再向前查看 k 个输入符号,就能确定栈顶的符号串是否构成相对于某一产生式的句柄,从而确定当前所应采取的分析动作(是移进还是按某一产生式进行归约等)。

LR 分析程序的模型及工作过程#

LR分析程序模型
LR分析程序模型

栈包括:(两者同步变化)

  • 状态栈 S;
  • 符号栈 X;

分析表包括:

  • 动作表 action:用于指导分析器在遇到不同输入符号时应采取的动作;
    • action[Sm,ai]action[S_m,a_i]:SmS_m 遇到输入符号 aia_i (包括终结符和非终结符)时的动作;
      • shift SS(移进):将当前输入符号 aia_i 和状态 S=goto[Sm,ai]S=goto[S_m,a_i] 压入栈;
      • reduce by A→βA\rightarrow\beta(归约):若 ∣β∣=r\left|\beta\right|=r,则从栈中弹出 rr 项,栈顶变为 Sm−rS_{m-r},然后把 AA 和状态 S=goto[Sm−r,A]S=goto[S_{m-r},A] 压入栈;
      • accept:宣布分析成功,停止分析;
      • error:调用出错处理程序,进行错误恢复;
  • 状态转移表 goto:用于指导分析器在采取归约动作后应转移到的新状态。
    • goto[Sm,X]goto[S_m,X]:SmS_m 经过 XX 的后继状态;

工作过程如下,初始二元式为 (S_0,a_1a_2\cdots a_n\),分析过程中的每步的结果均可以表示为,分析过程中的每步的结果均可以表示为 (S_0S_1\cdots S_m,a_ia_{i+1}\cdots a_n$)$:

  • 若 action[Sm,ai]=shift S,S=goto[Sm,ai]action[S_m,a_i]=shift\space S,S=goto[S_m,a_i],则二元式变为 (S_0S_1\cdots S_mS,a_{i+1}\cdots a_n\)$;
  • 若 action[Sm,ai]=reduce by A→βaction[S_m,a_i]=reduce\space by\space A\rightarrow\beta,则二元式变为 (S_0S_1\cdots S_{m-r}S,a_ia_{i+1}\cdots a_n\)$;
  • 若 action[Sm,ai]=acceptaction[S_m,a_i]=accept,则分析成功;
  • 若 action[Sm,ai]=erroraction[S_m,a_i]=error,则调用错误处理程序;
Note

活前缀:一个规范句型的一个前缀,如果不含句柄之后的任何符号,则称它为该句型的一个活前缀。

同样,分析方法的核心在于构造分析表,不同 LR 算法构造方法不同,下文将具体展开介绍。

LR(0) 算法#

关键概念#
Note

形态/LR(0) 项目: 一个产生式的解析程度,用一个产生式加一个位置黑点 ⋅\cdot 来表示,黑点左边表示已解析的部分,右部表示待解析的部分,一般也称为 LR(0) 项目。

产生式 A→XYZA\rightarrow XYZ 有 4 种 LR(0) 项目:

  • A→⋅XYZA\rightarrow\cdot XYZ:起始形态,
  • A→X⋅YZA\rightarrow X\cdot YZ;
  • A→XY⋅ZA\rightarrow XY\cdot Z;
  • A→XYZ⋅A\rightarrow XYZ\cdot:已完成形态/可折叠/形态,即归约项目;
    • 文法起始符号 SS 的归约项目又称之为接收项目;

注:产生式 A→εA\rightarrow\varepsilon 只有一个归约项目 A→⋅A\rightarrow\cdot。

前三种根据圆点 ⋅\cdot 后的第一个符号为终结符/非终结符又可以分为:

  • 移进项目:圆点后第一个符号为终结符号的 LR(0) 项目;
  • 待约项目:圆点后第一个符号为非终结符号的 LR(0) 项目;
Note

拓广文法:是对原始文法的一种扩展,通过在原始文法中添加一个新的开始符号 S′S^\prime 和产生式 S′→SS^\prime\rightarrow S,使得文法开始符号仅出现在一个产生式的左边,从而使分析器只有一个接受状态。(接收项目唯一)

教材上还讲了有效项目的定义,但没什么用,直接略去。

Note

闭包 closure(I)closure(I) 的定义和构造:设 II 是文法 GG 的一个 LR(0) 项目集合,则 closure(I)closure(I) 是从 II 出发,并使用以下方法构造的项目集合:

  1. II 中的每一个 LR(0) 项目均属于 closure(I)closure(I);
  2. 若 A→α⋅Bβ∈closure(I)A\rightarrow\alpha\cdot B\beta\in closure(I),且有产生式 B→ηB\rightarrow \eta,若 B→⋅η∉closure(I)B\rightarrow\cdot\eta\notin closure(I),则将 B→⋅ηB\rightarrow\cdot\eta 加入 closure(I)closure(I);
  3. 重复步骤 2 直至 closure(I)closure(I) 不再增大;

注:B→⋅ηB\rightarrow\cdot\eta 称之为 A→α⋅BβA\rightarrow\alpha\cdot B\beta 的延伸形态,延伸的方向是单向的。

上述过程就是把集合 II 里所有形态的所有延伸形态全部添加进这个集合。

Note

状态:设 II 是文法 GG 的一个 LR(0) 项目集合,状态即为 closure(I)closure(I),即状态是进行过闭合操作的 LR(0) 项目(形态)集合。

Note

转移函数 go:若 II 是文法 GG 的一个 LR(0) 项目集,XX 是一个文法符号,定义

go[I,X]=closure(J)go[I,X]=closure(J)

其中,J={A→αX⋅β∣当 A→α⋅Xβ∈I 时}J=\{A\rightarrow\alpha X\cdot\beta\mid\text{当 }A\rightarrow\alpha\cdot X\beta\in I\text{ 时}\}。

A→αX⋅βA\rightarrow\alpha X\cdot\beta 为 A→α⋅XβA\rightarrow\alpha\cdot X\beta 遇到符号 XX 时的后继形态,

构造文法 G 的 LR(0) 项目集规范族#

输入为文法 GG,输出为 GG 的 LR(0)LR(0) 项目集规范族 CC:

  1. 构造文法 GG 的拓广文法 G′G^\prime;
  2. C={closure({S′→⋅S})}C=\{closure(\{S^\prime\rightarrow\cdot S\})\};
  3. 对于 CC 中的每一个项目集 II 和每一个文法符号 XX,如果 go[I,X]go[I,X] 不为空且不在 CC 中,则将 go[I,X]go[I,X] 加入 CC;
  4. 重复步骤 2 直至 CC 不再更新;

简单来说,就是从初始项目集 I0=closure({S′→⋅S})I_0 = closure(\{S^\prime\rightarrow\cdot S\}) 开始(事实上 I0I_0 就是起始状态),按以上方法,不断的生成新的项目集(状态),直到不再出现新的项目集(状态)。

如上图所示,这个状态转移图和有限状态自动机的运行图很相似,一般称之为识别文法 G′G^\prime 所有活前缀的 DFA。不过解析过程并不能按照 DFA 运行,上下文无关文法需要下推自动机来进行识别。

LR(0) 项目集中的冲突及解决#
Note

可折叠形态:某状态/项目集中,只有一个项目,且该项目为归约项目。

语法分析中,如果遇到不可折叠状态,就会发生冲突,即:

  1. 一个项目集 II 中,既包含 X→α⋅bβX\rightarrow\alpha\cdot b\beta,又存在 A→α⋅A\rightarrow\alpha\cdot,即既包含不可折叠形态,又包含可折叠形态(归约项目);
    • 这会造成“移进-归约”冲突,也就是无法判断出应该 shift,还是应该 reduce;
  2. 一个项目集中,既包含 A→α⋅A\rightarrow\alpha\cdot,又包含 B→β⋅B\rightarrow\beta\cdot,即包含多条可折叠形态(归约项目);
    • 这会造成“归约-归约”冲突,也就是无法判断出应该采用哪条产生式来归约;

解决方法见下,即 SLR(1) 算法。

判断某文法是否属于 LR(0) 文法#

一个文法是LR(0)文法,每个状态/项目集中:

  1. 要么所有 LR(0) 项目都是“移进-待约项目”;(即不能同时含有可折叠和不可折叠形态)
  2. 要么只含有唯一的归约项目(可折叠形态);
  3. 起始符号不出现在任何产生式右部(一般通过拓广文法已经解决了);

SLR(1) 算法#

LR(0) 冲突的解决#

由于 LR(0) 不向前看下一个符号,大大增加了冲突的可能,因此这种冲突通过向前看若干个输入符号或许能够解决。

还是考虑上面的冲突场景,即 I={X→α⋅bβ,A→α⋅,B→β⋅}I=\{X\rightarrow\alpha\cdot b\beta,A\rightarrow\alpha\cdot,B\rightarrow\beta\cdot\}。现在,如果下一个要读入的符号为 xx,假设此时允许使用 B→β⋅B\rightarrow\beta\cdot 进行归约,那么进行归约后,符号 xx 紧随 BB 之后,即 X∈FOLLOW(B)X\in FOLLOW(B)。

所以,当

  • FOLLOW(A)∩FOLLOW(B)=∅FOLLOW(A)\cap FOLLOW(B)=\emptyset;
  • 终结符号 b∉FOLLOW(A),b∉FOLLOW(B)b\notin FOLLOW(A),b\notin FOLLOW(B);

则上述冲突全部消失,决策如下:

  1. 当下一个读入符号 x=bx=b 时,用 X→α⋅bβX\rightarrow\alpha\cdot b\beta 移进 bb,即将 bb 入栈;
  2. 当下一个读入符号 x∈FOLLOW(A)x\in FOLLOW(A) 时,用 A→αA\rightarrow\alpha 归约;
  3. 当下一个读入符号 x∈FOLLOW(B)x\in FOLLOW(B) 时,用 B→βB\rightarrow\beta 归约;

上述思路即为 SLR(1) 分析表构造的形式化描述。SLR(Simple LR) 算法的核心思想是利用“FOLLOW集”来解决冲突。

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

输入:拓广文法 G′G^\prime; 输出:G′G^\prime 的 SLR 分析表

  1. 构造 G′G^\prime 的 LR(0) 项目集规范族 C={I0,I1,⋯ ,In}C=\{I_0,I_1,\cdots,I_n\};
  2. 对于状态 IiI_i 的分析动作如下:
    1. 若 A→α⋅aβ∈IiA\rightarrow \alpha\cdot a\beta\in I_i,且 go(Ii,a)=Ijgo(I_i,a)=I_j,则 action[i,a]=Sj(shift Ij)action[i,a]=S_j(shift\space I_j);
    2. 若 A→α⋅∈IiA\rightarrow \alpha\cdot\in I_i,则对 ∀a∈FOLLOW(A)\forall a\in FOLLOW(A),则 action[i,a]=R A→αaction[i,a]=R\space A\rightarrow\alpha;
    3. 若 S′→S⋅∈IiS^\prime\rightarrow S\cdot\in I_i,则 action[i,\]=accept$,表示分析成功;
  3. 若 go(Ii,A)=Ijgo(I_i,A)=I_j,AA 为非终结符,则 goto[i,A]=jgoto[i,A]=j;
  4. 剩余空白项均置为 error;
  5. 分析程序的初态为包含 S′→⋅SS^\prime\rightarrow\cdot S 的有效项目集/状态;
判断某文法是否属于 SLR(1) 文法#

法一(一般用于证明是):若文法 G 的 SLR(1) 分析表中没有多重定义项,则该文法是 SLR(1) 文法。

法二(一般用于证明不是):不一定要全部计算出 SLR 分析表,可以挑选状态中存在“移进-归约”冲突的,计算一下应用 FOLLOW 集合后是否能解决冲突,如果仍存在冲突(即对应的动作表位置存在多重表项),则该文法不是 SLR(1) 文法。

定理:每一个 SLR(1) 文法都是无二义的文法,但并非无二义的文法都是 SLR(1) 文法。

LR(1) 算法#

SLR(1) 算法向前查看下一个符号,并利用 FOLLOW 集进行识别,但这样仍然不准确(不能确保一定能归约),仍会导致冲突。

而 LR(1) 算法更加强大,利用了下一个读入的符号的信息(lookahead,展望符),或者也被称为预测先行(预测下一个应该出现的输入符号)。

关键概念#
Note

LR(1) 项目:[A→α⋅β,a][A\rightarrow\alpha\cdot\beta,a],aa 即为展望符,表示在当前处理到 A→α⋅βA\rightarrow\alpha\cdot\beta 时,下一个读入的终结符必须是 aa 才能进行正确归约。

由于项目引入了展望符,后续的一些计算也发生了一些改变,但构造过程基本一致。

Note

后继形态:形态/项目 C=[A→X⋅YZ,a]C=[A\rightarrow X\cdot YZ,a] 遇到符号 YY 转移到后继形态 C′=[A→XY⋅Z,a]C^\prime=[A\rightarrow XY\cdot Z,a]。(这里的区别只是 LR(1) 项目的变化)

Note

转移函数 go:若 II 是文法 GG 的一个 LR(1) 项目集,XX 是一个文法符号,定义

go[I,X]=closure(J)go[I,X]=closure(J)

其中,J={[A→αX⋅β,a]∣当 [A→α⋅Xβ,a]∈I 时}J=\{[A\rightarrow\alpha X\cdot\beta,a]\mid\text{当 }[A\rightarrow\alpha\cdot X\beta,a]\in I\text{ 时}\}。

Note

延伸形态:若一个形态的黑点后是非终结符,形如 C=[A→α⋅Bβ,a]C=[A\rightarrow\alpha\cdot B\beta,a],且有 B→η,b∈FIRST(βa)B\rightarrow\eta,b\in FIRST(\beta a),则 C′=[B→⋅η,b]C^\prime=[B\rightarrow\cdot\eta,b] 是 CC 的延伸形态。

简单记忆的话,延伸形态的产生式左部是黑点后的非终结符 BB,展望符是非终结符 BB 之后的符号串 β\beta + 原展望符 aa 的 FIRST 集合。

为什么呢?形态 C=[A→α⋅Bβ,a]C=[A\rightarrow\alpha\cdot B\beta,a] 表示,当前符号栈为 α\alpha,期待遇到符号 BB,再遇到符号串 β\beta,最后遇到 aa 才能进行归约。我们现在准备“识别”非终结符 BB,接下来要读入能推导出 BB 的符号串,即对 BB 的每个产生式建立相应的“延伸形态”。

当我们识别完 B 后,会继续识别 β\beta,所以延伸形态的展望符号应该是 FIRST(β)FIRST(\beta),不过由于 FIRST(β)FIRST(\beta) 可能包含 ε\varepsilon,那么再后面就是 aa 了,所以展望符号即为 BB 的后继终结符集合,即 FIRST(βa)FIRST(\beta a)。

同理闭包的构造方法也进行微调即可:

Note

闭包的定义及构造方式:

  1. II 中的每一个 LR(1) 项目均属于 closure(I)closure(I);
  2. 若 [A→α⋅Bβ,a][A\rightarrow\alpha\cdot B\beta,a],且有产生式 B→ηB\rightarrow \eta,且 b∈FIRST(βa)b\in FIRST(\beta a),若 [B→⋅η,b]∉closure(I)[B\rightarrow\cdot\eta,b]\notin closure(I),则将 [B→⋅η,b][B\rightarrow\cdot\eta,b] 加入 closure(I)closure(I);
  3. 重复步骤 2 直至 closure(I)closure(I) 不再增大;
构造文法 G 的 LR(1) 项目集规范族#

输入为文法 GG,输出为 GG 的 LR(1)LR(1) 项目集规范族 CC:

  1. 构造文法 GG 的拓广文法 G′G^\prime;
  2. C=\{closure(\{[S^\prime\rightarrow\cdot S,\]})}$;
  3. 对于 CC 中的每一个项目集 II 和每一个文法符号 XX,如果 go[I,X]go[I,X] 不为空且不在 CC 中,则将 go[I,X]go[I,X] 加入 CC;
  4. 重复步骤 2 直至 CC 不再更新;

这里和 LR(0) 项目集规范族的构造过程是完全一致的(除了项目的表示形式不同)。同理,识别文法所有活前缀的 DFA 的构造方法也是一致的。

LR(1) 分析表的构造算法#

输入:拓广文法 G′G^\prime; 输出:G′G^\prime 的 SLR 分析表

  1. 构造 G′G^\prime 的 LR(1) 项目集规范族 C={I0,I1,⋯ ,In}C=\{I_0,I_1,\cdots,I_n\};
  2. 对于状态 IiI_i 的分析动作如下:
    1. 若 [A→α⋅aβ,b]∈Ii[A\rightarrow \alpha\cdot a\beta,b]\in I_i,且 go(Ii,a)=Ijgo(I_i,a)=I_j,则 action[i,a]=Sj(shift Ij)action[i,a]=S_j(shift\space I_j);
    2. 若 [A→α⋅,a]∈Ii[A\rightarrow \alpha\cdot,a]\in I_i,且 A≠S′A\ne S^\prime,则 action[i,a]=R A→αaction[i,a]=R\space A\rightarrow\alpha;
    3. 若 [S^\prime\rightarrow S\cdot,\]\in I_i,则,则 action[i,$]=accept$,表示分析成功;
  3. 若 go(Ii,A)=Ijgo(I_i,A)=I_j,AA 为非终结符,则 goto[i,A]=jgoto[i,A]=j;
  4. 剩余空白项均置为 error;
  5. 分析程序的初态为包含 [S^\prime\rightarrow\cdot S,\]$ 的有效项目集/状态;

相较于 SLR(1) 分析表,LR(1) 分析表的状态数量和每个状态中包含的项目数量都是及其恐怖的。不过状态数的增加带来的是分析能力的提升。

LR(1) 文法#
Warning

不要求。

相比于 LR(0) 文法,LR(1) 文法允许:

  • 一个状态/项目集中同时出现可折叠状态和不可折叠状态;
    • 只要可折叠形态的展望符不和不可折叠形态中黑点后面的符号冲突,不然遇到该输入,又分不清该移进还是归约了;
  • 也可以含有多条可折叠状态(归约状态);
    • 这些可折叠状态的展望符也不能冲突;

即要求:

  1. 起始符号 S 不能位于任何产生式的右边;(一般通过拓广文法来解决)
  2. 要求每个状态中:
    1. 不能同时含有 [A→α⋅aβ,b][A\rightarrow\alpha\cdot a\beta,b] 和 [B→η⋅,a][B\rightarrow\eta\cdot,a];
      • 否则会引起“移进-归约”冲突;
    2. 不能同时含有 [A→α⋅,a][A\rightarrow\alpha\cdot,a] 和 [B→β⋅,a][B\rightarrow\beta\cdot,a];
      • 否则会引起“归约-归约”冲突;

解决冲突的一种方法是利用符号的优先级,不过此处略。

LALR(1) 算法#

前面已经提到,LR(1) 构造算法复杂且状态、形态数量多。而 LALR(1) 算法对此进行了一定的优化,其基本思想是将一些相似的状态进行 merge。一种 merge 方法是将 LR(1) 项目集规范族中的所有同心状态集合并,合并时,对应项目的搜索符(展望符)也会合并。原先各自接收的有向边,都改为由合并后的项目集接收;原先各自发出的有向边,都改为由合并后的项目集发出。

关键概念#
Note

同心集:如果两个 LR(1) 项目集去掉搜索符号(展望符)之后是相同的,则称这两个项目集具有相同的心(core),即这两个项目集是同心集。

Note

项目集的核(kernal):除去初态项目集外,一个项目集的核(kernel)是由该项目集中那些圆点不在最左边的项目组成。

其中,初态项目集的核只有 [S^\prime\rightarrow\cdot S,\]$。

LALR(1) 的项目集规范族#
  1. 先得到 LR(1) 的项目集规范族;
  2. 合并 LR(1) 项目集规范族中的同心集;(减少分析表状态数)
  3. 用核代替项目集;(减少项目集的存储空间)
  4. 转移函数 go 同样进行合并修改;
LALR(1) 项目集中的冲突#

合并可能导致“归约-归约”冲突,但不会引入新的“移进-归约”冲突,因为:

如果合并后产生“移进-归约”冲突,例如 [A→α⋅aβ,b][A\rightarrow\alpha\cdot a\beta,b] 和 [B→η⋅,a][B\rightarrow\eta\cdot,a] 被合并到一个项目集中,这说明原来 [B→η⋅,a][B\rightarrow\eta\cdot,a] 所在的项目集中还含有形如 [A→α⋅aβ,c][A\rightarrow\alpha\cdot a\beta,c] 的项目,而这两者原来存在于一个项目集,即原来已经存在“移进-归约”冲突。

LALR(1) 分析表的构造算法#

输入:拓广文法 G′G^\prime; 输出:G′G^\prime 的 SLR 分析表

  1. 构造 G′G^\prime 的 LR(1) 项目集规范族 C={I0,I1,⋯ ,In}C=\{I_0,I_1,\cdots,I_n\};
  2. 合并同心集。得到 C′={J0,J1,⋯ ,Jm}C^\prime=\{J_0,J_1,\cdots,J_m\};
    • 分析程序的初态为包含 [S^\prime\rightarrow\cdot S,\]的的J_k$;
  3. 构造 action 子表,对于状态 JiJ_i 的分析动作如下:
    1. 若 [A→α⋅aβ,b]∈Ji[A\rightarrow \alpha\cdot a\beta,b]\in J_i,且 go(Ji,a)=Jjgo(J_i,a)=J_j,则 action[i,a]=Sj(shift Jj)action[i,a]=S_j(shift\space J_j);
    2. 若 [A→α⋅,a]∈Ji[A\rightarrow \alpha\cdot,a]\in J_i,则 action[i,a]=R A→αaction[i,a]=R\space A\rightarrow\alpha;
    3. 若 [S^\prime\rightarrow S\cdot,\]\in J_i,则,则 action[i,$]=accept$,表示分析成功;
  4. 构造 goto 子表:设合并后 Jk={Ii1,Ii2,⋯ ,Iit}J_k=\{I_{i1},I_{i2},\cdots,I_{it}\},原来这些 IiI_i 是同心集,那么显然 go(Ii1,X),go(Ii2,X),⋯ ,go(Iit,X)go(I_{i1},X),go(I_{i2},X),\cdots,go(I_{it},X) 也是同心集,把后继状态组成的同心集合并后的集合记为 JiJ_i,那么go(Jk,X)=Jigo(J_k,X)=J_i。
    • 若 go(Jk,A)=Iigo(J_k,A)=I_i,AA 为非终结符,则 goto[k,A]=igoto[k,A]=i;
    • 其实就是前面用文字描述的,原先各自接收的有向边,都改为由合并后的项目集接收;原先各自发出的有向边,都改为由合并后的项目集发出。
  5. 剩余空白项均置为 error;

基本与 LR(1) 分析表的构造算法一致,只需对同心集和对应的后继状态组成的同心集进行合并。

LALR(1) 文法#
  1. 先判断是否属于 LR(1) 文法。(构造 LR(1) 分析表,采用前面的方法判断是否有冲突)
  2. 如果不存在冲突,则是 LR(1) 文法,继续判断:
    1. 如果不存在同心集,即不用合并,那么显然也还是 LALR(1) 文法;
    2. 如果存在,则先进行合并,再检查合并后是否有冲突(即判断是否有“归约-归约”冲突);
      1. 无冲突,是 LALR(1) 文法;
      2. 有冲突,不是 LALR(1) 文法;
LALR(1) vs. LR(1)#
  • 形式上与 LR(1) 相同;
  • 大小上与 SLR(1)/LR(0) 相当;
  • 分析能力介于 SLR(1) 与 LR(1) 之间;

3.3 LR 分析的错误处理与恢复#

略。

4. 总结#

文法分类
文法分类

“二义性文法”表示那些无论如何都无法消除二义性的文法,它们不可能被任何确定性分析方法识别。我们在上面介绍的均为确定性分析方法,即左侧这些。

左侧的分析方法又可以分为左侧(自顶向下)和右侧(自底向上),其中越外层表示的能处理的文法范围越大,分析能力越强,越内层针对文法的限制越多。

TMD,很久没碰上这么一坨大的了💩。

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

浅谈编译原理——语法分析篇
https://blog.yokumi.cn/posts/notes-about-syntax-analysis/
作者
Yokumi
发布于
2025-10-19
许可协议
CC BY-NC-SA 4.0
相关文章智能推荐
1
浅谈编译原理——语法制导翻译篇
专业学习对于编译器,一般不能只判断输入的语言是否符合语法规则。以算术表达式为例,还需要能确定最后的结果,即翻译目标为计算表达式的值。 语法制导就是如此,根据翻译目标, 语法制导翻译过程就是根据语法分析过程中所使用的产生式,在适当的时机执行与之相应的语义规则,完成符号属性值的计算,从而完成翻译。
2
2026.5 Live Repo
生活杂谈翻了翻相册才发现上一次已经是去年 9 月的羊文学了,哎gszm坏事做尽。 这回的出行方式是经典京沪高铁二等座,去年锅贴得意之作绿皮 D9 真给我坐麻了,这次果断 G 系列。不过被课表背刺,本来还可以买早一班(),结果就是快 0 点才到虹桥。好像是第一次这么晚到沪国。
3
BUPT 计网实践不完全指北
专业学习计算机网络实践这门课每个老师使用的仿真器并不相同,如果你有幸选了张海旸老师的课,那么恭喜你,你将穿越回 2007 年,和 BUPT 的历届计算机学子一起,使用原本在 Windows XP 系统上构建的仿真器 Dynamips 进行实验,由于年代久远,实验中发生不稳定现象也是常有的事。
4
漫游计算机网络之情景篇
专业学习PSTN ,即 Public Switched Telephone Network ,公用交换电话网。 交换局和交换局之间,使用光纤连接。 中继线上传输的是数字信号,其中用到了时分复用技术,包括: 用户家里的电话和电脑都通过电话线,其中,电脑的数字信号先经过 ADSL Modem 转换为模拟信号,然后和语音信号一起经…
5
漫游计算机网络之设备篇
专业学习简单来说,网关是一种能连接两个不同网络的设备,它在网络层或更高层起作用,负责协议转换、地址转换、路由转发等功能。 你可以简单的理解为,所有网关都是路由器,不过路由器并不一定是网关。 NAT ,即 Network Address Translation ,网络地址转换。

评论区

文章目录