《Language Implementation Patterns》之访问&重写语法树
每个编程的人都学习过树遍历算法,但是AST的遍历并不是开始想象的那么简单。有几个因素会影响遍历算法:1)是否拥有节点的源码;2)是否子节点的访问方式是统一的;3)ast是homogeneous或heterogeneous;4)遍历的过程中是否需要修改ast;5)以何种顺序呢遍历。这一章会讨论常用的四个ast遍历模式。
- Pattern 12, Embedded Heterogeneous Tree Walker, AST的node类包含了对应的访问方法,后者执行嵌入的操作,并访问所有的子节点。这种模式将访问逻辑遍布所有的节点类,比较简单直接,但是缺乏灵活性;
- Pattern 13, External Tree Visitor, 一个独立于AST存在的Visitor类,很灵活,但是手动编写很复杂;
- Pattern 14, Tree Grammar, 通过一个语法来描述AST的结构,就像用语法来描述语言一样,这样可以通过工具来生成Visitor代码。
- Pattern 15, Tree Pattern Matcher, 该模式不通过语法描述整个AST,而是针对某些我们关注的subtree。与前面的模式不一样的是,该模式不关注如何访问整个AST,只关注寻找符合条件的子树
AST访问顺序
我们说“访问“一颗树,意思是我对树中的节点执行某些操作,因此访问节点的顺序是非常重要的,这直接影响了执行操作的顺序。与一般的树结构一样,存在前序、中序、后序3种遍历顺序。
对AST访问来说,情况稍微复杂一点,我们采用一种叫做depth-first search的算法,如果算法到达一个节点t,表示我们discover该节点,等到对t的访问、处理结束,表示我们finished该节点。
对某种的的访问机制来说,discover节点的顺序是固定的,但是会产生不同的遍历效果,取决与将相关操作放在walk()方法的哪个位置。
以表达式1+2+3为例,节点的访问顺序如下:

