C# 语法分析器(二)LR(0) 语法分析

系列导航

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

首先,需要介绍下 LALR 语法分析的基础:LR(0) 语法分析。

还是以之前的算式文法为例:

$E \to E + T$
$E \to T$
$T \to T * F$
$T \to F$
$F \to id$
$F \to (E)$

先来看一下 $(id+id)$ 是如何被 LR(0) 语法分析执行的。这里使用 $\$$ 这个特殊符号来标记输入的结束。

输入 动作
$(id_1+id_2)\$$ 移入
$($ $id_1+id_2)\$$ 移入
$(id_1$ $+id_2)\$$ 按照 $F \to id$ 归约
$(F$ $+id_2)\$$ 按照 $T \to F$ 归约
$(T$ $+id_2)\$$ 按照 $E \to T$ 归约
$(E$ $+id_2)\$$ 移入
$(E+$ $id_2)\$$ 移入
$(E+id_2$ $)\$$ 按照 $F \to id$
$(E+F$ $)\$$ 按照 $T \to F$
$(E+T$ $)\$$ 按照 $E \to E + T$
$(E$ $)\$$ 移入
$(E)$ $\$$ 按照 $F \to (E)$ 归约
$F$ $\$$ 按照 $T \to F$ 归约
$T$ $\$$ 按照 $E \to T$ 归约
$E$ $\$$ 接受

可以看到,LR(0) 语法分析会不断将输入的符号移入到栈中,如果栈里的符号是某个产生式的右部,就会弹出栈内符号并归约为其头部,再将头部符号入栈,直到找到起始非终结符,接受并完成语法分析。

每次都去比较栈里的符号和所有产生式,也可以完成语法分析,但显然这样太过低效,实际使用中会构造出 LR(0) 自动机,利用 LR 语法分析表来提高匹配效率。

一、项和 LR(0) 自动机

LR(0) 语法分析器会通过维护一些状态,来表明我们在语法分析过程中所处的位置,从而决定现在需要移入还是归约。

LR(0) 使用“项”(item)来表示现在已经看到了产生式的哪些部分。项是由产生式再加上一个位于它的右部中某处的点组成的。例如产生式 $A \to XYZ$ 产生了四个项:

$$\begin{matrix}
A \to \cdot \ XYZ \\
A \to X \cdot YZ \\
A \to XY \cdot Z \\
A \to XYZ\ \cdot \ \\
\end{matrix}$$

例如,项 $A \to \cdot \ XYZ$ 表示我们希望在接下来的输入中看到一个从 $XYZ$ 推导得到的串。项 $A \to X \cdot YZ$ 表示我们刚刚在输入中看到了一个可以由 $X$ 推导得到的串,并且我们希望接下来看到一个能从 $YZ$ 推导的串。项 $A \to XYZ\ \cdot \ $ 表示我们已经看到了产生式体 $XYZ$,已经是时候把 $XYZ$ 归约为 $A$ 了。

LR(0) 语法分析器的状态,就是这样的项的集合(或者称为“项集”),因此可以用于决定现在需要移入还是归约。这些状态的集合(或者称为“项集族”)就可以构造出 LR(0) 自动机,自动机的状态就对应一个项集。

二、构造 LR(0) 自动机

为了构造 LR(0) 自动机,首先定义一个增广文法(augmented grammar),如果 $G$ 是一个以 $S$ 为开始符号的文法,那么它的增广文法 $G'$ 就是在 $G$ 中加上新的开始符号 $S'$ 和产生式 $S' \to S$ 而得到的文法。

引入新的开始符号的目的是告诉语法分析器何时应该停止语法分析并接受输入符号串,当且仅当使用产生式 $S' \to S$ 进行归约时,输入符号串被接受。

上面算式文法对应的增广文法如下所示:

$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 id$
$6.\ F \to (E)$

然后,需要两个函数 $\text{CLOSURE}$(闭包) 和 $\text{GOTO}$。

项集的闭包

如果 $I$ 是文法 $G$ 的一个项集,那么 $\text{CLOSURE}(I)$ 就是能够从 $I$ 的定点右侧继续推导时可能用到的所有产生式对应的项。

构造闭包的方法很简单:

  1. 首先 $\text{CLOSURE}(I)$ 只包含 $I$ 本身
  2. 如果 $A \to \alpha \cdot B \beta$ 在 $\text{CLOSURE}(I)$ 中,且 $B \to \gamma$ 是一个产生式,且项 $B \to \cdot \gamma$ 不在 $\text{CLOSURE}(I)$ 中,那么就将这个项添加到闭包中。不断应用这个规则,直到没有新项可以添加到 $\text{CLOSURE}(I)$ 中为止。

还是以之前的算式文法为例,其增广文法的项 $E' \to \cdot E$ 对应的闭包如下所示:

$E' \to \cdot E$
$E \to \cdot E+T$
$E \to \cdot T$
$T \to \cdot T*F$
$T \to \cdot F$
$F \to \cdot id$
$F \to \cdot (E)$

