S-JIS[2011-09-08/2011-09-11] 変更履歴

Scalaコンパイラプラグイン変換サンプル

Scala2.9.1のコンパイラプラグインでコードを変換する方法について。


概要

Scalaコンパイラプラグインでコードを変換する場合は、PluginComponentにTransformトレイトをミックスインし、newPhaseでなくnewTransformerをオーバーライドする。
(Transformトレイトの中でnewPhaseがオーバーライドされ、newTransformerを呼び出すようになっている)

import scala.tools.nsc.{ Global, Phase }
import scala.tools.nsc.plugins.{ Plugin, PluginComponent }
import scala.tools.nsc.transform.Transform
class TransformSample(val global: Global) extends Plugin {
  selfPlugin =>

  import global._
  import global.definitions._

  override val name = "plugin-sample"
  override val description = "scala compiler plugin transfer-sample" //-Xplugin-list
  override val components = List[PluginComponent](Component)
  private object Component extends PluginComponent with Transform {

    override val global: selfPlugin.global.type = selfPlugin.global
    override val runsAfter = List("parser")
    override val phaseName = selfPlugin.name //-Xshow-phases

    override def newTransformer(unit: CompilationUnit) = new MyTransformer(unit)
    class MyTransformer(unit: CompilationUnit) extends Transformer {

      override def transform(tree: Tree) = {
        super.transform(preTransform(tree)) //自分の処理を呼び出してから、親クラスの処理(他の変換処理)を呼び出す
      }

      def preTransform(tree: Tree): Tree = tree match {
        case 〜 =>
          //ここで自前の変換処理を記述する

        case tree => tree
      }
    }
  }
}

preTransformは自分で用意したメソッド。(特に何かをオーバーライドしているわけではない)
examples/plugintemplateでは、preTransformの他にpostTransformを用意しているサンプルもある。


ソースを探索するにはTree(AST:抽象構文木)を辿るだけでいいが、ソースを変更する場合はTreeだけでなく、対応しているSymbolも変える必要がある。[2011-09-11]

パッケージ名やクラス名・メソッド名・フィールド名・変数名といった各要素は、Symbolで表されている。
例えばクラスなら、メソッド名やフィールド名(たぶん内部クラスも)のSymbol一覧をクラスのSymbol内に保持している。
むしろ後続フェーズではSymbolを使ってチェックを行っていたりするので、要素を増減させる場合はSymbol内の情報も変更する必要がある。

Symbolと対になってTreeがある。ほとんどのTreeはSymbolを持つ。
新しいTreeを作成したらSymbolを持たせておかないと、後続処理でエラーになることが多い。


Symbolの生成方法

シンボル(scala.tools.nsc.symtab.Symbols$Symbol)は、クラス名やオブジェクト名・変数名を表すもの。
scala.Symbolとは全くの別物)
名前だけでなく、型やソース内の定義位置も保持している。

  val sym = definitions.getClass("java.io.Serializable") //クラスのSymbol
  val sym = definitions.getModule("java.io")             //パッケージのSymbol(オブジェクトと同じ扱いらしい)
  val sym = definitions.getModule("scala.Predef")        //オブジェクトのSymbol
  val sym = definitions.PredefModule                     //PredefオブジェクトのSymbol。よく使われるSymbolは個別に用意されている
  val sym = definitions.IntClass                         //IntのSymbol

getClassやgetModuleの引数の型はNameだが、Stringへの暗黙変換がどこかで定義されているようで、文字列で直接指定できる。

getClassやgetModuleを使う場合、存在しない(クラスパス上に見つけられない)クラスやオブジェクトはエラーになる。
コンパイル(変換)対象である自分自身のクラスのSymbolはこの方法では取得できない。


変数名のSymbolを生成するには、その変数が属するスコープ(フィールドであればクラス)のSymbolのnewメソッドを呼び出す。[2011-09-09]

  val vs = cs.newValue("フィールド名")            //クラスのSymbolからフィールドを作る
  val ms = cs.newMethod("メソッド名")             //クラスのSymbolからメソッドを作る[2011-09-11]
  val vs = ms.newValueParameter(ds.pos, "引数名") //メソッドのSymbolから引数を作る[2011-09-11]

Typeglobal.Type)からは以下のようにしてSymbolを取り出せる。

  val sym = tpe.termSymbol
  val sym = tpe.typeSymbol
  val sym = tpe.termSymbolDirect
  val sym = tpe.typeSymbolDirect

違いはよー分からん(爆)
が、「java.io.Serializable」に対し、「java」「io」はtermで、「Serializable」はtypeらしい。→toTypedSelectTree
また、変数名もtermで表す。

Treeからは以下のようにしてSymbolを取り出せる。

  val sym = tree.symbol

Nameの生成

Nameは、名前を表すもの。[2011-09-11]
Stringとの暗黙変換がどこかで定義されているので、Nameを使用する場所には文字列を直接指定できる。

新しいNameは以下のようにして生成できる。

  val name = newTermName("名前")
  val name = newTypeName("名前")

NameにはTermNameとTypeNameがある。
型を表すのがTypeNameで、普通に変数名等の識別子を表すのがTermName。だと思われる。


Symbolからは以下のようにしてNameを取得できる。

  val name = symbol.name

また、クラス名やフィールド名・メソッド名といった“名称を持つTree”からもNameを取得できる。

  val name = defTree.name

Typeの生成方法

Type(global.Type)は型を表すもの。

