S-JIS[2011-09-08/2011-09-11] 変更履歴
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を持たせておかないと、後続処理でエラーになることが多い。
シンボル(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]
Type(global.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は、名前を表すもの。[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(global.Type)は型を表すもの。
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にはクラスを表す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だけでいいが、コード変換に使うにはsymbolとtpeが設定されている必要がある。 参考: 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を作った場合、SymbolやTypeの情報は入っていない。
この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) }
Treeの内容がどうなっているかをGUIで表示する方法が用意されている。[2011-09-10]
treeBrowser.browse(ツリー)
この命令を実行すると別ウィンドウが開き、ツリーの状態が照会できる。
このウィンドウが開いている間、コンパイルの実行は中断する。ウィンドウを閉じるとコンパイルが再開される。
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._ 〜 }