You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Consider the bar lazy val in the following snippet:
trait Test {
def root: Test
private final lazy val bar: Thing[Double] = {
if (this eq root) {
Thing { () =>
System.identityHashCode(bar)
}
} else {
root.bar
}
}
}
case class Thing[A](f: () => A)
If we look at the bytecode generated for that lazy val, we get the following:
Ah, lovely tail recursion! Unfortunately, this is not a function, and it cannot be optimized in this way! Specifically, the bad case is if the value of root has already realized bar. In this case, the return result from this.bar will be a Thing which in turn returns a different value of identityHashCode than root.bar would have returned. Additionally, this tail recursion means that root.bar will not be realized if it hasn't already been, thus a fundamental lazy val semantic is violated.
This seems to be an extremely tricky bug to actually reproduce. Simply changing the body of the function passed to Thing is sufficient to avoid the issue. Specifically:
Thing { () =>
bar
42
}
With things in this position, the compiler sees bar as a recursive call not in tail position and refuses to perform the optimization (based on the error message I get if I turn bar into a def and use @tailrec). Technically, that assessment of the compiler's is overly-strict, but it happens to be sufficient to avoid the issue in this case.
Anyway, the solution here is to just not ever do tail recursion on lazy vals unless you can prove that the 0 slot is remaining stable. That criterion would fail this lazy val (rightfully).
The text was updated successfully, but these errors were encountered:
Consider the
bar
lazy val in the following snippet:If we look at the bytecode generated for that lazy val, we get the following:
Specifically, here:
Ah, lovely tail recursion! Unfortunately, this is not a function, and it cannot be optimized in this way! Specifically, the bad case is if the value of
root
has already realizedbar
. In this case, the return result fromthis.bar
will be aThing
which in turn returns a different value ofidentityHashCode
thanroot.bar
would have returned. Additionally, this tail recursion means thatroot.bar
will not be realized if it hasn't already been, thus a fundamental lazy val semantic is violated.This seems to be an extremely tricky bug to actually reproduce. Simply changing the body of the function passed to
Thing
is sufficient to avoid the issue. Specifically:With things in this position, the compiler sees bar as a recursive call not in tail position and refuses to perform the optimization (based on the error message I get if I turn bar into a def and use @tailrec). Technically, that assessment of the compiler's is overly-strict, but it happens to be sufficient to avoid the issue in this case.
Anyway, the solution here is to just not ever do tail recursion on lazy vals unless you can prove that the 0 slot is remaining stable. That criterion would fail this lazy val (rightfully).
The text was updated successfully, but these errors were encountered: