【问题标题】:What is an LR(2) parser? How does it differ from an LR(1) parser?什么是 LR(2) 解析器?它与 LR(1) 解析器有何不同?
【发布时间】:2026-01-27 10:15:02
【问题描述】:

我熟悉 LR(1) 解析器,它们通常在传统的编译器课程中教授。我知道存在 LR(2) 解析器,但我之前从未见过构造器。

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

【问题讨论】:

    标签: algorithm parsing lr


    【解决方案1】:

    在许多方面,LR(2) 解析器的工作方式类似于 LR(1) 解析器。它反向追踪最右边的推导,维护堆栈,在堆栈上执行移位和归约操作,具有由 LR 项集组成的状态等。但是,有一些主要区别:

    • LR(2) 解析器为每个 LR 项维护两个前瞻标记,而不是像 LR(1) 中那样只保留一个前瞻标记。
    • 移位工作的规则与 LR(1) 解析器的标准规则不同,需要一个额外的前瞻概念,称为 dot lookahead,这在 LR(1) 解析器中不存在。李>
    • LR(2) 解析器的操作表的宽度远大于 LR(1) 解析器的宽度,但与直觉相反,goto 表的宽度相同。

    为了说明这是如何工作的,让我们举一个 LR(2) 语法的例子。 (这个语法来源于@rici's excellent answer to this earlier question中提到的一个)。

    S → RS |回复

    R → abT

    T → aT | c | ε

    要为这个语法构建我们的 LR(2) 解析器,我们将像往常一样,通过使用 S' → S 形式的产生式来扩充语法开始:

    S' → S

    S → RS |回复

    R → abT

    T → aT | c | ε

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

    (1)
        S' -> .S  [$$]
    

    请注意,前瞻是 $$,表示“流结束”标记的两个副本。在传统的 LR(1)(或 SLR(1) 或 LALR(1))解析器中,我们会在此处预读 $,这只是流结束标记的一个副本。

    我们现在开始扩展此配置集中的其他项目。由于我们有产生式 S → RS 和 S → R,我们添加这些项目:

    (1)
        S' -> .S  [$$]
        S  -> .R  [$$]  // New
        S  -> .RS [$$]  // New
    

    现在,让我们开始追查接下来会发生什么。就像在 LR(1) 解析器中一样,由于这里的非终结符 R 之前有点,我们需要将它们展开。就像在 LR(1) 解析中一样,当我们这样做时,我们需要确定使用什么前瞻。我们将首先扩展 S -> .R [$$] 项目,如下所示:

    (1)
        S' -> .S   [$$]
        S  -> .R   [$$]
        S  -> .RS  [$$]
        R  -> .abT [$$]  // New
    

    接下来,让我们扩展S -> .RS [$$] 选项。这是一个有趣的案例。我们需要确定这里发现的 R 产品的前瞻是什么。在 LR(1) 解析器中,这是通过查看产生式其余部分的第一个集合来找到的。在 LR(2) 解析器中,因为我们有两个前瞻标记,我们必须查看 FIRST2,它是 FIRST 集的泛化,列出了长度为 2 的字符串可以出现在产生式的前面,而不是长度为 1 的字符串可以出现在那里。在我们的例子中,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
    

    这填写了这个配置集。这是我们第一次发现一些完整的 reduce 项目(这里,T -> . 带有两个不同的前瞻)。我们这里也有一些轮班项目。所以我们不得不问 - 我们这里有移位/归约冲突还是归约/归约冲突?

    让我们从减少/减少冲突开始。与 LR(1) 解析中的情况一样,当有两个不同的 reduce 项(末尾带有点的项)具有相同的前瞻时,我们就会遇到 reduce/reduce 冲突。在这里,我们有两个不同的 reduce 项目,但它们有不同的前瞻。这意味着我们在减少/减少方面很好。

    现在,有趣的案例。我们这里有任何移位/减少冲突吗?这是与 LR(1) 解析相比有所改变的地方。与 LR(1) 解析中的情况一样,我们查看集合中的所有 shift 项(终端前带有点的项)和集合中的所有 reduce 项(末尾带有点的项)。我们正在查看是否存在任何冲突:

        T  -> .aT  [$$] // Shift
        T  -> .c   [$$] // Shift
        T  -> .    [$$] // Reduce
        T  -> .aT  [ab] // Shift
        T  -> .c   [ab] // Shift
        T  -> .    [ab] // Reduce
    

    不过,问题是这里的移位/减少冲突是什么样的。在 LR(2) 解析器中,我们有两个前瞻标记,我们基于这些标记来决定是移位还是归约。在减少项目的情况下,很容易看出两个前瞻标记将引导我们减少什么 - 它是括号中的两个字符前瞻。另一方面,考虑班次项目T -> .c [ab]。我们要转移的两个字符的前瞻是什么?对于 LR(1) 解析器,我们只会说“哦,点在 c 之前,所以我们转移到 c”,但这还不够。相反,我们会说与这个班次项目相关的前瞻是cac 来自生产本身,a 来自项目前瞻的第一个字符。

    同样,考虑换档项目T -> .aT [$$]。我们需要两个前瞻字符,我们可以很容易地看到其中一个(点后的a)。要获得第二个,我们必须看看T 能够生产什么。 T 有三种产生式:一种用 ε 代替 T,一种用 aT 代替 T,另一种用 c 代替 T。这意味着可以从 T 派生的任何字符串都以ac 开头,或者是空字符串。结果,T -> .aT [$$] 项目告诉我们在看到 acaa(我们从 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
    

    幸运的是,这里没有 shift/reduce 冲突,因为每个两个字符的前瞻都告诉我们做一些不同的事情。

    查看点来确定何时移位是 LR(2) 解析的新内容,它出现在 LR(k > 1) 解析中,但不是 LR(1) 解析。在 LR(1) 解析中,我们只需要查看点之后的终端。在 LR(2) 解析中,由于我们需要更多字符来确定要执行的操作,因此我们必须为每个移位项计算辅助 dot lookahead。具体来说,点前瞻的确定方式如下:

    在产生式 A → α.tω [γ] 中,其中 t 是终结符,点前瞻是所有长度为 2 的字符串的集合,这些字符串可以出现在可从 tωγ 导出的字符串的开头。换句话说,产生式 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 $$
    

    所以现在我们有了这个语法的 LR(2) 解析器。现在剩下要做的就是按照我们为 LR(1) 解析器所做的工作,将其编码为一个动作和转到表。

    正如您所料,LR(2) 解析器的操作表与 LR(1) 解析器的操作表不同,因为它由两个字符而不是一个字符给出的前瞻来键入。这意味着 LR(2) 解析器的操作表将比 LR(1) 解析器大得多。这是这里的样子:

     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
    

    如您所见,这里的每个条目都只是说明是移位还是减少。在实践中,每个 reduce 项目都会被标记为您实际上要为哪个生产进行归约,但是,嗯,我懒得输入。

    在 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) 解析器对每个 LR 项使用两个前瞻标记。这意味着我们需要使用 FIRST2 个集合而不是 FIRST 集合,并且在确定是移位还是减少时引入了一些新的复杂性。
    • LR(2) 解析具有点前瞻。对于每个移位项,我们使用 FIRST2 集来确定哪些字符可以合法地跟随我们所在的点,并在其中任何一个上移位。
    • LR(2) 操作表以字符对而非单个字符为键,但 goto 表仍以字符为键。

    有趣的是,一旦您知道如何构建 LR(2) 解析器,您就可以概括为任何 k ≥ 2 构建 LR(k) 解析器的想法。特别是,这里出现的所有“新惊喜”都是与您在 k 值越来越大时看到的相同。

    在实践中,LR(2) 解析器很少使用,因为它们的操作表的大小以及由于增加的前瞻,它们通常比 LR(1) 解析器具有更多的状态。但是,恕我直言,看看它们如何工作仍然是值得的。它让您了解 LR 解析的工作原理。

    希望这会有所帮助!


    非常感谢 @AProgrammer's answer on cs.stackexchange.com 关于点前瞻与项目前瞻,这帮助我更好地了解点前瞻的功能及其用途!

    如果您想阅读关于 LR(k) 解析的原始论文,其中详细说明了 LR(k) 解析的完整规则,请查看“On the Translation of Languages from Left to对”,Don Knuth。

    【讨论】: