AST在访问者或节点中遍历?

时间:2011-02-25 17:10:51

标签: c++ compiler-construction abstract-syntax-tree visitor

更新接受了Ira Baxter的答案,因为它指出了我正确的方向:我首先想出了我开始执行编译阶段时实际需要的内容,很快就会发现内部遍历节点使得这是一种不可能的方法。并非所有节点都应该被访问,其中一些节点的顺序相反(例如,首先是赋值的rhs,因此编译器可以检查类型是否与rhs /运算符匹配)。在访问者中进行遍历使得这一切变得非常容易。

在决定对应用程序中使用的迷你语言的处理进行重大修改之前,我正在玩AST和类似的东西。 我已经构建了一个Lexer / Parser,可以让AST很好。还有一个访问者,作为具体实现,我创建了一个ASTToOriginal,它只是重新创建原始源文件。最终,还有一些编译器也会实现Vsisitor并在运行时创建实际的C ++代码,所以我想确保一切从一开始就是正确的。 虽然现在一切正常,但由于遍历顺序在访问者本身中实现,因此存在一些相似/重复的代码。

在查找更多信息时,似乎某些实现更喜欢在访问对象本身中保留遍历顺序,以便不在每个具体访问者中重复此操作。 即便是GoF也只是以同样的方式对此进行了简要的讨论。所以我想尝试这种方法,但很快就会陷入困境。让我解释一下。

示例源代码行和相应的AST节点:

if(t>100?x=1;sety(20,true):x=2)
Conditional
  BinaryOp
    left=Variable [name=t], operator=[>], right=Integer [value=100]
  IfTrue
    Assignment
      left=Variable [name=x], operator=[=], right=Integer [value=1] 
    Method
      MethodName [name=sety], Arguments( Integer [value=20], Boolean [value=true] )
  IfFalse
    Assignment
      left=Variable [name=x], operator=[=], right=Integer [value=1]

一些代码:

class BinaryOp {
  void Accept( Visitor* v ){ v->Visit( this ); }
  Expr* left;
  Op* op;
  Expr* right;
};    
class Variable {
  void Accept( Visitor* v ){ v->Visit( this ); }
  Name* name;
};
class Visitor { //provide basic traversal, terminal visitors are abstract
  void Visit( Variable* ) = 0;
  void Visit( BinaryOp* p ) {
    p->left->Accept( this );
    p->op->Accept( this );
    p->right->Accept( this );        
  }
  void Visit( Conditional* p ) {
    p->cond->Accept( this );
    VisitList( p->ifTrue ); //VisitList just iterates over the array, calling Accept on each element
    VisitList( p->ifFalse );
  }
};

实现ASTToOriginal非常简单:所有抽象的Visitor方法只打印出终端的名称或值成员。 对于它所依赖的非终端;打印分配可以使用默认的访问者遍历,因为需要条件额外代码:

class ASTToOriginal {
  void Visit( Conditional* p ) {
    str << "if(";
    p->cond->Accept( this );
    str << "?";
      //VisitListWithPostOp is like VisitList but calls op for each *except the last* iteration
    VisitListWithPostOp( p->ifTrue, AppendText( str, ";" ) );
    VisitListWithPostOp( p->ifFalse, AppendText( str, ";" ) );
    str << ")";
  }
};

因此,可以看到访问者条件和ASTToOriginal中的条件访问方法确实非常相似。 然而,尝试通过将遍历放入节点来解决这个问题使得事情不仅变得更糟,而是完全混乱。 我尝试了一种使用PreVisit和PostVisit方法解决一些问题的方法,但只是在节点中引入了越来越多的代码。 它也开始看起来像我必须跟踪访问者内部的一些州,以便能够知道何时添加右括号等。

