LoginSignup
6
6

More than 5 years have passed since last update.

Groovy で AST 変換 I: AST 変換を作ってみよう

Last updated at Posted at 2016-11-27

初めに

Groovy とは?

Groovy とは、Java プラットフォーム上で動く動的型付け言語です。シンタックスがほとんど Java と変わらないので、Java を動的型付けにした言語と考えてもらえば良いと思います。Groovy から Java のコードを呼び出せるので、既存のあらゆる Java のライブラリがそのまま使えます。

動的な側面だけでなく静的コンパイルもサポートされているのが Groovy の特徴です。静的コンパイルによって、動的な機能 (ダックタイピングや実行時のメソッド追加など) が制限される代わりに、動的なメソッド呼び出しに関するオーバヘッドがなくなるため、実行速度が Java と大差なくなります。

AST 変換とは?

Groovy のソースコードをコンパイルすると、まず文法に則ってコードを木構造で表した中間表現に変換されます。これを 「AST (Abstract Syntax Tree)」と呼びます。その後、この AST が Java のバイトコードに変換されます。AST 変換とは、この AST をバイトコードに変換される前に書き換えてしまえる機能です。

例えば、あるフィールドのゲッターメソッドとセッターメソッドを自動生成することを考えてみます1。これを AST 変換を使わずに行おうとすると、実行時にメソッドを追加することになります。しかし、この方法ではメソッドの動的呼び出しによってオーバーヘッドが発生し、パフォーマンスが悪化します。しかし、AST 変換を用いて、AST の段階でゲッターとセッターを追加してしまえば、そのゲッターとセッターが書き込まれた状態でバイトコードが出力されるため、オーバーヘッドがかからず、最初からソースコードにゲッターとセッターが書かれている場合と同じパフォーマンスが得られます。

Java の Lombok を知っていれば、それの Groovy 版だと思ってもらえれば結構です。ただし、Groovy ではそれが比較的簡単に実装できてしまうのが特徴です。

以上のように AST 変換は非常に強力ですが、ソースコード上の (見た目の上での) 処理内容と実際の処理内容が異なってしまうため、諸刃の剣とも言えます。

標準で利用できる AST 変換

Groovy ではいくつかの便利な AST 変換が標準で使えるようになっています。例えば、@ToString アノテーションをクラスにつけることで、AST 変換が行われ、フィールドの内容を列挙して文字列にして返す toString メソッドがそのクラスに追加されます。

import groovy.transform.ToString

@ToString
class Student {
    int number
    String firstName
    String lastName
}

Student student = new Student(number: 5, firstName: "Taro", lastName: "Suzuki") 
println(student.toString())

このコードを実行すると Student(5, Taro, Suzuki) と表示されます。

AST 変換の作り方

仕様の決定

AST 変換は自分で新たに作成することもできます。試しに 1 つ作ってみることにしましょう。ただ、非実用的すぎる AST 変換を作っても仕方ないので、以下のような仕様のものを作成したいと思います。

Groovy では、演算子の /BigDecimal を返します。例えば、7 / 2 の評価結果は Java では 3 (int 型) ですが Groovy では 3.5 (BigDecimal 型) です。Java と同じように整数の商を得たいときは、intdiv というメソッドを使って 7.intdiv(2) のように書く必要があります。

この仕様ですが、Java に慣れているとたまに迷惑に感じることがあります。そこで、ソースコード中で使われている /intdiv に書き換える AST 変換を作ってみましょう。

アノテーションの作成

まず、ソースコードのどの部分に AST 変換を施すかを表すアノテーションを作る必要があります。ここで作成したアノテーションが付加された部分が AST 変換の対象になります。

アノテーションの名前はそのまま Intdiv にしてみます。

Intdiv.groovy
package ziphil.transform

import groovy.transform.CompileStatic
import java.lang.annotation.ElementType
import java.lang.annotation.Retention
import java.lang.annotation.RetentionPolicy
import java.lang.annotation.Target
import org.codehaus.groovy.transform.GroovyASTTransformationClass


@Retention(RetentionPolicy.SOURCE)
@Target([ElementType.METHOD, ElementType.TYPE])
@GroovyASTTransformationClass(["ziphil.transform.IntdivTransformation"])
@CompileStatic
public @interface Intdiv {
}

