如何解决while / while解析时的移位/减少冲突问题

时间:2018-12-21 14:53:05

标签: c++ bison flex-lexer

我在使用 do while while 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 <<“ D”;}命令{std :: cout <<“ D”;}条件{std :: cout <<“ D”;} ENDDO {std :: cout <<“ D “;}

并且在命令后加了下划线的代码,因此会引起问题。

我了解什么是移位/减少冲突,但是我可以通过if / then / else之类的简单语句来解决它,在这种情况下,这个问题对我而言更为复杂。

1 个答案:

答案 0 :(得分:1)

微小动作(MRA)强制解析器做出早期解析决定。例如,在这种情况下,解析器需要在while中的do ... while之前执行MRA,但是当解析器看到while时,要知道该{{1 }}终止while命令或启动do命令。

没有MRA,就没有问题(可能取决于语法的其余部分),因为它可以一直移动令牌,直到看到whiledo

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

但是,即使您决定采用似乎受益于MRA的方法,您也会发现通常最好通过将操作移入其遵循的非终结点来避免它们,或者使用明确的标记非终结符(即,空的非终结符,其唯一目的是执行操作)。这些策略通常会产生更具可读性的语法,并且在很多情况下(包括这种情况),重组解决了减少-减少冲突的问题。

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

例如,这是一个非常简化的语法,(直接)生成三地址代码。请注意{std::cout<<"D";}标记的使用,该标记插入标签(并以标签编号作为其语义值):

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-label的两个实例之前都使用过。只有一种情况是它实际创建的标签是实际需要的,但是不可能知道哪种生产最终会成功。

由于多种原因,上述代码并不完全令人满意。首先,由于它坚持要立即写出每一行,因此不可能在Jump语句中插入占位符。因此,插入条件跳转的标记始终向前跳转(即,它编译到尚未定义的标签的跳转),结果构造时最终测试执行的最终代码为(source:。 。while

do ... while a < 3 enddo

而不是效率更高的

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

可以通过以下方式解决此问题:将TAC存储在阵列中,而不是将其打印出来,然后将标签重新修补到分支中。 (但是,该更改实际上并不会影响语法,因为所有更改都是在最终操作中完成的。)但是要实现经典的测试前优化会变得更加困难,该优化会变成:

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

进入

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

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


注释

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

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