C# 语法分析器(三)LALR 语法分析

系列导航

  1. (一)语法分析介绍
  2. (二)LR(0) 语法分析
  3. (三)LALR 语法分析
  4. (四)二义性文法
  5. (五)错误恢复
  6. (六)构造语法分析器

上一章构造了 LR(0) 自动机,现在就可以来构造 LALR 语法分析表了。

这里先介绍一个新函数:FIRST

FIRST(α) 被定义为可以从 α 推导得到的串的首符号集合,其中 α 是任意文法符号。

  • 显然,如果 a 是一个终结符,那么 FIRST(a)=a
  • 如果 A 是一个非终结符,且 Aϵ 是一个产生式,那么将 ϵ 加入到 FIRST(A) 中。
  • 如果 AB1B2Bk 是一个产生式,那么如果对于某个 iaFIRST(Bi) 中且 ϵ 在所有的 FIRST(B1)FIRST(B2)、...、FIRST(Bi1) 中,那么就把 a 加入到 FIRST(A) 中。

构造 FIRST 函数具体实现可以参见这里

前面提到过,LR(0) 语法分析缺少后续输入信息,在算式文法中会出现移入-归约冲突,那我们就尝试向前看一个符号,引入更多信息来解决冲突。

LR(1) 项集

LR(1) 语法分析中需要用到下一个输入的信息,因此需要对项进行精化,使其包含第二个分量。LR(1) 的项一般形如 [Aαβ, a],其中前一部分的含义与 LR(0) 相同,是一个包含定点的产生式;后一部分则是一个终结符或输入结束标记 $,被称为向前看符号

在形如 [Aαβ, a]βϵ 的项中,向前看符号没有任何作用,但是在 [Aα, a] 这样的项中,只有在下一个输入是 a 时才要求按照 Aα 进行归约。这样我们就利用向前看符号限制了归约的场景,可以避免一些移入-归约冲突和归约-归约冲突。

构造 LR(1) 项集的方法与前一章中构造 LR(0) 项集的方法相同,只需要对 CLOSUREGOTO 略作修改,添加对向前看符号的支持。

构造 LR(1) 项集 CLOSURE 函数的方法为:

  1. 初始项集只有 [SS, $],这里很好理解,只需要在输入结束时才会归约到起始符号。
  2. 如果 [AαBβ, a]CLOSURE(I) 中,且 Bγ 是一个产生式,那么对于 FIRST(βa) 中的每个终结符 b,将 [Bγ, b] 加入 CLOSURE(I)。不断应用这个规则,直到没有新项可以添加到 CLOSURE(I) 中为止。

为什么这里 Bγ 的向前看符号在 FIRST(βa) 里?假设我们即将根据项 [AαBβ, a] 归约,那么显然我们已经看到了输入 αBβa(这里 a 是向前看符号,显然要求下一个输入是 a 时才能归约),那我们再向前倒推到按照 Bγ 归约前,就会有 αBβa=αγβa,就可以看到只有 γ 后跟 βa 时,才有可能按照 Bγ 归约,即 Bγ 的向前看符号在 FIRST(βa) 之中。

构造 LR(1) 项集 GOTO 函数的方法也非常简单,只要将向前看符号跟着项一起跟着传递即可。

由于 LR(1) 项集族包含了向前看符号,显然会比 LR(0) 项集族包含更多状态,不太利于实际的词法分析中。如果我们尝试合并一些 LR(1) 项集,以可能产生归约-归约冲突为代价,是可以将状态数减少到与 LR(0) 项集族同样水平的。这就是 LALR 语法分析,在实际使用中往往是更优的选择。

LALR 项集

前面提到,LALR 项集是可以直接根据 LR(1) 项集合并而来的,但构造 LR(1) 项集族的时间和空间成本都比较高,更实用的是根据 LR(0) 项集族,通过一个“传播和自发生成”过程直接生成向前看符号,高效计算 LALR 项的内核。

  1. 假设项集 I 包含项 [Aαβ, a],且 GOTO(I,X)=J。无论 a 为何值,在 GOTO(CLOSURE([Aαβ, a], X)) 时得到的结果中总是包含 [Bγδ, b]。那么对于 Bγδ 来说,向前看符号 b 就是自发生成的。
  2. 其它条件与 1 相同,但有 a=b,且结果中包含 [Bγδ, b] 的原因是项 Aαβ 有一个向前看符号 b,那我们就说向前看符号 bI 的项 Aαβ 传播到了 J 的项 Bγδ 中。需要注意的是,这里的传播关系与特定向前看符号无关,要么所有向前看符号都从一个项传播到另一个项,要么都不传播。

找到每个 LR(0) 项集中自发生成的向前看符号,和向前看符号的传播过程,就可以为 LR(0) 项添加上正确的向前看符号了。

