Skip to content

Commit cef08d9

Browse files
Merge pull request #7364 from dotty-staging/fix-6570-1
Fix #6570: Don't reduce match types with empty scrutinies
2 parents 157ef5d + 04b2c3e commit cef08d9

File tree

9 files changed

+234
-52
lines changed

9 files changed

+234
-52
lines changed

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

Lines changed: 80 additions & 41 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,19 +2137,52 @@ class TypeComparer(initctx: Context) extends ConstraintHandling[AbsentContext] w
21362137
/** Returns last check's debug mode, if explicitly enabled. */
21372138
def lastTrace(): String = ""
21382139

2139-
/** Are `tp1` and `tp2` disjoint types?
2140+
private def typeparamCorrespondsToField(tycon: Type, tparam: TypeParamInfo): Boolean =
2141+
productSelectorTypes(tycon, null).exists {
2142+
case tp: TypeRef =>
2143+
tp.designator.eq(tparam) // Bingo!
2144+
case _ =>
2145+
false
2146+
}
2147+
2148+
/** Is `tp` an empty type?
21402149
*
2141-
* `true` implies that we found a proof; uncertainty default to `false`.
2150+
* `true` implies that we found a proof; uncertainty defaults to `false`.
2151+
*/
2152+
def provablyEmpty(tp: Type): Boolean =
2153+
tp.dealias match {
2154+
case tp if tp.isBottomType => true
2155+
case AndType(tp1, tp2) => provablyDisjoint(tp1, tp2)
2156+
case OrType(tp1, tp2) => provablyEmpty(tp1) && provablyEmpty(tp2)
2157+
case at @ AppliedType(tycon, args) =>
2158+
args.lazyZip(tycon.typeParams).exists { (arg, tparam) =>
2159+
tparam.paramVariance >= 0
2160+
&& provablyEmpty(arg)
2161+
&& typeparamCorrespondsToField(tycon, tparam)
2162+
}
2163+
case tp: TypeProxy =>
2164+
provablyEmpty(tp.underlying)
2165+
case _ => false
2166+
}
2167+
2168+
2169+
/** Are `tp1` and `tp2` provablyDisjoint types?
2170+
*
2171+
* `true` implies that we found a proof; uncertainty defaults to `false`.
21422172
*
21432173
* Proofs rely on the following properties of Scala types:
21442174
*
21452175
* 1. Single inheritance of classes
21462176
* 2. Final classes cannot be extended
2147-
* 3. ConstantTypes with distinc values are non intersecting
2177+
* 3. ConstantTypes with distinct values are non intersecting
21482178
* 4. There is no value of type Nothing
2179+
*
2180+
* Note on soundness: the correctness of match types relies on on the
2181+
* property that in all possible contexts, the same match type expression
2182+
* is either stuck or reduces to the same case.
21492183
*/
2150-
def disjoint(tp1: Type, tp2: Type)(implicit ctx: Context): Boolean = {
2151-
// println(s"disjoint(${tp1.show}, ${tp2.show})")
2184+
def provablyDisjoint(tp1: Type, tp2: Type)(implicit ctx: Context): Boolean = {
2185+
// println(s"provablyDisjoint(${tp1.show}, ${tp2.show})")
21522186
/** Can we enumerate all instantiations of this type? */
21532187
def isClosedSum(tp: Symbol): Boolean =
21542188
tp.is(Sealed) && tp.isOneOf(AbstractOrTrait) && !tp.hasAnonymousChild
@@ -2181,33 +2215,19 @@ class TypeComparer(initctx: Context) extends ConstraintHandling[AbsentContext] w
21812215
// of classes.
21822216
true
21832217
else if (isClosedSum(cls1))
2184-
decompose(cls1, tp1).forall(x => disjoint(x, tp2))
2218+
decompose(cls1, tp1).forall(x => provablyDisjoint(x, tp2))
21852219
else if (isClosedSum(cls2))
2186-
decompose(cls2, tp2).forall(x => disjoint(x, tp1))
2220+
decompose(cls2, tp2).forall(x => provablyDisjoint(x, tp1))
21872221
else
21882222
false
21892223
case (AppliedType(tycon1, args1), AppliedType(tycon2, args2)) if tycon1 == tycon2 =>
2224+
// It is possible to conclude that two types applies are disjoint by
2225+
// looking at covariant type parameters if the said type parameters
2226+
// are disjoin and correspond to fields.
2227+
// (Type parameter disjointness is not enough by itself as it could
2228+
// lead to incorrect conclusions for phantom type parameters).
21902229
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
2210-
}
2230+
provablyDisjoint(tp1, tp2) && typeparamCorrespondsToField(tycon1, tparam)
22112231

22122232
args1.lazyZip(args2).lazyZip(tycon1.typeParams).exists {
22132233
(arg1, arg2, tparam) =>
@@ -2236,29 +2256,29 @@ class TypeComparer(initctx: Context) extends ConstraintHandling[AbsentContext] w
22362256
}
22372257
}
22382258
case (tp1: HKLambda, tp2: HKLambda) =>
2239-
disjoint(tp1.resType, tp2.resType)
2259+
provablyDisjoint(tp1.resType, tp2.resType)
22402260
case (_: HKLambda, _) =>
2241-
// The intersection of these two types would be ill kinded, they are therefore disjoint.
2261+
// The intersection of these two types would be ill kinded, they are therefore provablyDisjoint.
22422262
true
22432263
case (_, _: HKLambda) =>
22442264
true
22452265
case (tp1: OrType, _) =>
2246-
disjoint(tp1.tp1, tp2) && disjoint(tp1.tp2, tp2)
2266+
provablyDisjoint(tp1.tp1, tp2) && provablyDisjoint(tp1.tp2, tp2)
22472267
case (_, tp2: OrType) =>
2248-
disjoint(tp1, tp2.tp1) && disjoint(tp1, tp2.tp2)
2268+
provablyDisjoint(tp1, tp2.tp1) && provablyDisjoint(tp1, tp2.tp2)
22492269
case (tp1: AndType, tp2: AndType) =>
2250-
(disjoint(tp1.tp1, tp2.tp1) || disjoint(tp1.tp2, tp2.tp2)) &&
2251-
(disjoint(tp1.tp1, tp2.tp2) || disjoint(tp1.tp2, tp2.tp1))
2270+
(provablyDisjoint(tp1.tp1, tp2.tp1) || provablyDisjoint(tp1.tp2, tp2.tp2)) &&
2271+
(provablyDisjoint(tp1.tp1, tp2.tp2) || provablyDisjoint(tp1.tp2, tp2.tp1))
22522272
case (tp1: AndType, _) =>
2253-
disjoint(tp1.tp2, tp2) || disjoint(tp1.tp1, tp2)
2273+
provablyDisjoint(tp1.tp2, tp2) || provablyDisjoint(tp1.tp1, tp2)
22542274
case (_, tp2: AndType) =>
2255-
disjoint(tp1, tp2.tp2) || disjoint(tp1, tp2.tp1)
2275+
provablyDisjoint(tp1, tp2.tp2) || provablyDisjoint(tp1, tp2.tp1)
22562276
case (tp1: TypeProxy, tp2: TypeProxy) =>
2257-
disjoint(tp1.underlying, tp2) || disjoint(tp1, tp2.underlying)
2277+
provablyDisjoint(tp1.underlying, tp2) || provablyDisjoint(tp1, tp2.underlying)
22582278
case (tp1: TypeProxy, _) =>
2259-
disjoint(tp1.underlying, tp2)
2279+
provablyDisjoint(tp1.underlying, tp2)
22602280
case (_, tp2: TypeProxy) =>
2261-
disjoint(tp1, tp2.underlying)
2281+
provablyDisjoint(tp1, tp2.underlying)
22622282
case _ =>
22632283
false
22642284
}
@@ -2419,6 +2439,7 @@ class TrackingTypeComparer(initctx: Context) extends TypeComparer(initctx) {
24192439
}.apply(tp)
24202440

