LL剖析器

上下文无关文法语法分析器· LL剖析器· 算符优先分析器· LR剖析器· SLR剖析器· LALR剖析器

LL分析器是一种处理某些上下文无关文法的自顶向下分析器。因为它从左（Left）到右处理输入，再对句型执行最左推导出语法树（Left derivation，相对于LR分析器）。能以此方法分析的文法称为LL文法。

在解析句子时使用 $k$ 个词法单元作向前探查的LL分析器被称为 $\text{LL}(k)$ 解析器。若一个文法能构造出可以在不用回溯法进行回溯的情况下处理文法的分析器，则称该文法为 LL(k) 文法。如果一个形式语言拥有 $\text{LL}(k)$ 文法，则该语言被称为 $\text{LL}(k)$ 语言。对于每个 $k\geq0$，$\text{LL}(k)$ 语言集合都严格包含于 $\text{LL}(k+1)$ 语言集合中。因此，并非所有上下文无关语言都能被 $\text{LL}(k)$ 解析器识别。这些文法中，较严格的 LL(1) 文法相当受欢迎，因为它的分析器只需多看一个词法单元就可以产生分析结果。那些需要很大的 $k$ 才能产生分析结果的编程语言，在分析时的要求也比较高。

本文中将讨论表格驱动的分析器，而非通常由手工打造（非绝对，参看如ANTLR等的 LL(*) 递归下降分析器生成器）的递归下降分析器。

## 概述

对于给定的上下文无关文法，分析器尝试寻找该文法的最左推导。例如，给定一个文法$G$：

- $$S \to E$$
- $$E \to ( E + E )$$
- $$E \to i$$

对$w = ((i+i)+i)$的最左推导如下：

$$S\ \overset{(1)}{\Rightarrow}\ E\ \overset{(2)}{\Rightarrow}\ (E+E)\ \overset{(2)}{\Rightarrow}\ ((E+E)+E)\ \overset{(3)}{\Rightarrow}\ ((i+E)+E)\ \overset{(3)}{\Rightarrow}\ ((i+i)+E)\ \overset{(3)}{\Rightarrow}\ ((i+i)+i)$$

通常, 选择一条规则来展开给定的（最左的）非终结符时，有多个选择的可能。前一个关于最左推导的例子中，在第2步：

$$S\ \overset{(1)}{\Rightarrow}\ E\ \overset{(?)}{\Rightarrow}\ ?$$

我们有两条规则可以选择：

$$E \to ( E + E )$$

$$E \to i$$

