Description
Compiler version
3.2.2
Minimized code
class A:
class X:
def outer : A.this.type = A.this
class B extends A
class C extends A
@main def run() : Unit =
val b0 = new B
val b1 = b0
val b2 = new B
val c0 = new C
val c1 = c0
val c2 = new C
val b0x : A#X = new b0.X
val pathTypeMatch = b0x match
case _ : c2.X => "c2.X"
case _ : c1.X => "c1.x"
case _ : c0.X => "c0.X"
case _ : b2.X => "b2.X"
case _ : b1.X => "b1.X"
case _ : b0.X => "b0.X"
case _ => "ELSE"
println(pathTypeMatch)
val projectionTypeMatch = b0x match
case _ : C#X => "C#X"
case _ : B#X => "B#X"
case _ : A#X => "A#X"
case _ => "ELSE"
println(projectionTypeMatch)
val failingTypeMatch = b0x match
case cx : C#X =>
val c : C = cx.outer
c
println(failingTypeMatch)
Output
b1.X
C#X
java.lang.ClassCastException: class B cannot be cast to class C (B and C are in unnamed module of loader java.net.URLClassLoader @55e91e61)
Expectation
Path-dependent type matching behaves as expected. b0x
is indeed a b1.X
.
Projection type matching is inconsistent. b0x
is not a C#X
, but a B#X
.
Once in the branch, the legitimate type-ascription of (cx:C#X).outer
to type C
throws the exception.
Apparently, B#X
and C#X
have a type erasure in A#X
. In the byte code, the condition of all 3 branches are strictly identical (isinstance A$X). Thus, branches B#X
and A#X
are actually unreachable.
The paragraph below suggests the intention to take the prefix into account for the match.
https://scala-lang.org/files/archive/spec/2.13/08-pattern-matching.html#type-patterns
A type pattern
T
is of one of the following forms: A reference to a classC
,p.C
, orT#C
. This type pattern matches any non-null instance of the given class. Note that the prefix of the class, if it exists, is relevant for determining class instances.
Besides, https://scala-lang.org/files/archive/spec/2.13/03-types.html#type-erasure states:
The erasure of a type projection
T#x
is|T|#x
.
Here, C
is concrete, so the erasure of C#X
is expected to be C#X
, not A#X
.