Lets continue the development agile DSL for music notation with Groovy. If you remember our fundamental concepts are Scores, Parts (for instruments), Phrases and Notes.
At a certain time we are typically working only on a Score, Part and Phrase (indeed we might work only on a single Score during a session). So, we would like to have a concept of default Score, Part and Phrase, and avoid referring to it (unless of, course, we want to change the default). For instance, instead of writing:
... myScore = score(name:"Row Your Boat") myPart = part(title: "Flute", instrument: FLUTE, channel: 0) myPhrase = phrase(startTime: 0.0) myPhrase.addNoteList pitchArray, rhythmArray
(pitchArray and rhythmArray are pre-defined before)
We want to write, the much simpler
1 2 3 4 5 | ... score(name:"Row Your Boat") part(title: "Flute", instrument: FLUTE, channel: 0) phrase(startTime: 0.0) addNoteList pitchArray, rhythmArray |
All Score, Part and Phrase methods will implicitly refer to myScore, myPart and myPhrase. Note that you can still explicitly refer to them. Indeed this will be necessary has most scores.
In this first instalment (of 2) we will not deal with line 5 above. Part 1 is actually the bulk of the work. Breath deeply has this will be the tough part.
We will use Groovy ASTTransformations for this. The Groovy compiler allows us to attach code to it while it is working. We can manipulate the AST (Abstract Syntax Tree) of our code during most of the compilation stages. This means that we will need a separate program to attach to the compiler. So, if we step back we now have 3 artifacts:
- The code to do the AST transformation (called during compilation)
- The core DSL implementation (with all the other stuff except AST transforms)
- Your music scripts with your score
So we need kind of a sub-project to handle this as Groovy requires a separate jar with the AST transformation code. This separate jar will have to have a descriptor file in the META-INF/services directory called
org.codehaus.groovy.transform.ASTTransformation
That is the name of the file (big one eh?). Inside it should have only one line: the fully qualified name for the class implementing the transformation (SimpleTransformation in our case).
OK, now we need to develop SimpleTransformation. This is not a trivial bit of code, I will splash it here and the it line by line (only dealing with Scores – Parts and Phrases are similar):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | @GroovyASTTransformation(phase=CompilePhase.CONVERSION) public class SimpleTransformation implements ASTTransformation { public void visit(ASTNode[] astNodes, SourceUnit sourceUnit) { BlockStatement sblock = sourceUnit.getAST()?.getStatementBlock() List stmts = sblock.getStatements() int numStmts = stmts.size() for (int i=0;i < numStmts ;i ++) { Class cls = stmts.get(i).getClass() if (cls == ExpressionStatement) { Expression es = stmts.get(i).expression if (es.getClass() == MethodCallExpression) { String method = es.method.text if (method.equals("score")) { Expression e = transformBinary("myScore", es) stmts[i].setExpression(e) } } } } } ... |
So
- Lines 1-4 – Boilerplate of our class so that the Groovy compiler uses this. There is one important part here: the phase where the code will attach. For now I am attaching to the conversion phase. But this might change in the future (I would like to do some type analysis, but I do not even think that that is possible with Groovy. If it is possible, than analysis would have to be done at a later phase).
- 5-6 – We get the statements of the script that we are compiling
- 7 – Here we iterate through all statements. Note the for and not an each/closure. I do this because I might want to change the statement list (like adding stuff at the end – prints). That is not so easy with each/closures
- 10 – We get all expressions. This means we ignore fors, ifs, switches, function definitions, … We are not going deep, just changing methods at the top level of the code.
- 13-17 – If it is a Method Call, and it method name is called score then we apply our transformation (16) and replace the expression (line 17)
Our transformation is:
BinaryExpression transformBinary(String var, Expression expression) { BinaryExpression newExp = new BinaryExpression( new VariableExpression(var),new Token (100, "=", 1, 1), expression) return newExp }
OK, here the bulk of the work is done: We create a new BinaryExpression composed of a Variable (called myScore in our case - as per the code above), a Token and then we attach the old expression, as is. So score(name:"Row Your Boat") becomes myScore=score(name:"Row Your Boat").
Now, a confession. The 100 in the Token was a reverse engineering of an expression. I do not know where the table of options for token types is (If you know, please tell).
You will need a few imports to do the above, by the way
import org.codehaus.groovy.ast.* import org.codehaus.groovy.ast.expr.* import org.codehaus.groovy.ast.stmt.* import org.codehaus.groovy.control.* import org.codehaus.groovy.syntax.Token import org.codehaus.groovy.transform.*
With all this you now create a jar that will have to be on the classpath of the Groovy compiler. So, this code will be used by the Groovy compiler to manipulate the AST.
Note that this code is pretty basic: It will not recurse through for/switch statements, will not go in closures, functions, etc. It will also only look at the first token in a method call expression. I will deal with this in time (not in the second part of this article). For now it is good for illustrative purposes and good for my personal needs.
Some final notes...
You can inspect an AST from groovyConsole (helps a lot), here is an example for sc=score(name:"Row Your Boat"):
Another point if that these kind of transformations are a bit heavy in the theory and heavy in the approach. For instance, it was difficult, in netbeans to setup a project architecture that would allow easy build (an agile cycle of develop/build/test). This is of course because part of the code has to be hooked to the compiler and IDEs are not normally used to do that. It is a bit like compiling part of the compiler before going to the actual code. Well, I finally switched to emacs+gradle. Any excuse to stop using Oracle software (which netbeans nowadays is) is fair game for me.
In the second instalment we will trap method calls like addNoteList so that addNoteList listOfNotes becomes myPhrase.addNoteList listofNotes (like line 5 on the initial example above). In this case we will use some introspection to determine the method names of Score, Part and Phrase. The second part will be cooler as the bulk of the boilerplate work was done here.
You can find the code in launchpad. Note that this is still in early stages.
Comments and improvements will be most appreciated!











