【问题标题】:How can I manage mutual recursion, retaining associativity rules?如何管理相互递归,保留关联性规则?
【发布时间】:2018-10-05 14:04:30
【问题描述】:

总的问题是:

在允许任意嵌套的expr := '(' expr ')' => expr | expr_without_short_closureexpr_without_short_closure := [expr_without_short_closure => expr] | yield expr_without_short_closure => expr | expr_without_short_closure 'or' expr | '(' expr ')' 的同时仍允许像expr_without_short_closure 'or' expr 这样的低优先级左关联运算符,我的语法看起来应该如何?


目前 LALR(1) 野牛语法的结构如下(呈现实际语法文件的一个独立部分,稍微简化):

%left ','
%left T_LOGICAL_OR /* or */
%right T_YIELD
%right T_DOUBLE_ARROW /* => */
%left '+'

expr: /* entry point as well */
                expr_without_short_closure %prec ',' { $$ = $1; }
        |       expr_with_short_closure { $$ = $1; }
;

expr_with_short_closure:
                short_closure
        |       T_YIELD expr_without_short_closure T_DOUBLE_ARROW expr_with_short_closure { $$ = zend_ast_create(ZEND_AST_YIELD, $4, $2); }
;

short_closure:
                T_IDENTIFIER T_DOUBLE_ARROW expr { /* ... */ }
        |       '(' expr ')' T_DOUBLE_ARROW expr { /* ... */ }
;

expr_without_short_closure:
                T_IDENTIFIER %prec T_DOUBLE_ARROW { $$ = $1; }
        |       '(' expr ')' %prec T_DOUBLE_ARROW { $$ = $2; }
        |       T_YIELD expr_without_short_closure { $$ = zend_ast_create(ZEND_AST_YIELD, $2, NULL); }
        |       '[' array_pair_list ']' { $$ = $2; }
        |       T_YIELD expr_without_short_closure T_DOUBLE_ARROW expr_without_short_closure { $$ = zend_ast_create(ZEND_AST_YIELD, $4, $2); }
        |       expr_without_short_closure T_LOGICAL_OR expr_without_short_closure       { $$ = zend_ast_create_binary_op(ZEND_AST_OR, $1, $3); }
        |       expr_without_short_closure '+' expr_without_short_closure       { $$ = zend_ast_create_binary_op(ZEND_ADD, $1, $3); }
/*      | and about thirty similar alternate rules like the previous one */
;

non_empty_array_pair_list:
                non_empty_array_pair_list ',' array_pair { $$ = zend_ast_list_add($1, $3); }
        |       array_pair { $$ = zend_ast_create_list(1, ZEND_AST_ARRAY, $1); }
;

array_pair:
                expr_without_short_closure T_DOUBLE_ARROW expr
                        { $$ = zend_ast_create(ZEND_AST_ARRAY_ELEM, $3, $1); }
        |       expr_without_short_closure
                        { $$ = zend_ast_create(ZEND_AST_ARRAY_ELEM, $1, NULL); }
;

基本上我是在尝试引入“箭头函数”,左侧包含一个参数列表,右侧包含一个任意表达式,中间是 T_DOUBLE_ARROW。

现在的挑战是 T_DOUBLE_ARROW 令牌已在两个地方使用,即array_pair 规则中的expr_without_short_closure T_DOUBLE_ARROW expr 交替和expr_without_short_closure 中的T_YIELD expr_without_short_closure T_DOUBLE_ARROW expr_without_short_closure

这种当前的语法类型有效,但它(显然)无法解析例如:

[T_YIELD T_IDENTIFIER => T_IDENTIFIER => T_IDENTIFIER + T_IDENTIFIER => T_YIELD T_IDENTIFIER]
// must be grouped as:
[(T_YIELD T_IDENTIFIER => T_IDENTIFIER) => (T_IDENTIFIER + (T_IDENTIFIER => (T_YIELD T_IDENTIFIER)))]

在这种情况下,expr_without_short_closure '+' expr_without_short_closure 替代方案失败,因为它只接受右侧的expr_without_short_closure,显然不允许 short_closure 那里。

但是,我不能简单地将expr_without_short_closure 替换为expr,因为这与expr_without_short_closure T_DOUBLE_ARROW expr 表达式(array_pair 规则)或T_YIELD expr_without_short_closure T_DOUBLE_ARROW expr_without_short_closure 表达式(expr_without_short_closure 规则)冲突。

