Details for exercise 7

The VariableScope

Variable scopes are used internally by the compiler to determine what a variable, in the code, references. For example, imagine the following code:

def myMethod(String arg) { (1)
	arg.toUpperCase()  (2)
}
1 a Parameter named arg
2 a VariableExpression named arg, referencing the Parameter arg

The AST represents the arg variable inside the code body as a VariableExpression, which is a different instance from the Parameter instance of the same name. We do this because you cannot be sure at compile time (well, depending on the compile phase) what a token represents.

Internally, the compiler will create variable scopes to resolve those variables, and in the end, the VariableExpression will have an accessed variable set to the actual Parameter.

Tips and tricks

  • If you feel lazy, use the VariableScopeVisitor class, but make sure it does not visit the same code twice, or it would be broken!

Comments

Variables are resolved at the semantic analysis phase. That is to say that when you run an AST transformation, variables are already resolved. As a conclusion, you have to deal with the variable targets by yourself, either by specifying the accessed variable for each variable by yourself, or using the VariableScopeVisitor (with care).

Compatibility with @TypeChecked and @CompileStatic

Writing an AST transformation is not difficult. Writing an AST transformation which is compatible with type checking (and static compilation) is trickier. In particular, you can face cryptic error messages when mixing @CompileStatic with another AST transformation. There are various reasons for that and we will explain some ideas here.

Tips and tricks

  • Use types. If you create a field, a variable, or anything else that can have a type, define it, or your AST transformation will not be compatible

The groovy.stc.debug system property Incompatible AST transformations can generate code which cannot be visible even if you face a compiler crash. Often, the compiler would have been able to tell you about incompatibilies before it crashes, but in practice, it does not. The reason it does not is purely internal: in Groovy, generated code, as well as code generated by AST transformations, use negative line and column numbers (-1). While you can change the lines (and it is recommanded to do so if your AST transformation transforms code (compared to generating code), because it allows debuggers to step into the code. However, for purely generative code, for example when @ToString generates the toString() method, there’s no actual source code that you can point to. Groovy considers generated code as trusted. This means that internally generated code (such as the generated methods) and code generated by AST transformations is supposed to be safe and compatible with static compilation. But what happens if it is not? Imagine, for example, that the type checker analyzes this generated code:

def myToUpperCase(foo) {
   foo.toUpperCase() + "!"
}

The problem is that the AST transform did not specify the type of foo, so the type checker would normally complain on the foo.toUpperCase() method because it’s not defined on Object. Since it’s generated code, the line number is negative, so the type checker silently drops the error. The result is that if you try to statically compile this, there’s no target method set for the toUpperCase() call, and the compiler will crash. To debug this, we recommand to set the groovy.stc.debug to true. The compiler will then show you type checking errors which occur in generated code (like AST xforms). The drawback is that it will show you false positives, but at least, you should be able to find errors which occur in your own code.