Skip to content

Fix #3144: more aggressively check unreachability #3891

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jan 29, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
207 changes: 103 additions & 104 deletions compiler/src/dotty/tools/dotc/transform/patmat/Space.scala
Original file line number Diff line number Diff line change
Expand Up @@ -278,21 +278,8 @@ trait SpaceLogic {
}
}

object SpaceEngine {
private sealed trait Implementability {
def show(implicit ctx: Context) = this match {
case SubclassOf(classSyms) => s"SubclassOf(${classSyms.map(_.show)})"
case other => other.toString
}
}
private case object ClassOrTrait extends Implementability
private case class SubclassOf(classSyms: List[Symbol]) extends Implementability
private case object Unimplementable extends Implementability
}

/** Scala implementation of space logic */
class SpaceEngine(implicit ctx: Context) extends SpaceLogic {
import SpaceEngine._
import tpd._

private val scalaSomeClass = ctx.requiredClass("scala.Some")
Expand All @@ -301,77 +288,15 @@ class SpaceEngine(implicit ctx: Context) extends SpaceLogic {
private val scalaNilType = ctx.requiredModuleRef("scala.collection.immutable.Nil")
private val scalaConsType = ctx.requiredClassRef("scala.collection.immutable.::")

/** Checks if it's possible to create a trait/class which is a subtype of `tp`.
*
* - doesn't handle member collisions (will not declare a type unimplementable because of one)
* - expects that neither Any nor Object reach it
* (this is currently true due to both isSubType and and/or type simplification)
*
* See [[intersectUnrelatedAtomicTypes]].
*/
private def implementability(tp: Type): Implementability = tp.dealias match {
case AndType(tp1, tp2) =>
(implementability(tp1), implementability(tp2)) match {
case (Unimplementable, _) | (_, Unimplementable) => Unimplementable
case (SubclassOf(classSyms1), SubclassOf(classSyms2)) =>
(for {
sym1 <- classSyms1
sym2 <- classSyms2
result <-
if (sym1 isSubClass sym2) List(sym1)
else if (sym2 isSubClass sym1) List(sym2)
else Nil
} yield result) match {
case Nil => Unimplementable
case lst => SubclassOf(lst)
}
case (ClassOrTrait, ClassOrTrait) => ClassOrTrait
case (SubclassOf(clss), _) => SubclassOf(clss)
case (_, SubclassOf(clss)) => SubclassOf(clss)
}
case OrType(tp1, tp2) =>
(implementability(tp1), implementability(tp2)) match {
case (ClassOrTrait, _) | (_, ClassOrTrait) => ClassOrTrait
case (SubclassOf(classSyms1), SubclassOf(classSyms2)) =>
SubclassOf(classSyms1 ::: classSyms2)
case (SubclassOf(classSyms), _) => SubclassOf(classSyms)
case (_, SubclassOf(classSyms)) => SubclassOf(classSyms)
case _ => Unimplementable
}
case _: SingletonType =>
// singleton types have no instantiable subtypes
Unimplementable
case tp: RefinedType =>
// refinement itself is not considered - it would at most make
// a type unimplementable because of a member collision
implementability(tp.parent)
case other =>
val classSym = other.classSymbol
if (classSym.exists) {
if (classSym is Final) Unimplementable
else if (classSym is Trait) ClassOrTrait
else SubclassOf(List(classSym))
} else {
// if no class symbol exists, conservatively say that anything
// can implement `tp`
ClassOrTrait
}
}

override def intersectUnrelatedAtomicTypes(tp1: Type, tp2: Type) = {
val and = AndType(tp1, tp2)
// Precondition: !(tp1 <:< tp2) && !(tp2 <:< tp1)
// Then, no leaf of the and-type tree `and` is a subtype of `and`.
// Then, to create a value of type `and` you must instantiate a trait (class/module)
// which is a subtype of all the leaves of `and`.
val imp = implementability(and)
val res = inhabited(and)

debug.println(s"atomic intersection: ${and.show} ~ ${imp.show}")
debug.println(s"atomic intersection: ${and.show} = ${res}")

imp match {
case Unimplementable => Empty
case _ => Typ(and, true)
}
if (res) Typ(and, true) else Empty
}

/* Whether the extractor is irrefutable */
Expand Down Expand Up @@ -574,21 +499,92 @@ class SpaceEngine(implicit ctx: Context) extends SpaceLogic {

/** Can this type be inhabited by a value?
*
* Intersection between singleton types and other types is always empty
* the singleton type is not a subtype of the other type.
* See patmat/i3573.scala for an example.
* Check is based on the following facts:
*
* - single inheritance of classes
* - final class cannot be extended
* - intersection of a singleton type with another irrelevant type (patmat/i3574.scala)
*
*/
def inhabited(tpe: Type)(implicit ctx: Context): Boolean = {
val emptySingletonIntersection = new ExistsAccumulator({
case AndType(s: SingletonType, t) =>
!(s <:< t)
case AndType(t, s: SingletonType) =>
!(s <:< t)
case x =>
false
})

!emptySingletonIntersection(false, tpe)
def inhabited(tp: Type)(implicit ctx: Context): Boolean = {
// convert top-level type shape into "conjunctive normal form"
def cnf(tp: Type): Type = tp match {
case AndType(OrType(l, r), tp) =>
OrType(cnf(AndType(l, tp)), cnf(AndType(r, tp)))
case AndType(tp, o: OrType) =>
cnf(AndType(o, tp))
case AndType(l, r) =>
val l1 = cnf(l)
val r1 = cnf(r)
if (l1.ne(l) || r1.ne(r)) cnf(AndType(l1, r1))
else AndType(l1, r1)
case OrType(l, r) =>
OrType(cnf(l), cnf(r))
case tp @ RefinedType(OrType(tp1, tp2), _, _) =>
OrType(
cnf(tp.derivedRefinedType(tp1, refinedName = tp.refinedName, refinedInfo = tp.refinedInfo)),
cnf(tp.derivedRefinedType(tp2, refinedName = tp.refinedName, refinedInfo = tp.refinedInfo))
)
case tp: RefinedType =>
val parent1 = cnf(tp.parent)
val tp1 = tp.derivedRefinedType(parent1, refinedName = tp.refinedName, refinedInfo = tp.refinedInfo)

if (parent1.ne(tp.parent)) cnf(tp1) else tp1
case tp: TypeAlias =>
cnf(tp.alias)
case _ =>
tp
}

def isSingleton(tp: Type): Boolean = tp.dealias match {
case AndType(l, r) => isSingleton(l) || isSingleton(r)
case OrType(l, r) => isSingleton(l) && isSingleton(r)
case tp => tp.isSingleton
}

def superType(tp: Type): Type = tp match {
case tp: TypeProxy => tp.superType
case OrType(tp1, tp2) => OrType(superType(tp1), superType(tp2))
case AndType(tp1, tp2) => AndType(superType(tp1), superType(tp2))
case _ => tp
}

def recur(tp: Type): Boolean = tp.dealias match {
case AndType(tp1, tp2) =>
recur(tp1) && recur(tp2) && {
val bases1 = tp1.widenDealias.classSymbols
val bases2 = tp2.widenDealias.classSymbols

debug.println(s"bases of ${tp1.show}: " + bases1)
debug.println(s"bases of ${tp2.show}: " + bases2)

val noClassConflict =
bases1.forall(sym1 => sym1.is(Trait) || bases2.forall(sym2 => sym2.is(Trait) || sym1.isSubClass(sym2))) ||
bases1.forall(sym1 => sym1.is(Trait) || bases2.forall(sym2 => sym2.is(Trait) || sym2.isSubClass(sym1)))

debug.println(s"class conflict for ${tp.show}? " + !noClassConflict)

noClassConflict &&
(!isSingleton(tp1) || tp1 <:< tp2) &&
(!isSingleton(tp2) || tp2 <:< tp1) &&
(!bases1.exists(_ is Final) || tp1 <:< superType(tp2)) &&
(!bases2.exists(_ is Final) || tp2 <:< superType(tp1))
}
case OrType(tp1, tp2) =>
recur(tp1) || recur(tp2)
case tp: RefinedType =>
recur(tp.parent)
case tp: TypeRef =>
recur(tp.prefix) && !(tp.classSymbol.is(AbstractFinal))
case _ =>
true
}

val res = recur(cnf(tp))

debug.println(s"${tp.show} inhabited? " + res)

res
}

/** Instantiate type `tp1` to be a subtype of `tp2`
Expand Down Expand Up @@ -708,8 +704,10 @@ class SpaceEngine(implicit ctx: Context) extends SpaceLogic {
/** Abstract sealed types, or-types, Boolean and Java enums can be decomposed */
def canDecompose(tp: Type): Boolean = {
val dealiasedTp = tp.dealias
val res = tp.classSymbol.is(allOf(Abstract, Sealed)) ||
tp.classSymbol.is(allOf(Trait, Sealed)) ||
val res =
(tp.classSymbol.is(Sealed) &&
tp.classSymbol.is(AbstractOrTrait) &&
tp.classSymbol.children.nonEmpty ) ||
dealiasedTp.isInstanceOf[OrType] ||
(dealiasedTp.isInstanceOf[AndType] && {
val and = dealiasedTp.asInstanceOf[AndType]
Expand Down Expand Up @@ -905,24 +903,25 @@ class SpaceEngine(implicit ctx: Context) extends SpaceLogic {
def checkRedundancy(_match: Match): Unit = {
val Match(sel, cases) = _match
// ignore selector type for now
// val selTyp = sel.tpe.widen.dealias

if (cases.length == 1) return
val selTyp = sel.tpe.widen.dealias

// starts from the second, the first can't be redundant
(1 until cases.length).foreach { i =>
(0 until cases.length).foreach { i =>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe you could iterate over cases.inits instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I didn't know inits before, I'll consider use it later. In the current case, I do need i to get the current clause, init doesn't help much here.

// in redundancy check, take guard as false in order to soundly approximate
val prevs = cases.take(i).map { x =>
if (x.guard.isEmpty) project(x.pat)
else Empty
}.reduce((a, b) => Or(List(a, b)))
val prevs =
if (i == 0)
Empty
else
cases.take(i).map { x =>
if (x.guard.isEmpty) project(x.pat)
else Empty
}.reduce((a, b) => Or(List(a, b)))

val curr = project(cases(i).pat)

debug.println(s"---------------reachable? ${show(curr)}")
debug.println(s"prev: ${show(prevs)}")

if (isSubspace(curr, prevs)) {
if (isSubspace(intersect(curr, Typ(selTyp, false)), prevs)) {
ctx.warning(MatchCaseUnreachable(), cases(i).body.pos)
}
}
Expand Down
1 change: 1 addition & 0 deletions tests/patmat/3144.check
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
6: Pattern Match Exhaustivity: _: Foo
1 change: 1 addition & 0 deletions tests/patmat/3144c.check
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
6: Pattern Match Exhaustivity: _: Foo
9 changes: 9 additions & 0 deletions tests/patmat/3144c.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
sealed trait Foo
case class Bar(s: String)

object Test {
def shouldError(foo: Foo): String =
foo match {
case bar: Bar => bar.s
}
}
43 changes: 43 additions & 0 deletions tests/patmat/3145.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
object Test {
sealed trait Foo
class Bar(val s: String) extends Foo
sealed abstract class Baz(val s: String) extends Foo

val f: Foo => String = {
case bar: Bar => bar.s
case baz: Baz => baz.s
}
}

object Test2 {
sealed trait Foo
class Bar extends Foo
sealed trait Baz extends Foo

def f(x: Foo) = x match {
case bar: Bar => 1
case baz: Baz => 2
}
}

object Test3 {
sealed trait Foo
class Bar extends Foo
sealed trait Baz extends Foo

def foo = {
val x: Foo = new Baz {}
x match {
case bar: Bar => 1
case baz: Baz => 2
}
}

def bar = {
val x: Baz = new Baz {}
x match {
case bar: Bar => 1
case baz: Baz => 2
}
}
}
13 changes: 7 additions & 6 deletions tests/patmat/andtype-opentype-interaction.check
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
23: Pattern Match Exhaustivity: _: SealedClass & OpenTrait, _: AbstractClass & OpenTrait, _: Clazz & OpenTrait, _: Trait & OpenTrait
27: Pattern Match Exhaustivity: _: SealedClass & OpenTrait & OpenTrait2, _: AbstractClass & OpenTrait & OpenTrait2, _: Clazz & OpenTrait & OpenTrait2, _: Trait & OpenTrait & OpenTrait2
31: Pattern Match Exhaustivity: _: Trait & OpenClass
35: Pattern Match Exhaustivity: _: Trait & OpenTrait & OpenClass
43: Pattern Match Exhaustivity: _: Trait & OpenAbstractClass
47: Pattern Match Exhaustivity: _: Trait & OpenClass & OpenTrait & OpenClassSubclass
23: Pattern Match Exhaustivity: _: SealedAbstractClass & OpenTrait, _: SealedClass & OpenTrait, _: SealedTrait & OpenTrait, _: AbstractClass & OpenTrait, _: Clazz & OpenTrait, _: Trait & OpenTrait
27: Pattern Match Exhaustivity: _: SealedAbstractClass & OpenTrait & OpenTrait2, _: SealedClass & OpenTrait & OpenTrait2, _: SealedTrait & OpenTrait & OpenTrait2, _: AbstractClass & OpenTrait & OpenTrait2, _: Clazz & OpenTrait & OpenTrait2, _: Trait & OpenTrait & OpenTrait2
31: Pattern Match Exhaustivity: _: SealedTrait & OpenClass, _: Trait & OpenClass
35: Pattern Match Exhaustivity: _: SealedTrait & OpenTrait & OpenClass, _: Trait & OpenTrait & OpenClass
40: Match case Unreachable
43: Pattern Match Exhaustivity: _: SealedTrait & OpenAbstractClass, _: Trait & OpenAbstractClass
47: Pattern Match Exhaustivity: _: SealedTrait & OpenClass & OpenTrait & OpenClassSubclass, _: Trait & OpenClass & OpenTrait & OpenClassSubclass
3 changes: 3 additions & 0 deletions tests/patmat/andtype-refinedtype-interaction.check
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
32: Pattern Match Exhaustivity: _: Trait & C1{x: Int}
37: Match case Unreachable
43: Match case Unreachable
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}
54: Pattern Match Exhaustivity: _: Trait & (C1 | C2 | T1){x: Int} & C3{x: Int}
60: Match case Unreachable
65: Pattern Match Exhaustivity: _: Trait & (C1 | C2){x: Int} & (C3 | SubC1){x: Int}
72: Pattern Match Exhaustivity: _: Trait & (T1 & (C1 | SubC2)){x: Int} & (T2 & (C2 | C3 | SubC1)){x: Int} &
SubSubC1{x: Int}
Expand Down
6 changes: 4 additions & 2 deletions tests/patmat/i2253.check
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
27: Pattern Match Exhaustivity: HasIntXIntM, HasIntXStringM
28: Pattern Match Exhaustivity: HasIntXIntM
28: Pattern Match Exhaustivity: HasIntXIntM, HasIntXStringM
29: Pattern Match Exhaustivity: HasIntXIntM
29: Match case Unreachable
30: Pattern Match Exhaustivity: HasIntXIntM
30: Match case Unreachable
1 change: 1 addition & 0 deletions tests/patmat/i2253.scala
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ object HasIntXIntM extends S {
}

trait T
case class TA(val x: Int) extends T with S

class Test {
def onlyIntX(s: S { val x: Int }) = s match { case _: T => ; }
Expand Down
1 change: 1 addition & 0 deletions tests/patmat/patmatexhaust.check
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
49: Pattern Match Exhaustivity: _: Gp
59: Pattern Match Exhaustivity: Nil
75: Pattern Match Exhaustivity: _: B
87: Pattern Match Exhaustivity: _: C1
100: Pattern Match Exhaustivity: _: C1
114: Pattern Match Exhaustivity: D2(), D1
126: Pattern Match Exhaustivity: _: C1
2 changes: 1 addition & 1 deletion tests/patmat/patmatexhaust.scala
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ class TestSealedExhaustive { // compile only
case class C3() extends C
case object C4 extends C

def ma10(x: C) = x match { // exhaustive: abstract sealed C1 is dead end.
def ma10(x: C) = x match { // treat abstract sealed C1 is as inhabited.
case C3() => true
case C2 | C4 => true
}
Expand Down
2 changes: 1 addition & 1 deletion tests/patmat/t6450.scala
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
sealed abstract class FoundNode[T]
// case class A[T](x: T) extends FoundNode[T]
case class A[T](x: T) extends FoundNode[T]

object Foo {
val v: (Some[_], FoundNode[_]) = (???, ???)
Expand Down
2 changes: 1 addition & 1 deletion tests/patmat/t8511.check
Original file line number Diff line number Diff line change
@@ -1 +1 @@
18: Pattern Match Exhaustivity: Baz(), Bar(_)
18: Pattern Match Exhaustivity: EatsExhaustiveWarning(_), Baz(), Bar(_)
1 change: 0 additions & 1 deletion tests/patmat/t9677.check

This file was deleted.

1 change: 1 addition & 0 deletions tests/patmat/virtpatmat_reach_sealed_unsealed.check
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
16: Pattern Match Exhaustivity: false
18: Match case Unreachable
19: Match case Unreachable
20: Match case Unreachable