现在,我可以尝试将expr 仅放在表达式的右侧。这很好,除了左关联操作。现在,突然T_IDENTIFIER + T_IDENTIFIER T_LOGICAL_OR T_IDENTIFIER 被分组为T_IDENTIFIER + (T_IDENTIFIER T_LOGICAL_OR T_IDENTIFIER),而不是所需的(T_IDENTIFIER + T_IDENTIFIER) T_LOGICAL_OR T_IDENTIFIER。 (为什么?)

我也想避免expr_without_short_closure %prec ',' 中的%precexpr 规则)。出于某种原因,它是必需的(删除它会导致expr_without_short_closure 中每条规则的移位/减少冲突),我想这也是我的问题的根源,虽然我不太明白为什么(搜索产生的答案,如“关联性规则不通过间接”-但我真的不知道如何在这里避免间接)。


我试图让问题保持独立,但万一我遗漏了什么 - 实际的语法文件可以在 https://github.com/bwoebi/php-src/blob/0d98d8060bde88ac2e5904cb55ecb13d15316053/Zend/zend_language_parser.y#L898 找到 - 我认为很明显,人们并不想复制所有规则expr_without_short_closure 转换为 expr_with_short_closure(我什至不确定这是否真的有助于低优先级左关联运算符)。

【问题讨论】:

  • 您能否澄清“数组列表中的yield”示例中“必须分组为”的含义?我的印象是 php 会将[yield $a => 2 + 2] 解析为产生一个键/值对,然后创建一个数组列表,其中包含发送到生成器的值。当您写[(T_YIELD T_IDENTIFIER) => (T_IDENTIFIER + (T_IDENTIFIER => (T_YIELD T_IDENTIFIER)))] 时,我将其解释为yield 仅产生第一个标识符的值,而第一个粗箭头是数组列表语法的一部分,在我看来这不是现有的解析。
  • @rici 你是对的,这将被分组为[(T_YIELD T_IDENTIFIER => (T_IDENTIFIER + (T_IDENTIFIER => (T_YIELD T_IDENTIFIER))))]。我一定是把例子简化得太多了,试试[T_YIELD T_IDENTIFIER => T_IDENTIFIER => T_IDENTIFIER + T_IDENTIFIER => T_YIELD T_IDENTIFIER][(T_YIELD T_IDENTIFIER => T_IDENTIFIER) => (T_IDENTIFIER + (T_IDENTIFIER => (T_YIELD T_IDENTIFIER)))]——我想强调的是,最左边yield 的右手部分不能被解析为“箭头函数”。

标签: bison yacc


【解决方案1】:

我怀疑您将不得不在不过多依赖优先级声明的情况下执行此操作。但我还没有完全放弃希望 :-) 所以我将首先阐述优先级是如何工作的,通过展示它为什么不能以你尝试使用它的方式工作。

1。优先级比较总是在 ruletoken 之间进行

关于优先规则的基本准则非常简单:优先比较总是涉及一个规则(或产生式,如expr: expr '+' expr)和一个传入的令牌,称为前瞻令牌。没有例外。尽管声明优先级的形式使它看起来像是两个令牌之间的比较,但这是一种方便的虚构,使其在常见情况下使用起来更方便。但现实是,正如我之前所说(并且值得重复):优先比较总是在 ruletoken 之间进行。

要了解这意味着什么,了解 LR 解析算法的本质很有用。 LR 解析器是一个有限状态下推自动机,这意味着它是一个普通的有限状态机,增加了一个堆栈。在 LR 解析的情况下,堆栈完全由状态 ID 组成。自动机的一个状态对应一组“项目”;项目由生产规则和生产规则中的位置组成。实际上,一个状态代表一组可能的解析位置,所有这些位置都在并行探索。

每次解析器进行普通状态转换(其中读取输入 token 并使用规则移动到下一个状态),目标状态也被压入堆栈。这称为“移位”转换,因为输入标记被移位到解析器上。仅当状态的项目集中存在一个或多个项目,其中前瞻标记是紧随该位置的终端或可以启动紧随该位置的非终端的标记之一时,才会发生这种情况。

但还有另一种过渡:“还原”过渡。归约转换是解析器识别生产规则已被识别的方式。 (之所以称为归约,是因为它归约产生式的右侧,方法是用其左侧的非终结符替换它。)为了执行这种归约,自动机做了两件事:首先,它为规则右侧的每个符号从堆栈中弹出一个状态。其次,它通过使用非终结符的转换规则来移动非终结符(并且,与移位一样,将生成的状态 ID 压入堆栈。)

