最近、Groovyの書籍を読んでAST変換に興味を持ちまして…。んで、ちょっと試しにやってみたいことがあったのですが、それをやるにはScalaのCompiler Pluginを書く必要があるみたいですね。
Scala Compiler Pluginを利用すると、
なんてことが可能になるようです。
いきなりハードルが高いことやるのもなんなので、本家のサンプルをまずは写経してみましょう。
Writing Scala Compiler Plugins
http://www.scala-lang.org/node/140
子の例は、0除算チェックを行うプラグインで、こういうScalaソース
object DivByZeroTest { def main(args: Array[String]): Unit = { val a = 1 / 0 } }
を渡してコンパイルしようとすると、
DivByZeroTest.scala:3: error: definitely division by zero val a = 1 / 0 ^ one error found
とエラーになるようなプラグインです。
では、作成したsbtプロジェクト。
build.sbt
name := "divide-by-zero-plugin" version := "1.0.0" scalaVersion := "2.9.0" libraryDependencies += "org.scala-lang" % "scala-compiler" % "2.9.0"
src/main/scala/DivByZero.scala
import scala.tools.nsc.{Global, Phase} import scala.tools.nsc.Global import scala.tools.nsc.plugins.{Plugin, PluginComponent} class DivByZero(val global: Global) extends Plugin { val name: String = "divbyzero" val description: String = "checks for division by zero" val components: List[PluginComponent] = List(Component) private object Component extends PluginComponent { val global: DivByZero.this.global.type = DivByZero.this.global import global._ val runsAfter: List[String] = List("refchecks") val phaseName: String = DivByZero.this.name def newPhase(prev: Phase): Phase = new DivByZeroPhase(prev) class DivByZeroPhase(prev: Phase) extends StdPhase(prev) { override def name: String = DivByZero.this.name def apply(unit: CompilationUnit): Unit = { for { tree @ Apply(Select(rcvr, nme.DIV), List(Literal(Constant(0)))) <- unit.body if rcvr.tpe <:< definitions.IntClass.tpe } unit.error(tree.pos, "definitely division by zero") } } } }
src/main/resources/scalac-plugin.xml
<plugin> <name>divbyzero</name> <classname>DivByZero</classname> </plugin>
DivByZero.scalaがプラグインの本体です。また、プラグインを作成する際にはscalac-plugin.xmlというファイルを、作成するプラグインのJARファイルのルートに含めておく必要があるそうです。
では、パッケージング。
> package [info] Compiling 1 Scala source to /xxxxx/divide-by-zero-plugin/target/scala-2.9.0.final/classes... [info] Packaging /xxxxx/divide-by-zero-plugin/target/scala-2.9.0.final/divide-by-zero-plugin_2.9.0-1.0.0.jar ... [info] Done packaging. [success] Total time: 8 s, completed Aug 14, 2011 8:58:06 PM
以後、作成したプラグインを利用する際は、scalacコマンドに-Xpluginオプションを使用して、作成したJARファイルを指定します。fscでもいいのですが、プラグインもキャッシュしてしまうので、プラグイン自体を変更してもfscがシャットダウンするまで変更が反映されなくなってしまいます。プラグインが落ち着くまで、ちょっと重いですがscalacで確認しましょう。
コンパイラにプラグインとして認識できているかどうかは、以下のように「-Xplugin-list」で確認できます。
$ scalac -Xplugin:divide-by-zero-plugin/target/scala-2.9.0.final/divide-by-zero-plugin_2.9.0-1.0.0.jar -Xplugin-list divbyzero - checks for division by zero continuations - applies selective cps conversion
continuationsというのは、限定継続のプラグインですね。
また、作成したプラグインがどのフェーズに組み込まれているかを確認するには、以下のように「-Xshow-phases」で行います。
$ scalac -Xplugin:divide-by-zero-plugin/target/scala-2.9.0.final/divide-by-zero-plugin_2.9.0-1.0.0.jar -Xshow-phases phase name id description ---------- -- ----------- parser 1 parse source into ASTs, perform simple desugaring namer 2 resolve names, attach symbols to named trees packageobjects 3 load package objects typer 4 the meat and potatoes: type the trees superaccessors 5 add super accessors in traits and nested classes pickler 6 serialize symbol tables refchecks 7 reference/override checking, translate nested objects selectiveanf 8 divbyzero 9 liftcode 10 reify trees selectivecps 11 uncurry 12 uncurry, translate function values to anonymous classes tailcalls 13 replace tail calls by jumps specialize 14 @specialized-driven class and method specialization explicitouter 15 this refs to outer pointers, translate patterns erasure 16 erase types, add interfaces for traits lazyvals 17 allocate bitmaps, translate lazy vals into lazified defs lambdalift 18 move nested functions to top level constructors 19 move field definitions into constructors flatten 20 eliminate inner classes mixin 21 mixin composition cleanup 22 platform-specific cleanups, generate reflective calls icode 23 generate portable intermediate code inliner 24 optimization: do inlining closelim 25 optimization: eliminate uncalled closures dce 26 optimization: eliminate dead code jvm 27 generate JVM bytecode terminal 28 The last phase in the compiler chain
各フェーズの名前と説明を見ると、やっていることがなんとなくわかって面白いですね。
では、試してみましょう。
$ scalac -Xplugin:divide-by-zero-plugin/target/scala-2.9.0.final/divide-by-zero-plugin_2.9.0-1.0.0.jar DivByZeroTest.scala DivByZeroTest.scala:3: error: definitely division by zero val a = 1 / 0 ^ one error found
期待通りコンパイルエラーになってくれましたね。
今回のCompiler Pluginで重要なのは、以下の部分ですね。
val runsAfter: List[String] = List("refchecks") val phaseName: String = DivByZero.this.name def newPhase(prev: Phase): Phase = new DivByZeroPhase(prev)
runsAfterで、どのフェーズの次に実行するのか、phaseNameでフェーズの名前を、そしてnewPheaseでそのフェーズで実際に行うPhaseクラスのインスタンスを返します。
あとは、Phaseクラスを拡張して処理を書いていくのですが…この辺りの理解は、また今度やろうと思います…。