24212441
val defn.MatchCase(pat, body) = cas1
2442+
24222443
if (isSubType(scrut, pat))
24232444
// `scrut` is a subtype of `pat`: *It's a Match!*
24242445
Some {
@@ -2432,8 +2453,8 @@ class TrackingTypeComparer(initctx: Context) extends TypeComparer(initctx) {
24322453
}
24332454
else if (isSubType(widenAbstractTypes(scrut), widenAbstractTypes(pat)))
24342455
Some(NoType)
2435-
else if (disjoint(scrut, pat))
2436-
// We found a proof that `scrut` and `pat` are incompatible.
2456+
else if (provablyDisjoint(scrut, pat))
2457+
// We found a proof that `scrut` and `pat` are incompatible.
24372458
// The search continues.
24382459
None
24392460
else
@@ -2445,7 +2466,25 @@ class TrackingTypeComparer(initctx: Context) extends TypeComparer(initctx) {
24452466
case Nil => NoType
24462467
}
24472468

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

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,7 @@ class SpaceEngine(implicit ctx: Context) extends SpaceLogic {
302302
// Since projections of types don't include null, intersection with null is empty.
303303
if (tp1 == nullType || tp2 == nullType) Empty
304304
else {
305-
val res = ctx.typeComparer.disjoint(tp1, tp2)
305+
val res = ctx.typeComparer.provablyDisjoint(tp1, tp2)
306306

307307
if (res) Empty
308308
else if (tp1.isSingleton) Typ(tp1, true)
@@ -529,7 +529,7 @@ class SpaceEngine(implicit ctx: Context) extends SpaceLogic {
529529

530530
def inhabited(tp: Type): Boolean =
531531
tp.dealias match {
532-
case AndType(tp1, tp2) => !ctx.typeComparer.disjoint(tp1, tp2)
532+
case AndType(tp1, tp2) => !ctx.typeComparer.provablyDisjoint(tp1, tp2)
533533
case OrType(tp1, tp2) => inhabited(tp1) || inhabited(tp2)
534534
case tp: RefinedType => inhabited(tp.parent)
535535
case tp: TypeRef => inhabited(tp.prefix)

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

0 commit comments

Comments
 (0)