Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions compiler/src/dotty/tools/dotc/ast/Trees.scala
Original file line number Diff line number Diff line change
Expand Up @@ -627,8 +627,6 @@ object Trees {
case class CaseDef[+T <: Untyped] private[ast] (pat: Tree[T], guard: Tree[T], body: Tree[T])(implicit @constructorOnly src: SourceFile)
extends Tree[T] {
type ThisTree[+T <: Untyped] = CaseDef[T]
/** Should this case be considered partial for exhaustivity and unreachability checking */
def maybePartial(using Context): Boolean = !guard.isEmpty || body.isInstanceOf[SubMatch[T]]
}

/** label[tpt]: { expr } */
Expand Down
66 changes: 62 additions & 4 deletions compiler/src/dotty/tools/dotc/transform/patmat/Space.scala
Original file line number Diff line number Diff line change
Expand Up @@ -930,12 +930,65 @@ object SpaceEngine {
case _ => tp
})

/** Check if the SubMatch selector references the variable bound by the outer pattern.
*
* case x @ _ if x match
* ^ pat ^ selector
*
*/
private def selectorIsBoundVar(selector: Tree, pat: Tree)(using Context): Boolean =
pat match
case b: Bind => selector.symbol == b.symbol
case _ => false

/** Find the index of the parameter in an outer UnApply pattern that directly binds the selector symbol.
*
* case Wrapper(c) if c match
* ^ returns Some(0)
*
*/
private def selectorParamIndex(selector: Tree, pat: Tree)(using Context): Option[Int] =
unbind(pat) match
case UnApply(_, _, pats) =>
val idx = pats.indexWhere {
case b: Bind => b.symbol == selector.symbol
case _ => false
}
if idx >= 0 then Some(idx) else None
case _ => None

private def projectSubMatch(pat: Tree, sm: SubMatch)(using Context): Option[Space] =
val Match(selector, cases) = sm

val subSpace = Or(cases.map(projectCaseDef))
val selTyp = toUnderlying(selector.tpe)

if selectorIsBoundVar(selector, pat) then
Some(simplify(intersect(project(pat), subSpace)))
else selectorParamIndex(selector, pat) match
case Some(idx) =>
project(pat) match
case Prod(tp, unappTp, params) =>
val narrowedParam = simplify(intersect(params(idx), subSpace))
Some(simplify(Prod(tp, unappTp, params.updated(idx, narrowedParam))))
case other => Some(other)
case None =>
if simplify(minus(project(selTyp), subSpace)) == Empty then Some(project(pat))
else None

/** Project a single CaseDef to the space it definitely covers */
private def projectCaseDef(c: CaseDef)(using Context): Space =
if !c.guard.isEmpty then Empty
else c.body match
case sm: SubMatch => projectSubMatch(c.pat, sm).getOrElse(Empty)
case _ => project(c.pat)

