Skip to content

Commit 3362018

Browse files
committed
Emit deferred reachability warnings
There are three kinds of unreachable cases: 1. the cases that are provably disjoint from the scrutinee type 2. the cases that are not subtypes of the scrutinee type 3. the cases that are covered by a previous cases, aka an overlap The first one should be (I didn't review or stress test it) handled when type-checking the pattern. The third one is the safest, in terms of not emitting false positives to the user, because it's within the conservative approximation of the scrutinee space and the previous cases spaces. The second one is where it gets tricky, because we don't know they're part of the scrutinee type but we also don't know they're _not_ part of the scrutinee type. Erasure is the simplest example: if the scrutinee type is `type ThisTree <: Tree` then if you write cases for subtypes of Tree that aren't subtypes of ThisTree, then they're not subtypes nor are they provably disjoint from the scrutinee type. Martin mentioned that it's been tried a few times to restrict patterns to subtypes, but that it's been found too restrictive and provably disjoint is the right choice for type errors. So the logic that I recently reimplemented was to only emit reachability warnings after a reachable a case was found, therefore avoiding to emit any warnings in the ThisTree case, emitting for all the overlap unreachable cases. The problem was that accidentally introduced a dependency on the ordering of the cases, where unreachable initial cases aren't warned but reordered they would. So I hold onto those references and unbuffer as soon as we find a provably reachable case (covered != Empty) or a provably unreachable case (prev != Empty). Also, I've had separate conversations with Seth and Lukas about whether match analysis should just widen those non-class types, but it's also plausible that a user has created a whole type hierarchy with type members and would prefer for it to not discard it all by widening to class/object types, but instead reason within the bounds of the given scrutinee type.
1 parent dbbb210 commit 3362018

File tree

2 files changed

+26
-10
lines changed

2 files changed

+26
-10
lines changed

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

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -900,7 +900,9 @@ class SpaceEngine(using Context) extends SpaceLogic {
900900
def checkRedundancy(_match: Match): Unit = {
901901
debug.println(s"---------------checking redundant patterns ${_match.show}")
902902

903-
val Match(sel, cases) = _match
903+
val Match(sel, _) = _match
904+
val cases = _match.cases.toIndexedSeq
905+
904906
val selTyp = sel.tpe.widen.dealias
905907

906908
if (!redundancyCheckable(sel)) return
@@ -911,7 +913,14 @@ class SpaceEngine(using Context) extends SpaceLogic {
911913
else project(selTyp)
912914
debug.println(s"targetSpace: ${show(targetSpace)}")
913915

914-
cases.iterator.zipWithIndex.foldLeft(Nil: List[Space]) { case (prevs, (CaseDef(pat, guard, _), i)) =>
916+
var i = 0
917+
val len = cases.length
918+
var prevs = List.empty[Space]
919+
var deferred = List.empty[Tree]
920+
921+
while (i < len) {
922+
val CaseDef(pat, guard, _) = cases(i)
923+
915924
debug.println(i"case pattern: $pat")
916925

917926
val curr = project(pat)
@@ -923,18 +932,24 @@ class SpaceEngine(using Context) extends SpaceLogic {
923932
val covered = simplify(intersect(curr, targetSpace))
924933
debug.println(s"covered: ${show(covered)}")
925934

926-
if pat != EmptyTree // rethrow case of catch uses EmptyTree
927-
&& prev != Empty // avoid isSubspace(Empty, Empty) - one of the previous cases much be reachable
928-
&& isSubspace(covered, prev)
929-
then {
930-
if isNullable && i == cases.length - 1 && isWildcardArg(pat) then
931-
report.warning(MatchCaseOnlyNullWarning(), pat.srcPos)
932-
else
935+
if prev == Empty && covered == Empty then // defer until a case is reachable
936+
deferred ::= pat
937+
else {
938+
for (pat <- deferred.reverseIterator)
933939
report.warning(MatchCaseUnreachable(), pat.srcPos)
940+
if pat != EmptyTree // rethrow case of catch uses EmptyTree
941+
&& isSubspace(covered, prev)
942+
then {
943+
val nullOnly = isNullable && i == len - 1 && isWildcardArg(pat)
944+
val msg = if nullOnly then MatchCaseOnlyNullWarning() else MatchCaseUnreachable()
945+
report.warning(msg, pat.srcPos)
946+
}
947+
deferred = Nil
934948
}
935949

936950
// in redundancy check, take guard as false in order to soundly approximate
937-
(if guard.isEmpty then covered else Empty) :: prevs
951+
prevs ::= (if guard.isEmpty then covered else Empty)
952+
i += 1
938953
}
939954
}
940955
}

tests/patmat/i13485.check

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

0 commit comments

Comments
 (0)