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

系列导航

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

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

这里先介绍一个新函数:$\text{FIRST}$。

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

  • 显然,如果 $a$ 是一个终结符,那么 $\text{FIRST}(a)=a$
  • 如果 $A$ 是一个非终结符,且 $A \to \epsilon$ 是一个产生式,那么将 $\epsilon$ 加入到 $\text{FIRST}(A)$ 中。
  • 如果 $A \to B_1 B_2\cdots B_k$ 是一个产生式,那么如果对于某个 $i$,$a$ 在 $\text{FIRST}(B_i)$ 中且 $\epsilon$ 在所有的 $\text{FIRST}(B_1)$、$\text{FIRST}(B_2)$、...、$\text{FIRST}(B_{i-1})$ 中,那么就把 $a$ 加入到 $\text{FIRST}(A)$ 中。

构造 $\text{FIRST}$ 函数具体实现可以参见这里

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

LR(1) 项集

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

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

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

构造 LR(1) 项集 $\text{CLOSURE}$ 函数的方法为:

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

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

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

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

LALR 项集

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

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

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

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

  1. 为当前项集 $I$ 中的每个项 $A \to \alpha \cdot \beta$ 计算 $J = \text{CLOSURE}([A \to \alpha \cdot \beta,\ \#])$
  2. 如果 $[B \to \gamma \cdot X \delta,\ a]$ 在 $J$ 中,且 $a \ne \#$,那么 $\text{GOTO}(I, X)$ 中的项 $B \to \gamma X \cdot \delta$ 的向前看符号 $a$ 是自发生成的。
  3. 如果 $[B \to \gamma \cdot X \delta,\ \#]$ 在 $J$ 中,那么向前看符号会从 $I$ 中的项 $A \to \alpha \cdot \beta$ 传播到 $\text{GOTO}(I, X)$ 中的项 $B \to \gamma X \cdot \delta$ 上。

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

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

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

$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}([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),\ \# / + / * $

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

闭包中所有项都有 $\#$,说明 $I_0$ 中的项 $E' \to \cdot E$ 会将向前看符号传播到下面 7 个项中:

$I_1$ 中的 $E' \to E \cdot$
$I_1$ 中的 $E \to E \cdot+T$
$I_2$ 中的 $E \to T \cdot$
$I_2$ 中的 $T \to T \cdot * F$
$I_3$ 中的 $T \to F \cdot$
$I_5$ 中的 $F \to id \cdot$
$I_4$ 中的 $F \to ( \cdot E)$

下面列出所有传播过程:

$I_0\quad E' \to \cdot E$ $\begin{aligned} &I_1\quad E' \to E \cdot \\ &I_1\quad E \to E \cdot+T \\ &I_2\quad E \to T \cdot \\ &I_2\quad T \to T \cdot * F\\ &I_3\quad T \to F \cdot \\ &I_4\quad F \to ( \cdot E) \\ &I_5\quad F \to id \cdot \end{aligned}$
$I_4\quad F \to ( \cdot E)$ $\begin{aligned} &I_1\quad E \to E \cdot+T \\ &I_2\quad E \to T \cdot \\ &I_2\quad T \to T \cdot * F\\ &I_3\quad T \to F \cdot \\ &I_4\quad F \to ( \cdot E) \\ &I_5\quad F \to id \cdot \\ &I_8\quad F \to (E \cdot) \end{aligned}$
$I_6\quad E \to E+ \cdot T$ $\begin{aligned} &I_2\quad T \to T \cdot *F\\ &I_3\quad T \to F \cdot \\ &I_4\quad F \to ( \cdot E) \\ &I_5\quad F \to id \cdot \\ &I_9\quad E \to E+T\cdot \end{aligned}$
$I_7\quad T \to T* \cdot F$ $\begin{aligned} &I_4\quad F \to ( \cdot E) \\ &I_5\quad F \to id \cdot \\ &I_{10}\quad T \to T*F \cdot \end{aligned}$

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

项集 初始值 第一趟
$I_0\quad$ $E' \to \cdot E$ $\$$ $\$$
$I_1\quad$ $\begin{aligned} &E' \to E \cdot \\ &E \to E \cdot +T \end{aligned}$ $+$ $+/\$$
$I_2\quad$ $\begin{aligned} &E \to T \cdot \\ &T \to T \cdot *F \end{aligned}$ $+/*/)$ $+/*/)/\$$
$I_3\quad$ $T \to F \cdot$ $+/*/)$ $+/*/)/\$$
$I_4\quad$ $F \to (\cdot E)$ $+/*/)$ $+/*/)/\$$
$I_5\quad$ $F \to id \cdot$ $+/*/)$ $+/*/)/\$$
$I_6\quad$ $E \to E+\cdot T$ $+/)$ $+/)/\$$
$I_7\quad$ $T \to T*\cdot F$ $+/*$ $+/*/)/\$$
$I_8\quad$ $\begin{aligned} &E \to E \cdot +T\ &F \to (E \cdot ) \end{aligned}$ $+/*/)$ $+/*/)/\$$
$I_9\quad$ $\begin{aligned} &E \to E+T \cdot \ &T \to T \cdot *F \end{aligned}$ $+/*$ $+/*/)/\$$
$I_{10}\quad$ $T \to T*F\cdot$ $+/*$ $+/*/)/\$$
$I_{11}\quad$ $F \to (E)\cdot$ $+/*$ $+/*/)/\$$

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

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

LALR 语法分析表

LALR 语法分析表的构造与 LR(1) 是一样的。假设已构造 LRLR 的项集族 ${I_0, I_1, \cdots, I_n}$:

  1. 根据 $I_i$ 构造得到状态 $i$,状态 $i$ 的 $\text{ACTION}$ 根据以下方法决定:
    1. 如果 $[A \to \alpha \cdot a \beta,\ b]$ 在 $I_i$ 中,且 $\text{GOTO}(I_i, a) = I_j$,那么将 $\text{ACTION}[i, a]$ 设置为“移入 $j$”。这里的 $a$ 必须是一个终结符。
    2. 如果 $[A \to \alpha \cdot,\ b]$ 在 $I_i$中且 $A \ne S'$,那么将 $\text{ACTION}[i, b]$ 设置为“归约 $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,\ \$]$ 所在项集构造得到的状态。

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

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

$$\begin{array}
{|c|cccccc|ccc|}
状态 & id & + & * & ( & ) & \$ & 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 & & r5 & r5 & & r5 & r5 & & & \\
6 & s5 & & & s4 & & & & 9 & 3 \\
7 & s5 & & & s4 & & & & & 10\\
8 & & s6 & & & s11& & & & \\
9 & & r1 & s7 & & r1 & r1 & & & \\
10 & & r3 & r3 & & r3 & r3 & & & \\
11 & & r6 & r6 & & r6 & r6 & & & \\
\end{array}$$

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

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

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

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