S-JIS[2011-09-10/2011-09-12] 変更履歴

DefDefメモ

Scala2.9.1のコンパイラプラグインのDefDef(def: メソッド定義)のメモ。


概要

メソッドの定義(def)は、コンパイラプラグインではDefDefで表現されている。


DefDefのプロパティー

備考
symbol Symbol   メソッド名の情報
mods Modifiers c メソッドの修飾子。
name TermName c メソッド名
tparams List[TypeDef] c 型引数
vparamss List[List[ValDef]] c 引数リスト(Scalaでは引数のリストを複数持てるので、変数定義のListのListになっている)
tpt Tree c メソッドの戻り値の型
rhs Tree c メソッド本体

※「c」はDefDefのケースクラスで指定されている内容


クラスにメソッドを追加する例

zzzという自分で追加したフィールドにアクセスするゲッターメソッド「def zzz:Int = zzz」とセッターメソッド「def zzz_=(x$1: Int):Unit = zzz = x$1」を追加する例。

クラスにメソッドを追加するには、(フィールド(ValDef)と同様に、)ClassDefのbodyにDefDefを追加すると共に、クラスのSymbolに メソッドのSymbolを追加する必要がある。

この例ではTreeDSLを使用している。

class MyTransformer(unit: CompilationUnit) extends Transformer {
  override def transform(tree: Tree) = {
    super.transform(preTransform(tree))
  }
  import scala.reflect.generic.Flags._

  def preTransform(tree: Tree): Tree = tree match {
    case c @ ClassDef(mods, name, tparams, impl @ Template(parents, self, body)) =>
      atOwner(c.symbol) { //対象クラスをオーナーにする
        val typer2 = typer.atOwner(currentOwner)

        // フィールドのSymbol
//      val vname = "zzz " //末尾にスペース付き
        val vname = nme.getterToLocal("zzz")
        val vs = currentOwner.newValue(vname)
        vs.flags = MUTABLE | PRIVATE | LOCAL | TRIEDCOOKING | SYNTHETIC
        vs.info  = IntClass.tpe

        // フィールドのValDef
        val vd = typer2.typedValDef {
          VAR(vs) === LIT(0)
        }
        currentOwner.info.decls.enter(vs) //クラスSymbolへの新フィールドの追加
        // ゲッターメソッド
//      val gname = "zzz"
//      val gname = vs.name.toString.trim
        val gname = nme.getterName(vs.name)
        val gs = currentOwner.newMethod(gname, vs.pos)
//      val gs = vs.newGetter; gs.flags &= ~(PRIVATE | LOCAL)
        gs.flags |= ACCESSOR | TRIEDCOOKING | SYNTHETIC
        gs.info = NullaryMethodType(vs.tpe)
        val gd = typer2.typedDefDef {
          DEF(gs) === { This(currentOwner) DOT vs }
        }
        currentOwner.info.decls.enter(gs)
        // セッターメソッド
//      val sname= "zzz_$eq"
//      val sname = scala.reflect.NameTransformer.encode("zzz_=")
        val sname = nme.getterToSetter(gs.name)
        val ss = currentOwner.newMethod(sname, vs.pos)
        ss.flags |= ACCESSOR | SYNTHETIC
        ss.info = MethodType(ss.newSyntheticValueParams(List(vs.tpe)), UnitClass.tpe)
        val sd = typer2.typedDefDef {
          DEF(ss) === { (This(currentOwner) DOT vs) === ss.ARG(0) }
        }
        currentOwner.info.decls.enter(ss)
        // Treeへの反映
        val body2 = body ::: List(vd, gd, sd)
        val impl2 = treeCopy.Template(impl, parents, self, body2)
        val c2 = treeCopy.ClassDef(c, mods, name, tparams, impl2)
        c2 //戻り値
      }
    case tree => tree
  }
}

参考: adding a field to an object companion class


