Compile-Time Metaprogramming in Groovy, Part 1
Join the DZone community and get the full member experience.
Join For FreeI began playing with AST transformations in Groovy a few days ago (Review of the chapter about AST transformations (GINA2)). Being so excited about the technology I’ve decided to write a few posts (step by step tutorials) describing how to add compile-time metaprogramming to your everyday arsenal.
In this post I’m going to demonstrate how to write a local AST transformation. The transformations I’m going to write are a bit simplified but they really work and it won’t be a problem to add more functionality on top of them.
Being a strong believer that code speaks better than words here is a simple Spock test:
class ExecuteOnceTransformationTest extends Specification{ def 'should execute a method only once'() { when: def testObject = new GroovyShell().evaluate(''' class TestClass { int counter = 0 @victorsavkin.sample1.ExecuteOnce void executeOnceMethod(){ counter ++ } } new TestClass() ''') then: testObject.counter == 0 when: testObject.executeOnceMethod() then: testObject.counter == 1 when: testObject.executeOnceMethod() then: testObject.counter == 1 } def 'should generate a compilation error if return type is not void'() { when: new GroovyShell().evaluate(''' class TestClass { @victorsavkin.sample1.ExecuteOnce def executeOnceMethod(){ 'ReturnValue' } } new TestClass() ''') then: def e = thrown(MultipleCompilationErrorsException) e.errorCollector.errorCount == 1 def firstError = e.errorCollector.errors.first() firstError.cause.message.startsWith('ExecuteOnce can be applied only to methods returning void') == true } }
As you can see the transformation I’m going to write can be applied to methods and it will change an AST to guarantee that the method will be executed only once. Also, this transformation can be applied only to methods returning void. A real life example where it can be used: You can annotate your init method using @ExecuteOnce annotation and then call it in all your methods. Initialization will be performed only once. As our test is ready we can start implementing all the pieces.
Create an annotation
@Retention(RetentionPolicy.SOURCE) @Target([ElementType.METHOD]) @GroovyASTTransformationClass(['victorsavkin.sample1.ExecuteOnceTransformation']) @interface ExecuteOnce { }
As we only need our annotation to be presented at compile time we are using SOURCE retention policy. It’s not hard to get that @GroovyASTTransformationClass specifies a class that will be used to perform our transformation.
Create a class implementing ASTTransformation
@GroovyASTTransformation(phase = SEMANTIC_ANALYSIS) class ExecuteOnceTransformation implements ASTTransformation { ... void visit(ASTNode[] astNodes, SourceUnit sourceUnit) { ... } }
SEMANTIC_ANALYSIS is the earliest phase that can be used for a local transformation. The earlier your phase the less implementation details you have in your AST. So if you want to manipulate Groovy code you should use early phases. If you want to manipulate generated Java code (including generated getters and setters) INSTRUCTION_SELECTION probably would be a better choice.
ASTTransformation is a very confusing interface as it is used by both local and global transformations and it is used differently. In this example, we will use astNodes for manipulating AST and sourceUnit for adding compilation errors.
Create a Specification object to validate astNodes object.
class ExecuteOnceTransformationSpecification { boolean isRightReturnType(astNodes){...} SyntaxException getSyntaxExceptionForInvalidReturnType(astNodes){...} boolean shouldSkipTransformation(astNodes){...} }
To make this post shorter I didn’t include the code of ExecuteOnceTransformationSpecification. You can find all the source code on a github repository I’ve created for this post.
Create AstFactory for creating parts of AST
Creating pieces of AST looks for me a bit like black magic, so I think it is a good idea to isolate it inside this factory.
class ExecuteOnceAstFactory {
def createGuardIfStatement(fieldName) {
def ast = new AstBuilder().buildFromString SEMANTIC_ANALYSIS, true, """
if(! ${fieldName}){
${fieldName} = true
}
"""
ast[0].statements[0]
}
def generatePrivateFieldNode(fieldName) {
def ast = new AstBuilder().buildFromString SEMANTIC_ANALYSIS, false, """
class ${fieldName}_Class {
private boolean ${fieldName} = false
}
"""
ast[1].fields.find{it.name == fieldName}
}
}
As you can see I’am using AstBuilder.buildFromString to construct pieces of AST. There are many ways to create AST but buildFromString looks like the easiest for me so far. I’m using the same compilation phase SEMANTIC_ANALYSIS for both pieces. Don’t worry if didn’t get what this chunk of code does. I’d advice you to run tests with a debugger and just take a look what will be generated in every case. After a few tries the understanding will come to you.
Put all pieces together to finish your transformation
@GroovyASTTransformation(phase = SEMANTIC_ANALYSIS) class ExecuteOnceTransformation implements ASTTransformation { private specification = new ExecuteOnceTransformationSpecification() private astFactory = new ExecuteOnceAstFactory() void visit(ASTNode[] astNodes, SourceUnit sourceUnit) { if(specification.shouldSkipTransformation(astNodes)) return if(!specification.isRightReturnType(astNodes)){ sourceUnit.addError specification.getSyntaxExceptionForInvalidReturnType(astNodes) return } MethodNode method = astNodes[1] def fieldName = createFieldName(method.name) addGuardFieldToClass fieldName, method addGuardIfStatementToMethod fieldName, method } private addGuardFieldToClass(fieldName, method) { def field = astFactory.generatePrivateFieldNode(fieldName) method.declaringClass.addField field } private addGuardIfStatementToMethod(fieldName, method) { def guardStatement = astFactory.createGuardIfStatement(fieldName) addAllStatementsOfMethodIntoGuardIf(guardStatement, method) method.code = guardStatement } private addAllStatementsOfMethodIntoGuardIf(guardIfStatement, method) { BlockStatement ifBlock = guardIfStatement.ifBlock ifBlock.statements.addAll(method.code.statements) } private createFieldName(methodName) { '$_victorsavkin_samples_execute_once_guard_for_' + methodName } }
Coming back to our example, if you have this class:
class TestClass {
int counter = 0
@victorsavkin.sample1.ExecuteOnce
void executeOnceMethod(){
counter ++
}
}
The compiled code will look like:
class TestClass {
private $_victorsavkin_samples_execute_once_guard_for_executeOnceMethod = false;
int counter = 0
void executeOnceMethod(){
if(! $_victorsavkin_samples_execute_once_guard_for_executeOnceMethod){
$_victorsavkin_samples_execute_once_guard_for_executeOnceMethod = true;
counter ++
}
}
}
It is so simple. In real life we should handle concurrency issues but it will complicate the example, so I haven’t done it here.
To sum up:
- Writing your own AST transformation isn’t the easiest thing to do, but it’s doable and, what is also very important, testable. In addition, it allows you to change the semantics of the language which can’t be done using runtime metaprogramming.
- I’d advice you to buy Groovy in Action 2 if you want to learn more about the topic. I would not wait till the final version is released and buy it right now.
- If you want to get excited about compile-time metaprogramming take a look at Spock. It is a good illustration of what can be done using AST transformations and can’t be done using runtime metaprogramming.
GitHub Repository
https://github.com/avix1000/AST-Transformations
From http://victorsavkin.com/post/4568615925/compile-time-metaprogramming-in-groovy-part-1
Opinions expressed by DZone contributors are their own.
Comments