CLOVER🍀

That was when it all began.

Scala Compiler Pluginで遊ぶ

最近、Groovyの書籍を読んでAST変換に興味を持ちまして…。んで、ちょっと試しにやってみたいことがあったのですが、それをやるにはScalaのCompiler Pluginを書く必要があるみたいですね。

Scala Compiler Pluginを利用すると、

  • コンパイル時にチェックの追加
  • よく使うライブラリなどの最適化
  • ScalaのSyntaxを書き換える

なんてことが可能になるようです。

いきなりハードルが高いことやるのもなんなので、本家のサンプルをまずは写経してみましょう。

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クラスを拡張して処理を書いていくのですが…この辺りの理解は、また今度やろうと思います…。