Skip to content

Commit 0067b52

Browse files
authored
Merge pull request #6889 from dotty-staging/fix-6197
Fix #6197: erase pattern bound type symbols completely
2 parents 213517f + 080be4d commit 0067b52

File tree

9 files changed

+105
-50
lines changed

9 files changed

+105
-50
lines changed

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

Lines changed: 71 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import Inferencing._
1919
import ProtoTypes._
2020
import transform.SymUtils._
2121
import reporting.diagnostic.messages._
22+
import reporting.trace
2223
import config.Printers.{exhaustivity => debug}
2324
import util.SourcePosition
2425

@@ -110,7 +111,7 @@ trait SpaceLogic {
110111
*
111112
* This reduces noise in counterexamples.
112113
*/
113-
def simplify(space: Space, aggressive: Boolean = false): Space = space match {
114+
def simplify(space: Space, aggressive: Boolean = false)(implicit ctx: Context): Space = trace(s"simplify ${show(space)}, aggressive = $aggressive --> ", debug, x => show(x.asInstanceOf[Space]))(space match {
114115
case Prod(tp, fun, sym, spaces, full) =>
115116
val sp = Prod(tp, fun, sym, spaces.map(simplify(_)), full)
116117
if (sp.params.contains(Empty)) Empty
@@ -137,10 +138,10 @@ trait SpaceLogic {
137138
if (canDecompose(tp) && decompose(tp).isEmpty) Empty
138139
else space
139140
case _ => space
140-
}
141+
})
141142

142143
/** Flatten space to get rid of `Or` for pretty print */
143-
def flatten(space: Space): List[Space] = space match {
144+
def flatten(space: Space)(implicit ctx: Context): List[Space] = space match {
144145
case Prod(tp, fun, sym, spaces, full) =>
145146
spaces.map(flatten) match {
146147
case Nil => Prod(tp, fun, sym, Nil, full) :: Nil
@@ -156,11 +157,11 @@ trait SpaceLogic {
156157
}
157158

158159
/** Is `a` a subspace of `b`? Equivalent to `a - b == Empty`, but faster */
159-
def isSubspace(a: Space, b: Space): Boolean = {
160+
def isSubspace(a: Space, b: Space)(implicit ctx: Context): Boolean = trace(s"${show(a)} < ${show(b)}", debug) {
160161
def tryDecompose1(tp: Type) = canDecompose(tp) && isSubspace(Or(decompose(tp)), b)
161162
def tryDecompose2(tp: Type) = canDecompose(tp) && isSubspace(a, Or(decompose(tp)))
162163

163-
val res = (simplify(a), b) match {
164+
(simplify(a), b) match {
164165
case (Empty, _) => true
165166
case (_, Empty) => false
166167
case (Or(ss), _) =>
@@ -179,18 +180,14 @@ trait SpaceLogic {
179180
case (Prod(_, fun1, sym1, ss1, _), Prod(_, fun2, sym2, ss2, _)) =>
180181
sym1 == sym2 && isEqualType(fun1, fun2) && ss1.zip(ss2).forall((isSubspace _).tupled)
181182
}
182-
183-
debug.println(s"${show(a)} < ${show(b)} = $res")
184-
185-
res
186183
}
187184

188185
/** Intersection of two spaces */
189-
def intersect(a: Space, b: Space): Space = {
186+
def intersect(a: Space, b: Space)(implicit ctx: Context): Space = trace(s"${show(a)} & ${show(b)}", debug, x => show(x.asInstanceOf[Space])) {
190187
def tryDecompose1(tp: Type) = intersect(Or(decompose(tp)), b)
191188
def tryDecompose2(tp: Type) = intersect(a, Or(decompose(tp)))
192189

193-
val res: Space = (a, b) match {
190+
(a, b) match {
194191
case (Empty, _) | (_, Empty) => Empty
195192
case (_, Or(ss)) => Or(ss.map(intersect(a, _)).filterConserve(_ ne Empty))
196193
case (Or(ss), _) => Or(ss.map(intersect(_, b)).filterConserve(_ ne Empty))
@@ -223,18 +220,14 @@ trait SpaceLogic {
223220
else if (ss1.zip(ss2).exists(p => simplify(intersect(p._1, p._2)) == Empty)) Empty
224221
else Prod(tp1, fun1, sym1, ss1.zip(ss2).map((intersect _).tupled), full)
225222
}
226-
227-
debug.println(s"${show(a)} & ${show(b)} = ${show(res)}")
228-
229-
res
230223
}
231224

232225
/** The space of a not covered by b */
233-
def minus(a: Space, b: Space): Space = {
226+
def minus(a: Space, b: Space)(implicit ctx: Context): Space = trace(s"${show(a)} - ${show(b)}", debug, x => show(x.asInstanceOf[Space])) {
234227
def tryDecompose1(tp: Type) = minus(Or(decompose(tp)), b)
235228
def tryDecompose2(tp: Type) = minus(a, Or(decompose(tp)))
236229

237-
val res = (a, b) match {
230+
(a, b) match {
238231
case (Empty, _) => Empty
239232
case (_, Empty) => a
240233
case (Typ(tp1, _), Typ(tp2, _)) =>
@@ -273,10 +266,6 @@ trait SpaceLogic {
273266
})
274267

275268
}
276-
277-
debug.println(s"${show(a)} - ${show(b)} = ${show(res)}")
278-
279-
res
280269
}
281270
}
282271

@@ -307,20 +296,19 @@ class SpaceEngine(implicit ctx: Context) extends SpaceLogic {
307296
private val nullType = ConstantType(Constant(null))
308297
private val nullSpace = Typ(nullType)
309298

310-
override def intersectUnrelatedAtomicTypes(tp1: Type, tp2: Type): Space = {
299+
override def intersectUnrelatedAtomicTypes(tp1: Type, tp2: Type): Space = trace(s"atomic intersection: ${AndType(tp1, tp2).show}", debug) {
311300
// Precondition: !isSubType(tp1, tp2) && !isSubType(tp2, tp1)
312-
if (tp1 == nullType || tp2 == nullType) {
313-
// Since projections of types don't include null, intersection with null is empty.
314-
return Empty
315-
}
316-
val res = ctx.typeComparer.disjoint(tp1, tp2)
317301

318-
debug.println(s"atomic intersection: ${AndType(tp1, tp2).show} = ${!res}")
302+
// Since projections of types don't include null, intersection with null is empty.
303+
if (tp1 == nullType || tp2 == nullType) Empty
304+
else {
305+
val res = ctx.typeComparer.disjoint(tp1, tp2)
319306

320-
if (res) Empty
321-
else if (tp1.isSingleton) Typ(tp1, true)
322-
else if (tp2.isSingleton) Typ(tp2, true)
323-
else Typ(AndType(tp1, tp2), true)
307+
if (res) Empty
308+
else if (tp1.isSingleton) Typ(tp1, true)
309+
else if (tp2.isSingleton) Typ(tp2, true)
310+
else Typ(AndType(tp1, tp2), true)
311+
}
324312
}
325313

326314
/** Return the space that represents the pattern `pat` */
@@ -334,7 +322,7 @@ class SpaceEngine(implicit ctx: Context) extends SpaceLogic {
334322
case Ident(nme.WILDCARD) =>
335323
Or(Typ(pat.tpe.stripAnnots, false) :: nullSpace :: Nil)
336324
case Ident(_) | Select(_, _) =>
337-
Typ(pat.tpe.stripAnnots, false)
325+
Typ(erase(pat.tpe.stripAnnots), false)
338326
case Alternative(trees) => Or(trees.map(project(_)))
339327
case Bind(_, pat) => project(pat)
340328
case SeqLiteral(pats, _) => projectSeq(pats)
@@ -350,16 +338,17 @@ class SpaceEngine(implicit ctx: Context) extends SpaceLogic {
350338
Prod(erase(pat.tpe.stripAnnots), fun.tpe, fun.symbol, pats.take(arity - 1).map(project) :+ projectSeq(pats.drop(arity - 1)),isIrrefutableUnapply(fun))
351339
}
352340
else
353-
Prod(erase(pat.tpe.stripAnnots), fun.tpe, fun.symbol, pats.map(project), isIrrefutableUnapply(fun))
354-
case Typed(pat @ UnApply(_, _, _), _) => project(pat)
341+
Prod(erase(pat.tpe.stripAnnots), erase(fun.tpe), fun.symbol, pats.map(project), isIrrefutableUnapply(fun))
342+
case Typed(pat: UnApply, _) =>
343+
project(pat)
355344
case Typed(expr, tpt) =>
356345
Typ(erase(expr.tpe.stripAnnots), true)
357346
case This(_) =>
358347
Typ(pat.tpe.stripAnnots, false)
359348
case EmptyTree => // default rethrow clause of try/catch, check tests/patmat/try2.scala
360349
Typ(WildcardType, false)
361350
case _ =>
362-
debug.println(s"unknown pattern: $pat")
351+
ctx.error(s"unknown pattern: $pat", pat.sourcePos)
363352
Empty
364353
}
365354

@@ -375,19 +364,54 @@ class SpaceEngine(implicit ctx: Context) extends SpaceLogic {
375364
(arity, elemTp, resultTp)
376365
}
377366

378-
/* Erase pattern bound types with WildcardType */
379-
def erase(tp: Type): Type = {
367+
/** Erase pattern bound types with WildcardType
368+
*
369+
* For example, the type `C[T$1]` should match any `C[_]`, thus
370+
* `v` should be `WildcardType` instead of `T$1`:
371+
*
372+
* sealed trait B
373+
* case class C[T](v: T) extends B
374+
* (b: B) match {
375+
* case C(v) => // case C.unapply[T$1 @ T$1](v @ _):C[T$1]
376+
* }
377+
*
378+
* However, we cannot use WildcardType for Array[_], due to that
379+
* `Array[WildcardType] <: Array[Array[WildcardType]]`, which may
380+
* cause false unreachable warnings. See tests/patmat/t2425.scala
381+
*
382+
* We cannot use type erasure here, as it would lose the constraints
383+
* involving GADTs. For example, in the following code, type
384+
* erasure would loose the constraint that `x` and `y` must be
385+
* the same type, resulting in false inexhaustive warnings:
386+
*
387+
* sealed trait Expr[T]
388+
* case class IntExpr(x: Int) extends Expr[Int]
389+
* case class BooleanExpr(b: Boolean) extends Expr[Boolean]
390+
*
391+
* def foo[T](x: Expr[T], y: Expr[T]) = (x, y) match {
392+
* case (IntExpr(_), IntExpr(_)) =>
393+
* case (BooleanExpr(_), BooleanExpr(_)) =>
394+
* }
395+
*/
396+
private def erase(tp: Type, inArray: Boolean = false): Type = trace(i"$tp erased to", debug) {
380397
def isPatternTypeSymbol(sym: Symbol) = !sym.isClass && sym.is(Case)
381398

382-
val map = new TypeMap {
383-
def apply(tp: Type) = tp match {
384-
case tref: TypeRef if isPatternTypeSymbol(tref.typeSymbol) =>
385-
tref.underlying.bounds
386-
case _ => mapOver(tp)
387-
}
399+
tp match {
400+
case tp @ AppliedType(tycon, args) =>
401+
if (tycon.isRef(defn.ArrayClass)) tp.derivedAppliedType(tycon, args.map(arg => erase(arg, inArray = true)))
402+
else tp.derivedAppliedType(tycon, args.map(arg => erase(arg, inArray = false)))
403+
case OrType(tp1, tp2) =>
404+
OrType(erase(tp1, inArray), erase(tp2, inArray))
405+
case AndType(tp1, tp2) =>
406+
AndType(erase(tp1, inArray), erase(tp2, inArray))
407+
case tp @ RefinedType(parent, _, _) =>
408+
erase(parent)
409+
case tref: TypeRef if isPatternTypeSymbol(tref.typeSymbol) =>
410+
if (inArray) tref.underlying else WildcardType(tref.underlying.bounds)
411+
case mt: MethodType =>
412+
mt.derivedLambdaType(mt.paramNames, mt.paramInfos.map(info => erase(info)), erase(mt.resType))
413+
case _ => tp
388414
}
389-
390-
map(tp)
391415
}
392416

393417
/** Space of the pattern: unapplySeq(a, b, c: _*)
@@ -635,7 +659,7 @@ class SpaceEngine(implicit ctx: Context) extends SpaceLogic {
635659

636660
def doShow(s: Space, mergeList: Boolean = false): String = s match {
637661
case Empty => ""
638-
case Typ(c: ConstantType, _) => c.value.value.toString
662+
case Typ(c: ConstantType, _) => "" + c.value.value
639663
case Typ(tp: TermRef, _) => tp.symbol.showName
640664
case Typ(tp, decomposed) =>
641665
val sym = tp.widen.classSymbol

compiler/test/dotty/tools/vulpix/FileDiff.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ object FileDiff {
99
s"""Test output dumped in: $actualFile
1010
| See diff of the checkfile (`brew install icdiff` for colored diff)
1111
| > diff $expectFile $actualFile
12-
| Replace checkfile with current output output
12+
| Replace checkfile with current output
1313
| > mv $actualFile $expectFile
1414
""".stripMargin
1515

docs/docs/contributing/testing.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ If the actual output mismatches the expected output, the test framework will dum
6969
Test output dumped in: tests/playground/neg/Sample.check.out
7070
See diff of the checkfile
7171
> diff tests/playground/neg/Sample.check tests/playground/neg/Sample.check.out
72-
Replace checkfile with current output output
72+
Replace checkfile with current output
7373
> mv tests/playground/neg/Sample.check.out tests/playground/neg/Sample.check
7474
```
7575

@@ -132,5 +132,5 @@ with `with-compiler` in their name.
132132
$ sbt
133133
> testCompilation --from-tasty
134134
```
135-
135+
136136
This mode can be run under `dotty-compiler-bootstrapped/testCompilation` to test on a bootstrapped Dotty compiler.

tests/patmat/i6197.scala

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
object Test {
2+
sealed trait Cause[+E]
3+
4+
object Cause {
5+
final case class Fail[E](value: E) extends Cause[E]
6+
}
7+
8+
def fn(cause: Cause[Any]): String =
9+
cause match {
10+
case Cause.Fail(t: Throwable) => t.toString
11+
case Cause.Fail(any) => any.toString
12+
}
13+
}

tests/patmat/i6197b.scala

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
def foo(x: Option[Array[String]]) = x match {
2+
case Some(x) =>
3+
case None =>
4+
}

tests/patmat/i6197c.check

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3: Match case Unreachable

tests/patmat/i6197c.scala

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
def foo(x: Option[Any]) = x match {
2+
case _: Some[Some[_]] =>
3+
case _: Some[_] => // unreachable
4+
case None =>
5+
}

tests/patmat/i6197d.check

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
5: Pattern Match Exhaustivity: _: Array[String]

tests/patmat/i6197d.scala

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
def foo(x: Array[String]) = x match {
2+
case _: Array[_] =>
3+
}
4+
5+
def bar(x: Array[String]) = x match {
6+
case _: Array[_ <: Int] =>
7+
}

0 commit comments

Comments
 (0)