如何构建LR(2)解析器?它与LR(1)解析器有何不同?
为了说明这是如何工作的,让我们以LR(2)语法的示例为例。(此语法源自@rici对此先前问题的出色答案。)
S → RS | R
R → abT
T → aT | c | ε
为了为此语法构建LR(2)解析器,我们将像往常一样通过增加形式为S' → S的产生式来扩充语法:
S' → S
S → RS | R
R → abT
T → aT | c | ε
现在,我们开始生成配置集。我们从S' → S开始,就像LR(1)解析器一样。如下所示:
(1)
S' -> .S [$$]
(1)
S' -> .S [$$]
S -> .R [$$] // New
S -> .RS [$$] // New
S -> .R [$$]
项开始,如下所示:(1)
S' -> .S [$$]
S -> .R [$$]
S -> .RS [$$]
R -> .abT [$$] // New
S -> .RS [$$]
选项。这是一个有趣的情况。我们需要确定在此处发现的R产生式的前瞻将是什么。在LR(1)解析器中,这是通过查看产生式剩余部分的FIRST集合来找到的。在LR(2)解析器中,因为我们有两个前瞻标记,所以我们必须查看FIRST2集合,它是FIRST集合的泛化,列出了可以出现在产生式开头的长度为两个的字符串,而不是长度为一个的字符串。在我们的例子中,FIRST2(S) = {ab}(你看出为什么了吗?),因此我们有以下内容:(1)
S' -> .S [$$]
S -> .R [$$]
S -> .RS [$$]
R -> .abT [$$]
R -> .abT [ab] // New
此时,我们已经完成了第一个配置集的扩展。现在是时候考虑如果我们看到不同的字符会怎么做了。幸运的是,在这种情况下相当容易,因为由该语法产生的任何字符串的第一个字符都必须是 a
。所以让我们看看如果我们遇到一个 a
会发生什么:
(2)
R -> a.bT [$$]
R -> a.bT [ab]
目前看起来没问题。现在如果我们看到这里有一个b
会发生什么?那将把我们带到这个位置:
(3)
R -> ab.T [$$]
R -> ab.T [ab]
这里有两个 LR(2) 项目,在非终结符之前有点,因此我们需要将其展开。让我们从 R -> ab.T [$$]
开始扩展,得到如下结果:
(3)
R -> ab.T [$$]
R -> ab.T [ab]
T -> .aT [$$] // New
T -> .c [$$] // New
T -> . [$$] // New
接下来,我们将扩展R -> ab.T [ab]
的生产:
(3)
R -> ab.T [$$]
R -> ab.T [ab]
T -> .aT [$$]
T -> .c [$$]
T -> . [$$]
T -> .aT [ab] // New
T -> .c [ab] // New
T -> . [ab] // New
T -> .
与两个不同的展望)。 我们还有一些移位项。 所以我们必须问-我们在这里有移位/规约冲突还是减少/减少冲突? T -> .aT [$$] // Shift
T -> .c [$$] // Shift
T -> . [$$] // Reduce
T -> .aT [ab] // Shift
T -> .c [ab] // Shift
T -> . [ab] // Reduce
T -> .c [ab]
。我们需要移位的两个字符的前瞻是什么?在LR(1)解析器的情况下,我们只需说“哦,点在c
之前,所以我们在c
上移位”,但这在这里是不够的。相反,我们会说与此移位项相关联的前瞻是ca
,其中c
来自产生式本身,a
来自项的前瞻的第一个字符。T -> .aT [$$]
。我们需要两个字符的前瞻,我们可以很容易地看到其中一个(点后面的a
)。要获得第二个,我们必须查看T
能够生成的内容。T有三个产生式:一个将T替换为ε,一个将T替换为aT,一个将T替换为c。这意味着可以从T派生出的任何字符串都以a
或c
开头,或者为空字符串。因此,项T -> .aT [$$]
告诉我们,在看到ac
或aa
(从a和c得到的内容)或a$
(如果我们使用了T→ε的产生式,然后从正常前瞻中取出一个$字符)时进行移位。(3)
R -> ab.T [$$]
R -> ab.T [ab]
T -> .aT [$$] // Shift on aa, ac, a$
T -> .c [$$] // Shift on c$
T -> . [$$] // Reduce on $$
T -> .aT [ab] // Shift on aa, ac
T -> .c [ab] // Shift on ca
T -> . [ab] // Reduce on ab
幸运的是,这里没有移位/归约冲突,因为每个两个字符的前瞻都告诉我们要做不同的事情。
在LR(k > 1)分析中出现的向前查看点来确定何时进行移位是LR(2)分析中的新内容,但在LR(1)分析中没有。在LR(1)分析中,我们只需要查看点后面的终端符号。在LR(2)分析中,由于需要更多字符来确定要做什么,我们必须为每个移位项计算一个二级点前瞻。具体而言,点前瞻如下确定:
对于产生式A → α.tω [γ],其中t是终端符号,在tωγ可推导出字符串的开头处可以出现长度为2的所有字符串的集合就是点前瞻。换句话说,产生式A → α.tω [γ]的点前瞻等于FIRST2(tωγ)。
考虑到所有这些,我们可以构建完整的LR(2)分析器并描述与每个状态相关联的操作。整个LR(2)分析器如下所示:
(1)
S' -> .S [$$] // Go to 10
S -> .R [$$] // Go to 8
S -> .RS [$$] // Go to 8
R -> .abT [$$] // Shift on ab, go to (2)
R -> .abT [ab] // Shift on ab, go to (2)
(2)
R -> a.bT [$$] // Shift on ba, bc, b$, go to (3)
R -> a.bT [ab] // Shift on ba, bc, go to (3)
(3)
R -> ab.T [$$] // Go to 7
R -> ab.T [ab] // Go to 7
T -> .aT [$$] // Shift on aa, ac, a$, go to (4)
T -> .c [$$] // Shift on c$, go to (5)
T -> . [$$] // Reduce on $$
T -> .aT [ab] // Shift on aa, ac, go to (4)
T -> .c [ab] // Shift on ca, go to (5)
T -> . [ab] // Reduce on ab
(4)
T -> a.T [$$] // Go to 6
T -> a.T [ab] // Go to 6
T -> . [$$] // Reduce on $$
T -> .aT [$$] // Shift on aa, ac, a$, go to (4)
T -> .c [$$] // Shift on c$, go to (5)
T -> . [ab] // Reduce on ab
T -> .aT [ab] // Shift on aa, ac, go to (4)
T -> .c [ab] // Shift on ca, go to (5)
(5)
T -> c. [$$] // Reduce on $$
T -> c. [ab] // Reduce on ab
(6)
T -> aT. [$$] // Reduce on $$
T -> aT. [ab] // Reduce on ab
(7)
R -> abT. [$$] // Reduce on $$
R -> abT. [ab] // Reduce on ab
(8)
S -> R. [$$] // Reduce on $$
S -> R.S [$$] // Go to 9
S -> .RS [$$] // Go to 8
S -> .R [$$] // Go to 8
R -> .abT [$$] // Shift on ab, go to (2)
R -> .abT [ab] // Shift on ab, go to (2)
(9)
S -> RS. [$$] // Reduce on $$
(10)
S' -> S. [$$] // Accept on $$
state | aa | ab | ac | a$ | ba | bb | bc | b$ | ca | cb | cc | c$ | $$
------+----+----+----+----+----+----+----+----+----+----+----+----+----
1 | | S | | | | | | | | | | |
------+----+----+----+----+----+----+----+----+----+----+----+----+----
2 | | | | | S | | S | S | | | | |
------+----+----+----+----+----+----+----+----+----+----+----+----+----
3 | S | R | S | S | | | | | S | | | S | R
------+----+----+----+----+----+----+----+----+----+----+----+----+----
4 | S | R | S | S | | | | | S | | | S | R
------+----+----+----+----+----+----+----+----+----+----+----+----+----
5 | | R | | | | | | | | | | | R
------+----+----+----+----+----+----+----+----+----+----+----+----+----
6 | | R | | | | | | | | | | | R
------+----+----+----+----+----+----+----+----+----+----+----+----+----
7 | | R | | | | | | | | | | | R
------+----+----+----+----+----+----+----+----+----+----+----+----+----
8 | | S | | | | | | | | | | | R
------+----+----+----+----+----+----+----+----+----+----+----+----+----
9 | | | | | | | | | | | | | R
------+----+----+----+----+----+----+----+----+----+----+----+----+----
10 | | | | | | | | | | | | | A
正如您所看到的,这里的每个条目只是说明是要移位还是归约。在实践中,每个归约项都会标记为实际进行归约的产生式,但是,呃,我懒得打出来。
在LR(1)分析表中,您通常会将此表与“goto”表结合使用,以说明在看到每个符号后要转移到哪里。这是由于一个幸运的巧合。在LR(1)解析器中,向前看的大小为1,这恰好与goto表所说的在看到下一个字符后应该转换到哪里的事实相一致。在LR(2)解析器中,关于是移位还是归约的决策取决于两个字符的前瞻,但选择读入另一个字符后去哪里的决定仅取决于单个字符。也就是说,即使您有两个记号的前瞻来决定是否执行某个操作,您每次只能移动一个字符。这意味着LR(2)解析器的goto表看起来很像LR(0)或LR(1)解析器的goto表,并且在此处显示:
state | a | b | c | $ | S | R | T
-------+-----+-----+-----+-----+-----+-----+-----
1 | 2 | | | | 10 | 8 |
-------+-----+-----+-----+-----+-----+-----+-----
2 | | 3 | | | | |
-------+-----+-----+-----+-----+-----+-----+-----
3 | 4 | | 5 | | | | 7
-------+-----+-----+-----+-----+-----+-----+-----
4 | 4 | | 5 | | | | 6
-------+-----+-----+-----+-----+-----+-----+-----
5 | | | | | | |
-------+-----+-----+-----+-----+-----+-----+-----
6 | | | | | | |
-------+-----+-----+-----+-----+-----+-----+-----
7 | | | | | | |
-------+-----+-----+-----+-----+-----+-----+-----
8 | 2 | | | | 9 | 8 |
-------+-----+-----+-----+-----+-----+-----+-----
9 | | | | | | |
-------+-----+-----+-----+-----+-----+-----+-----
10 | | | | | | |
因此,总结一下:
有趣的是,一旦你知道如何构建LR(2)解析器,你就可以推广这个想法,为任何k≥2构建LR(k)解析器。特别是,所有出现在此处的“新惊喜”都是您将看到的更大值的k的相同惊喜。
实际上,由于动作表的大小以及它们通常比LR(1)解析器具有更多状态,因此很少使用LR(2)解析器。但是,我认为了解它们的工作原理仍然是值得的。这让您对LR解析的工作方式有了更普遍的了解。
希望这有所帮助!
非常感谢@AProgrammer在cs.stackexchange.com上的答案,关于点先行符和项先行符的区别,这帮助我更好地理解了点先行符的功能和目的!
如果您想阅读有关LR(k)分析的原始论文,其中详细规定了LR(k)分析的全部规则,请查看唐纳德·克努斯(Don Knuth)的《从左到右翻译语言》。