Skip to content

Commit d2d3da9

Browse files
authored
Fix #16899: Better handle X instanceOf P where X is T1 | T2 (#17382)
2 parents be32eb9 + 1f12656 commit d2d3da9

File tree

3 files changed

+83
-6
lines changed

3 files changed

+83
-6
lines changed

compiler/src/dotty/tools/dotc/transform/TypeTestsCasts.scala

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import util.Spans._
1616
import reporting._
1717
import config.Printers.{ transforms => debug }
1818

19+
import patmat.Typ
20+
1921
/** This transform normalizes type tests and type casts,
2022
* also replacing type tests with singleton argument type with reference equality check
2123
* Any remaining type tests
@@ -51,7 +53,8 @@ object TypeTestsCasts {
5153
* 6. if `P = T1 | T2` or `P = T1 & T2`, checkable(X, T1) && checkable(X, T2).
5254
* 7. if `P` is a refinement type, "it's a refinement type"
5355
* 8. if `P` is a local class which is not statically reachable from the scope where `X` is defined, "it's a local class"
54-
* 9. otherwise, ""
56+
* 9. if `X` is `T1 | T2`, checkable(T1, P) && checkable(T2, P) or (isCheckDefinitelyFalse(T1, P) && checkable(T2, P)) or (checkable(T1, P) && isCheckDefinitelyFalse(T2, P)).
57+
* 10. otherwise, ""
5558
*/
5659
def whyUncheckable(X: Type, P: Type, span: Span)(using Context): String = atPhase(Phases.refchecksPhase.next) {
5760
extension (inline s1: String) inline def &&(inline s2: String): String = if s1 == "" then s2 else s1
@@ -129,6 +132,41 @@ object TypeTestsCasts {
129132

130133
}
131134

135+
/** Whether the check X.isInstanceOf[P] is definitely false? */
136+
def isCheckDefinitelyFalse(X: Type, P: Type)(using Context): Boolean = trace(s"isCheckDefinitelyFalse(${X.show}, ${P.show})") {
137+
X.widenDealias match
138+
case AndType(x1, x2) =>
139+
isCheckDefinitelyFalse(x1, P) || isCheckDefinitelyFalse(x2, P)
140+
141+
case x =>
142+
P.widenDealias match
143+
case AndType(p1, p2) =>
144+
isCheckDefinitelyFalse(x, p1) || isCheckDefinitelyFalse(x, p2)
145+
146+
case p =>
147+
val pSpace = Typ(p)
148+
val xSpace = Typ(x)
149+
if pSpace.canDecompose then
150+
val ps = pSpace.decompose.map(_.tp)
151+
ps.forall(p => isCheckDefinitelyFalse(x, p))
152+
else if xSpace.canDecompose then
153+
val xs = xSpace.decompose.map(_.tp)
154+
xs.forall(x => isCheckDefinitelyFalse(x, p))
155+
else
156+
if x.typeSymbol.isClass && p.typeSymbol.isClass then
157+
val xClass = effectiveClass(x)
158+
val pClass = effectiveClass(p)
159+
160+
val bothAreClasses = !xClass.is(Trait) && !pClass.is(Trait)
161+
val notXsubP = !xClass.derivesFrom(pClass)
162+
val notPsubX = !pClass.derivesFrom(xClass)
163+
bothAreClasses && notXsubP && notPsubX
164+
|| xClass.is(Final) && notXsubP
165+
|| pClass.is(Final) && notPsubX
166+
else
167+
false
168+
}
169+
132170
def recur(X: Type, P: Type): String = (X <:< P) ||| (P.dealias match {
133171
case _: SingletonType => ""
134172
case _: TypeProxy
@@ -146,7 +184,20 @@ object TypeTestsCasts {
146184
// - T1 <:< T2 | T3
147185
// - T1 & T2 <:< T3
148186
// See TypeComparer#either
149-
recur(tp1, P) && recur(tp2, P)
187+
val res1 = recur(tp1, P)
188+
val res2 = recur(tp2, P)
189+
190+
if res1.isEmpty && res2.isEmpty then
191+
res1
192+
else if res2.isEmpty then
193+
if isCheckDefinitelyFalse(tp1, P) then res2
194+
else res1
195+
else if res1.isEmpty then
196+
if isCheckDefinitelyFalse(tp2, P) then res1
197+
else res2
198+
else
199+
res1
200+
150201
case _ =>
151202
// always false test warnings are emitted elsewhere
152203
X.classSymbol.exists && P.classSymbol.exists &&
@@ -302,8 +353,8 @@ object TypeTestsCasts {
302353

303354
/** Transform isInstanceOf
304355
*
305-
* expr.isInstanceOf[A | B] ~~> expr.isInstanceOf[A] | expr.isInstanceOf[B]
306-
* expr.isInstanceOf[A & B] ~~> expr.isInstanceOf[A] & expr.isInstanceOf[B]
356+
* expr.isInstanceOf[A | B] ~~> expr.isInstanceOf[A] | expr.isInstanceOf[B]
357+
* expr.isInstanceOf[A & B] ~~> expr.isInstanceOf[A] & expr.isInstanceOf[B]
307358
* expr.isInstanceOf[Tuple] ~~> scala.runtime.Tuples.isInstanceOfTuple(expr)
308359
* expr.isInstanceOf[EmptyTuple] ~~> scala.runtime.Tuples.isInstanceOfEmptyTuple(expr)
309360
* expr.isInstanceOf[NonEmptyTuple] ~~> scala.runtime.Tuples.isInstanceOfNonEmptyTuple(expr)

tests/neg-custom-args/isInstanceOf/i5826.scala

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
class Foo {
2-
def test[A]: List[Int] | A => Int = {
3-
case ls: List[Int] => ls.head // error
2+
def test[A]: (List[Int] | A) => Int = {
3+
case ls: List[Int] => ls.head // error, A = List[String]
44
case _ => 0
55
}
66

@@ -17,4 +17,25 @@ class Foo {
1717
case ls: A[X] => 4 // error
1818
case _ => 0
1919
}
20+
21+
def test4[A](x: List[Int] | (A => Int)) = x match {
22+
case ls: List[Int] => ls.head // error, List extends Int => T
23+
case _ => 0
24+
}
25+
26+
final class C[T] extends A[T]
27+
28+
def test5[T](x: A[T] | B[T] | Option[T]): Boolean = x.isInstanceOf[C[String]] // error
29+
30+
def test6[T](x: A[T] | B[T] | Option[T]): Boolean = x.isInstanceOf[C[T]]
31+
32+
def test7[A](x: Option[Int] | (A => Int)) = x match {
33+
case ls: Option[Int] => ls.head // OK, Option decomposes to Some and None
34+
case _ => 0
35+
}
36+
37+
def test8(x: List[Int] | A[String]) = x match {
38+
case ls: List[Int] => ls.head // OK, List decomposes to :: and Nil
39+
case _ => 0
40+
}
2041
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
sealed trait Unset
2+
3+
def foo(v: Unset|Option[Int]): Unit = v match
4+
case v: Unset => ()
5+
case v: Option[Int] => ()

0 commit comments

Comments
 (0)