其计算过程为:

  • 根据规则 1,将 $E' \to \cdot E$ 加入闭包。
  • 根据规则 2,定点右侧包含 $E$,因此将 $E$ 的产生式的项(定点位于最左端)$E \to \cdot E+T$ 和 $E \to \cdot T$ 加入闭包。
  • 现在定点右侧包含 $T$,因此将 $T$ 的产生式的项 $T \to \cdot T*F$ 和 $T \to \cdot F$ 加入闭包。
  • 现在定点右侧包含 $F$,因此将 $F$ 的产生式的项 $F \to \cdot id$ 和 $F \to \cdot (E)$ 加入闭包。
  • 现在定点右侧没有更多非终结符,过程终止。

该算法的具体实现可以参见这里

对于闭包,还可以进一步划分为如下两类:

  • 内核项:包含初始项 $S' \to \cdot S$ 和所有定点不在最左端的项。
  • 非内核项:除了初始项 $S' \to \cdot S$ 意外所有定点在最左端的项。

在上面的例子中,只有 $E' \to \cdot E$ 是内核项,其它的都是非内核项。或者说,在计算 $\text{CLOSURE}(I)$ 时,只有 $I$ 是内核项,其它后加入的都是非内核项。

这样区分的原因,是在生成语法分析器的过程中,只有内核项需要一直保存在内存中,非内核项只需要在使用时临时计算出来即可,可以有效减少不必要的内存占用。

GOTO 函数

接下来就是另一个函数 $GOTI(I, X)$ 了,其中 $I$ 是一个项集,$X$ 是一个符号(终结符或非终结符)。$\text{GOTO}(I, X)$ 表示了项集 $I$ 中所有形如 $A \to \alpha \cdot X \beta$ 的项所对应的 $ \to \alpha X \cdot \beta$ 的闭包。由于项集对应了 LR(0) 自动机中的状态,$\text{GOTO}(I,X)$ 就表示了自动机中的状态 $I$ 在看到输入 $X$ 后,需要转换到的新状态。

拿上面 $E' \to \cdot E$ 的闭包为例:

$E' \to \cdot E$
$E \to \cdot E+T$
$E \to \cdot T$
$T \to \cdot T*F$
$T \to \cdot F$
$F \to \cdot id$
$F \to \cdot (E)$

