如果要解释执行或转换一段语言,那么就无法在识别语法规则的同时达到目标,只有那些简单的,比如将wiki markup转换成html的功能,可以通过一遍解析来完成,这种应用叫做 syntax-directed应用。

更负载的功能,需要我们在完成parse的同时构建某种中间表示形式(Intermediate representation),简称为IR。实际上,我们一般构建一种叫做AST(abstract syntax tree)的结构,它保存了所有的token以及token之间的语法关系。在一个语言应用中,往往需要一遍一遍地遍历、修改这个AST。

接下来的内容晖介绍四种常见的AST构建模式:

  • Pattern 8, Parse Tree, 记录了输入语言的语法结构,内部节点对应rule名称,叶子节点对应token;
  • Pattern 9, Homogeneous AST, 对于tree来说,重要的是它的形状,而不是节点的数据类型,如果所有节点的数据类型是一样的,可以说他们是Homogeneous;
  • Pattern 10,Normalized Homogeneous AST,有多种节点类型的tree叫做heterogeneous tree,Normalized Homogeneous AST的节点拥有类型一致的子节点,可以用一个list来表示;
  • Pattern 11, Irregular Heterogeneous AST, 节点拥有不同类型的子节点,每个子节点占用一个field。

在这个系列的第一篇里面就展示了Parse Tree,Parse Tree在Parse的过程中就可以构造出来,完整地记录了Parse过程的轨迹;但Parse Tree并不是最好的IR,我们并不需要所有的内部节点。

构建AST

一个好的AST应该具有以下特征:

  • Dense,没有不必要的节点
  • Convenient,容易便利
  • Meaningful,强调了操作符、操作数以及他们之间的关系

    前两点意味着AST应该很容易、迅速地定位一个模式,语言应用需要多次地访问AST,这个结构应该足够简单;最后一点意味着AST应该对grammer定义的变更不敏感,一个与语法规则无关的变化(比如rule名字改变),不应该需要语言程序修改其他部分。

以一条语句x=0;为例,理想中的AST如下:



移除了";",因为他没有实际意义;移除了原来的内部节点,操作符变为字数的root,操作数变为叶子节点;这个AST没有了任何多余的节点。

AST如何解决操作符优先级

对于赋值操作x=1+2,连个操作符的求职顺序应该是先求职(1+2),再求值(x=*),AST只需要将优先级较高的操作符放在AST较深的节点即可。

通过文本来描述AST

有时候需要以文本的形式来呈现AST,使用如下的标记方法:

(a b c),a表示root;b、c表示子节点;

语句3+45的AST文本形式为(+ 3 ( 4 5))。

伪操作符

并不是所有的语句都有操作符,甚至有些语言本身就没有操作的概念。

比如c里面的变量声明:"int i",我们找不到一个操作符,因此需要制造一个;任何想象的符号都可以胜任,一般使用”VARDECL"这个符号。

java实现AST

在实现上,可以使用单一的类型来表示AST,这样的AST就是上文所说的homogeneous tree。

public class AST {
Token token; // node is derived from which token?
List<AST> children; // operands
public AST(Token token) { this.token = token; }
public void addChild(AST t) {
if ( children==null ) children = new ArrayList<AST>();
children.add(t);
}
}

对于ANTLR这样的工具来说,只有对normalized子节点类型,才能生成AST访问代码。统一的java类型并不意味这节点就不可以有类型,通过Token.getType()就可以获取足够的信息。

对于静态类型的语言,需要标记AST某些节点的类型,给这些节点加个字段叫做evalType,如果要保持homogeneous性,就要给所有的节点加上evalType,最终我们的节点包含了所有类型所需特殊字段的集合。为了解决这个问题,我们可以使用heterogeneous树,不同的节点有不同的类型。这些节点会以统一的AST类型作为基类。

public class ExprNode extends AST { DataType evalType; ... }
public class AddNode extends ExprNode { ... }
public class MultNode extends ExprNode { ... }
public class IntNode extends ExprNode { ... }

对于拥有normalized child list的节点来说,只能使用索引来访问子节点child[0],child[1],而不是“left","right"这样的名字。具备后者这种不规则子节点名字的AST就是Irregular Heterogeneous AST,显然具有更好的可读性。

ANTLR简介

ANTLR是一个语法解析器产生工具,本身是一个jar包,可以前往www.antlr.org下载,加载好之后添加响应的classpath。我用的是3.4版本,现在最新的是4.4版本,使用3.4是因为3.4有c语言的运行时库,而4.4只有java的运行时库。

antlr将语法定义放在一个.g文件里面,比如Graphics.g,包含所有的语法、词法规则:

grammar Graphics;

file : command+ ;
command : 'line' 'from' point 'to' point ;
point : INT ',' INT ;
INT : '0'..'9'+ ;
WS : (' '|'\t'|'\r'|'\n'){skip();} ;

语法规则的名字是小写单词,token的定义为大写单词。

命令行下cd到Graphics.g所在目录:

$ java org.antlr.Tool Graphics.g
$ ls
Graphics.g GraphicsLexer.java box Graphics.tokens GraphicsParser.java

