【小白打造编译器系列7】Anltr 重构脚本语言

表达式语法

表达式语法的着重点请参照:小白打造编译器系列4】确保表达式正确的优先级、左递归和结合性,我进行了总结与解释。

Antlr 可以自动的解决 左递归 的问题,所以在写表达式的时候,不用担心是否回进入死循环。但这样,我们还是要为每个运算写一个规则,逻辑运算写完了要写加法运算,加法运算写完了写乘法运算,这样才能实现对 优先级 的支持,还是有些麻烦。这时,Antlr 可以进一步帮助我们,用最简洁的方式支持表达式的优先级和结合性。

expression
    : primary
    | expression bop='.'
      ( IDENTIFIER
      | functionCall
      | THIS
      )
    | expression '[' expression ']'
    | functionCall
    | expression postfix=('++' | '--')
    | prefix=('+'|'-'|'++'|'--') expression
    | prefix=('~'|'!') expression
    | expression bop=('*'|'/'|'%') expression  
    | expression bop=('+'|'-') expression 
    | expression ('<' '<' | '>' '>' '>' | '>' '>') expression
    | expression bop=('<=' | '>=' | '>' | '<') expression
    | expression bop=INSTANCEOF typeType
    | expression bop=('==' | '!=') expression
    | expression bop='&' expression
    | expression bop='^' expression
    | expression bop='|' expression
    | expression bop='&&' expression
    | expression bop='||' expression
    | expression bop='?' expression ':' expression
    | <assoc=right> expression
      bop=('=' | '+=' | '-=' | '*=' | '/=' | '&=' | '|=' | '^=' | '>>=' | '>>>=' | '<<=' | '%=')
      expression
    ;

上述代码几乎包括了所有的表达式规则。

那么他是怎么支持优先级的呢?原来优先级是通过右侧不同产生式的顺序决定的。在标准的上下文无关文法中,产生式的顺序是无关的,但在具体的算法中,会按照确定的顺序来尝试各个产生式。

怎么支持结合性呢?在语法文件中,Antlr 对于赋值表达式做了<assoc=right> 的属性标注,说明赋值表达式是右结合的。如果不标注,就是左结合的,交给 Antlr 实现了!

通过这个简化的算法,AST 被成功简化,不再有加法节点、乘法节点等各种不同的节点,而是统一为表达式节点。你可能会问了:“如果都是同样的表达式节点,怎么在解析器里把它们区分开呢?怎么知道哪个节点是做加法运算或乘法运算呢?

很简单,我们可以查找一下当前节点有没有某个运算符的 Token。比如,如果出现了或者运算的 Token(“||”),就是做逻辑或运算,而且语法里面的 bop=、postfix=、prefix= 这些属性,作为某些运算符 Token 的别名,也会成为表达式节点的属性。通过查询这些属性的值,你可以很快确定当前运算的类型。

各类语句的语法

同表达式一样,一个 statement 规则就可以涵盖各类常用语句,包括 if 语句、for 循环语句、while 循环语句、switch 语句、return 语句等等。表达式后面加一个分号,也是一种语句,叫做表达式语句。

statement
    : blockLabel=block
    | IF parExpression statement (ELSE statement)?
    | FOR '(' forControl ')' statement
    | WHILE parExpression statement
    | DO statement WHILE parExpression ';'
    | SWITCH parExpression '{' switchBlockStatementGroup* switchLabel* '}'
    | RETURN expression? ';'
    | BREAK IDENTIFIER? ';'
    | SEMI
    | statementExpression=expression ';'
    ;

if 语句

通常情况下,if-else 语句的结构如下:

if (condition)
  做一件事情;
else
  做另一件事情;

但更多情况下,if-else 是带花括号的。

if (condition){
  做一些事情;
}
else{
  做另一些事情;
}

他的语法规则是这样的:

statement : 
          ...
          | IF parExpression statement (ELSE statement)? 
          ...
          ;
