Scalaのマクロのバグ見つけた(AbstractMethodErrorやNoSuchMethodException発生する)

https://issues.scala-lang.org/browse/SI-8949
https://gist.github.com/xuwei-k/0348d85d3b80fcce1094


2.10.4でも2.11.4でも発生します。

まず、Scalaのdefマクロは、見た目や使う側からは普通のメソッドのように見えるけれども、実装としては普通のメソッドではないですね?
それで

trait A{
  def x: Int = 1
}

というメソッドがあったときに、

object B {
  def impl(c: Context): c.Expr[Int] = {
    import c.universe._
    c.Expr[Int](Literal(Constant(2)))
  }
}
  
class B extends A {
  override def x: Int = macro B.impl
}

と、マクロで普通のメソッドをoverrideできてしまいます。これの何が問題か、というと、
「マクロの実装は普通のメソッドではないので、ポリモーフィックに振る舞うことが不可能」
です。

上記のような、A、Bのclassがあったときに、単にnew Bをして、その x というメソッドを呼ぶと正しく動いて2が返ります。

scala> val b = new B
b: B = B@47825164
 
scala> b.x
res0: Int = 2

「x というメソッドを呼ぶと」と書きましたが、実際は「x というdefマクロ」であって、普通のメソッドではありません。

さて、ここまではいいのですが、(実体はBクラスのインスタンスである)変数bを、A型にしてから呼ぶと、エラーが発生します。

scala> (b: A).x
java.lang.AbstractMethodError: B.x()I
        at .<init>(<console>:14)
        at .<clinit>(<console>)
        at .<init>(<console>:7)
        at .<clinit>(<console>)
        at $print(<console>)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)


さて、なぜエラーが発生するのか、Scalaコンパイラの気持ちになって考えてみましょう(謎)
まず、B型の変数であるbを、A型にしてから呼び出しました。しかし、実際はclass Bであり、Bでは

  override def x: Int = macro B.impl

となっていたので、おそらくScalaコンパイラさんは「メソッド x は B においてoverrideされた」 というバイトコードだけ吐いたみたいですね?
しかし、B#xの実体はマクロであり、マクロのコードはコンパイル時に生成されるものです。A型経由で、B#xを実行時に普通のメソッドとして呼び出しに行っても、普通のメソッドとしての実装は存在しないようです。


今回の例では、B#xのマクロの実装は、必ず2を返す単純なものですが、マクロはコンパイル時の呼び出しのコンテキストに影響を受けるし、実行時には、scala-compilerのjarがパスに存在しない可能性も考えると

「副作用がないマクロの場合は実行時にマクロ展開して頑張って返す」

とかは、基本的に不可能だし、そんなことは現状のScalaのマクロは全くやってくれません。



これの解決の方法としては、現状のScalaのマクロの仕様上、マクロは通常のメソッドは異なるものなので
「マクロで通常のメソッドをoverrideすることは禁止」
が個人的には一番妥当な気がしてます。
しかし、それをすると、ある程度互換性などにも影響あるし、すぐ直るのかどうか微妙ですね。
マクロでoverrideしていたら、2.11.5や2.10.5では警告だけを出すようにして、2.12.0から完全に禁止?とかもありえるかもしれません。
もしくは、他の解決方向もありえそうですが。


あと、同じような問題として、structural subtypingを組み合わせても、NoSuchMethodExceptionになります。

https://gist.github.com/xuwei-k/ec8fe83a230793b2b2fc


というわけで、マクロでoverrideしたり、マクロとstructural subtypingを組み合わせるのはとても危険なので絶対にやめましょう。コンパイラは助けてくれないので、頑張って気をつけましょう・・・。




追記:
traitではなく、(abstract含めた)classだとエラーにならないみたいです?
https://gist.github.com/xuwei-k/a7050cefd7a515f4770e
これはこれで、安全にアップキャスト(?)しただけで、動作変わってしまうのも、個人的には気持ち悪い挙動だと思いますが・・・(やっぱりoverride不可能にするべき?)