Graphics.tokens是一个数据文件,包含所有的token信息,GraphicsLexer.java是词法分析器, GraphicsParser.java是语法分析器。读者可以按原书的指引尝试一下。

通过ANTLR构造AST

暂且不管antlr的grammar语法细节,先粗略看一下grammarr如何为我们构造ast。

以一个向量计算的语法为例:

//示例语句
z = [1, 2] + [3, 4]
a = [1, 2] . [3, 4] //语法
statlist : stat+ ;
stat: ID '=' expr ;
expr: primary ('+' primary)* ;

antrl支持向grammar里面插入指令来构造ast:

expr returns [AST tr]
: a=primary {$tr = $a.tr;}
('+' b=primary {$tr = new AddNode($tr,$b.tr);})*

插入的指令是类java代码,antlr有内置的与语言无关的ast支持,如下:

grammar VecMathAST;
options {output=AST;} // we want to create ASTs
tokens {VEC;} // define imaginary token for vector literal // START: stat
statlist : stat+ ; // builds list of stat trees
stat: ID '=' expr -> ^('=' ID expr) // '=' is operator subtree root
;
primary : INT // automatically create AST node from INT's text
| ID // automatically create AST node from ID text
| '[' expr (',' expr)* ']' -> ^(VEC expr+)

在options里面指定了parser的输出是ast,对每条rule制定了生成对应子树的规则^(...),上面语法中的('=' ID expr),表示=是root节点,ID和expr是两个子节点,对于primary : INT这样的规则,antlr可以自动创建出叶子节点;有些时候我们要创建额外的token来充当子树的root,比如上面VEC。

默认antlr创建homogeneous AST,有统一的节点类型CommonTree,通过gammar可以告诉antrl创建heterogeneous AST:

primary
: INT<IntNode> // create IntNode from INT's text
| ID<VarNode> // create VarNode from ID's text
| '[' expr (',' expr)* ']' -> ^(VEC<VectorNode> expr+) ;

Pattern 8, Parse Tree,

优点在与Parse的过程可能很自然的构造Parse Tree,缺点在于过多的无用节点。

Parse Tree又叫做Syntax tree(对比于Abstract Syntax Tree),完整地体现了输入的语法结构;虽然对解释器和翻译器这样的应用来说Parse Tree不是很有用,但是在开发环境和文字重写系统中有广泛使用。

Parse Tree的特点在前面已经讲过,不在赘述。在实现上,应为Parser的过程其实就是识别语法树的过程,因此只要在Parser的每个rule方法里面加上对应的节点构建代码即可:

void «rule»() {
RuleNode r = new RuleNode("«rule»");
if ( root==null ) root = r; // we're the start rule
else currentNode.addChild(r); // add this rule to current node
ParseTree _save = currentNode;
currentNode = r; // "descend" into this rule
«normal-rule-code»
currentNode = _save; // restore node to previous value
}

Pattern 9,Homogeneous AST

优点:统一的节点类型,简单;缺点:单一的类型需要兼顾所有节点类型的需求。

实际上,对于非面向对象的语言(C语言)来说,Homogeneous AST是唯一的选择。

节点的定义类似一下代码:

public class AST { // Homogeneous AST node type
Token token; // From which token did we create node?
List<AST> children; // normalized list of children
}

Pattern 10, Normalized Heteogeneous AST

优点:可以为操作符和操作数增加自定义的字段和方法;缺点:大量的节点类型需要被定义

该AST有不同的节点类型,仍然有统一的child list,因此节点类都继承自统一的AST。

public abstract class ExpreNode extends AST {
int evalType //expression value type
}
public class AddNode extedns ExprNode {
pulic AddNode(ExprNode left, Token addToken, ExprNode right) {
super(addToken);
addChild(left);
addChild(right);
}
}

Pattern 11, Irregular Heterogeneours AST

优点:对子节点的访问更加可读,体现了子树的语法含义;缺点:与Pattern 10一样大量的节点类型被定义,而且相应的ast遍历算法也比较复杂。

该AST的节点类型不一致,而且对子节点的访问方式也不一致:

public class AddNode extends ExprNode {
ExprNode left, right; // named, node-specific, irregular children
public AddNode(ExprNode left, Token addToken, ExprNode right) {
super(addToken);
this.left = left;
this.right = right;
}
}

如果手动构建ast的话,很自然会选择这种方式;这种方式只适合比较小的应用。