まず、(Transformerの)atOwnerを使って、クラスのSymbolをオーナーにしてみた。
atOwnerを呼び出すと、currentOwnerという変数が指定したSymbolになる。
(ただそれだけのようなので、atOwnerを使わずc.symbolを直接使っても問題なさそう)

そして、typerのatOwnerを使って新しいtyperを用意している。[2011-09-11]
typerはContextという内部状態を持っていて、クラスや変数等を参照できるスコープのような役割を果たしている模様。
Typer#typedなんちゃらというメソッドは、必要な情報をコピーする他に、識別子(変数)をContextのスコープでアクセス可能かどうかという判定もしている。
例えば(トップレベルクラスの)ClassDefが呼ばれた時のスコープはそのクラスが属するパッケージとなっている。
typer2のオーナーはcurrentOwner(すなわち対象クラス)なので、スコープはそのクラス内となる。

上記の例でtyper.typed(typedDefDef)を使うと以下のようなエラーが発生する。

フィールドの属性 エラー内容
PRIVATE | LOCAL
PRIVATE
Exception in thread "main" scala.tools.nsc.symtab.Types$TypeError: variable フィールド in class クラス cannot be accessed in クラス

typerのスコープはパッケージなので、クラス内のフィールドにアクセスできない…ということなのかな?
typer2.typedを使うとスコープがクラスなので、エラーにならない。


フィールド名には、末尾にスペースを付けている。[2011-09-11]
これは、メソッド名と全く同じ名前のフィールドは定義できないので、フィールドとメソッドのどちらかを別の名前にする必要がある為。
アクセッサーメソッド(ゲッター・セッター)によってアクセスするフィールドは他からアクセスできる必要が無い(アクセスさせない)ので、フィールド名の方を変化させている。
nmeというオブジェクトのgetterToLocalメソッドを使うと、スペース付きの名前に変換してくれる。
(この「Local」はメソッド内のローカル変数のことではなく、フィールド属性のLOCAL(つまりクラス内ローカル)という意味だと思う)

フィールドの属性(修飾子)は通常のフィールドだと「private[this]」になっているようなので、それに合わせている。[/2011-09-11]
「[this]」の部分は、フィールド属性のLOCALで表す。

そして、クラスのシンボル情報のdeclsへのフィールドの登録を、メソッド定義より前の段階で行っている。
メソッド定義より前に登録しておかないと、メソッド定義の中でフィールドを見つけられない為。
(最後の方で新たに作るClassDefは元のClassDefの情報が引き継がれるので、先に登録しても問題ない)


メソッドSymbolは「newMethod」で生成する。
(第2引数はソース内の位置を表すpos。省略するとNoPositionになる。[2011-09-12]
 なるべくposを渡す方が良い。後続フェーズでエラーになったときにその位置を表示してくれるから。
 上記の例ではフィールドのposを渡しているが、フィールド自体も新規作成だからposはNoPositionなので意味が無いけど^^;)

ゲッターメソッド名はフィールド名と同じ。(ただしスペースは除去する。nme.getterNameで除去してくれる)[/2011-09-11]
ゲッターの場合は「newGetter」というメソッドもあるのだが、元となる変数の属性(privateとか)を引き継ぐので、今回のパターンでは使えない。
(PRIVATEとかLOCALとかのフラグをリセットしてやれば使えるけど)

セッターの場合は「newSetter」というメソッドは無いが、nme.getterToSetterでゲッターメソッド名からセッターメソッド名を生成できる。

メソッドのシンボル情報(info)には、メソッドの引数の型と戻り値の型を指定する。
引数リスト自体が無い(メソッド名の後ろに丸括弧を付けない)場合はNullaryMethodTypeを使って戻り型のみ指定する。
MethodTypeの第1引数は引数リストのリスト。Scalaでは複数の引数リストを持つことが出来るので、こうなっているのだろう。
引数リストは引数(変数)のSymbolのリストだが、newSyntheticValueParamsを使うと引数名は自動的に付けてくれる(x$1とかになる)。

