yacc shift-reduce 用于不明确的 lambda 语法

yacc shift-reduce for ambiguous lambda syntax

我正在用 Yacc(与 Go 打包在一起的语言)为一种玩具语言编写语法,由于以下伪问题,我有一个预期的 shift-reduce 冲突。我必须将问题语法提炼为以下内容。

start:
  stmt_list

expr:
  INT | IDENT | lambda | '(' expr ')' { $$ =  }

lambda:
  '(' params ')' '{' stmt_list '}'

params:
  expr | params ',' expr

stmt:
  /* empty */ | expr

stmt_list:
  stmt | stmt_list ';' stmt

lambda 函数看起来像这样:

map((v) { v * 2 }, collection)

我的解析器发出:

conflicts: 1 shift/reduce

给定输入:

(a)

它根据 '(' expr ')' 规则正确解析了 expr。但是给定输入:

(a) { a }

(这将是身份函数的 lambda,返回其输入)。我得到:

syntax error: unexpected '{'

这是因为当读取 (a) 时,解析器选择将其缩减为 '(' expr ')',而不是将其视为 '(' params ')'。鉴于此冲突是移位减少而不是减少减少,我假设这是可以解决的。我只是不知道如何构建语法来支持这种语法。

编辑 |这很丑陋,但我正在考虑定义一个标记,以便词法分析器可以识别 ')' '{' 序列并将其作为单个标记发送以解决此问题。

编辑 2 |实际上,更好的是,我会让 lambda 在语法中需要像 ->(a, b) { a * b} 这样的语法,但是让词法分析器发出 -> 而不是在实际的源代码中。

您的分析确实正确;虽然语法没有歧义,但解析器不可能决定将输入减少到 ( <expr> 并使用前瞻性 ) 是否应该将 expr 减少到 params 在移动 ) 之前,或者 ) 是否应该作为 lambda 的一部分移动。如果下一个标记可见,则可以做出决定,因此语法 LR(2) 超出了 go/yacc.

的权限

如果您使用的是 bison,您可以通过请求 GLR 解析器轻松解决此问题,但我不相信 go/yacc 提供该功能。

该语言有一个 LR(1) 文法(对于 k 的任何值,总是有一个 LR(1) 文法对应于任何 LR(k) 文法)但是它很烦人手写。 LR(k) 到 LR(1) 转换的基本思想是通过将上下文的 k-1 个标记累积到每个生产中来将减少决策 k-1 个标记前移。所以在 k 为 2 的情况下,每个产生式 P: N → α 将被替换为产生式 <sup>T</sup>N<sup>U </sup> → <sup>T</sup>α U 对于 FIRST(α) 中的每个 TFOLLOW(N) 中的每个 U . [见注释 1] 这会导致在任何非平凡语法中大量使用非终结符。

与其追求那个想法,不如让我提出两个更简单的解决方案,这两个方案您似乎都非常接近。

首先,在您提供的语法中,问题实际上只是当两个标记为 ){。这可以很容易地在词法分析器中检测到,并导致一个解决方案仍然是 hacky 但更简单的 hack:Return ){ 作为单个标记。您需要处理中间的空格等,但不需要在词法分析器中保留任何上下文。这有额外的好处,您不需要将 params 定义为 expr 的列表;它们可以只是 IDENT 的列表(如果相关;评论表明它不是)。

我认为更简洁的替代方案是扩展您似乎已经提出的解决方案:接受太多并拒绝语义操作中的错误。在这种情况下,您可以这样做:

start:
  stmt_list

expr:
    INT
  | IDENT
  | lambda
  | '(' expr_list ')'
        { // If  has more than one expr, report error
          $$ = 
        }

lambda:
  '(' expr_list ')' '{' stmt_list '}'
        { // If anything in expr_list is not a valid param, report error
          $$ = make_lambda(, )
        }

expr_list:
  expr | expr_list ',' expr

stmt:
  /* empty */ | expr

stmt_list:
  stmt | stmt_list ';' stmt

备注

  1. 这只是一个大纲;完整的算法包括恢复原始解析树的机制。如果 k 大于 2,则 TUFIRST<sub>k-1</sub> 的字符串和 FOLLOW<sub>k-1</sub> 组。

如果确实是移位-归约冲突,而您只需要移位行为,您的解析器生成器可能会为您提供一种方法,让您更喜欢移位与归约。这就是 "if-then-stmt" 和 "if-then-stmt-else-stmt" 的语法规则冲突的经典解决方式,当时 if 语句也可以是语句。

http://www.gnu.org/software/bison/manual/html_node/Shift_002fReduce.html

您可以通过两种方式获得此效果: a) 依靠解析引擎的偶然行为。 如果 LALR 解析器首先处理移位,然后在没有移位的情况下处理缩减,那么您将免费获得此 "prefer shift"。无论如何,解析器生成器所要做的就是构建解析表,即使检测到冲突也是如此。 b) 加强意外行为。设计(或获得)解析器生成器以接受 "prefer shift on token T"。然后可以抑制歧义。仍然需要像 a) 中那样实现解析引擎,但这很容易。

我认为这 easier/cleaner 而不是滥用词法分析器来制作奇怪的标记(而且这并不总是有效)。

显然,您可以偏好减少,以相反的方式进行调整。通过一些额外的黑客攻击,您可以使 shift-vs-reduce 特定于发生冲突的状态;您甚至可以使其特定于一对相互冲突的规则,但现在解析引擎需要为非终结符保留偏好数据。那还是不难。最后,您可以为每个非终结符添加一个谓词,当即将发生 shift-reduce 冲突时调用它,并让它提供一个决定。

重点是您不必接受 "pure" LALR 解析;如果您愿意稍微修改解析器 generator/engine,您可以通过多种方式轻松地改变它。这为了解这些工具的工作原理提供了一个很好的理由;然后你可以滥用它们来为你谋利。