1. Complete transformation behavior is specified by Java classes
The semantics for ELIDE modifiers are specified with Java classes. For
example, to add the modifier visited<SomeVisitor>, a developer would write:
public class Visited extends Transform
{
// behavior
}
where Transform is the base class for all ELIDE transformations. This
approach provides developers with all of the benefits of the OO
paradigm: Transformations can inherit behavior from other
transformations, and the behavior can be cleanly encapsulated. As
well, this allows a degree of consistency among transformation
specifications and the base code.
2. Individual 'atoms' of behavior for a given transform are specified by
methods on these classes
A given transformation may involve a number of separate operations.
For example, the visited<> modifier both adds an appropriate visit()
method to the visitor and adds an accept() method to the visited
class. These are represented by the methods addVisited() and
addAccept(), giving us the following:
public class Visited extends Transform
{
public void addVisited(ClassNode target, String visitorName) {...}
public void addAccept(ClassNode target, String visitorName) {...}
}
Each of these methods takes as their first argument an object representing
the "target" of the transformation, the entity in the source code to which the modifier
was attached (in this case a class modified by visited<SomeVisitor>). The remaining arguments
correspond to any parameters that appeared within the angle brackets of the modifier.
The transformation methods can be overloaded both on target type and on number of arguments.
For example, the modifier
synchronized<> class MyClass { ... }
would try to invoke any methods of Synchronized that took a ClassNode and no other arguments, whereas
synchronized<globalLock> void reset() { ... }
would try to invoke any methods of Synchronized that took a MethodNode and a single String.
3. Implementing transform behavior
ELIDE allows developers to implement transform behavior in several
ways, all designed to provide as much convenience and familiarity as
possible. The basic API builds on that provided by java.lang.reflect,
providing accessor methods such as getDeclaringClass(),
getModifiers(), getDeclaredFields(), etc. and adding mutator
functionality such as add(someNode), makePublic(), ... This API can
be quite useful for simple transformations (or as part of more complex
ones). For example, a simple method on a transform that makes all
fields of a class private could be written as:
public void makeAllFieldsPrivate(ClassNode target)
{
FieldNode[] fields = target.getDeclaredFields();
for(int i = 0; i < fields.length; i++)
fields[i].makePrivate();
}
For more complex transformation methods, such as those that add
methods to classes, a more convenient representation is helpful. Using
the extended string literal syntax, blocks of code, methods, or entire
classes can be specified in a code template, which allows developers to
write transformations in a more familiar way. For example,
addAccept() could be written as:
public void addAccept(ClassNode target, String visitorName)
{
target.extend(
%{
public void accept(#{visitorName}# visitor)
{
visitor.visit(this);
}
}%);
}
The #{}# delimiters provide "holes" in the string literal where
arbitrary code can be inserted. This can be used simply to insert
the value of a variable, as we have done here, or to perform more complex
computations.