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

posted on 2014-10-03 00:29  longhuihu  阅读(448)  评论(0编辑  收藏  举报