《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。