yacc/bison 的最大咀嚼规则——似乎采用最小咀嚼

maximum munch rule with yacc/bison -- seems to be taking minimum munch

我正在尝试将一种新的语言结构引入 large/complex 语法中。我知道这会使语法含糊不清,但我希望用 'maximum munch' 规则解决这个问题。也就是说,将我的结构放在首位,因此它会被优先采用。我取得了一些成功,但树的其他部分并没有那么多。考虑:

( 1, 2, 3 )          // triple
( 4, 5 )             // twople
( 6 )                // *not* a oneple, see below
()                   // zerople

(7 + 8) * 9          // single parens used to override usual precedence
( (( 7 + 8 )) * 9 )  // currently this means the same, but nobody would write that!

(( 6 ))              // I want to be a oneple
( ( 6 ) )            // this also a oneple
( ( ( 6 ) ) )        // this also a oneple with a superfluous pair of parens, ambiguous
((  ( 6 )  ))        // superflous parens are innermost
(  (( 6 ))  )        // superfluous parens are outermost

((7 + 8) * (9 + 10)) // not a oneple, despite the (( ... ))

在三对括号内带有 6 的那三个例子是有歧义的,我不在意语法采用其中的哪一个,它们在语义上是相同的。根据 'maximum munch' 规则,它应该取三者的中间,即最里面的括号是多余的。

词法分析器将每个 () 作为一个单独的标记。目前,解析器将 ( ( ( 6 ) ) ) 等同于 6 (其中解析为 expr/int) -- 即在我尝试更改语法之前语法做了什么。

该文法具有许多由其他标记定义的标记级别。某些上下文中的某些表达式可以识别双括号 OK。其他人则没有那么多(而且很难将其归结为一个合理大小的例子)。有什么通用的技巧可以让野牛吃得最多?

补充: 这种歧义类似于第一种使用 BNF 的语言中著名的歧义:ALGOL 60

if cond1    then 
  if cond2  then blah2
 else            blah3;      // is this on cond2 False or cond1 False?

解决方法是说 else 附加到尚未获得 else 的最里面的 if/then -- 也就是说,在 cond2 上 False这个案例;离开 cond1 没有 else 分支。 (ALGOL 没有 'offside rule',谢天谢地!)

Addit2: 好的,应大众需求,我开始修改之前的yacc代码是here(现在你会希望你从来没有问过。)这个例如在规则 aexp 中工作(中间线是我的 mod)

  | '(' exp ')'         {$$ = gc3();}
  | '(' '(' exp ')' ')'     {$$ = gc5(buildTuple(cons(,NIL)));}   /* ADC Apr-2020  */
  | '(' exps2 ')'       {$$ = gc3(buildTuple());}

此行无效 -- 在规则 apat 中,pat 一直被解析为多括号普通模式。

apat      : NUMLIT          {$$ = ;}
  | var             {$$ = ;}
  | '(' '(' pat ')' ')'     {$$ = gc5(buildTuple(singleton()));}  /* ADC Apr-2020 */
  | apat_vI         {$$ = ;}
  ;

我试过在 pat 树上下的各种地方插入那条线;我什至尝试过同时将它放入多个作品中,这看起来很狡猾。解析器似乎总是忽略它。

感谢@Chris Dodd 的回答。我认为尽管这给出了 shift/reduce 错误,但我已经阅读了野牛 'does the right thing' 的其他 SO 答案——也就是说,如果你把一个作品放在另一个作品之上,它会优先选择那个作品。

所以你没有显示你的代码,但我猜你正在尝试类似的东西:

%token NUM NAME
%left '+' '-'
%left '*' '/'
%nonassoc PREFIX
%%

expr: expr '+' expr
    | expr '-' expr
    | expr '*' expr
    | expr '/' expr
    | '-' expr %prec PREFIX
    | '(' expr ')'   /* parentheses for grouping */
    | NUM
    | NAME
    | '(' exprlist ',' expr ')' /* tuple */
    | '(' '(' expr ')' ')'      /* one-ple */
    ;

exprlist: expr | exprlist ',' expr ;

此语法有歧义,因此有一个 shift/reduce 冲突。但是默认的 prefer-shift-over-reduce 解决方案将导致它使用 "one-ple" 规则而不是 "parentheses" 规则解析两次。

但是,如果您添加一个 %left ')' 指令,它将以相反的方式解决这个问题(并报告一个 "rule useless due to conflicts" 作为一个规则)

我已经解决了这个问题;这与 %left / %right 优先级无关。

请注意,在 Addit2: 有效的代码中,相同的非终结符 pat 出现在两个可能不明确的规则中;并且区分它们的终端也出现(单对 parens 与加倍对)。而在有问题的示例中, 'competing' 非终端的名称不同;而且没有终端。

apat 的规则中,NUMLIT 不能以 ( 开头,var 也不能。 apat_vI 可能以 ( 开头,因此将 pat 的规则移入其中。 (如果你在家里跟随,请查看我链接到的回购协议。)

但仅仅将规则移入 apat_vI 还不够好。这是关键的几行:

  | '(' pat_npk ')'     {$$ = gc3();}   
  | '(' npk ')'         {$$ = gc3();}                  
/*    | '(' '(' pat ')' ')'     {$$ = gc5(buildTuple(singleton()));}      /*   ignored */
  | '(' '(' pat_npk ')' ')'     {$$ = gc5(buildTuple(singleton()));}  /* same non-term */

pat的注释掉的那一行是我移动的那一行,但它并没有解决歧义。 (并且不会减少 shift/reduce 冲突的计数。)

查看 pat 的作品,它们是 npkpat_npk。在这种情况下,npk 不能以 ( 开头——所以忽略它; pat_npk 可能。因此,将 pat 更改为 pat_npk,这样可以将 shift/reduce 冲突减少 2 个。

( ( ( 6 ) ) ) 现在被解析为与 (( 6 )) 相同,不同于裸 6。这是使用以下语言的会话:

Main> let foo x = (( x )) in foo 6           -- (( x )) in exp always worked
(( 6 ))
Main> let bar (( x )) = x in bar (( 6 ))     -- (( x )) in pat got treated as bare x
6
Main> let bar (( x )) = x in bar (( (6) ))   -- three pairs of parens treated as a double
6
Main> let bar (( x )) = x in bar (( ((6)) )) -- four pairs of parens treated as two doubles
(( 6 ))

关键特征是:

  • bison 必须能够看到 apat_vI 的规则中有两个产生式包含相同的非终结符 pat_npk
  • 它们的上下文因终端而异(一对括号与双括号)。
  • 然后在传入的令牌字符串中遇到 ( 时,解析器将转移并等待看它是否得到另一个。

就像在 if/else 冲突中一样:鼓励解析器移动第一个 else,然后等待看它是否得到另一个。