Thursday, October 13, 2011

Groovy の AST を楽に書く方法

AST を書くのは大変

Groovy は AST 変換の実装用に AST を生成する DSL を提供しています。ところがこの DSL は複雑で習得が困難です。次のような GString の AST を生成したいとします:
"Hi, $name."
その場合、DSL はこうなります。こんなもの構文と同じ数覚えられません:
gString 'Hi, $name.', {
    strings {
        constant 'Hi, '
        constant '.'
    }
    values {
        variable 'name'
    }
}

ソースコードから直接生成することもできますが、コンパイラによるチェックの恩恵を受けられないですし (これについては Joachim Baumann が説明しています)、DSL よりパフォーマンスが悪いです。次のベンチマークを見てください:
@Grab('com.googlecode.gbench:gbench:0.2.2')
import gbench.BenchmarkBuilder
import org.codehaus.groovy.ast.builder.AstBuilder

def benchmarks = new BenchmarkBuilder().run {
    'DSL to AST' {
        new AstBuilder().buildFromSpec {
            gString 'Hi, $name.', {
                strings {
                    constant 'Hi, '
                    constant '.'
                }
                values {
                    variable 'name'
                }
            }
        }
    }
    'Code to AST' {
        new AstBuilder().buildFromString('"Hi, $name"')
    }
}
benchmarks.prettyPrint()

            user system cpu    real

DSL to AST     0      0   0  339918
Code to AST    0      0   0 2076590
というわけで、DSL を実装コードやテストコードを見ながら書く羽目になるのですが、これが大変な作業になることは簡単に想像してもらえるはずです。

どうすれば楽に書けるのか

この問題を解決する為に DSL をコードから自動生成してくれるライブラリを作りました。 AstSpecBuilder と名付けたこのライブラリはここで公開しています。使い方はとても簡単、build メソッドに馴染みのあるコードを文字列で渡すだけです:
import astspecbuilder.*

def spec = new AstSpecBuilder().build('"Hi, $name."')

def expectedSpec =  '''\
block {
    returnStatement {
        gString 'Hi, $name.', {
            strings {
                constant 'Hi, '
                constant '.'
            }
            values {
                variable 'name'
            }
        }
    }
}
'''
assert expectedSpec == spec
AST からも生成できます。というより実は上のメソッドは次のコードのショートカットに過ぎません:
import astspecbuilder.*

def ast = new AstBuilder.buildFromString('"Hi, $name."')
def spec = new AstSpecBuilder().build(ast)

インデントはデフォルトではスペース4つですが、変更するオプションが付いてます。次のようにすると DSL がタブ1つでインデントされます:
def spec = new AstSpecBuilder(indent: '\t').build('"foo"')

def expectedSpec = '''\
block {
\treturnStatement {
\t\tconstant 'foo'
\t}
}
'''
assert expectedSpec == spec

No comments:

Post a Comment