finally連鎖が止まらない

注意

(12/4追加)
コンパイラのバグでした。
fixされている模様です。

概要

finallyの中でenclosing関数の再帰呼び出しをするのは非常に危険。

知識

Scalaのfinallyは、その中で例外が発生すると
なぜかfinallyを最初からもう一度繰り返すらしい。

package finallytest

object FinallyTest {
  def main(args : Array[String]) : Unit = {
    try {} finally {
      println("finally")
      throw new Throwable()
      println("after throw")
    }
  }
}

実行結果

finally
finally
java.lang.Throwable
        at finallytest.FinallyTest$.main(FinallyTest.scala:7)
        at finallytest.FinallyTest.main(FinallyTest.scala)
...(以下略)

"finally"の出力が2回繰り返されているが、
"after throw"の出力は実行されていないことがわかる。

問題の現象

これを応用して、
関数の中のfinallyの中で再帰呼びだしすると、大変なことになる。

package finallytest

object FinallyRec {
  def main(args : Array[String]) : Unit = {
    //no problem
    // finally_rec(100)
    finally_rec(10000)
  }

  def finally_rec(n : Int){
    if(n < 0){ return }
    println(n)
    try {} finally {
      finally_rec (n - 1)
    }
  }
}


実行結果

10000
9999
...(略)
503250325038
5037
5036
5035
5034
5033
503250325033
503250325034
5033
503250325033
503250325035
5034
5033
503250325033
503250325034
5033
503250325033
503250325036
5035
5034
5033
503250325033
503250325034
5033
...(止まらず)

考察

この原理をよく分かっていないが、
引数が100程度ならば問題は起こらないので、
スタックオーバーフローが原因と考えている。
関数の呼び出し先でスタックオーバーフロー例外 ->
finallyの中をもう一度実行 →
やはり関数呼び出し先でスタックオーバーフロー例外 →…
という連鎖に陥っていると考えている。

しかしなぜ5032〜5038と、幅があるのだろうか?

ただfinally中でスタックオーバーフローしただけでは、この問題は起こらない。
finallyの中でfinallyを含む自分自身を呼び出すのが重要らしい。
以下は問題にならない例。

package finallytest

object FinallyRec2 {
  def main(args : Array[String]) : Unit = {
    try {} finally {
      rec(10000)
    }
  }
  
  def rec(n : Int) {
    if (n < 0){ return }
    println(n)
    rec(n - 1)
    ()
  }
}

この場合、10000からスタックオーバーフローするまで出力を続ける、を2回繰り返して止まる。
recの最後のユニット()は、
末尾再帰では最適化のせいでスタックオーバーフローしないので、その対策(笑)。