Description
Here's an unsoundness hole I wasn't aware about, and I can't find in the issue tracker — it involves initialization, but not null. Instead of using null
, we can just select an ill-bounded type from a not-yet-initialized path that's going to crash/loop when we get there — so we get a ClassCastException instead of an abort (exception) or loop.
scala> new {
| val a: String = (((1: Any): b.A): Nothing): String
| val b: { type A >: Any <: Nothing } = ???
| }
java.lang.ClassCastException: java.lang.Integer cannot be cast to scala.runtime.Nothing$
at rs$line$1$$anon$1.<init>(rs$line$1:2)
scala> new {
| val a: String = (((1: Any): b.A): Nothing): String
| def loop(): Nothing = loop()
| val b: { type A >: Any <: Nothing } = loop()
| }
|
java.lang.ClassCastException: java.lang.Integer cannot be cast to scala.runtime.Nothing$
at rs$line$4$$anon$1.<init>(rs$line$4:2)
at rs$line$4$.<init>(rs$line$4:5)
One can verify that commenting out val a
gives code that involves no nulls.
Discussion
We've known for a while that we can inhabit types with bad bounds with expressions that don't return a value — val v { type A >: Any <: Nothing } = failing/looping
. But we always reasoned that v.A
would only be available after the crash. The above examples show a loophole we missed.
This loophole is outside the taxonomy of "scaling-up" loopholes in https://www.scala-lang.org/blog/2016/02/17/scaling-dot-soundness.html.
And it doesn't appear in any known DOT variant that I'm familiar with, because it requires both paths and potentially diverging initialization expressions. If this issue is indeed novel to us, I'll ask other DOT researchers.
Above, the type of b
has obvious bad bounds, but I assume we can give it non-obvious bad bounds (as usual) to get the same error.
I can see two likely sound potential fixes, but they are not yet realistic.
- checking strong normalization for value members. Likely sound, pretty restrictive. This could "work" if we required programmers to use some escape hatch (
@total
) when we can't check totality ourselves — the main benefit would be that@total
documents the danger. - forbidding paths starting in
this.f
beforef
is initialized, following @liufengyun et al.'s safe initialization work. I'd expect this to be pretty restrictive, but I don't know. - We could also declare these errors out-of-scope.
Regarding 2: In fact, I realized this problem by comparing DOT's and @liufengyun's typing rules for object creation: one declares all fields to be instantly initialized and accessible in the typing context when typing other fields (which is valid if initializers are syntactic values), the other only has in context the actually initialized fields.