虽然减少转换不吸收前瞻标记,但它确实将其考虑在内。为了使归约转换可行,必须可以在归约(或多个归约,因为可能不止一个)之后移动前瞻令牌。这些前瞻集是在构建解析自动机期间计算的;状态机都是静态的。

因此,移位转换对应于尚无法识别任何右侧的决定,而归约转换对应于已识别某些生产的决定。

有时会发生移位和归约都可用的情况:也就是说,解析器状态处于某个产生式的末尾,但它也处于另一个产生式中的某个点,其中前瞻令牌是其中之一可能的下一个令牌。

这被称为“shift-reduce 冲突”,因为 shift 和 reduce 都是可能的。为了解决这个冲突,解析器生成器(不是解析器)从状态的转换表中消除了一个转换。如果没有适用的优先关系,则消除 reduce 动作。 (换句话说,解析器更喜欢移位。)但是如果配置的优先级可用,则解析器生成器通过将可用缩减的优先级与前瞻标记的优先级进行比较来使用它。以较大者为准(通过关联性解决关系)。

如果您使用最新版本的 bison 并提供 --report=all 选项,您实际上可以看到优先规则在起作用,该选项比 -v 选项显示更多信息。 (在这两种情况下,报告都会写入<filename>.output,除非您提供自定义报告文件名。)我鼓励您这样做。

2。优先级不继承

优先级决策的静态性质的一个结果是优先级不是通过归约继承的。我们可以通过一个非常简单的例子看出这一点。

我们从这个琐碎的语法开始:

%token NUMBER
%left '+'
%%
expr: NUMBER
    | expr '+' expr

这导致机器具有五种状态,其中最后一种特别令人感兴趣:(摘自bison --report=all precedence.y之后的文件precedence.output

State 5

    2 expr: expr . '+' expr
    2     | expr '+' expr .  [$end, '+']

    $default  reduce using rule 2 (expr)

    Conflict between rule 2 and token '+' resolved as reduce (%left '+').

我们在这里看到的是解析器已经达到了一个状态,可以通过移动+来推进.(代表解析进度),或者等到减少expr '+' expr之后。因为加法是左结合的,所以减法是正确的;这会导致2 + 3 · + 4立即减少为expr · + 4,相当于说解析有效为(2 + 3) + 4

现在,让我们添加一个间接级别:

%token NUMBER
%left '+'
%%
expr : NUMBER
     | left '+' right
left : expr
right: expr 

在新机中,State 5 有点不同:

State 5

    1 expr: . NUMBER
    2     | . left '+' right
    2     | left '+' . right
    3 left: . expr
    4 right: . expr

    NUMBER  shift, and go to state 1

    expr   go to state 6
    left   go to state 3
    right  go to state 7

现在完全没有冲突了,因为leftright 是不同的非终结符。所以根本不需要优先规则,结果证明它是未使用的。但请注意,在这台新机器的状态 5 中,解析器识别出它可能即将解析 left 或即将解析 right(在最后两个规则中编号为 3 和 4)。这就是问题所在,处于状态 6:

State 6

    3 left: expr .  ['+']
    4 right: expr .  [$end, '+']

    $end      reduce using rule 4 (right)
    '+'       reduce using rule 3 (left)
    '+'       [reduce using rule 4 (right)]
    $default  reduce using rule 3 (left)

一旦它成功解析了expr,它需要决定它是left 还是right。这里的冲突是在两个不同的归约之间,所以是归约-归约冲突。而且由于优先级总是将 ruleterminal 进行比较,因此它不适用于需要比较两个规则的情况。所以冲突没有优先解决。

(使用 yacc/bison 的 reduce/reduce 冲突的默认解决算法解决冲突:选择文件中最先出现的规则。)

因此,如果+ 操作的左右操作数确实具有重叠的不同语法,我们将很难通过优先声明来解决歧义。

在这一点上,我们可能会放弃优先权(无论如何我们最终可能不得不这样做),但我认为尝试一些可能有效的东西是值得的。它运行得并不完美,但这种尝试很有趣,我认为值得展示。

3。问题的简要探讨

我相信您自己已经做到了这一点,因为您的语法似乎包含一些通常建议的 LR(2) 语法解决方法。但尝试将问题减少到最低限度以便清楚可能的解决方案似乎很有用。

实际上,这里存在三个不同的问题:

  • “短闭包”语法是 LR(2);也就是说,它需要两个前瞻令牌来决定两个不同的归约;

  • => 令牌以两种互不兼容的方式使用,因此需要根据上下文定义两种不同的expr 语法;

  • 短闭包的前言——参数列表和后面的=>——具有不对称优先级。

第三个问题与yield前缀运算符的语法没有太大区别,语法已经有了解决方案(无论它是否是语言设计者和/或用户所希望的),所以我将把它留到以后(或者可能是另一篇文章)并专注于前两个问题。 [注2]

问题的本质在于下面两个sn-ps的代码(其实我只对赋值运算符后面的expr感兴趣,但提供完整的上下文似乎更具可读性):

b = ( a ) + 2
b = ( a ) => 2

对于本说明的其余部分,我们将假设解析器刚刚读取了标记 a

这两个都是特殊情况,各自的语法不同,大致是:

expr : expr '+' expr

expr : parameter_list "=>" expr

为了完整性,我们还需要看看:

expr           : '(' expr ')'
               | ID
parameter_list : '(' ')' | '(' parameters ')'
parameters     : parameters ',' ID
               | ID

这两种语法的其他实例没有问题:

b = ( a + 3 ) + 2
b = ( a , c ) => 2

这里( a + 3 ) 不能是parameter_list( a , c ) 不能是expr,因此在每种情况下只有一个规则适用,+, 标记每个都足以排除另一种选择。但在( a ) 的情况下(解析器查看) 标记),尚无法知道跳转的方式。

