bug
bug copied to clipboard
Incorrect typing under match with alternatives
Given
sealed trait A[T] { def x: T }
final case class B(x: String) extends A[String]
final case class C(x: Int) extends A[Int]
def f[T](a: A[T]): Unit = {
def v: T = a match { case C(x) => 42 }
a match {
case B(_) | C(_) =>
val v1: T = v
val v2: T = a match { case C(x) => 42 }
()
}
}
the definition of v1 typechecks correctly, but v2 fails:
$ ~/scala/scala-2.13.14/bin/scala adt.scala
adt.scala:10: error: type mismatch;
found : Int(42)
required: T
val v2: T = a match { case C(x) => 42 }
^
The failure is consistent in Scala 2.12, 2.13 and 3.6, but compiling with 3.6 gives a better hint at the problem:
$ ~/scala/scala3-3.6.3-aarch64-apple-darwin/bin/scala adt.scala
Compiling project (Scala 3.6.3, JVM (11))
[error] ./adt.scala:10:42
[error] Found: (42 : Int)
[error] Required: T
[error]
[error] where: T is a type in method f which is an alias of String
[error] val v2: T = a match { case C(x) => 42 }
[error] ^^
I took a quick look. In
def f[T](a: A[T]): T = a match { case C(x) => 42 }
the bounds of T are temporarily mutated to >: Int <: Int and later restored.
For an alternative pattern case C(_) | B(_), the bounds are first set to >: Int <: Int, and then the constraints are not updated because they are incompatible. That is actually unsound:
scala> sealed trait A[T] { def x: T }
| final case class B(x: String) extends A[String]
| final case class C(x: Int) extends A[Int]
scala> def f[T](a: A[T]): T = a match { case B(_) | C(_) => "plop" }
scala> f(C(1)) + 1
java.lang.ClassCastException: class java.lang.String cannot be cast to class java.lang.Integer (java.lang.String and java.lang.Integer are in module java.base of loader 'bootstrap')
at scala.runtime.BoxesRunTime.unboxToInt(BoxesRunTime.java:99)
It's really fascinating that bugs like this are still discovered after so many years.
For the original example with the nested pattern
def f[T](a: A[T]): T = a match {
case B(_) | C(_) =>
a match { case C(_) => 42 }
}
I don't know if it's possible to support that. We'd have to know which of the temporary GADT bounds to reset for typing the nested pattern, and for which ones to keep the constraints from the enclosing pattern?
CC @dwijnand. I created a ticket on Scala 3 (https://github.com/scala/scala3/issues/22805).
~I guess in Scala 3 it could be String | Int and in Scala 2 it could be Any~
No, better to reject this then