zoukankan      html  css  js  c++  java
  • 《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。

  • 相关阅读:
    IE hasLayout详解
    seajs引入jquery
    jquery实现轮播插件
    CSS视觉格式化模型
    js事件冒泡和事件捕获详解
    你尽力了么===BY cloudsky
    前向否定界定符 python正则表达式不匹配某个字符串 以及无捕获组和命名组(转)
    php safe mode bypass all <转>
    WAF指纹探测及识别技术<freebuf>
    linux集群管理<转>
  • 原文地址:https://www.cnblogs.com/longhuihu/p/4002264.html
Copyright © 2011-2022 走看看