不幸的是,解析器需要知道这一点,因为它必须在:

expr       : ID
parameters : ID

它需要遵循其中一条规则:

expr           : '(' expr ')'

parameter_list : '(' parameters ')'

但要这样做,它必须在两个ID 减少之间进行选择。由于不能仅基于一个前瞻令牌做出该决定,因此 bison 会报告 reduce/reduce 冲突,并且正如我们在上面看到的那样,reduce/reduce 冲突无法通过优先级声明来解决。

4。部分解决方案

如果解析器可以在未来再查看一个标记,它将看到) 之后的标记,这足以做出决定:如果第二个下一个标记是=>,那么它必须在parameter_list;否则,它必须在expr 中。所以语法(或它的简化版本)是 LR(2),而不是 LR(1)。如果野牛可以生成 LR(2) 语法,那就太好了,但它不能。 [注1]所以我们需要找到不同的解决方案。

有一个不同的解决方案,因为不存在 LR(2) 语言。很容易证明任何具有 LR(k) 文法的语言也具有等价的 LR(1) 文法——在原始分析树可以从 LR(1) 文法的分析树机械派生的意义上说是等价的。这种等效语法甚至可以通过算法生成,因此数学家可能会说“存在解决方案”。不幸的是,这不是一个特别实用的解决方案,因为没有工具(据我所知)可以实际进行转换——当然野牛也不会——而且因为转换使语法变得更大、更大。尽管如此,必须存在 LR(1) 语法这一事实使得尝试找到它是值得的。

将 LR(2) 文法转换为 LR(1) 文法的基本方法是延迟决策。在实际的语法中,问题在于解析器需要在parameter_listexpr 之间做出决定,然后才能获得足够的信息。我们可以通过重写语法来简化工作,以便稍后做出决定。

我们可以从以下最小语法开始,如上:

%token ID
%right "=>"
%left '+'
%%
expr           : expr '+' expr
               | parameter_list "=>" expr
               | '(' expr ')'
               | ID
parameter_list : '(' ')' | '(' parameters ')'
parameters     : parameters ',' ID
               | ID

和上面的“左/右”例子比较类似,这个语法在状态5存在reduce/reduce冲突:

State 5

    4 expr: ID .  ['+', ')']
    8 parameters: ID .  [')', ',']

    ')'       reduce using rule 4 (expr)
    ')'       [reduce using rule 8 (parameters)]
    ','       reduce using rule 8 (parameters)
    $default  reduce using rule 4 (expr)

作为解决方案的第一个近似值,我们可以添加一些简单的冗余规则:

%token ID
%right "=>"
%left '+'
%%
expr           : paren_id_paren
parameter_list : paren_id_paren
paren_id_paren : '(' ID ')'
expr           : expr '+' expr
               | parameter_list "=>" expr
               | '(' expr ')'
               | ID
