LoginSignup
2
1

More than 5 years have passed since last update.

Groovy で AST 変換 II: GroovyConsole の使い方

Last updated at Posted at 2016-11-28

初めに

前回の記事で、二項演算子 / をメソッド 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 を見てみます。

1.png

ツリービューの右側に選択したオブジェクトのプロパティ情報が表示されます。ここの「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 を見てみましょう。

2.png

「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 の再帰呼び出しはしっかりと

実は、上のコードのままではうまく行きません。なぜかというと、このままでは leftnextArguments に対して 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 に変換されているのが分かりますね。

このシリーズ


  1. ここでは getOperation メソッドを呼び出して expression.getOperation() と書いていますが、Groovy なら expression.operation でも OK です。 

  2. 2 番目のコンストラクタを用いると、メソッド名も式にすることができます。メソッドの動的呼び出しです。 

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1