Skip to content

Commit 21944e9

Browse files
committed
Fix reachability of non-reducing match type children
The reduction of a match type gets "stuck" when a case doesn't match but is also not provably disjoint from it. In these situations the match type reduction returns NoType, which means that in `A <:< B` if B reduces to NoType, then false will be returned. In the reachability/Space logic subtype checks are used to refine the candidate children of the scrutinee type so that impossible children aren't marked as missing and possible cases aren't marked as unreachable. To achieve that there is already some type mapping that runs before subtyping to approximate the parent. We extend that parent approximating logic so that non-reducing match types in the parent don't result in all the candidate children being rejected. The motivating case is NonEmptyTuple, its single-child `*:`, and the `Tuple.Tail` match type. In `Tuple.Tail[NonEmptyTuple]`, `case _ *: xs =>` won't match because NonEmptyTuple isn't a subtype of `*:` but it's also not provably disjoint from it (indeed it's `*:` parent)! So the reduction gets stuck, leading to NoType, leading to not a subtype. I had initially looked to fix this for single-child abstract sealed types, but that would still cause false positives in legitimate multi-child sealed types and an exhaustive match type (see WithExhaustiveMatch in patmat/i13189.scala). Also it's less scary to change the subtyping wrapping logic rather than the actually match type's reduction/matching logic. In a way the problem is that subtyping checks is too boolean: the candidate rejection wants to drop children that are definitely not subtypes, but the match type reduction is too conservative by returning NoType as soon as the obvious case isn't met. What subtyping should say there is ¯\_(ツ)_/¯
1 parent 525f4a5 commit 21944e9

File tree

3 files changed

+98
-7
lines changed

3 files changed

+98
-7
lines changed

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

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -707,8 +707,12 @@ object TypeOps:
707707
val boundTypeParams = util.HashMap[TypeRef, TypeVar]()
708708

709709
def apply(tp: Type): Type = tp.dealias match {
710-
case _: MatchType =>
711-
tp // break cycles
710+
case tp: MatchType =>
711+
val reduced = tp.reduced
712+
if reduced.exists then tp // break cycles
713+
else mapOver(tp.bound) // if the match type doesn't statically reduce
714+
// then to avoid it failing the <:<
715+
// we'll approximate by widening to its bounds
712716

713717
case ThisType(tref: TypeRef) if !tref.symbol.isStaticOwner =>
714718
tref
@@ -729,7 +733,7 @@ object TypeOps:
729733
tv
730734
end if
731735

732-
case AppliedType(tycon: TypeRef, _) if !tycon.dealias.typeSymbol.isClass =>
736+
case tp @ AppliedType(tycon: TypeRef, _) if !tycon.dealias.typeSymbol.isClass && !tp.isMatchAlias =>
733737

734738
// In tests/patmat/i3645g.scala, we need to tell whether it's possible
735739
// that K1 <: K[Foo]. If yes, we issue a warning; otherwise, no

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

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -427,10 +427,7 @@ object Types {
427427
def isMatch(using Context): Boolean = stripped match {
428428
case _: MatchType => true
429429
case tp: HKTypeLambda => tp.resType.isMatch
430-
case tp: AppliedType =>
431-
tp.tycon match
432-
case tycon: TypeRef => tycon.info.isInstanceOf[MatchAlias]
433-
case _ => false
430+
case tp: AppliedType => tp.isMatchAlias
434431
case _ => false
435432
}
436433

tests/patmat/i13189.scala

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
// original report
2+
def foo(opt: Option[Tuple.Tail[NonEmptyTuple]]): Unit =
3+
opt match
4+
case None => ???
5+
case Some(a) => ???
6+
7+
8+
// again with a mini-Tuple with the extra NonEmptyTupExtra parent, to test transitivity
9+
object WithExtraParent:
10+
sealed trait Tup
11+
12+
object Tup {
13+
type Tail[X <: NonEmptyTup] <: Tup = X match {
14+
case _ **: xs => xs
15+
}
16+
}
17+
18+
object EmptyTup extends Tup
19+
20+
sealed trait NonEmptyTup extends Tup
21+
sealed trait NonEmptyTupExtra extends NonEmptyTup
22+
sealed abstract class **:[+H, +T <: Tup] extends NonEmptyTupExtra
23+
24+
object **: {
25+
def unapply[H, T <: Tup](x: H **: T): (H, T) = null
26+
}
27+
28+
def foo(opt: Option[Tup.Tail[NonEmptyTup]]): Unit =
29+
opt match
30+
case None => ???
31+
case Some(a) => ???
32+
end WithExtraParent
33+
34+
35+
// again with a non-abstract parent
36+
object WithNonAbstractParent:
37+
sealed trait Tup
38+
39+
object Tup {
40+
type Tail_+[X <: NonEmptyTup] <: Tup = X match {
41+
case _ *+: xs => xs
42+
}
43+
}
44+
45+
object EmptyTup extends Tup
46+
47+
sealed class NonEmptyTup extends Tup
48+
sealed class *+:[+H, +T <: Tup] extends NonEmptyTup
49+
sealed class **:[+H, +T <: Tup] extends NonEmptyTup
50+
51+
object *+: {
52+
def unapply[H, T <: Tup](x: H *+: T): (H, T) = null
53+
}
54+
55+
def foo(opt: Option[Tup.Tail_+[NonEmptyTup]]): Unit =
56+
opt match
57+
case None => ???
58+
case Some(a) => ???
59+
end WithNonAbstractParent
60+
61+
62+
// again with multiple children, but an exhaustive match
63+
object WithExhaustiveMatch:
64+
sealed trait Tup
65+
66+
object Tup {
67+
type Tail[X <: NonEmptyTup] <: Tup = X match {
68+
case _ *+: xs => xs
69+
case _ **: xs => xs
70+
}
71+
}
72+
73+
object EmptyTup extends Tup
74+
75+
sealed trait NonEmptyTup extends Tup
76+
sealed abstract class *+:[+H, +T <: Tup] extends NonEmptyTup
77+
sealed abstract class **:[+H, +T <: Tup] extends NonEmptyTup
78+
79+
object *+: {
80+
def unapply[H, T <: Tup](x: H *+: T): (H, T) = null
81+
}
82+
object **: {
83+
def unapply[H, T <: Tup](x: H **: T): (H, T) = null
84+
}
85+
86+
def foo(opt: Option[Tup.Tail[NonEmptyTup]]): Unit =
87+
opt match
88+
case None => ???
89+
case Some(a) => ???
90+
end WithExhaustiveMatch

0 commit comments

Comments
 (0)