Skip to content

Commit 8815b40

Browse files
authored
Merge pull request #2155 from AleksanderBG/wip-andtype-exhaustivity
Extend exhaustivity checks to and-types
2 parents c9441e5 + 50b03c0 commit 8815b40

12 files changed

+334
-6
lines changed

compiler/src/dotty/tools/dotc/transform/patmat/Space.scala

Lines changed: 110 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,13 @@ trait SpaceLogic {
7474
/** Is `tp1` the same type as `tp2`? */
7575
def isEqualType(tp1: Type, tp2: Type): Boolean
7676

77+
/** Return a space containing the values of both types.
78+
*
79+
* The types should be atomic (non-decomposable) and unrelated (neither
80+
* should be a subtype of the other).
81+
*/
82+
def intersectUnrelatedAtomicTypes(tp1: Type, tp2: Type): Space
83+
7784
/** Is the type `tp` decomposable? i.e. all values of the type can be covered
7885
* by its decomposed types.
7986
*
@@ -171,7 +178,7 @@ trait SpaceLogic {
171178
else if (isSubType(tp2, tp1)) b
172179
else if (canDecompose(tp1)) tryDecompose1(tp1)
173180
else if (canDecompose(tp2)) tryDecompose2(tp2)
174-
else Empty
181+
else intersectUnrelatedAtomicTypes(tp1, tp2)
175182
case (Typ(tp1, _), Kon(tp2, ss)) =>
176183
if (isSubType(tp2, tp1)) b
177184
else if (isSubType(tp1, tp2)) a // problematic corner case: inheriting a case class
@@ -239,10 +246,96 @@ trait SpaceLogic {
239246
}
240247
}
241248

249+
object SpaceEngine {
250+
private sealed trait Implementability {
251+
def show(implicit ctx: Context) = this match {
252+
case SubclassOf(classSyms) => s"SubclassOf(${classSyms.map(_.show)})"
253+
case other => other.toString
254+
}
255+
}
256+
private case object ClassOrTrait extends Implementability
257+
private case class SubclassOf(classSyms: List[Symbol]) extends Implementability
258+
private case object Unimplementable extends Implementability
259+
}
260+
242261
/** Scala implementation of space logic */
243262
class SpaceEngine(implicit ctx: Context) extends SpaceLogic {
263+
import SpaceEngine._
244264
import tpd._
245265

266+
/** Checks if it's possible to create a trait/class which is a subtype of `tp`.
267+
*
268+
* - doesn't handle member collisions (will not declare a type unimplementable because of one)
269+
* - expects that neither Any nor Object reach it
270+
* (this is currently true due to both isSubType and and/or type simplification)
271+
*
272+
* See [[intersectUnrelatedAtomicTypes]].
273+
*/
274+
private def implementability(tp: Type): Implementability = tp.dealias match {
275+
case AndType(tp1, tp2) =>
276+
(implementability(tp1), implementability(tp2)) match {
277+
case (Unimplementable, _) | (_, Unimplementable) => Unimplementable
278+
case (SubclassOf(classSyms1), SubclassOf(classSyms2)) =>
279+
(for {
280+
sym1 <- classSyms1
281+
sym2 <- classSyms2
282+
result <-
283+
if (sym1 isSubClass sym2) List(sym1)
284+
else if (sym2 isSubClass sym1) List(sym2)
285+
else Nil
286+
} yield result) match {
287+
case Nil => Unimplementable
288+
case lst => SubclassOf(lst)
289+
}
290+
case (ClassOrTrait, ClassOrTrait) => ClassOrTrait
291+
case (SubclassOf(clss), _) => SubclassOf(clss)
292+
case (_, SubclassOf(clss)) => SubclassOf(clss)
293+
}
294+
case OrType(tp1, tp2) =>
295+
(implementability(tp1), implementability(tp2)) match {
296+
case (ClassOrTrait, _) | (_, ClassOrTrait) => ClassOrTrait
297+
case (SubclassOf(classSyms1), SubclassOf(classSyms2)) =>
298+
SubclassOf(classSyms1 ::: classSyms2)
299+
case (SubclassOf(classSyms), _) => SubclassOf(classSyms)
300+
case (_, SubclassOf(classSyms)) => SubclassOf(classSyms)
301+
case _ => Unimplementable
302+
}
303+
case _: SingletonType =>
304+
// singleton types have no instantiable subtypes
305+
Unimplementable
306+
case tp: RefinedType =>
307+
// refinement itself is not considered - it would at most make
308+
// a type unimplementable because of a member collision
309+
implementability(tp.parent)
310+
case other =>
311+
val classSym = other.classSymbol
312+
if (classSym.exists) {
313+
if (classSym is Final) Unimplementable
314+
else if (classSym is Trait) ClassOrTrait
315+
else SubclassOf(List(classSym))
316+
} else {
317+
// if no class symbol exists, conservatively say that anything
318+
// can implement `tp`
319+
ClassOrTrait
320+
}
321+
}
322+
323+
override def intersectUnrelatedAtomicTypes(tp1: Type, tp2: Type) = {
324+
val and = AndType(tp1, tp2)
325+
// Precondition: !(tp1 <:< tp2) && !(tp2 <:< tp1)
326+
// Then, no leaf of the and-type tree `and` is a subtype of `and`.
327+
// Then, to create a value of type `and` you must instantiate a trait (class/module)
328+
// which is a subtype of all the leaves of `and`.
329+
val imp = implementability(and)
330+
331+
debug.println(s"atomic intersection: ${and.show} ~ ${imp.show}")
332+
333+
imp match {
334+
case Unimplementable => Empty
335+
case _ => Typ(and, true)
336+
}
337+
}
338+
246339
/** Return the space that represents the pattern `pat`
247340
*
248341
* If roundUp is true, approximate extractors to its type,
@@ -325,6 +418,12 @@ class SpaceEngine(implicit ctx: Context) extends SpaceLogic {
325418
debug.println(s"candidates for ${tp.show} : [${children.map(_.show).mkString(", ")}]")
326419

327420
tp.dealias match {
421+
case AndType(tp1, tp2) =>
422+
intersect(Typ(tp1, false), Typ(tp2, false)) match {
423+
case Or(spaces) => spaces
424+
case Empty => Nil
425+
case space => List(space)
426+
}
328427
case OrType(tp1, tp2) => List(Typ(tp1, true), Typ(tp2, true))
329428
case _ if tp =:= ctx.definitions.BooleanType =>
330429
List(
@@ -377,9 +476,14 @@ class SpaceEngine(implicit ctx: Context) extends SpaceLogic {
377476

378477
/** Abstract sealed types, or-types, Boolean and Java enums can be decomposed */
379478
def canDecompose(tp: Type): Boolean = {
479+
val dealiasedTp = tp.dealias
380480
val res = tp.classSymbol.is(allOf(Abstract, Sealed)) ||
381481
tp.classSymbol.is(allOf(Trait, Sealed)) ||
382-
tp.dealias.isInstanceOf[OrType] ||
482+
dealiasedTp.isInstanceOf[OrType] ||
483+
(dealiasedTp.isInstanceOf[AndType] && {
484+
val and = dealiasedTp.asInstanceOf[AndType]
485+
canDecompose(and.tp1) || canDecompose(and.tp2)
486+
}) ||
383487
tp =:= ctx.definitions.BooleanType ||
384488
tp.classSymbol.is(allOf(Enum, Sealed)) // Enum value doesn't have Sealed flag
385489

@@ -477,6 +581,10 @@ class SpaceEngine(implicit ctx: Context) extends SpaceLogic {
477581
ctx.settings.YcheckAllPatmat.value ||
478582
tp.typeSymbol.is(Sealed) ||
479583
tp.isInstanceOf[OrType] ||
584+
(tp.isInstanceOf[AndType] && {
585+
val and = tp.asInstanceOf[AndType]
586+
isCheckable(and.tp1) || isCheckable(and.tp2)
587+
}) ||
480588
tp.typeSymbol == ctx.definitions.BooleanType.typeSymbol ||
481589
tp.typeSymbol.is(Enum) ||
482590
canDecompose(tp) ||

compiler/test/dotty/tools/dotc/transform/PatmatExhaustivityTest.scala

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,11 @@ class PatmatExhaustivityTest {
2828
e.printStackTrace()
2929
}
3030

31-
val actual = stringBuffer.toString.trim
31+
val actual = stringBuffer.toString.trim.replaceAll("\\s+\n", "\n")
3232
val checkFilePath = file.getAbsolutePath.stripSuffix(".scala") + ".check"
3333
val checkContent =
3434
if (new File(checkFilePath).exists)
35-
fromFile(checkFilePath).getLines.mkString("\n").trim
35+
fromFile(checkFilePath).getLines.map(_.replaceAll("\\s+$", "")).mkString("\n").trim
3636
else ""
3737

3838
(file, checkContent, actual)
@@ -55,11 +55,11 @@ class PatmatExhaustivityTest {
5555
e.printStackTrace()
5656
}
5757

58-
val actual = stringBuffer.toString.trim
58+
val actual = stringBuffer.toString.trim.replaceAll("\\s+\n", "\n")
5959
val checkFilePath = file.getPath + File.separator + "expected.check"
6060
val checkContent =
6161
if (new File(checkFilePath).exists)
62-
fromFile(checkFilePath).getLines.mkString("\n").trim
62+
fromFile(checkFilePath).getLines.map(_.replaceAll("\\s+$", "")).mkString("\n").trim
6363
else ""
6464

6565
(file, checkContent, actual)

tests/patmat/aliasing.check

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
14: Pattern Match Exhaustivity: _: Clazz & Test.Alias1, _: Trait & Test.Alias1
2+
19: Pattern Match Exhaustivity: _: Trait & Test.Alias2
3+
23: Pattern Match Exhaustivity: _: Trait & (Test.Alias2 & OpenTrait2){x: Int}

tests/patmat/aliasing.scala

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
sealed trait T
2+
trait Trait extends T
3+
class Clazz extends T
4+
object Obj extends T
5+
6+
trait OpenTrait
7+
trait OpenTrait2
8+
class OpenClass
9+
10+
trait Unrelated
11+
12+
object Test {
13+
type Alias1 = OpenTrait
14+
def traitAndClass(s: T & Alias1) = s match {
15+
case _: Unrelated => ;
16+
}
17+
18+
type Alias2 = OpenTrait & OpenClass
19+
def classOnly(s: T & Alias2) = s match {
20+
case _: Unrelated => ;
21+
}
22+
23+
def classOnlyRefined(s: T & (Alias2 & OpenTrait2) { val x: Int }) = s match {
24+
case _: Unrelated => ;
25+
}
26+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
23: Pattern Match Exhaustivity: _: SealedClass & OpenTrait, _: AbstractClass & OpenTrait, _: Clazz & OpenTrait, _: Trait & OpenTrait
2+
27: Pattern Match Exhaustivity: _: SealedClass & OpenTrait & OpenTrait2, _: AbstractClass & OpenTrait & OpenTrait2, _: Clazz & OpenTrait & OpenTrait2, _: Trait & OpenTrait & OpenTrait2
3+
31: Pattern Match Exhaustivity: _: Trait & OpenClass
4+
35: Pattern Match Exhaustivity: _: Trait & OpenTrait & OpenClass
5+
43: Pattern Match Exhaustivity: _: Trait & OpenAbstractClass
6+
47: Pattern Match Exhaustivity: _: Trait & OpenTrait & OpenClassSubclass
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
sealed trait T
2+
trait Trait extends T
3+
class Clazz extends T
4+
abstract class AbstractClass extends T
5+
// none of the below can be extended
6+
sealed trait SealedTrait extends T
7+
final class FinalClass extends T
8+
sealed class SealedClass extends T
9+
sealed abstract class SealedAbstractClass extends T
10+
object Obj extends T
11+
12+
trait OpenTrait
13+
class OpenClass
14+
class OpenClassSubclass extends OpenClass
15+
abstract class OpenAbstractClass
16+
17+
trait Unrelated
18+
19+
trait OpenTrait2
20+
class OpenClass2
21+
22+
object Test {
23+
def m1a(s: T & OpenTrait) = s match {
24+
case _: Unrelated => ;
25+
}
26+
27+
def m1b(s: T & OpenTrait & OpenTrait2) = s match {
28+
case _: Unrelated => ;
29+
}
30+
31+
def m2(s: T & OpenClass) = s match {
32+
case _: Unrelated => ;
33+
}
34+
35+
def m2b(s: T & OpenTrait & OpenClass) = s match {
36+
case _: Unrelated => ;
37+
}
38+
39+
def m2c(s: (T & OpenClass) & (OpenTrait & OpenClass2)) = s match {
40+
case _: Unrelated => ;
41+
}
42+
43+
def m3(s: T & OpenAbstractClass) = s match {
44+
case _: Unrelated => ;
45+
}
46+
47+
def m4(s: (T & OpenClass) & (OpenTrait & OpenClassSubclass)) = s match {
48+
case _: Unrelated => ;
49+
}
50+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
32: Pattern Match Exhaustivity: _: Trait & C1{x: Int}
2+
48: Pattern Match Exhaustivity: _: Clazz & (C1 | C2 | T1){x: Int} & (C3 | C4 | T2){x: Int}, _: Trait & (C1 | C2 | T1){x: Int} & (C3 | C4 | T2){x: Int}
3+
54: Pattern Match Exhaustivity: _: Trait & (C1 | C2 | T1){x: Int} & C3{x: Int}
4+
65: Pattern Match Exhaustivity: _: Trait & (C1 | C2){x: Int} & (C3 | SubC1){x: Int}
5+
72: Pattern Match Exhaustivity: _: Trait & (T1 & C1 | T1 & SubC2){x: Int} &
6+
(T2 & C2 | T2 & C3 | T2 & SubC1){x: Int}
7+
& SubSubC1{x: Int}
8+
79: Pattern Match Exhaustivity: _: Trait & (T1 & C1 | T1 & SubC2){x: Int} &
9+
(T2 & C2 | T2 & C3 | T2 & SubC1){x: Int}
10+
& SubSubC2{x: Int}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
sealed trait S
2+
trait Trait extends S
3+
class Clazz extends S
4+
object Obj extends S
5+
6+
trait T1
7+
trait T2
8+
9+
class C1
10+
class C2
11+
class C3
12+
class C4
13+
14+
class SubC1 extends C1
15+
class SubSubC1 extends SubC1
16+
class SubC2 extends C2
17+
class SubSubC2 extends SubC2
18+
19+
trait Unrelated
20+
21+
object Test {
22+
/*
23+
Note: error message on this suggests Obj to be unmatched as well.
24+
This isn't a bug in atomic type intersection, but a consequence
25+
of approximating all types to be a subtype of a structural type.
26+
27+
def basic1(s: S & { val x: Int }) = s match {
28+
case _: Unrelated => ;
29+
}
30+
*/
31+
32+
def basic2(s: S & C1 { val x: Int }) = s match {
33+
case _: Unrelated => ;
34+
}
35+
36+
def doubleInheritance1(s: S & C1 { val x: Int } & C2) = s match {
37+
case _: Unrelated => ;
38+
}
39+
40+
def doubleInheritance2(s: S & C1 { val x: Int }
41+
& C2 { val x: Int }) =
42+
s match {
43+
case _: Unrelated => ;
44+
}
45+
46+
def classOrTrait(s: S & (C1 | (C2 | T1)) { val x: Int }
47+
& (C3 | (C4 | T2)) { val x: Int }) =
48+
s match {
49+
case _: Unrelated => ;
50+
}
51+
52+
def traitOnly(s: S & (C1 | (C2 | T1)) { val x: Int }
53+
& C3 { val x: Int }) =
54+
s match {
55+
case _: Unrelated => ;
56+
}
57+
58+
def nestedDoubleInheritance(s: S & (C1 & C2) { val x: Int }) =
59+
s match {
60+
case _: Unrelated => ;
61+
}
62+
63+
def subclassingA(s: S & (C1 | C2) { val x: Int }
64+
& (C3 | SubC1) { val x: Int }) =
65+
s match {
66+
case _: Unrelated => ;
67+
}
68+
69+
def subclassingB(s: S & (T1 & (C1 | SubC2)) { val x: Int }
70+
& (T2 & (C2 | C3 | SubC1)) { val x: Int }
71+
& SubSubC1 { val x: Int }) =
72+
s match {
73+
case _: Unrelated => ;
74+
}
75+
76+
def subclassingC(s: S & (T1 & (C1 | SubC2)) { val x: Int }
77+
& (T2 & (C2 | C3 | SubC1)) { val x: Int }
78+
& SubSubC2 { val x: Int }) =
79+
s match {
80+
case _: Unrelated => ;
81+
}
82+
}

tests/patmat/andtype.check

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
9: Pattern Match Exhaustivity: _: B
2+
13: Pattern Match Exhaustivity: _: B
3+
17: Pattern Match Exhaustivity: _: B

tests/patmat/andtype.scala

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
object Test {
2+
trait Marker
3+
4+
sealed trait T
5+
trait A extends T with Marker
6+
trait B extends T with Marker
7+
case object C extends T
8+
9+
def m1(s: T & Marker) = s match {
10+
case _: A => ;
11+
}
12+
13+
def m2(s: Marker & T) = s match {
14+
case _: A => ;
15+
}
16+
17+
def m3(s: (A | B) & Marker) = s match {
18+
case _: A => ;
19+
}
20+
}

tests/patmat/nontrivial-andtype.check

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
16: Pattern Match Exhaustivity: D, _: C, B(), _: A

0 commit comments

Comments
 (0)