uehaj's blog

Grな日々 - GroovyとかGrailsとかElmとかRustとかHaskellとかReactとかFregeとかJavaとか -

Groovy 1.7のキモはAST変換である

AST変換についてはこの資料もどうぞ。

こないだ出たGroovy 1.7 の新機能の目玉は、AST変換周りです。これは間違いないです。power assertも興味深いですが、実際にはpower assert自体、グローバルなAST変換と呼ばれるものによって、AST変換として実現されてます。グローバルAST変換は

groovy-1.7-beta-1/src/main/META-INF/services/org.codehaus.groovy.transform.ASTTransformation

で定義されていて、これをみると他にGrab変換もAST BuilderでもグローバルAST変換を使ってる。AST Builderの場合は「from code」のケースで使ってるとのこと*1

話をpower assertに戻すと、power assertが何かについては、説明するより、見るのが一目瞭然なのですが、先のURLにある例のとおり、

assert new File('foo.bar') /* hoge*/ == new File('example.txt')

こんなassesrtを書いておいて失敗すると、assertのエラーメッセージとして

$ groovy x.groovy
Caught: Assertion failed: 

assert new File('foo.bar') /* hoge*/ == new File('example.txt')
       |                             |  |
       foo.bar                       |  example.txt
                                     false

        at x.run(x.groovy:1)

こんなんでます。ちょっとこれすごくないっすか。だって、assertに渡してるのは複雑な式で、その式の構成要素それぞれの値が個別にわかるんですよ。変数を介在させても同じです。

a = 'foo.bar'
assert new File(a) /* hoge*/ == new File('example.txt')

でやると、

$ groovy x.groovy
Caught: Assertion failed: 

assert new File(a) /* hoge*/ == new File('example.txt')
       |        |            |  |
       foo.bar  foo.bar      |  example.txt
                             false

        at x.run(x.groovy:2)

です。これがAST変換のすさまじさです。実際に何をやってるかというと、assertに渡している式をコンパイル中にAST変換でトラバースして、部分式をその都度で保存しつつassertに渡すコードを生成し置き換えてるんですね。groovycでコンパイルしてjadでコンパイルするとわかるのですが、上のassert1行は以下のようにAST変換された上でコンパイルされてます。

CallSite acallsite[];
Object $valueRecorder;
acallsite = $getCallSiteArray();
$valueRecorder = acallsite[1].callConstructor($get$$class$org$codehaus$groovy$transform$powerassert$ValueRecorder());
Object obj = acallsite[2].call(
             $get$$class$org$codehaus$groovy$transform$powerassert$AssertionVerifier(),
               acallsite[3].call($valueRecorder,
               ScriptBytecodeAdapter.compareEqual(acallsite[4].call($valueRecorder,
                                acallsite[5].callConstructor($get$$class$java$io$File(),
                                     "foo.bar"),
                                new Integer(8)),
                          acallsite[6].call($valueRecorder,
                                acallsite[7].callConstructor($get$$class$java$io$File(),
                                     "example.txt"),
                                new Integer(41))) ? ((Object) (Boolean.TRUE)) : ((Object) (Boolean.FALSE)),
               new Integer(38)),
               "assert new File('foo.bar') /* hoge*/ == new File('example.txt')",
               $valueRecorder);
acallsite[8].call($valueRecorder);
return obj;
Exception exception;
exception;
acallsite[10].call($valueRecorder);
throw exception;

恐るべし。んで、AST Builderというのはこんなにも強力なAST変換における変換後のAST生成をgroovyで簡単に書ける仕組みです。
今まででもAST変換はgroovyで書けたのですが、変換後のツリー生成はいちいち手で組み立てなくてはならなくて、しち面倒くさかった。それが「ASTを表現するDSL」や、あるいは「文字列」で書けるようになった。

あと、ASTを処理する際には、groovy consoleに追加されたASTビューワが力を発揮するでしょう。さらに欲を言えば、将来的には、既存のASTに対する簡易なクエリで値がとってこれる仕組みがあるといいですね。

ASTの面では、Groovyは今後結構な発展が望めるのではないかと思いますね。良いのが思いつきませんけどね。

なお、ASTは本来コンパイラの内部構造なので、未来への互換性に関してのある種のリスクがあります。しかし、AST Builderによる「from code」の指定を行うなら、その心配はありません。同じバージョンのgroovyがASTを作る限り同じAST構造を持つはずだからです。from stringもそうですけど。

あともうひとつの懸念は、AST変換がコンパイル時に走ると遅くなるのではないかということです。回避策のひとつは上のように事前にgroovycしておくことでしょうか。なお、from stringよりfrom codeのほうが速そうですが、実際にはfrom codeの場合でもfrom stringにしてから再パースしてるようなので、事実はその逆です。

なんか作ってみよっと。

*1:./main/org/codehaus/groovy/ast/builder/AstBuilderTransformation.groovyにそう書いてある。