首先需要选择一个不在当前文法中的符号 #,由于它不在文法当中,所以不可能被自发生成。如果计算后的向前看符号是否包含 #,说明向前看符号发生了传播,其它向前看符号就是自发生成的。接下来:

  1. 为当前项集 I 中的每个项 Aαβ 计算 J=CLOSURE([Aαβ, #])
  2. 如果 [BγXδ, a]J 中,且 a#,那么 GOTO(I,X) 中的项 BγXδ 的向前看符号 a 是自发生成的。
  3. 如果 [BγXδ, #]J 中,那么向前看符号会从 I 中的项 Aαβ 传播到 GOTO(I,X) 中的项 BγXδ 上。

确定了向前看符号的自发生和传播过程,就可以不断在项集间传播向前看符号直到停止。

  1. 首先,每个项集只包含其自发生成的向前看符号。
  2. 不断扫描每个项集,确定当前项集 I 可以将向前看符号传播到哪些项集,并将 I 的向前看符号添加到被传播到的项集中。
  3. 不断重复步骤 2,直到每个项集的向前看符号都不再增加。

还是继续使用算式文法作为示例:

0. EE
1. EE+T
2. ET
3. TTF
4. TF
5. Fid
6. F(E)

先来计算 CLOSURE([EE,#]),即:

EE, #
EE+T, #/+
ET, #/+
TTF, #/+/
TF, #/+/
Fid, #/+/
F(E), #/+/

在闭包中,6 个项都有自发生成的向前看符号。例如其中 EE+T,项中定点右边是 E,它生成了 [EE+T, +],就意味着 =I1E>E+T 自发生成的向前看符号。类似的,$[F \to \cdot (E),\ \# / + / ]+/I_4F \to ( \cdot E)$ 自发生成的向前看符号。

闭包中所有项都有 #,说明 I0 中的项 EE 会将向前看符号传播到下面 7 个项中:

I1 中的 EE
I1 中的 EE+T
I2 中的 ET
I2 中的 TTF
I3 中的 TF
I5 中的 Fid
I4 中的 F(E)

下面列出所有传播过程:

I0EE I1EEI1EE+TI2ETI2TTFI3TFI4F(E)I5Fid
I4F(E) I1EE+TI2ETI2TTFI3TFI4F(E)I5FidI8F(E)
I6EE+T I2TTFI3TFI4F(E)I5FidI9EE+T
I7TTF I4F(E)I5FidI10TTF

向前看符号的计算过程如下:

项集 初始值 第一趟
I0 EE $ $
I1 EEEE+T + +/$
I2 ETTTF +//) +//)/$
I3 TF +//) +//)/$
I4 F(E) +//) +//)/$
I5 Fid +//) +//)/$
I6 EE+T +/) +/)/$
I7 TTF +/ +//)/$
I8 EE+T F(E) +//) +//)/$
I9 EE+T TTF +/ +//)/$
I10 TTF +/ +//)/$
I11 F(E) +/ +//)/$

这一过程的实现可以参见这里

现在就已经基于 LR(0) 项集族得到了 LALR 项集族的内核,只要再为每个项集计算 LR(1) 的闭包,就可以当作 LR(1) 项集族来计算语法分析表了。

LALR 语法分析表

LALR 语法分析表的构造与 LR(1) 是一样的。假设已构造 LRLR 的项集族 I0,I1,,In

  1. 根据 Ii 构造得到状态 i,状态 iACTION 根据以下方法决定:
    1. 如果 [Aαaβ, b]Ii 中,且 GOTO(Ii,a)=Ij,那么将 ACTION[i,a] 设置为“移入 j”。这里的 a 必须是一个终结符。
    2. 如果 [Aα, b]Ii中且 AS,那么将 ACTION[i,b] 设置为“归约 Aα
    3. 如果 [SS, $]Ii 中,那么将 ACTION[i,$] 设置为“接受”。
  2. 状态 iGOTO 根据以下方法决定:设 A 是一个非终结符,如果 GOTO(Ii,A)=Ij,那么 GOTO[i,A]=j
  3. 规则 1 和 2 未定义的所有条目都设置为“报错”。
  4. 语法分析器的初始状态就是根据 [SS, $] 所在项集构造得到的状态。

同样的,如果上述规则产生了任何冲突动作,就说明文法不是 LALR 的。

上面算式文法生成的 LALR 语法分析表如下所示:

id+()$ETF0s5s41231s6acc2r2s7r2r23r4r4r4r44s5s48235r5r5r5r56s5s4937s5s4108s6s119r1s7r1r110r3r3r3r311r6r6r6r6

可以看到与 LR(0) 语法分析表相比,少了一些动作,也不再存在移入-归约冲突。

现在就可以得到一个“可执行”的语法分析器了,但为了让语法分析真正可用,还缺少一个重要的组成部分:解决冲突和错误恢复。

本系列相关代码都可以在这里找到。

posted @   CYJB  阅读(436)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 【杭电多校比赛记录】2025“钉耙编程”中国大学生算法设计春季联赛(1)
Fork me on GitHub
点击右上角即可分享
微信分享提示