parExpression : '(' expression ')';

我们用了 IF 和 ELSE 这两个关键字,也复用了已经定义好的语句规则和表达式规则。

但是 if 语句也有让人不省心的地方,比如会涉及到 二义性文法 问题。

解决二义性文法

我们学计算机语言的时候,提到 if 语句,会特别提一下嵌套 if 语句和悬挂 else 的情况,比如下面这段代码:

if (a > b)
if (c > d)
做一些事情;
else
做另外一些事情;

在上面的代码中,我故意取消了代码的缩进。一旦你语法规则写得不够好,就很可能形成二义性,也就是 用同一个语法规则可以推导出两个不同的句子,或者说生成两个不同的 AST。这种文法叫做二义性文法,比如下面这种写法:

stmt -> if expr stmt
      | if expr stmt else stmt
      | other

按照这个语法规则,先采用第一条产生式推导或先采用第二条产生式推导,会得到不同的 AST。左边的这棵 AST 中,else 跟第二个 if 配对;右边的这棵 AST 中,else 跟第一个 if 配对。

那么,有没有办法把语法写成没有二义性的呢?当然有了。

stmt -> fullyMatchedStmt | partlyMatchedStmt
fullyMatchedStmt -> if expr fullyMatchedStmt else fullyMatchedStmt
                   | other
partlyMatchedStmt -> if expr stmt
                   | if expr fullyMatchedStmt else partlyMatchedStmt

按照上面的语法规则,只有唯一的推导方式,也只能生成唯一的 AST:

解析第一个 if 语句时只能应用 partlyMatchedStmt 规则,解析第二个 if 语句时,只能适用 fullyMatchedStmt 规则。

这时,我们就知道可以 通过改写语法规则来解决二义性文法 。至于怎么改写规则,确实不像左递归那样有清晰的套路,但是可以多借鉴成熟的经验。

Antlr 的语法似乎并不复杂,怎么就能确保不出现二义性问题呢?

因为 Antlr 解析语法时用到的是  LL 算法 。LL 算法是一个深度优先 的算法,所以在解析到第一个 statement 时,就会建立下一级的 if 节点,在下一级节点里会把 else 子句解析掉。如果 Antlr 不用 LL 算法,就会产生二义性。

for 语句

for 语句一般写成下面的样子:

for (int i = 0; i < 10; i++){
  println(i);
}

相关的语法规则如下:

statement : 
         ...
          | FOR '(' forControl ')' statement
         ...
          ;

forControl 
          : forInit? ';' expression? ';' forUpdate=expressionList?
          ;

forInit 
          : variableDeclarators 
          | expressionList 
          ;

expressionList
          : expression (',' expression)*
          ;

从上面的语法规则中看到,for 语句归根到底是由语句、表达式和变量声明构成的。代码中的 for 语句,解析后形成的 AST 如下:

用 Vistor 模式升级脚本解释器

我们在纯手工编写的脚本语言解释器里,用了一个 evaluate() 方法自上而下地遍历了整棵树。随着要处理的语法越来越多,这个方法的代码量会越来越大,不便于维护。

而 Visitor 设计模式针对每一种 AST 节点,都会有一个单独的方法来负责处理,能够让代码更清晰,也更便于维护。

总结

  • Antlr 可以自动处理左递归的情况。同时根据语法规则中不同产生式的顺序来处理优先级的问题。同时,通过属性标注<assoc=right> 来处理结合性的问题。
  • if-else 语句可能会产生二义性。所谓的二义性指的是同一个语法规则下推导出两种不同的语法结构,生成两种不同的AST。我们可以通过修改规则的方式解决。(当然,需要参考一些最佳实践)
  • for 循环语句实际上有三块组成:forInit,Expression 和 Statement 组成。

参考课程:《极客时间-编译原理之美》。宫老师讲的太好了! 

发布了62 篇原创文章 · 获赞 34 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/weixin_41960890/article/details/105263228