访问函数(Visitor Function)是verilator算法实现的主要工作方式之一,本节将介绍一下访问函数。
访问者模式
Verilator采用“访问者”的设计模式(visitor pattern)来实现提炼和优化传递(passes),此方法允许传递算法与其操作的抽象语法树(AST)分离。
所有的访问者都是类VNVisitor的派生类,而AstNode的所有派生类都实现了accept方法,accept方法将VNVisitor的派生类实例作为参数,进而可以应用VNVisitor的visit方法到活跃的AstNode实例(AstNode派生类的accept方法以VNVisitor的派生类实例作为参数并调用对应实例的visit函数并通过this指针将VNVisitor派生实例的指针传入)。
一种可能的问题是,调用accept方法可能会执行一种编辑,从而有可能毁坏作为参数传入的node,AstNode提供了acceptSubtreeReturnEdits方法来执行accept并返回编辑后的node(即使原始node被销毁,若未销毁则返回原始node)。
访问者类通过重载visit函数来访问不同的AstNode派生类,如果没有发现具体的visit函数实现,根据C++的规则会按顺序逐层网上找,例如如果要执行AstIf类的函数accept,查找顺序如下:
void visit(AstIf* nodep)
void visit(AstNodeIf* nodep)
void visit(AstNodeStmt* nodep)
void visit(AstNode* nodep)
参数传递
有三种方式在visitor函数之间传递参数:
1. visitor类的成员变量,通常是信息从父类传递到子类;
2.用户特征(user attributes),每一个AstNode(注意是AST node,不是visitor)有5个用户特征,可以通过函数user1-user5()来获取,得到的是一个整数,或者获取一个指针(类型为AstNUser)通过函数user1p - user5p(图遍历中经常用到的一种技术)。
一个visitor首先通过调用AstNode::user#ClearTree()来清理掉其想用的用户特征,然后就可以用任何其想要的数据标记任何node的user#()。Readers仅仅会调用 nodep->user(),当然需要通过cast转换成合适的类型,所以经常会看到VN_CAST(nodep->userp(), SOMETYPE)这种类型转换操作。在每一个visitor的顶层都有注释来说明user()特征如何应用到visitor类的。例如:
// NODE STATE
// Cleared entire netlist
// AstModule::user1p() // bool. True to inline this module
这说明,在AstNetlist中user1ClearTree()被调用,每一个AstModule的user1()用来标识我们是否要内嵌它。
这些注释非常重要,必须要确定user#()在一个给定的AstNode类型中绝不允许被用做两种不同的目的。
需要注意,调用user#ClearTree的速度很快,不会遍历整棵树,所以它经常在每个模块中调用。
3.参数可以在visitor之间传递,这种方式类似于“正常”函数调用者(caller)到被调用者(callee)的方式。这是类型AstNUser的第二个参数vup,在大多数visitor函数中都会被忽略。V3Width实现了这种方式,但被证明比上面说的还要糟糕。(V3Width几乎是第一个写入的模块,由于其要在每个地方都传递vup进而拖慢了速度,将来很有可能被删除)。
迭代器
VNVisitor提供了一套迭代器来方便游走整个树结构,每一个操作都在VNVisitor类,并且取参数类型:AstNode*的参数。
iterate: 应用AstNode的accept方法到visitor函数;
iterateAndNextIgnoreEdit:应用一个List中每一个AstNode的accept方法(通过指针nextp和backp连接);
iterateAndNextNull:只有给出的node是非空的时候,应用一个List中每一个AstNode的accept方,法,如果一个node被一个accept调用编辑了,将再一次调用accept;
iterateListBackwards:从最后一个开始应用一个List中每一个AstNode的accept方法;
iterateChildren:在每一个子类执行操作op1p通过op4p按顺序应用iterateAndNextNull;
iterateChildrenBackwards:在每一个子类执行操作op1p通过op4p按顺序应用iterateListBackwards。
注意子类改变时迭代器的使用
Visitors经常用一个顶点去替代另一个顶点,V3Width和V3Const是两个主要的例子。对于这种替换的父类visitor,应该意识到调用这种迭代可能导致子类改变,例如:
// nodep->lhsp() is 0x1234000
iterateAndNextNull(nodep->lhsp()); // and under covers nodep->lhsp() changes
// nodep->lhsp() is 0x5678400
iterateAndNextNull(nodep->lhsp());
此段代码是可以正常运行的,因为即使第一次迭代会导致新的顶点替代lhsp(),编辑完之后会更新nodep->lhsp(),第二次调用将能正确地看到改变。而另一种情况:
lp = nodep->lhsp();
// nodep->lhsp() is 0x1234000, lp is 0x1234000
iterateAndNextNull(lp); **lhsp=NULL;** // and under covers nodep->lhsp() changes
// nodep->lhsp() is 0x5678400, lp is 0x1234000
iterateAndNextNull(lp);
会引起bug或者core,因为lp是一个悬空指针(dangling pointer),因此建议设置lhsp=NULL来保证避免这些悬空指针。另一种是,V3Width在特殊情况下使用acceptSubtreeReturnEdits,该方法在单个顶点上操作并且如果存在新的指针会返回。需要注意acceptSubtreeReturnEdits并不会执行nextp()转到对应的链接。
lp = acceptSubtreeReturnEdits(lp)
鉴别派生类
一个基本的要求是鉴别AstNode的派生类具体是哪一个,例如,一个visitor对于AstIf和AstGenIf可能不区分visit方法,都对应函数:
void visit(AstNodeIf* nodep)
然而有些方法如果AstGenIf的话,会添加一些额外的代码。Verilator为此提供了VN_IS的方法,对于任意可能的node类型,如果是此类型则返回true否则返回false。所以visit方法可以使用:
if (VN_IS(nodep, AstGenIf) {
<code specific to AstGenIf>
}
并且,VN_CAST类似于C++中的dynamic_cast,如果类型相符则返回指针,否则返回NULL空指针(ture 或者 false的情况用VN_IS,速度会更快)。