更新接受了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?
}
};
问题:这种做法是不适合我的情况,还是我忽略了一些必要的东西?是否有一个共同的设计来应对这些问题?如果我还需要在不同的方向进行遍历怎么办?
答案 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。它可以使您的工作更轻松。