class BinaryOp {
  void Accept( Conditional* v ) { 
    v->Visit( this );
    op->Accept( v )
    VisitList( ifTrue, v );
    VisitList( ifFalse, v );
};
class Vistor {
  //now all methods are pure virtual
};
class ASTToOriginal {
  void Visit( Conditional* p ) {
    str << "if(";
    //now what??? after returning here, BinaryOp will visit the op automatically so I can't insert the "?"
    //If I make a PostVisit( BinaryOp* ), and call it it BinaryOp::Accept, I get the chance to insert the "?",
    //but now I have to keep a state: my PostVisit method needs to know it's currently being called as part of a Conditional
    //Things are even worse for the ifTrue/ifFalse statement arrays: each element needs a ";" appended, but not the last one,
    //how am I ever going to do that in a clean way?
  }
};

问题:这种做法是不适合我的情况,还是我忽略了一些必要的东西?是否有一个共同的设计来应对这些问题?如果我还需要在不同的方向进行遍历怎么办?

3 个答案:

答案 0 :(得分:6)

有两个问题:

  • 可以访问哪些子节点?
  • 您应该以什么顺序访问他们?

可以说节点类型应该知道实际的子节点;实际上,应该通过语法知道,并从语法“反映”到普通访客。

访问节点的顺序完全取决于您需要执行的操作。如果您正在进行漂亮打印,则从左到右的子顺序是有意义的(如果子节点按语法的顺序列出,它们可能不是这样)。如果您正在构建符号表,您肯定希望在访问声明正文子之前访问声明子项。

此外,您需要担心在树上向上或向下流动的信息。变量列表访问将向上流动。构造的符号表从声明向上流动树,然后返回到语句体子。此信息流强制访问顺序;要使符号表传递到语句体中,首先必须构造符号表并从声明子项中传递出来。

我认为这些问题会给你带来好处。您试图在访问者上施加单一结构,而实际上访问顺序完全取决于任务,并且您可以对树执行许多不同的任务,每个任务都有自己的信息流,因此依赖于顺序。

解决这个问题的方法之一是使用attribute(d) grammar (AG)的概念,其中一个用各种类型的属性来装饰语法规则以及如何计算/使用它们。您可以将计算作为语法规则的注释编写,例如:

  method = declarations statements ;
  <<ResolveSymbols>>: { declarations.parentsymbols=method.symboltable;
                        statements.symboltable = declarations.symboltable;
                      }

语法规则告诉您必须具有哪些节点类型。 atrribute计算告诉你在树下传递什么值(对method.symboltable的引用是来自父类的东西),在树上(对declarations.symbol表的引用是由该子计算的东西),或者跨越tree(statements.symboltable从declarations.symboltable计算的值传递给statement子级)。属性计算定义访问者。执行的计算称为“属性评估”。

此特定属性语法的表示法是我们DMS Software Reengineering Toolkit的一部分。其他AG工具使用类似的符号。与所有(AG)方案一样,特定规则用于为特定节点(“方法”)制造特定于目的的(“ResolveSymbols”)访问者。通过为每个节点提供一组此类规范,您将获得一组可以执行的特定于目的的访问者。 AG方案的价值在于您可以轻松编写并生成所有样板块。

您可以像这样抽象地思考您的问题,然后像您一样手动生成特定目的的访问者。

答案 1 :(得分:1)

对于树的递归通用遍历,访客和复合通常一起使用,如(第一个相关的谷歌链接)there。我首先阅读了这个想法there。还有visitor combinators这是一个不错的主意。

顺便说一句......

这是功能语言发光的地方,它们的代数数据类型和模式匹配。如果可以,请切换到功能语言。由于缺乏对ADT和模式匹配的语言支持,Composite和Visitor只是丑陋的解决方法。

答案 2 :(得分:0)

IMO,我会让每个具体的类(例如BinaryOp,Variable)扩展Visitor类。这样,创建BinaryOp对象所需的所有逻辑都将驻留在BinaryOp类中。这种方法类似于Walkabout pattern。它可以使您的工作更轻松。