Consider the `bar` lazy val in the following snippet:
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:
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).