每个编程的人都学习过树遍历算法,但是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》之访问&重写语法树的更多相关文章

  1. 《Language Implementation Patterns》之 构建语法树

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

  2. 《Language Implementation Patterns》之 符号表

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

  3. 《Language Implementation Patterns》之 解释器

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

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

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

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

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

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

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

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

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

  8. Clang之语法抽象语法树AST

    语法分析器的任务是确定某个单词流是否能够与源语言的语法适配,即设定一个称之为上下文无关语言(context-free language)的语言集合,语法分析器建立一颗与(词法分析出的)输入单词流对应的 ...

  9. JavaScript的工作原理:解析、抽象语法树(AST)+ 提升编译速度5个技巧

    这是专门探索 JavaScript 及其所构建的组件的系列文章的第 14 篇. 如果你错过了前面的章节,可以在这里找到它们: JavaScript 是如何工作的:引擎,运行时和调用堆栈的概述! Jav ...

随机推荐

  1. 《你不知道的 JavaScript 上卷》 学习笔记

    第一部分: 作用域和闭包 一.作用域 1. 作用域:存储变量并且查找变量的规则 2. 源代码在执行之前(编译)会经历三个步骤: 分词/此法分析:将代码字符串分解成有意义的代码块(词法单元) 解析/语法 ...

  2. 从“跳一跳”来看微信小程序的未来

    从“跳一跳”来看微信小程序的未来   相信大家这两天都被微信新推出的小程序跳一跳刷爆了朋友圈,为了方便用户在使用过程中切换小程序,微信在这次6.6.1版本中加入了下拉可快速切换小程序的功能,而“跳一跳 ...

  3. Aspose实现Office转PDF (ASP.NET)

    0.添加Aspose的DLL 1.可以直接去官网下载,不过默认是带水印的,如需去除水印可以购买 2.当然也可以在国内的一些下载站下载 3.将Aspose.Cells.dll.Aspose.Words. ...

  4. 安卓中webview读取html,同时嵌入Flex的SWF,交互

    安卓中webview读取html,同时嵌入Flex的SWF,交互 安卓activity与html交互很简单,用javascript接口即可,网上一堆的例子,基本上没多大问题. 在html里面嵌入swf ...

  5. 说说你对用SSH框架进行开发的理解

    SSH框架指的是Struts,Spring,Hibernate.其中,Struts主要用于流程控制:Spring的控制反转能祈祷解耦合的作用:Hibernate主要用于数据持久化.

  6. Luogu P1410 子序列

    题目大意: 给定一个长度为\(N\)(\(N\)为偶数)的序列,] 问能否将其划分为两个长度为\(\frac{N}{2}\)的严格递增子序列, 输入一共有\(50\)组数据,每组数据保证\(N \le ...

  7. 【SHOI2012】魔法树(树链剖分,线段树)

    [SHOI2012]魔法树 题面 BZOJ上找不到这道题目 只有洛谷上有.. 所以粘贴洛谷的题面 题解 树链剖分之后直接维护线段树就可以了 树链剖分良心模板题 #include<iostream ...

  8. linux下 mysql5.7.20安装(精华)

    在linux 系统中mysql配置文件的读取顺序为: /etc/my.cnf /etc/mysql/my.cnf /usr/local/mysql/etc/my.cnf ~/.my.cnf 第一步 创 ...

  9. 31.Django缓存和信号

    缓存 由于Django是动态网站,所有每次请求均会去数据进行相应的操作,当程序访问量大时,耗时必然会更加明显,最简单解决方式是使用:缓存,缓存将某个views的返回值保存至内存或者memcache中, ...

  10. Java高并发秒杀系统【观后总结】

    项目简介 在慕课网上发现了一个JavaWeb项目,内容讲的是高并发秒杀,觉得挺有意思的,就进去学习了一番. 记录在该项目中学到了什么玩意.. 该项目源码对应的gitHub地址(由观看其视频的人编写,并 ...