这个闭包中,定点右边会可能出现 $E$、$T$、$F$、$id$ 和 $($ 这五个符号,因此对应的 $\text{GOTO}$ 也只存在五个,其内容分别为(只列出内核项):

$\text{GOTO}(I, E) = [ E' \to E \cdot,\ E \to E \cdot +T ] $
$\text{GOTO}(I, T) = [ E \to T \cdot,\ T \to T \cdot *F ] $
$\text{GOTO}(I, F) = [ T \to F \cdot] $
$\text{GOTO}(I, id) = [ F \to id \cdot] $
$\text{GOTO}(I, () = [ F \to ( \cdot E)] $

如果计算出算式文法的完整项集,那么其自动机如下图所示,其中阴影部分表示闭包:

图 1 算式文法的 LR(0) 自动机

图 1 算式文法的 LR(0) 自动机,图片来自编译原理

构造 LR(0) 自动机的具体实现可以参见这里

三、构造 LR 语法分析表

当然,在实际使用中,肯定要将自动机转换为其它易于处理的的数据结构,就是 LR 语法分析表。

LR 语法分析器一般都会包含两个栈:状态栈和符号栈。状态栈就代表了已归约的非终结符,与余下的输入一起表示了如下的最右句型(状态栈右侧为栈顶)。

$$X_1X_2 \cdots X_ma_ia_{i+1} \cdots a_n$$

本来根据状态栈就足够复原出相应的符号了,但在实际使用中,符号一般都会附加一些额外数据,因此需要一个符号栈来维护这些额外数据。

然后,就需要两个表格 $\text{ACTION}$ 和 $\text{GOTO}$。

$\text{ACTION}[i, a]$ 表示当前处于自动机的状态 $i$ 时,下一个输入是终结符 $a$($a$ 也可能是输入的结束 $\$$)需要执行的动作,其可能的值为:

  1. 移入 $j$,其中 $j$ 是一个状态。表示需要将 $j$ 移入栈中,同时将 $a$ 也移入符号栈。
  2. 归约 $A \to \beta$,其中 $k$ 是产生式的索引。表示需要将栈顶的 $\beta$ 归约为产生式头 $A$,弹出栈顶的多个状态和符号($\beta$ 长度个),再将归约后的状态和符号压入栈中。
  3. 接受,表示完成了语法分析过程。
  4. 报错,$\text{ACTION}$ 表格中一般不会特意写明。表示在输入中发现了一个错误并应当执行某个错误恢复动作,会在后面再来具体讨论。

$\text{GOTO}$ 表格则与之前的 $\text{GOTO}$ 函数一致,只是用状态来代表项集,并且只需要包含非终结符部分。它的用途是在遇到归约动作时,确认需要将哪个状态压入状态栈中。

对于 LR(0) 文法来说,可以如下构造语法分析表,假设已构造 LR(0) 的项集族 ${I_0, I_1, \cdots, I_n}$:

  1. 根据 $I_i$ 构造得到状态 $i$,状态 $i$ 的 $\text{ACTION}$ 根据以下方法决定:
    1. 如果 $A \to \alpha \cdot a \beta$ 在 $I_i$ 中,且 $\text{GOTO}(I_i, a) = I_j$,那么将 $\text{ACTION}[i, a]$ 设置为“移入 $j$”。
    2. 如果 $A \to \alpha \cdot$ 在 $I_i$中,那么对于任意非终结符 $x$(包含输入结束),将 $\text{ACTION}[i, x]$ 设置为“归约 $A \to \alpha$”
    3. 如果 $S' \to S \cdot$ 在 $I_i$ 中,那么将 $\text{ACTION}[i, \$]$ 设置为“接受”。
  2. 状态 $i$ 的 $\text{GOTO}$ 根据以下方法决定:设 $A$ 是一个非终结符,如果 $\text{GOTO}(I_i, A) = I_j$,那么 $\text{GOTO}[i, A] = j$。
  3. 规则 1 和 2 未定义的所有条目都设置为“报错”。
  4. 语法分析器的初始状态就是根据 $S' \to \cdot S$ 所在项集构造得到的状态。

上面算式文法生成的 LR(0) 语法分析表如下所示:

$$\begin{array}
{|c|cccccc|ccc|}
状态 & id & + & * & ( & ) & \$ & E & T & F \\
0 & s5 & & & s4 & & & 1 & 2 & 3 \\
1 & & s6 & & & & acc & & & \\
2 & r2 & r2 & r2/s7 & r2 & r2 & r2 & & & \\
3 & r4 & r4 & r4 & r4 & r4 & r4 & & & \\
4 & s5 & & & s4 & & & 8 & 2 & 3 \\
5 & r5 & r5 & r5 & r5 & r5 & r5 & & & \\
6 & s5 & & & s4 & & & & 9 & 3 \\
7 & s5 & & & s4 & & & & & 10 \\
8 & & s6 & & & s11& & & & \\
9 & r1 & r1 & r1/s7 & r1 & r1 & r1 & & & \\
10 & r3 & r3 & r3 & r3 & r3 & r3 & & & \\
11 & r6 & r6 & r6 & r6 & r6 & r6 & & & \\
\end{array}$$

这里使用 si 表示“移入 $i$,rj 表示按照索引为 $j$ 的产生式归约,acc 表示接受,空白表示报错。

如果注意检查前面的 LR(0) 自动机和语法分析表,可以发现状态 2 是包含 $E \to T \cdot$ 和 $T \to T \cdot * F$ 这两个项的,这两个项在 * 上对应的动作应当是 r2 和 s7 —— 同一个非终结符上可能出现两个不同的动作,无法不在查看更多输入的前提下决定使用哪个动作。这就说明上面的算式文法存在冲突动作,不是 LR(0) 文法,状态 9 也会有同样问题。

这里的移入-归约冲突,就是 LR 语法分析中可能遇到的冲突之一,另一个则是归约-归约冲突,这种情况下无法选择使用哪个产生式进行归约。为了解决冲突,最简单的办法就是向前查看更多符号。例如同样是基于 LR(0) 自动机,但利用 $\text{FOLLOW}$ 集减少冲突的 SLR 技术,或者利用向前看符号的 LALR 技术,或者是直接扩展为 LR(1) 语法分析。

如果允许向前查看一个字符,那么在到达状态 2 时,就可以发现在后一个字符是“”时,只能选择移入而不能归约,因为归约后的非终结符是 $E$,但却不存在 $X \to E * \cdots$ 这样的产生式。状态 9 也是同理,在遇到“”时只能选择移入。

使用修正后的语法分析表,就可以正确对 $(id+id)$ 进行语法分析了,其过程如下所示:

状态栈 符号栈 输入 动作
0 $(id_1+id_2)\$$ 移入到 4
0 4 $($ $id_1+id_2)\$$ 移入到 5
0 4 5 $(id_1$ $+id_2)\$$ 按照 5 $F \to id$ 归约
0 4 3 $(F$ $+id_2)\$$ 按照 4 $T \to F$ 归约
0 4 2 $(T$ $+id_2)\$$ 按照 2 $E \to T$ 归约
0 4 8 $(E$ $+id_2)\$$ 移入到 6
0 4 8 6 $(E+$ $id_2)\$$ 移入到 5
0 4 8 6 5 $(E+id_2$ $)\$$ 按照 5 $F \to id$
0 4 8 6 3 $(E+F$ $)\$$ 按照 4 $T \to F$
0 4 8 6 9 $(E+T$ $)\$$ 按照 1 $E \to E + T$
0 4 8 $(E$ $)\$$ 移入到 11
0 4 8 11 $(E)$ $\$$ 按照 6 $F \to (E)$ 归约
0 3 $F$ $\$$ 按照 4 $T \to F$ 归约
0 2 $T$ $\$$ 按照 2 $E \to T$ 归约
0 1 $E$ $\$$ 接受

有了 LR(0) 语法分析作为基础,下一章就会来介绍 LALR 语法分析。

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

posted @ 2022-11-02 10:25  CYJB  阅读(875)  评论(1编辑  收藏  举报
Fork me on GitHub