如何在解析do while / while时解决移位/减少冲突

Posted

技术标签:

【中文标题】如何在解析do while / while时解决移位/减少冲突【英文标题】:How to fix shift/reduce conflict in parsing do while / while 【发布时间】:2018-12-21 14:53:05 【问题描述】:

我在使用 do whilewhile do 解析语法时遇到问题。

commands: commands command | command
;
command: WHILE std::cout<<"D"; condition std::cout<<"D"; DO std::cout<<"D"; commands std::cout<<"D"; ENDWHILE std::cout<<"D";
| DO std::cout<<"D"; commands std::cout<<"D"; WHILE condition std::cout<<"D"; ENDDO std::cout<<"D";
;

打印D只是为了测试目的,我想在那儿写几行代码。

它会产生警告:由于冲突,规则在解析器中无用 [-Wother] | DO std::cout

commands 后面有下划线的代码,所以导致问题。

我了解什么是移位/减少冲突,但我可以用 if/then/else 等简单语句解决它,在这种情况下,这个问题对我来说更复杂。

【问题讨论】:

【参考方案1】:

中间规则操作 (MRA) 强制解析器做出早期解析决策。在这种情况下,例如,解析器需要在 do ... while 中的 while 之前执行 MRA,但是当它看到 while 时,要知道 while 是否终止 do 命令还为时过早或启动while 命令。

没有 MRA 就没有问题(可能取决于您的其余语法),因为它可以不断移动标记,直到看到 doenddo

除非绝对必要,否则应避免 MRA。 [注 1] 在大多数 MRA 看起来很诱人的情况下,事实证明您试图在解析器内部做太多事情。通常最好将解析器限制为生成抽象语法树 (AST),或在基本块中生成三地址代码 (TAC) 段,在控制流图结构内部而不是作为单片指令数组。 [注 2] 这些中间数据结构使基本算法(例如填充分支目标)更简单,并且是各种更复杂且极其有用的算法的基础,这些算法可以生成更快更小的代码。 (常用子表达式消除、死代码消除、常量折叠等)

但是,即使您已决定采用似乎从 MRA 中受益的方法,您也会发现通过将操作移至它们遵循的非终结符或使用显式标记来避免它们通常会更好非终端(即一个空的非终端,其唯一目的是执行一个动作)。这些策略通常会产生更易读的语法,并且在许多情况下——包括这个——重组解决了归约-归约冲突。

Bison 有效地将 MRA 转换为标记 - 您可以在使用 -v 选项生成的语法报告中看到这一点 - 但真正的标记具有可以多次使用的优势。相比之下,每个 MRA 都是不同的(在 bison 实现中),即使动作是逐个字符相同的。例如,在您问题的简化语法中,bison 生成九个不同的标记非终结符,它们都具有相同的操作:std::cout&lt;&lt;"D";。结果,bison 最终抱怨减少-减少冲突,因为它无法在两个不同的标记之间做出决定,这两个标记都做同样的事情。显然,在这种情况下没有潜在的冲突,并且用明确的标记替换动作将完全避免该问题,而无需进行大手术。

例如,这是一个非常简化的语法,它(直接)产生三地址代码。注意插入标签的new-label 标记的使用(并以标签编号作为其语义值):

%
#include <stdarg.h>
#include <stdio.h>

void yyerror(const char*);
int yylex(void);

int pc = 0;     /* Program counter */
int label = 0;  /* Current label */
int temp = 0;   /* Current temporary variable */

void emit_label(int n)  printf("%10s_L%d:\n", "", n); 

void emit_stmt(const char* fmt, ...) 
  va_list ap;
  va_start(ap, fmt);
  printf("/* %3d */\t", pc++);
  vprintf(fmt, ap);
  putchar('\n');
  va_end(ap);


%

%token T_DO "do" T_ENDDO "enddo" T_ENDWHILE "endwhile" T_WHILE "while"
%token ID NUMBER

%%

program
     : statements

/* Inserts a label. 
 * The semantic value is the number of the label.
 */
new-label
     : %empty             $$ = label++; emit_label($$); 

/* Parses a series of statements as a block, preceded by a label
 * The semantic value is the label preceding the block.
 */
statements               
     : new-label
     | statements statement

statement
     : while-statement
     | do-statement
     | assign-statement

assign-statement
     : ID '=' expr        emit_stmt("%c = _t%d", $1, $3); 

while-statement
     : new-label "while" condition-jump-if-false "do" statements "endwhile"
                          emit_stmt("JUMP _L%d", $1, 0); emit_label($3); 

do-statement
     : "do" statements new-label "while" condition-jump-if-false "enddo"
                          emit_stmt("JUMP _L%d", $2, 0); emit_label($5); 

/* Semantic value is the label which will be branched to if the condition
 * evaluates to false.
 */
condition-jump-if-false
     : compare            $$ = label++; emit_stmt("IFZ   _t%d, _L%d", $1, $$); 

compare
    : expr '<' expr       $$ = temp++; emit_stmt("_t%d = _t%d < _t%d", $$, $1, $3); 