メソッド本体はtypeDefDefの中にTreeDSLを使って書いていくので、見ただけで何となく分かると思う。
ARGはメソッドの引数名を取得するDSL。「ss.ARG(0)」は、メソッドの第1引数を表す。


TreeDSLでフィールドを指定する際、vs(Symbol型)でなくvs.name(Name型)を使うと、同名のフィールドとメソッドを区別できなくてエラーになる。(フィールド名にスペースが入っていない名前を使う場合)

        val gd = typer.typedDefDef {
          DEF(gs) === { This(currentOwner) DOT vs.name }
        }
Exception in thread "main" scala.tools.nsc.symtab.Types$TypeError: ambiguous reference to overloaded definition,
both method zzz in class クラス名 of type => Int
and variable zzz in class クラス名 of type Int
match expected type ?
	at scala.tools.nsc.typechecker.Contexts$Context.ambiguousError(Contexts.scala:332)

メソッドの中を変える例

セッターメソッドの本体を、代入からフィールドのsetメソッド呼び出しに変える例。[2011-09-11]
def v1_=(arg: Text) = this.v1 = arg

def v1_=(arg: Text) = this.v1.set(arg)」(フィールドv1は別途インスタンス生成されている前提)

    // 戻り型がUnitで引数がTextひとつのアクセッサーメソッド
    case dd @ DefDef(mods, name, tparams, vparamss @ List(List(ValDef(_, _, vtpt, _))), tpt, _)
        if tpt.tpe == UnitClass.tpe && vtpt.tpe == TextTpe && mods.hasFlag(ACCESSOR) =>

      val cs = dd.symbol.owner //クラスのSymbol
      val ds = dd.symbol       //メソッドのSymbol

      //対象フィールド
      val fn = nme.getterToLocal(nme.setterToGetter(name))
      val fs = cs.info.decls.lookup(fn)

      val rhs2 = typer.atOwner(ds).typed {
        This(cs) DOT fs DOT "set" APPLY (ds.ARG(0))
      }

      treeCopy.DefDef(dd, mods, name, tparams, vparamss, tpt, rhs2)

dd.symbolがメソッド定義のSymbolなので、dd.symbol.ownerはクラスのSymbolになる。
セッターメソッドの更新対象フィールドはクラスのSymbolの中から探す(lookup)。


中身が空のメソッド定義

戻り型がUnitで中身が空のメソッドは、TreeDSLでは以下のように書く。[2011-09-11]
def メソッド(引数…): Unit = {}

      val cs = クラスのSymbol

      val ds = cs.newMethod("メソッド名")
      ds.flags |= SYNTHETIC
      ds.info = MethodType(List(引数のSymbol,…), UnitClass.tpe)
      cs.info.decls.enter(ds)

      val dd = typer.atOwner(ds).typedDefDef {
//×    DEF(ds) === BLOCK()
//×    DEF(ds) === EmptyTree
//×    DEF(ds) === TypeTree(UnitClass.tpe)
//      DEF(ds) === LIT(())
        DEF(ds) === UNIT
     }

LITはリテラル定義。
UNITは「LIT(())」として定義されている。(「()」はUnitの唯一の値(定数))


staticメソッドを呼び出す例

Javaのstaticメソッドを呼び出すには、該当クラスをオブジェクト(Module)として取得し、それに対してApplyする。[2011-09-11]

Text.writeString(out, this.s)」の例。

  val TextModule = definitions.getModule("org.apache.hadoop.io.Text")
        val cs  = 自分のクラスを表すSymbol
        val s   = フィールドを表すSymbol
        val out = DataOutputの変数を表すSymbol

        val rhs = typer.atOwner(cs).typed {
          Ident(TextModule) DOT "writeString" APPLY (Ident(out), This(cs) DOT s)
        }

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