Skip to content

Commit be54633

Browse files
Fix #6570: Don't reduce MT with empty scrutinies
1 parent 19b4216 commit be54633

File tree

7 files changed

+213
-30
lines changed

7 files changed

+213
-30
lines changed

compiler/src/dotty/tools/dotc/core/TypeComparer.scala

Lines changed: 63 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import transform.TypeUtils._
1616
import transform.SymUtils._
1717
import scala.util.control.NonFatal
1818
import typer.ProtoTypes.constrained
19+
import typer.Applications.productSelectorTypes
1920
import reporting.trace
2021

2122
final class AbsentContext
@@ -2136,6 +2137,32 @@ class TypeComparer(initctx: Context) extends ConstraintHandling[AbsentContext] w
21362137
/** Returns last check's debug mode, if explicitly enabled. */
21372138
def lastTrace(): String = ""
21382139

2140+
/** Is `tp` an empty type?
2141+
*
2142+
* `true` implies that we found a proof; uncertainty default to `false`.
2143+
*/
2144+
def provablyEmpty(tp: Type): Boolean =
2145+
tp.dealias match {
2146+
case tp if tp.isBottomType => true
2147+
case AndType(tp1, tp2) => disjoint(tp1, tp2)
2148+
case OrType(tp1, tp2) => provablyEmpty(tp1) && provablyEmpty(tp2)
2149+
case at @ AppliedType(tycon, args) =>
2150+
args.lazyZip(tycon.typeParams).exists { (arg, tparam) =>
2151+
tparam.paramVariance >= 0 &&
2152+
provablyEmpty(arg) &&
2153+
productSelectorTypes(tycon, null).exists {
2154+
case tp: TypeRef =>
2155+
(tp.designator: Any) == tparam
2156+
case _ =>
2157+
false
2158+
}
2159+
}
2160+
case tp: TypeProxy =>
2161+
provablyEmpty(tp.underlying)
2162+
case _ => false
2163+
}
2164+
2165+
21392166
/** Are `tp1` and `tp2` disjoint types?
21402167
*
21412168
* `true` implies that we found a proof; uncertainty default to `false`.
@@ -2144,8 +2171,12 @@ class TypeComparer(initctx: Context) extends ConstraintHandling[AbsentContext] w
21442171
*
21452172
* 1. Single inheritance of classes
21462173
* 2. Final classes cannot be extended
2147-
* 3. ConstantTypes with distinc values are non intersecting
2174+
* 3. ConstantTypes with distinct values are non intersecting
21482175
* 4. There is no value of type Nothing
2176+
*
2177+
* Note on soundness: the correctness of match types relies on on the
2178+
* property that in all possible contexts, a same match type expression
2179+
* is either stuck or reduces to the same case.
21492180
*/
21502181
def disjoint(tp1: Type, tp2: Type)(implicit ctx: Context): Boolean = {
21512182
// println(s"disjoint(${tp1.show}, ${tp2.show})")
@@ -2187,26 +2218,17 @@ class TypeComparer(initctx: Context) extends ConstraintHandling[AbsentContext] w
21872218
else
21882219
false
21892220
case (AppliedType(tycon1, args1), AppliedType(tycon2, args2)) if tycon1 == tycon2 =>
2221+
// It is possible to conclude that two types appiles are disjoints by
2222+
// looking at covariant type parameters if The said type parameters
2223+
// are disjoin and correspond to fields.
2224+
// (Type parameter disjointness is not enought by itself as it could
2225+
// lead to incorrect conclusions for phantom type parameters).
21902226
def covariantDisjoint(tp1: Type, tp2: Type, tparam: TypeParamInfo): Boolean =
2191-
disjoint(tp1, tp2) && {
2192-
// We still need to proof that `Nothing` is not a valid
2193-
// instantiation of this type parameter. We have two ways
2194-
// to get to that conclusion:
2195-
// 1. `Nothing` does not conform to the type parameter's lb
2196-
// 2. `tycon1` has a field typed with this type parameter.
2197-
//
2198-
// Because of separate compilation, the use of 2. is
2199-
// limited to case classes.
2200-
import dotty.tools.dotc.typer.Applications.productSelectorTypes
2201-
val lowerBoundedByNothing = tparam.paramInfo.bounds.lo eq NothingType
2202-
val typeUsedAsField =
2203-
productSelectorTypes(tycon1, null).exists {
2204-
case tp: TypeRef =>
2205-
(tp.designator: Any) == tparam // Bingo!
2206-
case _ =>
2207-
false
2208-
}
2209-
!lowerBoundedByNothing || typeUsedAsField
2227+
disjoint(tp1, tp2) && productSelectorTypes(tycon1, null).exists {
2228+
case tp: TypeRef =>
2229+
(tp.designator: Any) == tparam // Bingo!
2230+
case _ =>
2231+
false
22102232
}
22112233

22122234
args1.lazyZip(args2).lazyZip(tycon1.typeParams).exists {
@@ -2419,6 +2441,7 @@ class TrackingTypeComparer(initctx: Context) extends TypeComparer(initctx) {
24192441
}.apply(tp)
24202442

24212443
val defn.MatchCase(pat, body) = cas1
2444+
24222445
if (isSubType(scrut, pat))
24232446
// `scrut` is a subtype of `pat`: *It's a Match!*
24242447
Some {
@@ -2433,7 +2456,7 @@ class TrackingTypeComparer(initctx: Context) extends TypeComparer(initctx) {
24332456
else if (isSubType(widenAbstractTypes(scrut), widenAbstractTypes(pat)))
24342457
Some(NoType)
24352458
else if (disjoint(scrut, pat))
2436-
// We found a proof that `scrut` and `pat` are incompatible.
2459+
// We found a proof that `scrut` and `pat` are incompatible.
24372460
// The search continues.
24382461
None
24392462
else
@@ -2445,7 +2468,25 @@ class TrackingTypeComparer(initctx: Context) extends TypeComparer(initctx) {
24452468
case Nil => NoType
24462469
}
24472470

2448-
inFrozenConstraint(recur(cases))
2471+
inFrozenConstraint {
2472+
// Empty types break the basic assumption that if a scrutinee and a
2473+
// pattern are disjoint it's OK to reduce passed that pattern. Indeed,
2474+
// empty types viewed as a set of value is always a subset of any other
2475+
// types. As a result, we first check that the scrutinee isn't empty
2476+
// before proceeding with reduction. See `tests/neg/6570.scala` and
2477+
// `6570-1.scala` for examples that exploit emptiness to break match
2478+
// type soundness.
2479+
2480+
// If we revered the uncertainty case of this empty check, that is,
2481+
// `!provablyNonEmpty` instead of `provablyEmpty`, that would be
2482+
// obviously sound, but quite restrictive. With the current formulation,
2483+
// we need to be careful that `provablyEmpty` covers all the conditions
2484+
// used to conclude disjointness in `disjoint`.
2485+
if (provablyEmpty(scrut))
2486+
NoType
2487+
else
2488+
recur(cases)
2489+
}
24492490
}
24502491
}
24512492

tests/neg/6314-1.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ object G {
1818
}
1919

2020
def main(args: Array[String]): Unit = {
21-
val a: Bar[X & Y] = "hello"
21+
val a: Bar[X & Y] = "hello" // error
2222
val i: Bar[Y & Foo] = Foo.apply[Bar](a)
2323
val b: Int = i // error
2424
println(b + 1)

tests/neg/6314-2.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@ object G {
1212
case Y => Int
1313
}
1414

15-
val a: Bar[X & Foo] = "hello"
15+
val a: Bar[X & Foo] = "hello" // error
1616
val b: Bar[Y & Foo] = 1 // error
1717

1818
def main(args: Array[String]): Unit = {
19-
val a: Bar[X & Foo] = "hello"
19+
val a: Bar[X & Foo] = "hello" // error
2020
val i: Bar[Y & Foo] = Foo.apply[Bar](a)
2121
val b: Int = i // error
2222
println(b + 1)

tests/neg/6570-1.scala

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import scala.Tuple._
2+
3+
trait Trait1
4+
trait Trait2
5+
6+
case class Box[+T](t: T)
7+
8+
type N[x] = x match {
9+
case Box[String] => Trait1
10+
case Box[Int] => Trait2
11+
}
12+
13+
trait Cov[+T]
14+
type M[t] = t match {
15+
case Cov[x] => N[x]
16+
}
17+
18+
trait Root[A] {
19+
def thing: M[A]
20+
}
21+
22+
class Asploder extends Root[Cov[Box[Int & String]]] {
23+
def thing = new Trait1 {} // error
24+
}
25+
26+
object Main {
27+
def foo[T <: Cov[Box[Int]]](c: Root[T]): Trait2 = c.thing
28+
29+
def explode = foo(new Asploder)
30+
31+
def main(args: Array[String]): Unit =
32+
explode
33+
}

tests/neg/6570.scala

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
object Base {
2+
trait Trait1
3+
trait Trait2
4+
type N[t] = t match {
5+
case String => Trait1
6+
case Int => Trait2
7+
}
8+
}
9+
import Base._
10+
11+
object UpperBoundParametricVariant {
12+
trait Cov[+T]
13+
type M[t] = t match {
14+
case Cov[x] => N[x]
15+
}
16+
17+
trait Root[A] {
18+
def thing: M[A]
19+
}
20+
21+
trait Child[A <: Cov[Int]] extends Root[A]
22+
23+
// we reduce `M[T]` to `Trait2`, even though we cannot be certain of that
24+
def foo[T <: Cov[Int]](c: Child[T]): Trait2 = c.thing
25+
26+
class Asploder extends Child[Cov[String & Int]] {
27+
def thing = new Trait1 {} // error
28+
}
29+
30+
def explode = foo(new Asploder) // ClassCastException
31+
}
32+
33+
object InheritanceVariant {
34+
// allows binding a variable to the UB of a type member
35+
type Trick[a] = { type A <: a }
36+
type M[t] = t match { case Trick[a] => N[a] }
37+
38+
trait Root {
39+
type B
40+
def thing: M[B]
41+
}
42+
43+
trait Child extends Root { type B <: { type A <: Int } }
44+
45+
def foo(c: Child): Trait2 = c.thing
46+
47+
class Asploder extends Child {
48+
type B = { type A = String & Int }
49+
def thing = new Trait1 {} // error
50+
}
51+
}
52+
53+
object ThisTypeVariant {
54+
type Trick[a] = { type A <: a }
55+
type M[t] = t match { case Trick[a] => N[a] }
56+
57+
trait Root {
58+
def thing: M[this.type]
59+
}
60+
61+
trait Child extends Root { type A <: Int }
62+
63+
def foo(c: Child): Trait2 = c.thing
64+
65+
class Asploder extends Child {
66+
type A = String & Int
67+
def thing = new Trait1 {} // error
68+
}
69+
}
70+
71+
object ParametricVariant {
72+
type Trick[a] = { type A <: a }
73+
type M[t] = t match { case Trick[a] => N[a] }
74+
75+
trait Root[B] {
76+
def thing: M[B]
77+
}
78+
79+
trait Child[B <: { type A <: Int }] extends Root[B]
80+
81+
def foo[T <: { type A <: Int }](c: Child[T]): Trait2 = c.thing
82+
83+
class Asploder extends Child[{ type A = String & Int }] {
84+
def thing = new Trait1 {} // error
85+
}
86+
87+
def explode = foo(new Asploder)
88+
}
89+
90+
object UpperBoundVariant {
91+
trait Cov[+T]
92+
type M[t] = t match { case Cov[t] => N[t] }
93+
94+
trait Root {
95+
type A
96+
def thing: M[A]
97+
}
98+
99+
trait Child extends Root { type A <: Cov[Int] }
100+
101+
def foo(c: Child): Trait2 = c.thing
102+
103+
class Asploder extends Child {
104+
type A = Cov[String & Int]
105+
def thing = new Trait1 {} // error
106+
}
107+
108+
def explode = foo(new Asploder)
109+
}

tests/neg/matchtype-seq.scala

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -102,10 +102,10 @@ object Test {
102102

103103
identity[T9[Tuple2[Int, String]]](1)
104104
identity[T9[Tuple2[String, Int]]]("1")
105-
identity[T9[Tuple2[Nothing, String]]](1)
106-
identity[T9[Tuple2[String, Nothing]]]("1")
107-
identity[T9[Tuple2[Int, Nothing]]](1)
108-
identity[T9[Tuple2[Nothing, Int]]]("1")
105+
identity[T9[Tuple2[Nothing, String]]](1) // error
106+
identity[T9[Tuple2[String, Nothing]]]("1") // error
107+
identity[T9[Tuple2[Int, Nothing]]](1) // error
108+
identity[T9[Tuple2[Nothing, Int]]]("1") // error
109109
identity[T9[Tuple2[_, _]]]("") // error
110110
identity[T9[Tuple2[_, _]]](1) // error
111111
identity[T9[Tuple2[Any, Any]]]("") // error

tests/run/tuples1.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ object Test extends App {
6262
def head2[X <: NonEmptyTuple](x: X): Tuple.Head[X] = x.head
6363

6464
val hd1: Int = head1(x3)
65-
val hd2: Int = head2(x3)
65+
val hd2: Int = head2[x3.type](x3)
6666

6767
def tail1(x: NonEmptyTuple): Tuple.Tail[x.type] = x.tail
6868
def tail2[X <: NonEmptyTuple](x: X): Tuple.Tail[X] = x.tail

0 commit comments

Comments
 (0)