expr: term
    | expr '+' term       $$ = temp++; emit_stmt("_t%d = _t%d + _t%d", $$, $1, $3);  

term: factor
    | term '*' factor     $$ = temp++; emit_stmt("_t%d = _t%d * _t%d", $$, $1, $3);  

factor
    : ID                  $$ = temp++; emit_stmt("_t%d = %c", $$, $1); 
    | NUMBER              $$ = temp++; emit_stmt("_t%d = %d", $$, $1); 
    | '(' expr ')'        $$ = $2; 

该代码创建的标签比它真正需要的要多。直接输出架构强制打印这些标签,但真正重要的是生成代码中的位置被保存为表示基本块头部的非终结符(可能)的语义值。始终如一地这样做意味着最终操作可以访问他们需要的信息。

值得注意的是标记new-labelwhile 的两个实例之前使用。只有一种情况是它创建的标签实际上是需要的,但无法知道哪种生产最终会成功。

由于各种原因,上述代码并不完全令人满意。首先,由于它坚持立即写出每一行,因此不可能为跳转语句插入占位符。因此,插入条件跳转的标记总是向前跳转(也就是说,它将跳转编译到尚未定义的标签),结果是 end-test do while 构造以类似代码结束(来源:.. .do ... while a &lt; 3 enddo)

         _L4:
/* ... Loop body omitted */
/*  23 */       _t16 = a
/*  24 */       _t17 = 3
/*  25 */       _t18 = _t16 < _t17
/*  26 */       IFZ   _t18, _L6
/*  27 */       JUMP _L4
          _L6:

而不是稍微更有效率的

         _L4:
/* ... Loop body omitted */
/*  23 */       _t16 = a
/*  24 */       _t17 = 3
/*  25 */       _t18 = _t16 < _t17
/*  26 */       IFNZ  _t18, _L4

可以通过将 TAC 存储在数组中而不是将其打印出来,然后将标签回补到分支中来解决此问题。 (不过,这种变化并没有真正影响语法,因为它都是在最终操作中完成的。)但是实现经典的预测试优化会更难:

          _L1:
/*   2 */       _t1 = a
/*   3 */       _t2 = 0
/*   4 */       _t3 = _t1 < _t2
/*   5 */       IFZ   _t3, _L2
/* ...   Loop body omitted */
/*  14 */       JUMP _L1

进入

          _L1:
/*   2 */      JUMP _L2
/* ...   Loop body omitted */
          _L2:
/*  12 */       _t1 = a
/*  13 */       _t2 = 0
/*  14 */       _t3 = _t1 < _t2
/*  15 */       IFNZ  _t3, _L1

(重新排序基本块通常可以节省分支;通常,以最佳顺序输出基本块比按文本顺序构建它们然后移动它们更容易。)


注意事项

    MRA 当然不应该用于尝试跟踪解析器,因为(在这种情况下)它们本质上会改变解析的性质。如果你想跟踪你的解析器,请按照野牛手册tracing parses 部分中的步骤进行操作(并阅读调试解析器的其余章节。)

    通过打印语句生成 TAC 的方式可以追溯到早期的计算时代,当时内存非常昂贵且非常有限,以至于编译必须分多个阶段完成,每个阶段都将其结果写入“外部存储”(例如.纸带),以便在下一次通过时可以顺序读取。当这种编写方式不再需要编译器时,绝大多数真正的程序员还没有出生,但是大量的教学资源仍然有意或无意地从这种基本架构开始。您用来阅读此答案的浏览器会毫不犹豫地使用 2 GB 的(虚拟内存)来显示它。在这种情况下,担心在同一台计算机上编译程序时使用几百 KB 的临时存储来保存 AST 似乎很愚蠢。

【讨论】:

我不想使用 MRA 进行跟踪,我正在编写一个编译器,并且我必须在两个命令中的第一个和最后一个标记之后使用 MRA(对于 jumping)。有没有可能在没有冲突的情况下做到这一点?当我仅在最后一个标记之后使用动作时,它们就会消失,但正如我所说,我需要更多的东西来创建跳跃。 @jg:我想您正在尝试直接从源代码生成三地址代码,绕过 AST(和/或控制流图)的构造。这增加了使用 MRA 的诱惑,尽管它们仍然可以解决。 你能给我一个提示吗? do whilewhile 在我的代码中都有效(当注释后一个命令时),但正如我所提到的,我使用 DO commands WHILE condition ENDDO WHILE condition DO commands ENDWHILE 来保存正确的跳转位置,我没有任何想法如何以不同的方式保存它们。 @J.G.行。我不确切知道您的代码是什么样子或您要做什么,所以我只是做了一些似是而非的假设。我希望它与您的用例有一些模糊的相似之处。 (供将来参考,问题越精确,答案就越准确。)

以上是关于如何在解析do while / while时解决移位/减少冲突的主要内容,如果未能解决你的问题,请参考以下文章

如何在我的 do-while 循环中解决此分段错误?

循环结构while-----do while

Python入门教程第57篇 循环进阶之模拟do…while语句

R中的do-while循环

循环结构 while 和 do while 方法使用

JS中 do while循环问题