def checkExhaustivity(m: Match)(using Context): Unit = trace(i"checkExhaustivity($m)") {
val selTyp = toUnderlying(m.selector.tpe.stripUnsafeNulls()).dealias
val targetSpace = trace(i"targetSpace($selTyp)")(project(selTyp))

val patternSpace = Or(m.cases.foldLeft(List.empty[Space]) { (acc, x) =>
val space = if x.maybePartial then Empty else trace(i"project(${x.pat})")(project(x.pat))
val space = trace(i"projectCaseDef(${x.pat})")(projectCaseDef(x))
space :: acc
})

Expand Down Expand Up @@ -978,7 +1031,12 @@ object SpaceEngine {
cases match
case Nil =>
case (c @ CaseDef(pat, _, _)) :: rest =>
val curr = trace(i"project($pat)")(projectPat(pat))
val (curr, hasContrib) = c.body match
case sm: SubMatch =>
projectSubMatch(pat, sm) match
case Some(smSpace) => (smSpace, true)
case None => (projectPat(pat), false)
case _ => (projectPat(pat), true)
val covered = trace("covered")(simplify(intersect(curr, targetSpace)))
val prev = trace("prev")(simplify(Or(prevs)))
if prev == Empty && covered == Empty then // defer until a case is reachable
Expand All @@ -1003,8 +1061,8 @@ object SpaceEngine {
hadNullOnly = true
report.warning(MatchCaseOnlyNullWarning(), pat.srcPos)

// in redundancy check, take guard as false (or potential sub cases as partial) for a sound approximation
val newPrev = if c.maybePartial then prevs else covered :: prevs
// in redundancy check, take guard as false for a sound approximation
val newPrev = if hasContrib && c.guard.isEmpty then covered :: prevs else prevs
recur(rest, newPrev, Nil)

recur(m.cases, Nil, Nil)
Expand Down
50 changes: 50 additions & 0 deletions tests/pos/sub-cases-exhaustivity.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
//> using options -Werror
import scala.language.experimental.subCases

case class NotFound(id: String)
enum AcceptError:
case IsCancelled(id: String)
case Denial
case object CatastrophicError
type Error = NotFound | AcceptError | CatastrophicError.type

import AcceptError.*

def errorToString: Error => String =
case NotFound(id) => s"NotFound: $id"
case CatastrophicError => s"It is all doom"
case ae if ae match
case Denial => s"In Denial"
case IsCancelled(id) => s"IsCancelled: $id"

def errorToString2: Error => String =
case NotFound(id) => s"NotFound: $id"
case CatastrophicError => s"It is all doom"
case ae if ae match
case Denial => s"In Denial"
case ea if ea match
case IsCancelled(id) => s"IsCancelled: $id"

enum Color:
case Red, Green, Blue

def colorName(c: Color): String = c match
case c1 if c1 match
case Color.Red => "red"
case Color.Green => "green"
case Color.Blue => "blue"

case class Wrapper(c: Color)

def wrappedColorName(w: Wrapper): String = w match
case Wrapper(c) if c match
case Color.Red => "red"
case Color.Green => "green"
case Color.Blue => "blue"

def wrappedColorName2(w: Wrapper): String = w match
case Wrapper(c) if c match
case Color.Red => "red"
case Color.Green => "green"
case Wrapper(c) if c match
case Color.Blue => "blue"
20 changes: 20 additions & 0 deletions tests/warn/sub-cases-exhaustivity.check
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,23 @@
32 | case A(_) => 3 // warn: unreacheable
| ^^^^
| Unreachable case
-- [E029] Pattern Match Exhaustivity Warning: tests/warn/sub-cases-exhaustivity.scala:41:2 -----------------------------
41 | w match // warn: match may not be exhaustive: It would fail on pattern case: Wrapper(Red)
| ^
| match may not be exhaustive.
|
| It would fail on pattern case: Wrapper(Red)
|
| longer explanation available when compiling with `-explain`
-- [E030] Match case Unreachable Warning: tests/warn/sub-cases-exhaustivity.scala:53:10 --------------------------------
53 | case AB.A => "unreachable" // warn
| ^^^^
| Unreachable case
-- [E030] Match case Unreachable Warning: tests/warn/sub-cases-exhaustivity.scala:59:14 --------------------------------
59 | case Wrapper(Color.Red) => "unreachable" // warn
| ^^^^^^^^^^^^^^^^^^
| Unreachable case
-- [E030] Match case Unreachable Warning: tests/warn/sub-cases-exhaustivity.scala:68:14 --------------------------------
68 | case Wrapper(_) => "unreachable" // warn
| ^^^^^^^^^^
| Unreachable case
35 changes: 35 additions & 0 deletions tests/warn/sub-cases-exhaustivity.scala
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,38 @@ object Test:
case A(_) => 3 // nowarn: should not be reported as unreachable
case A(_) => 3 // warn: unreacheable
case C => 4

enum Color:
case Red, Green, Blue

case class Wrapper(c: Color)

def wrappedColorName(w: Wrapper): String =
w match // warn: match may not be exhaustive: It would fail on pattern case: Wrapper(Red)
case Wrapper(c) if c match
case Color.Green => "green"
case Color.Blue => "blue"

enum AB:
case A, B

def testBoundVarReachability(ab: AB) = ab match
case x if x match
case AB.A => "a"
case AB.B => "b"
case AB.A => "unreachable" // warn

def testParamIndexReachability(w: Wrapper) = w match
case Wrapper(c) if c match
case Color.Red => "red"
case Color.Green => "green"
case Wrapper(Color.Red) => "unreachable" // warn
case Wrapper(Color.Blue) => "blue" // not unreachable

def testCombinedReachability(w: Wrapper) = w match
case Wrapper(c) if c match
case Color.Red => "red"
case Color.Green => "green"
case Wrapper(c) if c match
case Color.Blue => "blue"
case Wrapper(_) => "unreachable" // warn
Loading