左侧的图描述了节点的discover和finish顺序,右侧图种的星指出了操作可能执行的时机。如果所有的操作发生在discover的时候,那么相当于前序遍历;如果所有的操作发生在两个子节点之间,相当与中序遍历;如果所有的操作发生在finish的时候,相当于后序遍历。
Pattern 12 Embedded Heterogeneous Tree Walker
每中节点类型增加一个访问方法,递归调用
public void walk() {
«preorder-action»
left.walk();
«inorder-action»
right.walk();
«postorder-action»
}
Pattern 13, External Tree Visitor
<b》将上面嵌入式的访问代码,抽取出来放入一个独立的类里面
第一种实现适合heterogeneous tree,依赖传统的double-dispatcher设计模式,每个节点类型添加一个方法来dispatch自身的访问到合适的visotor方法。
/** A generic heterogeneous tree node used in our vector math trees */
public abstract class Node {
public abstract void visit(Visitor visitor); // dispatcher
}
节点子类的visit方法实现基本是一样的:public void visit(VecMathVisitor visitor) { visitor.visit(this); }。
Visitor的实现如下:
public interface VecMathVisitor {
void visit(AssignNode n);
void visit(PrintNode n);
void visit(StatListNode n);
void visit(VarNode n);
void visit(AddNode n);
void visit(DotProductNode n);
void visit(IntNode n);
void visit(MultNode n);
void visit(VectorNode n);
}
visitor的节点的visit方法也是递归形式:
public void visit(AssignNode n) {
n.id.visit(this);
System.out.print("=" );
n.value.visit(this);
System.out.println();
}
第二种方式通过node的token类型来分别执行访问操作。
public class Visitor {
public void print(ExprNode n) {
switch ( n.token.type ) { // switch on token type
case Token.PLUS : print((AddNode)n); break;
case Token.INT : print((IntNode)n); break;
default : «error-unhandled-node-type»
}
public void print(AddNode n) {
print(n.left); // walk left child
System.out.print("+"); // print operator
print(n.right); // walk right child
}
public void print(IntNode n) {...}
}
这个模式依据节点类型来执行不同的访问操作,只要节点能够提供type信息即可。
Pattern 14,Tree Grammar
描述AST节点树结构的语法,在前面的Parser语法里面也有涉及,通过Tree Grammar来生成的visitor,与Pattern 13具备的能力一致,更加紧凑。
下面是Tree Grammar的一个片段:
expr: ^('+' expr {print("+");} expr)
| ^('*' expr {print("*");} expr)
| ^('.' expr {print(".");} expr)
| ^(VEC {print("[");} expr ({print(", ");} expr)* {print("]");})
| INT {print($INT.text);}
| ID {print($ID.text);}
;
里面嵌入了操作代码,可以控制这些代码的插入位置来达到PreOrder,InOrder,PostOrder的效果。
通过Tree Grammar来访问AST的过程,类似通过语言Grammar来解析语句,因此如果能先把AST转换成线性结构,就可以使用传统的Parser模式来生成访问代码;
在前面的章节说过如何通过文本来表示树结构,表达式1+2可以表示为(+ 1 2),将括号替换成特殊的token DOWN和UP,得到序列 + DOWN 1 2 UP。DOWN和UP模拟了tree访问的移动操作。
对上面的Tree Grammar,生成的访问代码类似:
void expr() { // match an expression subtree
if ( LA(1)==Token.PLUS ) { // if next token is +
match(Token.PLUS);
match(DOWN); // simulate down movement
expr();
expr();
match(UP); // simulate up movement
}
...
}
因此Tree Grammar,不是基于Node类型来执行操作,而是基于某种子树模式来执行操作。
Tree Grammar同时定义了AST有效的结构,运行基于Tree Grammar的visitor,可以在运行时检查AST的合法性。
Pattern 15, Tree Pattern Matcher
该模式用于扫描AST,当遇到感兴趣的子树模式的时候,执行操作或树重写。这种书重写操作叫做”项重写“(term rewriting)。
Pattern Matcher就好像文本匹配&改写工具:awk、sed、perl等;Tree Grammar需要所有子树对应的Grammar,而该模式只需要为关注的子树模式指定Grammar,因而并不会发现ast的所有节点。
下面先看一个通过项重写来简化向量乘法的例子,我们想把向量乘法4[0,50,3]简化为[40,450,43],进一步简化”乘0"运算,得到[0,0,4*3]。

向量乘法的Grammar为:^('*' INT ^(VEC .+)),其中“.”表示任意的节点类型,转换的规则定义如下:
scalarVectorMult : ^('*' INT ^(VEC (e+=.)+)) -> ^(VEC ^('*' INT $e)+)
“e”是引入的变量,通过e+=.成为包含向量元素的list.
简化“乘零”运算的规则如下:
zeroX : ^('*' a=INT b=INT {$a.int==0}?) -> $a ; // 0*x -> 0
xZero : ^('*' a=INT b=INT {$b.int==0}?) -> $b ; // x*0 -> 0
{$a.int==0}?是语法谓词,用来控制该匹配选项。
剩下的事情,就是指定上述规则的运用时机:
topdown : scalarVectorMult ; // tell ANTLR when to attempt which rule
bottomup: zeroX | xZero ;
ANTLR采用depth-first搜寻,当dicovery一个node时候,执行topdown;finish一个node的时候执行bottomup。
有时候"项重写”需要对AST多次执行Tree Pattern Matcher;比如将操作3+3重写为3<<1,需要尽力3+3=》3*2》3<<1。
《Language Implementation Patterns》之访问&重写语法树的更多相关文章
- 《Language Implementation Patterns》之 构建语法树
如果要解释执行或转换一段语言,那么就无法在识别语法规则的同时达到目标,只有那些简单的,比如将wiki markup转换成html的功能,可以通过一遍解析来完成,这种应用叫做 syntax-direct ...
- 《Language Implementation Patterns》之 符号表
前面的章节我们学会了如何解析语言.构建AST,如何访问重写AST,有了这些基础,我们可以开始进行"语义分析"了. 在分析语义的一个基本方面是要追踪"符号",符号 ...
- 《Language Implementation Patterns》之 解释器
前面讲述了如何验证语句,这章讲述如何构建一个解释器来执行语句,解释器有两种,高级解释器直接执行语句源码或AST这样的中间结构,低级解释器执行执行字节码(更接近机器指令的形式). 高级解释器比较适合DS ...
- 《Language Implementation Patterns》之 强类型规则
语句的语义取决于其语法结构和相关符号:前者说明了了要"做什么",后者说明了操作"什么对象".所以即使语法结构正确的,如果被操作的对象不合法,语句也是不合法的.语 ...
- 《Language Implementation Patterns》之 数据聚合符号表
本章学习一种新的作用域,叫做数据聚合作用域(data aggregate scope),和其他作用域一样包含符号,并在scope tree里面占据一个位置. 区别在于:作用域之外的代码能够通过一种特殊 ...
- 《Language Implementation Patterns》之 增强解析模式
上一章节讲述了基本的语言解析模式,LL(k)足以应付大多数的任务,但是对一些复杂的语言仍然显得不足,已付出更多的复杂度.和运行时效率为代价,我们可以得到能力更强的Parser. Pattern 5 : ...
- 《Language Implementation Patterns》之 语言翻译器
语言翻译器可以从一种计算机语言翻译成另外一种语言,比如一种DSL的标量乘法axb翻译成java就变成a*b:如果DSL里面有矩阵运算,就需要翻译成for循环.翻译器需要完全理解输入语言的所有结构,并选 ...
- Clang之语法抽象语法树AST
语法分析器的任务是确定某个单词流是否能够与源语言的语法适配,即设定一个称之为上下文无关语言(context-free language)的语言集合,语法分析器建立一颗与(词法分析出的)输入单词流对应的 ...
- JavaScript的工作原理:解析、抽象语法树(AST)+ 提升编译速度5个技巧
这是专门探索 JavaScript 及其所构建的组件的系列文章的第 14 篇. 如果你错过了前面的章节,可以在这里找到它们: JavaScript 是如何工作的:引擎,运行时和调用堆栈的概述! Jav ...
随机推荐
- 优化一个小时不出结果的SQL
今天刚清闲点,在网上看些资料,这时,用户QQ上发来求助,说一个更新数据的SQL语句很慢,都一个小时了也不出结果,于是,了解下具体的情况,略施小计,5s出结果,下面是我分析该SQL时用到的执行计划,略去 ...
- clientTop,scrollTop,兼容
在开发中常见的额兼容性问题: scrollTop问题: function scroll() { // 开始封装自己的scrollTop if(window.pageYOffset != null) { ...
- Redis进阶实践之十八 使用管道模式加速Redis查询
一.引言 学习redis 也有一段时间了,该接触的也差不多了.后来有一天,以为同事问我,如何向redis中批量的增加数据,肯定是大批量的,为了这主题,我从新找起了解决方案.目前 ...
- js筛选
1.filter():筛选函数 1>:筛选单个元素, object.filter("selector") 2>筛选多个元素: object.filter("s ...
- Numpy库(个人学习笔记)
一样,咱的计算机还是得先拥有Python,并且安装了Numpy库.有疑问的话可以看这里呦~~~~ 下面开讲: NumPy的主要对象是齐次多维数组.它是一个元素表(通常是数字),并且都是相同类型,由正整 ...
- Nginx配置ThinkPHP下的url重写(隐藏入口)
搭建好项目后,在网址上输入域名,只能访问首页,其他页面全是404. 在域名后面和控制器前面加上index.php就可以访问. 在tp5官网手册查找后进行配置修改. 打开nginx.conf 后 ,在s ...
- 使用python UIAutomation从QQ2017(v8.9)群界面获取所有群成员详细资料,
首先安装pip install uiautomation, 运行本文代码.或者下载https://github.com/yinkaisheng/Python-UIAutomation-for-Wind ...
- Hibernate之三态
1.瞬时态 瞬时态也成为临时态或者自由态,瞬时态的实例是new命令创建.开辟内存空间的对象,不存在持久化标识OID,尚未与Hibernate Session关联,在数据库中没有任何记录,失去引用将被G ...
- Microsoft AI - Custom Vision
概述 前几天的 Windows Developer Day 正式发布了 Windows AI Platform,而作为 Windows AI Platform 的模型定义和训练,更多还是需要借助云端来 ...
- Vue之七导航守卫
{ path:'/',component:Recommend,beforeEnter: (to, from, next) => { console.log(to); ajax('get','/a ...