Java で独自のアノテーションを作る場合とほとんど同じですが、1 つだけ違うのは @GroovyASTTransformationClass アノテーションがあるところです。このアノテーションで、AST 変換の処理が記述されているクラスを指定します。ここでは ziphil.transform.IntdivTransformation にしました。

また、メソッドを付加できる位置ですが、ここではメソッドとクラスにしておきました。メソッドに付加した場合はそのメソッド内に現れる /intdiv に変え、クラスに付加した場合はそのクラスに定義されている全てのメソッド内の /intdiv に変えるようにします。

AST 変換で用いる主要なクラスについて

実装の前に、AST 変換を行うコートを書くときに出てくるクラスについて軽く説明しておきます。

まず、IfStatementForStatement のような ~Statement という名前のクラスは、Groovy の文に対応しています。IfStatementForStatement は、それぞれ if 文 と for 文の AST を表現しています。これらのクラスは、org.codehaus.groovy.ast.stmt パッケージに定義されています。

次に、MethodCallExpressionConstantExpression のような ~Expression という名前のクラスは、Groovy の式に対応しています。ここに挙げた MethodCallExpression"string".startsWith("s") のようなメソッド呼び出しの AST を表現しており、ConstantExpression"aaa"3L などのリテラルの AST を表現しています。これらのクラスは、org.codehaus.groovy.ast.expr パッケージに定義されています。

最後に、MethodNode クラスはメソッドの AST を、ClassNode クラスはクラスの AST を表現します。また、ClassNode は、ソースコード中の型 (変数宣言やメソッド引数などで書かれるもの) を AST 内で表現するのにも使われます。

AST 変換の処理の実装 I: クラスの作成

では、AST 変換を実際に行うコードを書いていきます。

まず、ASTTransformation インターフェースを実装したクラスを作ります。このインターフェースは唯一の抽象メソッド visit をもっているので、そこに AST を処理するコードを書いていきます。

このとき、@GroovyASTTransformation アノテーションで AST 変換のどの段階でその処理を行うかを指定します。実は AST 変換は 8 つの段階に分かれて行われており、どの段階で記述した処理を行うかを選ぶことができるのですが、これについては別の機会で説明するとして、ここでは深入りしないことにします。今回は、CompilePhase.SEMANTIC_ANALYSIS を指定しておきます。

IntdivTransformation.groovy
import org.codehaus.groovy.ast.ASTNode
import org.codehaus.groovy.control.CompilePhase
import org.codehaus.groovy.control.SourceUnit
import org.codehaus.groovy.transform.ASTTransformation
import org.codehaus.groovy.transform.GroovyASTTransformation

@GroovyASTTransformation(phase=CompilePhase.SEMANTIC_ANALYSIS)
public class IntdivTransformation implements ASTTransformation {

  public void visit(ASTNode[] nodes, SourceUnit sourceUnit) {

    // ここに処理を書く

  }

visit の第 1 引数には大きさ 2 の配列が渡されます。この第 0 要素は付加したアノテーション (今の場合は @Intdiv) を表す AST で、第 1 要素はアノテーションが付加された方 (今の場合はクラスやメソッド) の AST になっています。第 2 引数は使わないので説明は省きます。

AST 変換の処理の実装 II: ユーテリティクラスの利用

さて、visit の第 1 引数の第 1 要素に処理したい AST が格納されているわけなので、その中に含まれる二項演算子 / の利用部分を intdiv の呼び出しに置き換えるコードを書けば良いのですが、AST は非常に複雑な構造をしているので、正直大変です。そこで、Groovy が提供しているユーティリティクラスを使うことにしましょう。

ClassCodeExpressionTransformer クラスは、visit~ というメソッドがたくさん定義されており、AST を表現するオブジェクトを引数に渡して呼び出すと、その中身を走査します。そして、中身に存在する全ての式 (メソッド呼び出しやリテラルなど) を引数にして transform メソッドを呼び出し、その返り値に書き変えます。したがって、このクラスの transform メソッドを上書きすれば、比較的簡単に式の変換が行えます。

IntdivTransformation.groovy
import org.codehaus.groovy.ast.ASTNode
import org.codehaus.groovy.ast.AnnotatedNode
import org.codehaus.groovy.ast.ClassCodeExpressionTransformer
import org.codehaus.groovy.control.CompilePhase
import org.codehaus.groovy.control.SourceUnit
import org.codehaus.groovy.transform.ASTTransformation
import org.codehaus.groovy.transform.GroovyASTTransformation

@GroovyASTTransformation(phase=CompilePhase.SEMANTIC_ANALYSIS)
public class IntdivTransformation extends ClassCodeExpressionTransformer implements ASTTransformation {