parameter_list : '(' ')' | '(' parameters ')'
parameters     : parameters ',' ID
               | ID

通过 bison 运行它表明我们现在有一个三向冲突的状态(shift/reduce/reduce):

State 6

    3 paren_id_paren: '(' ID . ')'
    7 expr: ID .  ['+', ')']
   11 parameters: ID .  [')', ',']

    ')'  shift, and go to state 13

    ')'       [reduce using rule 7 (expr)]
    ')'       [reduce using rule 11 (parameters)]
    ','       reduce using rule 11 (parameters)
    $default  reduce using rule 7 (expr)

这是解析器刚刚读取'(' ID 且前瞻标记为) 的状态。因为新文法是模棱两可的,所以包含这个序列的每个输入都可以用两种方式解析:使用移位或使用两种归约中的一种。解析器仍然无法判断使用哪个归约。但是这种转变总是有效的,并且因为 bison/yacc 的默认冲突解决算法更喜欢转变,转变就是已经融入解析自动机的内容。这很好,因为这正是我们想要的。唯一的缺点是解析器生成器每次运行时都会产生警告,有些人真的很讨厌在生产构建期间看到警告。

我并不是要贬低对警告的厌恶;我分享它。但我也注意到,这种解决方案实际上是由 yacc 的原始作者考虑的,甚至在 Dragon book 关于使用 yacc 处理歧义语法的部分中也描述了一个示例。这正是 yacc 默认冲突解决算法以它的方式工作的原因。 Bison 和 yacc 甚至实现了一个pair of directives,其目的是在预期的时候消除这个警告。所以我们可以把它留在那里,继续讨论另一个问题(=> 的双重用途),但是当我在思考这个问题时,我突然想到可以使用优先级来提供一个根据 Bison 手册的建议,明确的解决方案:

我们不建议使用%expect‘%expect 0’ 除外!),因为相同数量的冲突并不意味着它们相同。如果可能,您应该使用优先指令明确地修复冲突。 (强调原文。)

优先级声明需要优先转换) 而不是减少ID。这么说来,声明很简单:

%token ID
%precedence ID
%precedence ')'
%right "=>"
%left '+'
%%
expr           : paren_id_paren
parameter_list : paren_id_paren
paren_id_paren : '(' ID ')'
expr           : expr '+' expr
               | parameter_list "=>" expr
               | '(' expr ')'
               | ID
parameter_list : '(' ')' | '(' parameters ')'
parameters     : parameters ',' ID
               | ID

这很好,所以我们可以继续讨论第二个问题,看看解决方案在上下文中是否成立。

待续


注意事项

  1. 事实上,yacc 会生成 LALR(1) 语法,它们在使用前瞻方面稍有限制,但 LR(1) 和 LALR(1) 之间的区别在这里不必困扰我们。

    Bison 能够生成 GLR 语法,它适用于任何明确的语法,并且这种语法是明确的。然而,许多项目不愿意使用 GLR 语法,因为他们认为效率低下以及对操作的限制。如果这里不是这种情况,那么使用 GLR 语法无疑是最简单的解决方案。

  2. => 的预先存在的使用具有合理定义的优先级,这完全由预先存在的优先级级别声明指定。

【讨论】:

  • 我知道 shift 和 reduce 是什么意思;我什至在问这个问题之前又查了一遍。直到现在我才知道--report=all,只使用-v。你提出了一个很好的观点 优先级比较总是在规则和令牌之间。 (这几乎描述了我目前遇到的问题) - 但是我很好奇,我怎么能避免需要额外的规则?等待你的“待续”部分:-)
  • @bwoebi:我敢肯定你是,但不是每个将来会读到这个答案的人都是。每次我回答一个与优先级相关的问题时,我都会尝试改进演示文稿,希望我不必再写一遍:) 但你的问题的实际解决方案并不简单,所以我需要一段时间。我会尽快做的。
  • 是的,没问题 ;-) 无论如何感谢您的努力 - 我已经尝试了几天无济于事。我只是希望最后的判决不是“不可能使用像野牛这样的 LALR(1) 解析器”。
  • @bwoebi:语言仍然是 LALR(1),但语法很棘手。
  • 上个月我一直忙于其他事情,但我仍然有这个问题 - 知道这个(类型)问题的解决方案真是太棒了。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-02-26
  • 1970-01-01
相关资源
最近更新 更多