Dispatch

A rewriter describes how a COL tree is transformed. The root of the tree is passed to one of the dispatch methods: each dispatch method decides what should happen next. By default we always recurse into the tree, querying dispatch for the child nodes of our current node. You might imagine that the default implementation of dispatch for a Plus node is structured like this:

def dispatch(e: Expr[Pre]): Expr[Post] =
  e match {
    case Plus(left, right) =>
      Plus(dispatch(left), dispatch(right))(e.o)

    /* ... other alternatives ... */
  }

Structural transformations

The idea is now that you override dispatch methods for the nodes you would like to rewrite. For example, we might want to make an optimization that simplifies away additions of 0:

val ZERO = BigInt(0)

override def dispatch(e: Expr[Pre]): Expr[Post] =
  e match {
    case Plus(IntegerValue(ZERO), e) => dispatch(e)
    case Plus(e, IntegerValue(ZERO)) => dispatch(e)

    case other => other.rewriteDefault()
  }

A new concept here is rewriteDefault: it is the mechanism used to peel off one layer of a program tree and recurse into its children using dispatch. It is automatically generated for each node, here in PlusRewrite. It takes an implicit value of type AbstractRewriter, which is the rewriter used to call dispatch again. By default it picks the implicit val rewriter which is defined for each AbstractRewriter. Note that Scala requires that a match statement is complete. A default case as above also makes the match complete.

A common mistake is to erroneously call rewriteDefault too often: when recursing into nodes yourself you should almost always call dispatch, which will decide to do with the sub-node.

Transforming Declarations

See also: Succession

Declarations are special because it is not required that a declaration is transformed into a declaration. This is also represented in the signature of the dispatch method for Declaration[_]:

def dispatch(decl: Declaration[Pre]): Unit

Nevertheless declarations have to be placed in the tree, and references to declarations need to be rewritten. Both are arranged through an instance of the Scopes class — one for each kind of declaration. Rewriting references is tricky, so this is explained later on in the section on succession.

A node that contains declaration, such as Class[G](decls: Seq[ClassDeclaration[G]], ...), needs to come up with a new list of ClassDeclaration[G]. We first have to consider the appropriate Scopes instance: in this case classDeclarations. We call classDeclaration.collect, which pushes a collection buffer onto a stack. A lambda is passed to collect in which we can take any actions, chiefly to declare declarations of kind ClassDeclaration. By convention we call dispatch on all the Pre-state declarations that we are interested in, and (optionally) we can declare one or more other ClassDeclarations we need to add. All this is also allowed to happen recursively: we can be in a statement somewhere in the class and still declare a helper method to the classDeclarations scope.

Combining this information we can obtain the new declarations of a class as such:

val newDeclarations = classDeclarations.collect {
  cls.decls.foreach(dispatch)
}._1

Collect returns a tuple: a list of new declarations, and whatever the result of the lambda is (here: Unit). On the other side of the fence, the old class declarations are rewritten and declared to the scope. Reproducing the default behaviour for ClassDeclarations might look like this, skipping over succession for the moment:

override def dispatch(decl: Declaration[Pre]): Unit =
  decl match {
    case decl: ClassDeclaration[Pre] =>
      classDeclarations.declare(decl.rewriteDefault())

    case other =>
      allScopes.anyDeclare(other.rewriteDefault())
  }

Our friend rewriteDefault makes another appearence, which peels off the ClassDeclaration constructor, and recurses into its components. In the case that a ClassDeclaration contains further declarations, it will call collect again in a similar way. For example, an InstanceMethod is a ClassDeclaration and contains declarations of kind Variable for its arguments.

After rewriting the declaration fully by opting to rewrite it in a one-to-one manner, it is declared to the buffer of ClassDeclarations that we are currently collecting.

Lastly we must always complete the match, which is done by also rewriting other declarations one-to-one, and using allScopes to find the correct scope to put the declaration in.