Algorithm 什么是LR(2)解析器?它与LR(1)解析器有何不同?

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(1)解析器,它通常在传统的编译器课程中教授。我知道LR(2)解析器是存在的,但我以前从未见过构造过

LR(2)解析器是如何构造的?它与LR(1)解析器有何不同?

在许多方面,LR(2)解析器的工作方式与LR(1)解析器类似。它反向追踪最右边的派生,维护堆栈,在堆栈上执行shift和reduce操作,具有由LR项集组成的状态,等等。但是,有几个主要区别:

  • LR(2)解析器为每个LR项维护两个先行标记,而不是像LR(1)中那样只维护一个先行标记
  • 移位如何工作的规则不同于LR(1)解析器的标准规则,并且需要一个称为点先行的额外的先行概念,该概念在LR(1)解析器中不存在
  • LR(2)解析器的操作表的宽度远大于LR(1)解析器的操作表的宽度,尽管与直觉相反,goto表的宽度是相同的
为了说明这是如何工作的,让我们以LR(2)语法为例。(此语法源自中提到的语法)

→ RS | R

R→ abT

T→ aT | c |ε

为了构建这个语法的LR(2)解析器,我们将像往常一样,通过生成形式的→ S:

是的→

→ RS | R

R→ abT

T→ aT | c |ε

现在,我们开始生成配置集。与LR(1)解析器一样,我们从产品的→ 如图所示:

(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]
到目前为止还不错。现在如果我们在这里看到a
b
会发生什么?这将把我们带到这里:

(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。