-
Notifications
You must be signed in to change notification settings - Fork 25
Visitor Guide
Note: This guide assumes basic knowledge of the structure of the Overture AST. Head over to the AST Guide for an intro to the topic.
Visitors are one of the most important concepts in Overture development. This guide will teach you the basic ideas of the visitor implementation in Overture and how to use it to implement functionalities. For more general information about the visitor pattern, Wikipedia is a nice place to start.
Code stubs and validation tests for the exercises in this guide are available in
the dev tutorials repository. To use them, clone the
repository then build and import the visitor
project. You will get test failures in the beginning but they will go away once you solve the exercises.
The basic idea behind the visitor pattern is to detach an algorithm from the
data it runs on. To put it another way, we replace method invocation with
visitor application. For example: node.typecheck()
becomes
node.apply(typecheckVisitor)
.
This pattern of application holds pretty much every time you need to interact with the AST.
A visitor is developed in a separate class from the AST and this class must be
marked as a visitor in order for the AST classes to accept it. The easiest
way to do this is to subclass the AnalysisAdaptor
class in the
org.overture.ast.analysis
package of the AST module.*
* : There are various mechanisms for declaring visitors and we will discuss them later. For now, the basic one suffices.
Once we have subclassed the AnalysisAdaptor
, we can apply the visitor right away but
nothing will happen. We need to add some behavior to the visitor and we do this
by implementing cases. Cases are implemented by overriding the existing methods
in the AnalysisAdaptor
. Take a look at it now and see how many cases we have!
A visitor has a specific case method for each kind of node in the AST. When the
visitor is applied to an AST node, the corresponding case method is executed.
For example, the caseASetEnumSetExp
method is executed when the visitor is
applied to a ASetEnumSetExp
node. The case method has a ASetEnumSetExp
argument and that corresponds to the node that it was applied to. The node can
then be analyzed in whatever way is necessary. For example, the code below
checks if a sequence enumeration is empty.
public class EmptySeqVisitor extends AnalysisAdaptor {
boolean isEmpty;
@Override
public void caseASeqEnumSeqExp(ASeqEnumSeqExp node)
throws AnalysisException {
isEmpty = node.getMembers().isEmpty();
}
}
Exercise 1: Write a visitor that counts the number of elements in a set
enumeration. Store the result in an instance variable and make it
accessible via getter method. If you are using the exercise project, add the
cases to Exercise01Visitor
in the exercises123
package. Check your work
with the tests in Exercise01Test
in the exercises123
package.
Click here for solutions.
In addition to the specific cases for each kind of node, there are lots of situations where you want to process several related nodes in the same way. For example, all binary expression nodes.
The visitor framework provides you with a mechanism for doing this: the default cases. Just as the AST is divided in various families of nodes, the default cases can be applied to any member of a node family. There are 3 types of default cases:
-
defaultINode
: this applies to any node in the AST; -
defaultP*
: these apply to a family of nodes. Ex:defaultPExp
applies to anyPExp
node; -
defaultS*
: these apply to a sub-family of nodes. Ex:defaultSFunctionDefinition
applies to anySFunctionDefinition
node.
There is a default case method for every root node in the AST so it is possible to figure out which one you need to use by looking for the common ancestor.
Note that the default cases only have the root superclass as argument so
attributes that are only present in the subclasses are not visible. For
example, the defaultSSetExp
cannot see the members in ASetCompSetExp
node.
If you need access to attributes in the specific node class, use the specific
case method. Don't do instanceof
checks and casts on nodes inside default case
methods!
Finally, it is very important to note that if a visitor has multiple cases that
can be applied to a node, the most specific one is applied. So, for
APlusNumericBinaryExp
it would go:
caseAPlusNumericBinaryExp
> defaultSNumericBinaryExp
> defaultSBinaryExp
> defaultPExp
> defaultINode
There is a direct correspondence between case priority and node class
hierarchy. For reference, here is the class hierarchy for APlusNumericBinaryExp
:
APlusNumericBinaryExp
> SNumericBinaryExp
> SBinaryExp
> PExp
> INode
If you're getting some unexpected behaviour, check to make sure there isn't a narrower case method being applied.
Exercise 2: Write a visitor that computes the name of any definition. This
can be done with 1 case. Store and provide access to the result in the same way
as you did for exercise 1. If you are using the exercise project, add the
cases to Exercise02Visitor
in the exercises123
package. Check your work
with the tests in Exercise02Test
in the exercises123
package.
Click here for solutions.
So far, the visitor uses we have discussed were single case. In other words, apply once and you're done. Most of the time, you will not be so lucky and after visiting the initial node, you will have to visit additional sub-nodes. While you can do this by direct invocation of the case method, it's better to just reapply the visitor, like this:
caseAPlusNumericBinaryExp(APlusNumericBinaryExp node) throws AnalysisException {
// do stuff...
node.getLeft().apply(this);
}
Exercise 3: Write a visitor that, for any arithmetic expression,
calculates how many terms the expression has. Handle the result as before.
If you are using the exercise project, add the
cases to Exercise03Visitor
in the exercises123
package. Check your work
with the tests in Exercise03Test
in the exercises123
package.
Click here for solutions.
All the visitors we have used until now have stored their results in an
instance variable that must be attained via getter method. Likewise, we need
instance variables to hold any auxiliary data the visitor requires. While this
approach works for simple functionalities, it can get out of hand and we have a
better solution this in the form of question and answer visitors. There are
3 versions that can be subclasses in the org.overture.ast.analysis
package:
-
QuestionAdaptor<Q>
is the parameterizable question visitor.Q
can be of any class you wish, though it must be the same for all cases in a given visitor. In this visitor, the questionQ
is passed as a second argument to each case method. This allows you to pass additional information to the case. -
AnswerAdaptor<A>
is the parameterizable answer visitor.A
can be of any class you wish, though it must be the same for all cases in a given visitor. In this visitor, each case must return an object of classA
. Also, this visitor has two additional methods that must be implemented:createNewReturnValue(INode node)
andcreateNewReturnValue(Object node)
. These methods are invoked when no other case can be matched because something must be returned. -
QuestionAnswerAdaptor
this visitor combines the functionalities of the two previous ones.Q
andA
can any two classes you wish. This visitor also needs the twocreateNewReturnValue
methods. The Overture type checker is implemented as aQuestionAnswerAdaptor
where the question is the environment for the definition being type checked and the answer is the of the definition.
Exercise 4: Rewrite all previous exercises to use question and answers
visitors. If you've implemented all exercises in a single class, you will need
to separate them now. If you are using the exercise project, add the
cases to Exercise4{1,2,3}Visitor
in the exercise4
package. Check your work
with the tests in Exercise4{1,2,3}Test
in the exercise4
package.
Click here for solutions.
There is another way to declare visitors, in addition to subclassing the
adaptors. You can also implement the various interfaces in
org.overture.ast.analysis.intf
. These provide the same set of case methods
but because they are interfaces you must implement all of them. Generally,
you're better off subclassing the adaptors but the interfaces are necessary
for some advanced cases.
You may have seen a DepthFirstAdaptor
in the analysis package. This is a
special visitor that, in addition to any behaviour you implement, performs an
automatic depth-first traversal of the entire tree it is applied to. There is
no need to do case dispatching with this visitor. We'll learn how and when to
use it in an upcoming tutorial.