求解Bison中do块引发的shift/reduce冲突:while循环与函数调用歧义
问题根源
你遇到的是典型的悬挂do二义性:当解析器处理到while call()后的do关键字时,存在两个合法分支:
- 选择移进
do,将其作为call后续do_block的开头 - 选择归约当前的
call()为完整表达式,再将do作为while循环的分隔符
Yacc系解析器默认优先执行移进操作,所以do永远会被绑定给函数调用,导致语法解析不符合预期。
解决方案
方案1:文法拆分(推荐,无隐式依赖)
通过拆分表达式规则,强制while后的条件表达式不能直接跟裸do块,只有被括号包裹的表达式才允许绑定do块,完全符合你要的语法规则:
%token IDENT DO END WHILE %% program: %empty | stmts ; stmts: stmt | stmts ';' stmt | stmts ';' ; opt_stmts: %empty | stmts ; opt_semi: %empty | ';' ; term: ';' | DO opt_semi ; while_loop: WHILE expr term opt_stmts END | WHILE term opt_stmts END ; stmt: expr | while_loop ; /* while条件位使用的表达式:不允许直接尾随do块 */ expr: IDENT | '(' inner_expr ')' | call_no_do ; /* 完整表达式:允许包含带do块的函数调用 */ inner_expr: expr | call_with_do ; do_block: DO '|' args '|' opt_stmts END | DO opt_stmts END ; /* 拆分call为不带do块、带do块两个分支 */ call_no_do: IDENT '(' args ')' ; call_with_do: IDENT '(' args ')' do_block | IDENT do_block ; /* 其他场景使用的call可以同时支持两种形式 */ call: call_no_do | call_with_do ; args: %empty | expr | args ',' expr | args ',' ; %%
该方案下:
while call() do stuff() end会正确将do绑定给while- 要给函数调用绑定
do块必须加括号:while (call() do stuff() end) do more_stuff() end,完全符合预期。
方案2:优先级标记(改动最小)
如果不想调整文法结构,可以通过优先级声明强制归约无do块的函数调用优先级高于移进do的优先级:
%token IDENT END WHILE %nonassoc DO %nonassoc CALL_NO_DO /* 虚拟优先级标记,优先级高于DO */ %% /* 原有规则不变,仅修改call的第一个产生式 */ call: IDENT '(' args ')' %prec CALL_NO_DO | IDENT '(' args ')' do_block | IDENT do_block ;
遇到冲突时,解析器会优先把call()归约为完整表达式,后续的do就会被识别为while的分隔符。
Ruby的实现逻辑
Ruby的官方解析器就是采用文法拆分的方案,它明确区分了可出现在条件位的表达式和普通表达式,条件位的表达式默认不允许直接跟裸do块,必须用括号包裹才能给内部的函数调用绑定do块,和你想要的行为完全一致。
内容的提问来源于stack exchange,提问作者Ravener
相关产品推荐
相关产品推荐