  private SourceUnit sourceUnit

  public void visit(ASTNode[] nodes, SourceUnit sourceUnit) {
    // アノテーションが付加された部分の AST を取得する
    AnnotatedNode parent = (AnnotatedNode)nodes[1]
    // 第 2 引数をフィールドに保持する (必要なのか分からないが一応)
    this.sourceUnit = sourceUnit
    if (parent instanceof ClassNode) {
      // アノテーションがクラスに付加された場合は、クラスの中身を走査する
      // クラスに含まれる全ての式に対して、下の transform メソッドが呼び出される
      visitClass(parent)
    } else if (parent instanceof MethodNode) {
      // アノテーションがメソッドに付加された場合は、メソッドの中身を走査する
      visitMethod(parent)
    }
  }

  public Expression transform(Expression expression) {
    if (expression != null) {

      // ここに処理を書く

      // expression の中身に対しても変換を行いそれを返す
      return expression.transformExpression(this)
    } else {
      return null
    }
  }

  public SourceUnit getSourceUnit() {
    return this.sourceUnit
  }

}

visit の第 2 引数である sourceUnit をフィールドして保持していますが、Groovy が標準で提供する AST 変換のコードを見るとどれもこうしているため、それを真似しただけで、これの意味は正直よく分かりません。

また、expression.transformExpression(this) ですが、式はネストしている (2 + (4 * 3)string.substring(1, length-1) など) 可能性があるので、その内側の式にも変換を施したものを返すための記述です。transformExpression は、第 1 引数に AST 変換の処理が書かれたオブジェクトを渡すと、内側の式に対してその AST 変換を施します。

AST 変換の処理の実装 III: 式の変換

では、実際に処理を書いていきます。

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
  }
}

基本的には、まず instanceof 演算子で何を表現している AST なのかを判別して、情報を取得していくという流れになります。

作成した AST 変換の利用

では、実際に今作った AST 変換を使ってみましょう。

ここまでで作ったソースコード (Intdiv.groovy, IntdivTransformation.groovy) を Groovy でコンパイルします。出力されたクラスファイルがクラスパスに含まれている状態で、以下のコードを実行してみましょう。

Test.groovy
import ziphil.transform.Intdiv

@Intdiv
class Test {

  public static void main(String... args) {
    println(7 / 2)
  }

}

3 が表示されれば成功です。@Intdiv を取り除くと Groovy の仕様通り 3.5 になります。

問題点

ここで実装した AST 変換ですが、必要最低限の処理しかしていないので問題点がいくつかあります。

まず、今のままでは単純に /intdiv に置き換えているだけなので、例えば 3.4 / 2 なども 3.4.intdiv(2) に置き換えてしまい UnsupportedOperationException が発生します。これを防ぐには、/ の両側の式の型を調べて、それが intlong などの場合のみに変換を施すようにするなどの工夫が必要です。

さらに、変換の際に新たに MethodCallExpression を生成しているので、その式がソースコードのどの位置にあったかなどの情報が失われてしまっています。そのため、エラーが起こった際にスタックトレースに行番号などが表示されないなどの不具合が発生する可能性があります。これは、setSourcePosition メソッドなどを用いてソースコード上の位置情報をセットし直せば解決できます。

AST 変換を作る際の注意点など

ここでは AST 変換自体も Groovy で実装しましたが、最終的にコンパイルされたクラスファイルがあれば良いので、Java で書いても問題ありません。実際、Groovy が標準でサポートしている AST 変換は Java で書かれています。

また、AST 変換を利用する場合は、AST 変換の処理が書かれたソースコードがすでにクラスファイルにコンパイルされている必要があります。したがって、AST 変換を実装するコードとその AST 変換を利用しているコードを同時にコンパイル (もしくは実行) しようとしてもうまくいきません。

このシリーズ


  1. Groovy は標準機能としてゲッターとセッターを自動生成しますが、動的メソッド生成と AST 変換の違いを説明する例として挙げているだけなので、ここでは忘れてください。 

6
6
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
6
6