Algorithm 什么是LR(2)解析器?它与LR(1)解析器有何不同?
我熟悉LR(1)解析器,它通常在传统的编译器课程中教授。我知道LR(2)解析器是存在的,但我以前从未见过构造过 LR(2)解析器是如何构造的?它与LR(1)解析器有何不同?在许多方面,LR(2)解析器的工作方式与LR(1)解析器类似。它反向追踪最右边的派生,维护堆栈,在堆栈上执行shift和reduce操作,具有由LR项集组成的状态,等等。但是,有几个主要区别:Algorithm 什么是LR(2)解析器?它与LR(1)解析器有何不同?,algorithm,parsing,lr,Algorithm,Parsing,Lr,我熟悉LR(1)解析器,它通常在传统的编译器课程中教授。我知道LR(2)解析器是存在的,但我以前从未见过构造过 LR(2)解析器是如何构造的?它与LR(1)解析器有何不同?在许多方面,LR(2)解析器的工作方式与LR(1)解析器类似。它反向追踪最右边的派生,维护堆栈,在堆栈上执行shift和reduce操作,具有由LR项集组成的状态,等等。但是,有几个主要区别: LR(2)解析器为每个LR项维护两个先行标记,而不是像LR(1)中那样只维护一个先行标记 移位如何工作的规则不同于LR(1)解析器的
- LR(2)解析器为每个LR项维护两个先行标记,而不是像LR(1)中那样只维护一个先行标记
- 移位如何工作的规则不同于LR(1)解析器的标准规则,并且需要一个称为点先行的额外的先行概念,该概念在LR(1)解析器中不存在
- LR(2)解析器的操作表的宽度远大于LR(1)解析器的操作表的宽度,尽管与直觉相反,goto表的宽度是相同的
(1)
S' -> .S [$$]
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 | | | | | | |
注意这里的lookahead是$$,表示“流结束”标记的两个副本。在传统的LR(1)(或SLR(1)或LALR(1))解析器中,我们在这里有一个$的前瞻,它只是流结束标记的一个副本
现在,我们开始扩展此配置集中的其他项。因为我们有一个→ RS和S→ R、 我们添加了以下项目:
(1)
S' -> .S [$$]
S -> .R [$$] // New
S -> .RS [$$] // New
现在,让我们开始追踪接下来会发生什么。就像在LR(1)解析器中一样,因为在非终结符R之前有点,所以我们需要将它们展开。正如在LR(1)解析中一样,在这样做时,我们需要确定要使用的lookahead。我们将首先展开S->.R[$]
项,如下所示:
(1)
S' -> .S [$$]
S -> .R [$$]
S -> .RS [$$]
R -> .abT [$$] // New
(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 $$
接下来,让我们展开s->.RS[$]
选项。这是一个有趣的案例。我们需要确定这里发现的R产品的前瞻性。在LR(1)解析器中,这是通过查看产品剩余部分的第一组来发现的。在LR(2)解析器中,因为我们有两个先行标记,所以我们必须查看FIRST2集合,它是第一个集合的泛化,列出了可以出现在产品前面的长度为2的字符串,而不是可以出现在产品前面的长度为1的字符串。在我们的例子中,FIRST2(S)={ab}(你明白为什么了吗?),所以我们有以下内容:
(1)
S' -> .S [$$]
S -> .R [$$]
S -> .RS [$$]
R -> .abT [$$]
R -> .abT [ab] // New
(3)
R -> ab.T [$$]
R -> ab.T [ab]
T -> .aT [$$] // New
T -> .c [$$] // New
T -> . [$$] // New
现在,我们已经完成了第一个配置集的扩展。现在是时候考虑一下,如果我们接下来看到不同的角色,我们会怎么做。幸运的是,在这种情况下,这相当容易,因为该语法生成的任何字符串的第一个字符必须是a
。那么让我们看看如果遇到a
,会发生什么:
(2)
R -> a.bT [$$]
R -> a.bT [ab]
到目前为止还不错。现在如果我们在这里看到ab
会发生什么?这将把我们带到这里:
(3)
R -> ab.T [$$]
R -> ab.T [ab]
这里有两个LR(2)项在非终结符之前有点,所以我们需要将它们展开。让我们首先为R->ab.T[$]
展开这些函数,给出以下内容:
(1)
S' -> .S [$$]
S -> .R [$$]
S -> .RS [$$]
R -> .abT [$$]
R -> .abT [ab] // New
(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
这将填充此配置集。这是我们第一次发现一些完整的reduce项(这里,T->.
有两个不同的lookahead)。我们这里还有一些轮班项目。因此,我们必须问——我们这里有一个转移/减少冲突还是一个减少/减少冲突
让我们从减少/减少冲突开始。与LR(1)解析中的情况一样,当两个不同的reduce项(末尾带有点的项)具有相同的lookahead时,我们会遇到reduce/reduce冲突。在这里,我们有两个不同的reduce项,但它们的外观不同。这意味着我们在reduce/reduce方面做得很好
现在,有趣的案例。我们是否有任何转移/减少冲突?这就是LR(1)解析的一些变化。与LR(1)解析中的情况一样,我们查看集合中的所有移位项(终端前有点的项)和集合中的所有reduce项(末尾有点的项)。我们正在查看是否存在任何冲突:
T -> .aT [$$] // Shift
T -> .c [$$] // Shift
T -> . [$$] // Reduce
T -> .aT [ab] // Shift
T -> .c [ab] // Shift
T -> . [ab] // Reduce
然而,问题是这里的转移/减少冲突是什么样子的。在LR(2)解析器中,我们有两个前瞻标记,我们根据它们来决定是移位还是缩减。在reduce项的情况下,很容易看出两个lookahead标记将引导我们进行reduce-括号中是两个字符的lookahead。另一方面,考虑移位项<代码> t> > c[ab] < /代码>。在这里,我们要转换的两个字符的前瞻是什么?在LR(1)解析器的情况下,我们只会说“哦,点在c
之前,所以我们切换到c
”,但这还不够。相反,我们会说与这个移位项目相关联的先行是ca
,其中c
来自生产本身,a
来自项目先行的第一个字符
同样,考虑移位项<代码> T->。在[$$] < /代码>。我们需要两个前视字符,我们可以很容易地看到其中一个(点后的a
)。为了得到第二个,我们必须看看T
能够产生什么。T有三个结果:一个用ε代替T,一个用aT代替T,还有一个用c代替T。