SymbolTreeからは以下のようにして取得できる。

  val tpe = symbol.tpe
  val tpe = definitions.getClass("java.io.Serializable").tpe
  val tpe = StringClass.tpe

  val tpe = tree.tpe

「type」という変数名を使わないのは、typeは別名を定義する予約語だからだろう。


TypeRef(Typeのサブクラス)を使ってTypeを取得する例。

  val m = definitions.getModule("java.io")
  val s = definitions.getClass("java.io.Serializable")
  val tpe = TypeRef(m.tpe, s, List.empty)

TypeRefでは、親の型と自分のSymbolと型引数(Typeのリスト)を指定するようだ。


Treeの生成方法

Treeにはクラスを表すClassDefやvalを表すValDef・defを表すDefDefなど、基本的にScalaの予約語に沿ったケースクラスが用意されているので、それを使う。
(→TeeDSL

備考
val ClassDef(mods, name, tparams, impl) = tree
val val Template(parents, self, body) = impl
val body2 = 〜
val impl2 = Template(parents, self, body2)
val tree2 = ClassDef(mods, name, tparams, impl2)
古いクラス定義を元に新しいクラス定義を作る例。
(これだけだとsymbolが設定されないので、後続の変換処理の中でエラーになる)
val tree = Select(Select(Ident(newTermName("java")), newTermName("io")), newTypeName("Serializable"))
tree match {
  case s @ Select(i @ Select(j @ Ident(_), _), _) =>
    s.symbol = definitions.getClass("java.io.Serializable")
    i.symbol = definitions.getModule("java.io")
    j.symbol = definitions.getModule("java")
    s.tpe = s.symbol.tpe
    i.tpe = i.symbol.tpe
    j.tpe = j.symbol.tpe
}
Select(Treeのサブクラス)を使ったクラス宣言の例。
FQCNの各語を組み合わせて生成する。
Treeを作るだけならSelectだけでいいが、コード変換に使うにはsymboltpeが設定されている必要がある。
参考: avro-scala-compiler-pluginのtoTypedSelectTree
val sym = definitions.getClass("java.io.Serializable")
val tpe = sym.tpe
val tree = TypeTree(tpe)
クラス名のSymbolから型のTreeを作る例。
Selectを組み合わせるより簡単。
val tree = Ident(sym)
変数名のSymbolから識別子のTreeを作る例。[2011-09-11]

ClassDefやSelect等のTreeのサブクラス(のcase class)を使って新たなTreeを作った場合、SymbolTypeの情報は入っていない。
このTreeをそのまま後続処理に渡すとエラーになる。

生成元の情報を引き継ぐには、global.treeCopyを使用する。

val c1 @ ClassDef(mods, name, tparams, impl) = tree
val Template(parents, self, body) = impl
val body2 = 〜
val impl2 = treeCopy.Template(impl, parents, self, body2)
val tree2 = treeCopy.ClassDef(c1, mods, name, tparams, impl2)

また、atOwnerというメソッドもある。[/2011-09-11]
currentOwnerをいう変数を一時的に指定したオーナーに変更する。したがって、currentOwnerを使わないならあまり意味が無い…と思う。
“オーナー”というのは、そのシンボルを保持しているシンボルのことらしい。例えばフィールドやメソッドのオーナーはクラスになる。たぶんローカル変数のオーナーはメソッドになるだろう。

val impl2 = atOwner(シンボル){ treeCopy.Template(impl, parents, self, body2) }
val tree2 = atOwner(シンボル){ treeCopy.ClassDef(c1, mods, name, tparams, impl2) }

atOwnerの使用例


Treeの内容がどうなっているかをGUIで表示する方法が用意されている。[2011-09-10]

treeBrowser.browse(ツリー)

この命令を実行すると別ウィンドウが開き、ツリーの状態が照会できる。
このウィンドウが開いている間、コンパイルの実行は中断する。ウィンドウを閉じるとコンパイルが再開される。


TreeDSL

Treeのインスタンスを新しく作る場合、例えば変数ならValDefを使ってSymbolから生成する。
しかし色々と項目を設定する必要があり、ちょっと面倒。
そこで、TreeDSLを使って簡単に書くことが出来る。

// まずSymbolを生成
val vs = クラスのシンボル.newValue("変数名").setFlag(PROTECTED | SYNTHETIC).setInfo(IntClass.tpe)
// 普通にValDefを使って変数を定義する例
val vd = ValDef(Modifiers(vs.flags), vs.name, TypeTree(vs.tpe), Literal(123).setType(vs.tpe))
vd.symbol = vs
vd.tpe = NoType

// TreeDSLを使って変数を定義する例
val vd = typer.typedValDef{ VAL(vs) === LIT(123) }

「VAL」が変数で「LIT」がリテラル、「===」が代入を表している。余計な項目を記述する必要も無く、分かりやすい。

参考: adding a field to an object companion class


TreeDSLを使うには、TreeDSLトレイトをミックスインし、CODEをインポートする。

import scala.tools.nsc.{ Global, Phase }
import scala.tools.nsc.plugins.{ Plugin, PluginComponent }
import scala.tools.nsc.transform.Transform
import scala.tools.nsc.ast.TreeDSL
class SampleTransform(val global: Global) extends Plugin with TreeDSL {
  selfPlugin =>

  import global._
  import global.definitions._
  import CODE._

  〜
}

コンパイラプラグインへ戻る / Scala目次へ戻る / 技術メモへ戻る
メールの送信先:ひしだま