初めに
前回の記事で、二項演算子 /
をメソッド intdiv
に変換する AST 変換を作成しました。このとき、以下のようなコードを書いたのですが、完成品だけ見せられてもなぜそのようなコードになったのかが分からないので、このコードが産まれるまでの経緯をまとめておこうと思います。
public Expression transform(Expression expression) {
if (expression != null) {
// AST が二項演算子の呼び出しである場合
if (expression instanceof BinaryExpression) {
// 二項演算子を表すトークンを取得
Token operation = expression.getOperation()
// 二項演算子の名称が「/」である場合
if (operation.getText() == "/") {
// 左側の AST を取得
Expression left = expression.getLeftExpression()
// 右側の AST を取得
Expression right = expression.getRightExpression()
// 左側の AST に対して再帰的に変換を行う
Expression nextReceiver = transform(left)
// 右側の AST だけからなる引数リストを作成して再帰的に変換を行う
Expression nextArguments = transform(new ArgumentListExpression(right))
// intdiv メソッド呼び出しの AST を作成する
Expression nextExpression = new MethodCallExpression(nextReceiver, "intdiv", nextArguments)
return nextExpression
}
}
return expression.transformExpression(this)
} else {
return null
}
}
GroovyConsole の利用
基本的な使い方
AST 変換を実装するには、変換前の AST と変換後の AST がどのような構造をしているかを把握する必要があります。GroovyConsole を使うことで、Groovy コードのどの部分がどのような AST になるかを調べることができます。
GroovyConsole は、Windows であれば、Groovy がインストールされているディレクトリ内の bin/groovysh.bat をクリックすることで起動することができます。起動すると、上下に 2 つに分かれたウィンドウが表示されます。上がテキスト入力画面で、下が実行結果が表示される画面です。Groovy コードを入力してメニューから Script → Run (もしくは Ctrl+R キー) でコードを実行できます。
さて、GroovyConsole には AST ブラウザが内蔵されていて、Groovy コードがどのような AST で表現されるかを調べることができます。メニューから Script → Inspect Ast (もしくは Ctrl+T キー) で AST ブラウザを開くことができます。
二項演算子 /
の AST の調査
さて、AST 変換 @Intdiv
の実装には、7 / 2
のような /
の呼び出しがどのような AST で表現されるかを調べる必要があります。これを GroovyConsole で調べてみましょう。
GroovyConsole を開き、コード入力画面に以下のコードを入力してください。
class Test {
public static void main(String... args) {
7 / 2
}
}
その後、AST ブラウザを開いてください。
この AST 変換は、変換の 8 つの段階のうち Semantic Analysis という段階で行うよう設定した (@GroovyASTTransformation
で設定した) ので、AST ブラウザの「At end of Phase」の部分を「Semantic Analysis」にします。ツリービューに「ClassNode - Test」という要素が表示されるので、それを辿って 7 / 2
に当たる部分の AST を見てみます。
ツリービューの右側に選択したオブジェクトのプロパティ情報が表示されます。ここの「class」という項目を見ることで、二項演算子の呼び出しは BinaryExpression
クラスで表現されることがわかります。「operatrion」という項には「("/" at 4:7: "/")」と表示されていて、おそらくこれが何の二項演算子であるか (+
なのか /
なのか) の情報なのでしょう。また、「leftExpression」という項を見ると 「ConstantExpression[7]」と表示されており、どうやら二項演算子の左側に書かれた式がここに格納されているらしいことが分かります。同様に、「rightExpression」は右側に書かれた式であることが分かります。
さて、これで必要な情報が揃いました。まず、transform
メソッドに渡された AST が BinaryExpression
クラスのオブジェクトかどうか判定すれば良いわけです。もしそうであったら、operation
プロパティにアクセスすることで、何の演算子なのかの情報が得られます1。ただ、operation
プロパティは Token
オブジェクトになっていて、このクラスの扱いがよく分からないので、Token
のGroovyDoc を見ることにしましょう。すると、getText
というメソッドがあることが分かるので、これを用いて演算子が /
であるか判定することにします。
演算子が /
であると判定された場合は、intdiv
メソッドの呼び出しのために /
の左右に書かれた式を取得しておく必要があります。それは leftExpression
プロパティと rightExpression
プロパティでしたね。
ということで、ここまでで以下のコードが書けました。
public Expression transform(Expression expression) {
if (expression != null) {
// AST が二項演算子の呼び出しである場合
if (expression instanceof BinaryExpression) {
// 二項演算子を表すトークンを取得
Token operation = expression.getOperation()
// 二項演算子の名称が「/」である場合
if (operation.getText() == "/") {
// 左側の AST を取得
Expression left = expression.getLeftExpression()
// 右側の AST を取得
Expression right = expression.getRightExpression()
// intdiv メソッドの呼び出しに対応する AST の生成をここに書く
}
}
return expression.transformExpression(this)
} else {
return null
}
}
intdiv
メソッドの呼び出しの AST の調査
さて、次は変換後の intdiv
メソッドの呼び出しがどのような AST で表現されるか調べます。
GroovyConsole の入力画面に以下のコードを入力してください。その後、AST ブラウザの右上にある「Reflesh」ボタンをクリックするか F5 キーを押すと、表示が更新されます。
class Test {
public static void main(String... args) {
7.intdiv(2)
}
}
7.intdiv(2)
の AST を見てみましょう。
「class」の項目を見ると、メソッド呼び出しを表現するクラスが MethodCallExpression
であることが分かります。そこで MethodCallExpression
の Groovydoc を見てみると、コンストラクタが 2 種類あることが確認できます。作りたい AST のメソッドの名前は「intdiv」で固定なので、1 番目の MethodCallExpression(Expression, String, Expression)
を使うことにします2。したがって、以下のようにすれば良さそうです。
public Expression transform(Expression expression) {
if (expression != null) {
// AST が二項演算子の呼び出しである場合
if (expression instanceof BinaryExpression) {
// 二項演算子を表すトークンを取得
Token operation = expression.getOperation()
// 二項演算子の名称が「/」である場合
if (operation.getText() == "/") {
// 左側の AST を取得
Expression left = expression.getLeftExpression()
// 右側の AST を取得
Expression right = expression.getRightExpression()
// intdiv メソッド呼び出しの AST を作成する
Expression nextExpression = new MethodCallExpression(left, "intdiv", right)
return nextExpression
}
}
return expression.transformExpression(this)
} else {
return null
}
}
これでも動くのですが、Groovy が生成する AST では、メソッド呼び出しの引数は全て ArgumentListExpression
オブジェクトになっているので、それに倣ってここでも ArgumentListExpression
オブジェクトにしてコンストラクタに渡そうと思います。ArgumentListExpression
のGroovydoc を見るとコンストラクタがたくさんありますが、その中で Expression
オブジェクトを 1 つだけとるものがちょうど良さそうです。これを使ってみます。
public Expression transform(Expression expression) {
if (expression != null) {
// AST が二項演算子の呼び出しである場合
if (expression instanceof BinaryExpression) {
// 二項演算子を表すトークンを取得
Token operation = expression.getOperation()
// 二項演算子の名称が「/」である場合
if (operation.getText() == "/") {
// 左側の AST を取得
Expression left = expression.getLeftExpression()
// 右側の AST を取得
Expression right = expression.getRightExpression()
// 右側の AST だけからなる引数リストを作成する
Expression nextArguments = new ArgumentListExpression(right) // ← ここ!
// intdiv メソッド呼び出しの AST を作成する
Expression nextExpression = new MethodCallExpression(left, "intdiv", nextArguments)
return nextExpression
}
}
return expression.transformExpression(this)
} else {
return null
}
}
これで一通り完成しましたね。
transform
の再帰呼び出しはしっかりと
実は、上のコードのままではうまく行きません。なぜかというと、このままでは left
や nextArguments
に対して transform
メソッドが呼び出されないので、例えば (7 / 2) / (5 / 3)
のようなネストした部分があると、一番外側しか intdiv
に変換されません。そこで、しっかり transform
を呼ぶようにしましょう。
public Expression transform(Expression expression) {
if (expression != null) {
// AST が二項演算子の呼び出しである場合
if (expression instanceof BinaryExpression) {
// 二項演算子を表すトークンを取得
Token operation = expression.getOperation()
// 二項演算子の名称が「/」である場合
if (operation.getText() == "/") {
// 左側の AST を取得
Expression left = expression.getLeftExpression()
// 右側の AST を取得
Expression right = expression.getRightExpression()
// 左側の AST に対して再帰的に変換を行う
Expression nextReceiver = transform(left) // ← ここ!
// 右側の AST だけからなる引数リストを作成して再帰的に変換を行う
Expression nextArguments = transform(new ArgumentListExpression(right)) // ← ここ!
// intdiv メソッド呼び出しの AST を作成する
Expression nextExpression = new MethodCallExpression(nextReceiver, "intdiv", nextArguments)
return nextExpression
}
}
return expression.transformExpression(this)
} else {
return null
}
}
これで完成です。
自前で新しく AST を生成したら transform
を呼ぶのを忘れないでください。
作った AST 変換を試してみる
GroovyConsole のメニューから Script → Add Jars to Classpath や Script → Add Directory to Classpath を選択することで、クラスパスに jar ファイルやディレクトリを追加することができます。これを用いて、作成した AST 変換を GroovyConsole 上で試すことができます。
GroovyConsole の AST ブラウザは、AST の構造を表示するだけではなく、変換された AST を通常の Groovy コードの形でも表示してくれます。これによって、意図した通りに変換が行われているかを一目で確認することができます。
例えば、今回作成した @Intdiv
のクラスファイルをクラスパスに追加した状態で、GroovyConsole に以下を入力してみます。
import ziphil.transform.Intdiv
@Intdiv
class Test {
public static void main(String... args) {
println(7 / 2)
}
}
ここで AST ブラウザを開くと、ウィンドウ下部に以下のように表示されるはずです。「At end of Phase」の部分は「Semantic Analysis」にしています。
import ziphil.transform.Intdiv as Intdiv
@ziphil.transform.Intdiv
public class Test extends java.lang.Object {
public static void main(java.lang.String[] args) {
this.println(7.intdiv(2))
}
}
しっかり intdiv
に変換されているのが分かりますね。
このシリーズ
- Groovy で AST 変換 I: AST 変換を作ってみよう
- Groovy で AST 変換 II: GroovyConsole の使い方
- Groovy で AST 変換 III: Tips など