10.1 LL(1) 分析法基本流程

为了解释什么是 LL(1) 法,首先来看一个简单的例子,语法为:

  1. S -> aS | bS | c

需要解析的句子为 abac ,按上一章最后一节的方法,解析过程如下:

Working-string Production
S S -> aS
aS S -> bS
abS S -> aS
abaS S -> c
abac ACCEPT

下面,一步一步跟踪这个解析过程,查看每一步是如何选择出需要的产生式的。首先,我们的目标是将起始符号 S 展开到 最终句子 abac 。把它们写在同一行来进行比较,如下:

Working-string Final-string Production
S a bac

假设有一个 strcmp 函数来比较符号串 “S” 和 “abac” ,从左向右一个符号一个符号的比较,找到第一个不匹配的符号,也就是 “S” 和 “a” ,上面的表格中,将第一个不匹配的符号加粗表示了。

因此,此时必须将中间句子中的 “S” 展开,才能得到最终句子。那如何展开呢?将最终句子中不匹配的这个 “a” ,和 S 的三个产生式的右边 aS 、 bS 和 c 相比,可以看出,只能选择 S -> aS 展开,才可以和 “a” 匹配上,展开后得到中间句子 aS :

Working-string Final-string Production
S a bac S -> aS
a S a b ac

再次比较此时的中间句子 “aS” 和最终句子 “abac” ,找到的第一个不匹配的符号分别为 “S” 和 “b” ,将 “b” 和 S 的三个产生式比较,发现只能选择 S -> bS ,展开后得到中间句子 abS :

Working-string Final-string Production
S a bac S -> aS
a S a b ac S -> bS
ab S ab a c

按以上原则,每次都对中间句子和最终句子进行比较,找到第一个不匹配的符号,然后利用不匹配的符号挑选出需要的产生式,最终展开到最终句子:

Working-string Final-string Production
S a bac S -> aS
a S a b ac S -> bS
ab S ab a c S -> aS
aba S aba c S -> c
abac abac ACCEPT

因此 LL(1) 法的基本思路为:

每个推导步中,从左向右比较中间句子和最终句子,找到第一个不匹配的符号,如:中间句子为 u X v 、最终句子为 u a w 。显然,a 一定是终结符, X 则可能为非终结符,也可能为终结符,有以下 4 种情况:

情况 A : X 为终结符,这种情况表明无论怎么展开都不可能展开到最终句子,即最终句子不合语法,此时终止推导。

情况 B : X 为非终结符,假设它的所有产生式为 X -> u1 | u2 | … | un ,将 a 和这些产生式的右边 u1, u2, … , un 相比较,找出可以和 a 匹配的 ui ,将 ui 代替中间句子 u X v 中的 X ,得到下一个中间句子 u ui v,然后开始下一轮展开。

情况 C : X 为非终结符,但它的所有产生式的右边 u1, u2, … , un 中,没有一个可以和 “a” 匹配上,这种情况表明最终句子不合语法,此时终止推导。

情况 D : X 为非终结符,但它的所有产生式的右边 u1, u2, … , un 中,有两个或以上的 ui 可以和 “a” 匹配上,这种情况表明此语法不适于用 LL(1) 分析法,需要修改语法。

以上算法中,有一个重要的问题没有讲清楚,那就是怎么找出可以和 a 匹配的 ui 来,上面这个例子当然是简单的,直接比较产生式的第一个字符和 a 就可以找到,但遇到复杂的情况,比如产生式的最前面是一连串的非终结符怎么办? 比如 X 可以展开成空串时怎么办?这个问题,我们稍后再讲,先来对这个算法稍微优化一下,先把基本的流程搞清楚。

上面的算法中可以优化的地方在于,其实没必要每次都从头开始比较中间句子和最终句子,上一轮推导步中已经比较过了的部分就没必要再比较了,比如说这一轮的中间句子为 u X v ,最终句子为 u a w ,可以应用的产生式是 X -> ui ,那么下一轮,可以把中间句子改为 ui v ,把最终句子改为 a w ,也就是把已经匹配的符号都去掉。这样每次不匹配的第一个符号就是最左边的符号。

按此思路,在展开的过程中插入一个 Match 动作,将已经匹配的符号去掉。另外,在起始符号和最终句子的末尾添加一个结束符 EOF (用 $ 表示),整个推导过程如下:

Stack Input Action
S $ a bac$ Predict S -> aS
a S$ a bac$ Match “a”
S $ b ac$ Predict S -> bS
b S$ b ac$ Match “b”
S $ a c$ Predict S -> aS
a S$ a c$ Match “a”
S $ c $ Predict S -> c
c $ c $ Match “c”
$ $ ACCEPT

上面的过程中,Match 动作是将中间句子和最终句子最左边已匹配的符号去掉,这样每次不匹配的第一个符号就是最左边的符号,因此只需要根据最左边的两个符号来选择需要的动作。

Predict 动作就是应用一个产生式,将中间句子中的最左边的非终结符替换成该产生式的右边。

上面的列表的表头中,原来的 Working-string 改成了 Stack ,原来的 Final-string 改成了 Input ,这是因为这两列的符号串的操作方式就像一个栈和一个输入流一样。

以上分析的具体步骤为:

(1) 将结束符(EOF) $ 和起始符号 S 压入栈中;

(2) 从输入流(token stream)中读入下一个终结符(token),赋给 a ,也就是执行一次 a = yylex();

(3) 设栈顶符号为 X ,有以下三种情况:

情况 A : X == a 且 a == $ ,解析成功,终止解析;

情况 B : X == a 且 a != $ ,执行 Match 动作,将 X 出栈,转到(2);

情况 C : X != a 且 X 是非终结符,有三种情况:

情况 C1 : 在 X 的所有产生式 X -> u1 | u2 | … | un 的右边中,只有一个 ui 可以与 a 匹配上。此时,执行动作 Predict X -> ui ,将 X 出栈,将 ui 入栈,转到(3);

情况 C2 : 在 X 的所有产生式 X -> u1 | u2 | … | un 的右边中,没有一个 ui 可以与 a 匹配上。此情况表明最终句子不合语法,终止解析。

情况 C3 : 在 X 的所有产生式 X -> u1 | u2 | … | un 的右边中,有两个或以上的 ui 可以与 a 匹配上。此情况表明此语法不适于用 LL(1) 分析法,需要修改语法。

情况 D : X != a 且 X 是终结符,输入不合语法,终止解析。

以上就是 LL(1) 分析法的基本流程,所谓的 LL(1) ,第一个 L 表示从左向右扫描输入流,第二个 L 表示每一步展开的时候取中间句子中左边第一个非终结符进行展开,括号里面的 1 表示每次只读入 1 个符号,每次只利用这 1 个符号的信息来挑选产生式。

事实上,还有 LL(2) 、LL(3) 、 LL(k) 等分析法,每次一次性读入多个符号,然后利用这些符号的信息来挑选产生式。

以上所说的 Predict 动作,之所以叫 Predict ,是因为这个动作是预测的,只看到了第一个符号 a ,就预测接下来的一串符号必须是 ui 。