GRAILS.IO Avatar

Lessons learnt developing Groovy AST transformations

Updated 17/01/2012: Info on handling exceptions 

During the development of Grails 2.0 we shifted a significant amount of meta-programming logic to Groovy AST transformations. 

The advantages of AST transformations are several. More flexible DSLs can be created, you don’t pay a runtime startup cost to use these DSLs and as a framework developer you can inline method calls where necessary.

Having said that Groovy AST development has its dark sides and below I’m going to list some lessons learnt developing AST transforms:

1. Avoid Global Transforms

Global AST transformations have their place, but they also have limitations. Their order is undetermined, you cannot have dependencies between transformations and they tend to be more difficult to debug.

We ended up rolling our own equivalent to global transforms using Spring class path scanning and other the Spring Ordered interface to control ordering.

2. Write Utility Methods

AST transformation code can be very repetitive. We ended up coming up with GrailsASTUtils as well as a series of base classes to make common transforms easy. Things like adding a method that delegates to property (like Groovy’s @Delegate) or the equivalent of Java reflection APIs for introspecting ClassNode instances are available via GrailsASTUtils.

We also wrote an alternative to Groovy’s @Delegate called ApiDelegate that allows you to get a reference to the delegating instance and honors any annotations found in methods of the delegate.

3. Don’t Reuse AST nodes

Say you have a reference to a ClassNode instance obtained by inspecting the AST you’re transforming. Do NOT under any circumstances use this ClassNode to add a new method or property to the class being transformed. Doing stuff like:

 
  MethodNode fooMethod = classNode.getMethod("foo", new Parameter[0]) 
  classNode.addMethod( new MethodNode("newMethod",                                       
                                       Modifier.PUBLIC, 
                                       fooMethod.getReturnType(),                                       
                                       new Parameter[0],                                      
                                       new ClassNode[0],
                                       methodBody);  ) )

Where you are reusing the ClassNode returned from getReturnType() is very dangerous and lead to all sorts of generics related errors. Always create a new ClassNode reference from an existing one using ClassHelper:

 
  MethodNode fooMethod = classNode.getMethod("foo", new Parameter[0]) 
  classNode.addMethod( new MethodNode("newMethod",                                       
                                       Modifier.PUBLIC, 
                                       ClassHelper.make(fooMethod.getReturnType().getName()),                                       
                                       new Parameter[0],                                      
                                       new ClassNode[0],
                                       methodBody);  ) )

The above using of ClassHelper.make(..) will save you loads of time, trust me.

4. Get comfortable with byte code and decompilers

Nobody said writing AST transformations was for the feint hearted and if you’re going to write any non-trivial transform you’ll need to get comfortable with using javap and a Java decompiler just in case something goes wrong in the byte code generation phase.

I recommend JD-GUI for byte code decompilation and easy viewing. Whilst javap is fine for seeing the raw byte code.

5. Never throw Exceptions from an AST transform

The Groovy compiler really doesn’t like it if you throw an exception from an AST transform. It also results in ugly errors to the user of your transform.

The proper way to report an error is via the SourceUnit which has an errorCollector property:

 
String messageText = "You can only negate a binary expressions in queries."
Token token = Token.newString(
                       expression.getText(),
                       expression.getLineNumber(), 
                       expression.getColumnNumber())
LocatedMessage message = new LocatedMessage(messageText, token, sourceUnit)
sourceUnit
   .getErrorCollector()
   .addError(message);

This will properly report a compilation error to the user. Notice too how you can pass information about the current expression node (line number, column number etc.) so that the compiler reports the location of the error nicely to the user.

Example AST Transforms in Grails

Finally, I thought I would list some of the AST transforms and what they do in Grails if you wish to see some examples in action:

Replies

Likes

  1. graemerocher posted this

 

Reblogs