为了提高分析的效率，分析器必须能够尽可能确切地、无回溯地进行规则的选择。对于一些文法，它可以透过偷看不回推（即读取之后不将它退回输入流）的输入符号来做到这点。在我们的例子中，如果分析器知道下一个无回推符号是 $($，那么唯一正确可用的就是规则 2。

通常，$LL(k)$ 分析器可以向前探查 $k$ 个符号。然而，给定一个文法，若存在一个能识别该文法 $LL(k)$ 分析器，则其 $k$ 值的确定问题是不可判定的。也就是说，无法判定需要向前探查多少个符号才能识别它。对于每一个 $k$ 的取值，总存在无法被 $LL(k)$ 分析器识别的语言，而 $LL(k+1)$ 分析器却可以识别它。

通过上述梗概，下面我们给出 $LL(k+1)$ 的形式化定义：

设 $G$ 是一个上下文无关文法，且 $k \ge 1$。对于任意两个最左推导，当且仅当满足下述条件时，我们称 $G$ 是 $LL(k)$ 文法：

- $$S\ \Rightarrow\ \dots\ \Rightarrow\ wA\alpha\ \Rightarrow\ \dots\ \Rightarrow\ w\beta\alpha\ \Rightarrow\ \dots\ \Rightarrow\ wx$$
- $$S\ \Rightarrow\ \dots\ \Rightarrow\ wA\alpha\ \Rightarrow\ \dots\ \Rightarrow\ w\gamma\alpha\ \Rightarrow\ \dots\ \Rightarrow\ wy$$

以下条件成立：串 $x$ 中长度为 $k$ 的前缀等价于串 $y$ 中长度为 $k$ 的前缀，表明 $\beta\ =\ \gamma$.

在该定义中， $S$ 文法的开始符号， $A$ 是任意非终结符。之前取得的输入 $w$，以及还没回推的 $x$ 和 $y$ 均为终结符串。希腊字母 $\alpha$, $\beta$ 和 $\gamma$ 代表任意终结符和非终结符组成的串（也可能是空串）。前缀长度与用于保存向前探查结果的缓冲区尺寸一致，并且该定义表明了，缓冲区足以区分任意两个不同单词的推导。

本分析器可以处理特定形式文法的符号串。

本分析器由以下部件组成：

- 一个输入缓冲区，存放输入符号串（由语法建立的）。
- 一个分析栈，用于储存等待处理的终结符与非终结符的。
- 一张分析表，标记了是否存在可用于目前分析栈与下一个输入符号的语法规则。

分析器根据分析栈的栈顶符号（行）以及当前输入流中的符号（列）来决定使用哪一条规则。

当分析器一开始执行时，分析栈中已经有两个符号：

```
[ S, $ ]
```

'$'时一个特殊的终结符，用于表示分析栈的栈底或者输入的结束；而'S'则时文法的开始符号。分析器会尝试根据它在输入流中看到的符号来改写分析栈中的数据，但只会将仍需修改的数据存回分析栈中。

## 实际的例子

### 设定

为解释LL剖析器的工作方式，我们创造了以下这个小语法：

- S → F
- S → ( S + F )
- F → 1

并处理以下输入：

( 1 + 1 )

这个语法的剖析表如下：

（｜）｜1｜+｜$

S｜2｜1

F｜3

（注意到有一列特殊终端符号，在这里表示为$，是用来标示输入结束的。）

### 剖析流程

剖析器先从输入资料流中读到第一个 '('，以及堆叠中的'S'。从表格中他发现必须套用规则 (2)；它必须将堆叠中的'S'重写为 '( S + F )'，并将规则的号码输出。最后堆叠变成：

```
[ (, S, +, F, ), $ ]
```

再来它移除输入及堆叠中的 '('：

```
[ S, +, F, ), $ ]
```

现在剖析器从输入资料流中抓到一个'1'，所以他知道必须套用规则 (1)与规则 (3)，并将结果输出。则堆叠变成：

```
[ F, +, F, ), $ ]
[ 1, +, F, ), $ ]
```

接下来的两个步骤中，剖析器读到'1'及 '+'，因为他们跟堆叠中的资料一样，所以从堆叠中移除。最后堆叠剩下：

```
[ F, ), $ ]
```

再接着的三个步骤中，堆叠中的'F'会'1'被取代，而规则 (3)会被输出。再来堆叠与输入资料流中的'1'与')'都会被移除。而剖析器看到堆叠与输入资料流都只剩下'$'的时候，就知道自己的事情做完了。

在这个例子中，剖析器接受了输入资料，并产生以下输出（规则的代号）：

2, 1, 3, 3

这的确是从输入的左边优先推导。我们可以看出由左至右的输入顺序为：

S → ( S + F ) → ( F + F ) → ( 1 + F ) → ( 1 + 1 )

## 备注

由以上范例可以看出剖析器根据堆叠最上层为非终端符号、终端符号、还是特殊符号$来决定采取三种不同的步骤：

- 若堆叠最上层为非终端符号，则根据输入资料流中的符号对照剖析表，决定要用语法中的哪条规则来取代堆叠中的资料，顺带输出规则的号码。若表格中并没有这么个规则，则回报错误并终止执行。
- 若堆叠最上层为终端符号，则与输入资料流中的符号比较。若相同则移除，若不同则回报错误并终止执行。
- 若堆叠最上层为'$'，并且输入资料流中也是'$'，则表示剖析器成功的处理了输入，否则将回报错误。不管怎样，最后剖析器都将终止执行。

这些步骤会持续到输入结束，然后剖析器成功处理了一则左边优先推导，或者会回报错误。

## 建构LL(1)剖析表格

为了要填满剖析表格，我们必须决定剖析器在堆叠看到非终端(nonterminal)符号A又在输入资料流看到a的时候应该选用哪一条文法规则。我们可以轻松的发现到这种规则应该有A → w一类的格式，并且语言中的w应至少有一个字串由a开头。为了这个目的，我们设定 第一个集合(first set)的w，记作Fi（w），表示可以在w中找到的所有字串的集合，如果空字串也属于w的话还要再加上ε。而透过文法规则A1 → w1, ..., An → wn，就可以使用以下方法演算每条规则的Fi(wi)及Fi(Ai)了：

- 将每个Fi(wi)及Fi(Ai)初始成空集合
- 将Fi(wi)加入每条Ai → wi规则中的Fi(Ai)，Fi定义如下：
- 所有的a皆为终端符号时，Fi（a w'）= { a }
- Fi（A）不包含ε时，相对于每个非终端符号A，Fi（A w'）= Fi（A）
- Fi（A）包含ε时，相对于每个非终端符号A，Fi（A w'）= Fi（A）\ { ε } ∪ Fi（w'）

- 针对每条Ai → wi规则，将Fi(wi)加入Fi(Ai)
- 重复步骤2与步骤3，直到所有Fi集合固定下来。

不幸的是，第一集合还不够用来产生出剖析表。由于规则中右手边的w可能无限制的被覆写成空字串，所以剖析器也在ε位于Fi（w）并且输入资料流中的符号可以符合A的时候套用A → w。所以还需要一个记作Fo（A）的A的跟随集合(follow set)，表示可以由开始的符号衍生出αAaβ字串的终端符号a的集合。非终端符号的跟随集合可以用以下方法得出：

- 将每个Fo(Ai)初始成空集合
- 若存在Aj → wAiw' 格式的规则，则
- 若终端符号a存在Fi（w'）中，则将a加入Fo(Ai)
- 若ε存在Fi（w'）中，则将Fo(Aj)加入Fo(Ai)
- 重复步骤2直到所有Fo集合固定下来

现在我们可以清楚定义每条规则要放在剖析表的哪里了。若TA,a用以表示表格中代表非终端符号A及终端符号a的规则，则

TA,a包含A → w规则，当且仅当

a在Fi（w）之中，或

ε在Fi（w）之中，且a在Fo（A）之中。

若表格的每格中都仅包含一个规则，则剖析器总是知道该套用什么规则，所以可在不用回溯的前提下剖析字串。在此情形下，这个语法可以称为LL(1)语法。

## 建构LL(k)剖析表格

剖析表格可能（一般来说，在最差状况下）必须有k次的指数复杂度的观念在1992年左右PCCTS发表后改观，它示范了许多程式语言可以用LL(k)来有效率的处理，而不会触发剖析器的最差状况。再者，在某些必须无限前瞻的状况下，LL剖析也是合理的。相反的，传统剖析器产生器，如yacc使用LALR(1)剖析表格建立被限制的LR剖析器，这种剖析器只能向后看固定的一个语汇符号。

## 外部链接

- An easy explanation of First and Follow Sets （使用一种比c较直观的方法解释产生First与Follow集合的过程）
- A tutorial on implementing LL(1) parsers in C#