《Language Implementation Patterns》之 构建语法树的更多相关文章

  1. 《Language Implementation Patterns》之访问&重写语法树

    每个编程的人都学习过树遍历算法,但是AST的遍历并不是开始想象的那么简单.有几个因素会影响遍历算法:1)是否拥有节点的源码:2)是否子节点的访问方式是统一的:3)ast是homogeneous或het ...

  2. 《Language Implementation Patterns》之 解释器

    前面讲述了如何验证语句,这章讲述如何构建一个解释器来执行语句,解释器有两种,高级解释器直接执行语句源码或AST这样的中间结构,低级解释器执行执行字节码(更接近机器指令的形式). 高级解释器比较适合DS ...

  3. 《Language Implementation Patterns》之 强类型规则

    语句的语义取决于其语法结构和相关符号:前者说明了了要"做什么",后者说明了操作"什么对象".所以即使语法结构正确的,如果被操作的对象不合法,语句也是不合法的.语 ...

  4. 《Language Implementation Patterns》之 数据聚合符号表

    本章学习一种新的作用域,叫做数据聚合作用域(data aggregate scope),和其他作用域一样包含符号,并在scope tree里面占据一个位置. 区别在于:作用域之外的代码能够通过一种特殊 ...

  5. 《Language Implementation Patterns》之 符号表

    前面的章节我们学会了如何解析语言.构建AST,如何访问重写AST,有了这些基础,我们可以开始进行"语义分析"了. 在分析语义的一个基本方面是要追踪"符号",符号 ...

  6. 《Language Implementation Patterns》之 语言翻译器

    语言翻译器可以从一种计算机语言翻译成另外一种语言,比如一种DSL的标量乘法axb翻译成java就变成a*b:如果DSL里面有矩阵运算,就需要翻译成for循环.翻译器需要完全理解输入语言的所有结构,并选 ...

  7. 《Language Implementation Patterns》之 增强解析模式

    上一章节讲述了基本的语言解析模式,LL(k)足以应付大多数的任务,但是对一些复杂的语言仍然显得不足,已付出更多的复杂度.和运行时效率为代价,我们可以得到能力更强的Parser. Pattern 5 : ...

  8. 淘宝数据库OceanBase SQL编译器部分 源代码阅读--解析SQL语法树

    OceanBase是阿里巴巴集团自主研发的可扩展的关系型数据库,实现了跨行跨表的事务,支持数千亿条记录.数百TB数据上的SQL操作. 在阿里巴巴集团下,OceanBase数据库支持了多个重要业务的数据 ...

  9. 《淘宝数据库OceanBase SQL编译器部分 源码阅读--解析SQL语法树》

    淘宝数据库OceanBase SQL编译器部分 源码阅读--解析SQL语法树   曾经的学渣 2014-06-05 18:38:00 浏览1455 云数据库Oceanbase   OceanBase是 ...

随机推荐

  1. Qtree3题解(树链剖分(伪)+线段树+set)

    外话:最近洛谷加了好多好题啊...原题入口 这题好像是SPOJ的题,挺不错的.看没有题解还是来一篇... 题意: 很明显吧.. 题解: 我的做法十分的暴力:树链剖分(伪)+线段树+\(set\)... ...

  2. 【UVA 11426】gcd之和 (改编)

    题面 \(\sum_{i=1}^{n}\sum_{j=1}^m\gcd(i,j)\mod998244353\) \(n,m<=10^7\) Sol 简单的一道莫比乌斯反演题 \(原式=\sum_ ...

  3. sql性能优化之多表联查

    先贴上我优化后的核心代码: select * into #result from ( select p.AchivementCount,isnull(a.ByAttentionCount,0) ByA ...

  4. Nancy启用跨站攻击防护(CSRF)

    什么是CSRF(跨站攻击) 可能很多人已经对CSRF有所了解,就简单的介绍下: CSRF全程是 Cross-Site Request Forgery .大概意思就是在登录用户不知情的情况下,由一个网站 ...

  5. Jexus 5.8.3正式发布:Asp.Net Core在Linux上最友好服务器平台

    Jexus Web Serever 是一款运行于 Linux 操作系统,以支持 ASP.NET.ASP.NET CORE.PHP 为特色的高性能 WEB 服务器和反向代理服务器.最新版 5.8.3 已 ...

  6. [转]SVN使用log,list,cat,diff查看所有及特定文件版本信息

    [转]SVN使用log,list,cat,diff查看所有及特定文件版本信息 http://onefishum.blog.163.com/blog/static/5184730520113153402 ...

  7. 关于现在IT行业从业者一些建议

    本人从事IT行业,确切的说应该是软件开发行业已经9个年头了,从刚开始小白也慢慢的已经有了自己独有的开发习惯. 近些年进入行业的人越来越多,有可能确实看到了这行业就业及薪资待遇,更多的也是随着互联网及移 ...

  8. Eclipse中JRE(unbound)问题的一种解决方法

    (如果有写的不对的地方,欢迎指正!) 1.检查Java环境变量配置是否有问题 jdk1.8环境变量配置(1.8和8是一个意思) jdk9环境变量配置 注:配置不成功的一种可能是安装多个jdk,解决方法 ...

  9. unix及Linux发展历程

    unix及Linux发展历程 版权申明:本文资料为网上收集,由本人整理而成,转载请注明 一,unix诞生: Ken Thompson肯·汤普森 -------- unix之父 在1969年到1970间 ...

  10. delete操作字符串会报错吗?

    其实是不会的."delete只能对对象属性进行操作"的说法是错误的,严谨来说,delete可以对任意变量类型进行操作,只不过只有对对象的属性才能操作成功,对其他变量类型操作失败罢了 ...