Skip to content

Fix #2363: better handle extractors in exhaustivity check #2424

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
May 16, 2017
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/ast/Desugar.scala
Original file line number Diff line number Diff line change
Expand Up @@ -859,7 +859,7 @@ object desugar {
case IdPattern(named, tpt) =>
Function(derivedValDef(pat, named, tpt, EmptyTree, Modifiers(Param)) :: Nil, body)
case _ =>
makeCaseLambda(CaseDef(pat, EmptyTree, body) :: Nil, unchecked = false)
makeCaseLambda(CaseDef(pat, EmptyTree, body) :: Nil)
}

/** If `pat` is not an Identifier, a Typed(Ident, _), or a Bind, wrap
Expand Down
8 changes: 5 additions & 3 deletions compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala
Original file line number Diff line number Diff line change
Expand Up @@ -351,15 +351,17 @@ class PatternMatcher extends MiniPhaseTransform with DenotTransformer {
/* (Nil, body) means that `body` is the default case
* It's a bit hacky but it simplifies manipulations.
*/
def extractSwitchCase(treeMakers: List[TreeMaker]): (List[Int], BodyTreeMaker) = treeMakers match {
def extractSwitchCase(treeMakers: List[TreeMaker]): (List[Int], BodyTreeMaker) = (treeMakers: @unchecked) match {
// case 5 =>
case List(IntEqualityTestTreeMaker(intValue), body: BodyTreeMaker) =>
(List(intValue), body)

// case 5 | 6 =>
case List(AlternativesTreeMaker(_, alts, _), body: BodyTreeMaker) =>
val intValues = alts.map {
case List(IntEqualityTestTreeMaker(intValue)) => intValue
val intValues = alts.map { alt =>
(alt: @unchecked) match {
case List(IntEqualityTestTreeMaker(intValue)) => intValue
}
}
(intValues, body)

Expand Down
163 changes: 127 additions & 36 deletions compiler/src/dotty/tools/dotc/transform/patmat/Space.scala
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import config.Printers.{ exhaustivity => debug }
* 3. A union of spaces `S1 | S2 | ...` is a space
* 4. For a case class Kon(x1: T1, x2: T2, .., xn: Tn), if S1, S2, ..., Sn
* are spaces, then `Kon(S1, S2, ..., Sn)` is a space.
* 5. `Fun(S1, S2, ..., Sn)` is an extractor space.
*
* For the problem of exhaustivity check, its formulation in terms of space is as follows:
*
Expand Down Expand Up @@ -63,6 +64,9 @@ case class Typ(tp: Type, decomposed: Boolean) extends Space
/** Space representing a constructor pattern */
case class Kon(tp: Type, params: List[Space]) extends Space

/** Space representing an extractor pattern */
case class Fun(tp: Type, fun: Type, params: List[Space]) extends Space

/** Union of spaces */
case class Or(spaces: List[Space]) extends Space

Expand Down Expand Up @@ -98,19 +102,31 @@ trait SpaceLogic {
def show(sp: Space): String

/** Simplify space using the laws, there's no nested union after simplify */
def simplify(space: Space): Space = space match {
def simplify(space: Space, aggressive: Boolean = false): Space = space match {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you update the documentation of this method to explain what the aggressive parameter is for?

case Kon(tp, spaces) =>
val sp = Kon(tp, spaces.map(simplify _))
val sp = Kon(tp, spaces.map(simplify(_)))
if (sp.params.contains(Empty)) Empty
else sp
case Fun(tp, fun, spaces) =>
val sp = Fun(tp, fun, spaces.map(simplify(_)))
if (sp.params.contains(Empty)) Empty
else sp
case Or(spaces) =>
val set = spaces.map(simplify _).flatMap {
val set = spaces.map(simplify(_)).flatMap {
case Or(ss) => ss
case s => Seq(s)
} filter (_ != Empty)

if (set.isEmpty) Empty
else if (set.size == 1) set.toList(0)
else if (aggressive && spaces.size < 5) {
val res = set.map(sp => (sp, set.filter(_ ne sp))).find {
case (sp, sps) =>
isSubspace(sp, Or(sps))
}
if (res.isEmpty) Or(set)
else simplify(Or(res.get._2), aggressive)
}
else Or(set)
case Typ(tp, _) =>
if (canDecompose(tp) && decompose(tp).isEmpty) Empty
Expand All @@ -137,26 +153,33 @@ trait SpaceLogic {
def tryDecompose1(tp: Type) = canDecompose(tp) && isSubspace(Or(decompose(tp)), b)
def tryDecompose2(tp: Type) = canDecompose(tp) && isSubspace(a, Or(decompose(tp)))

val res = (a, b) match {
val res = (simplify(a), b) match {
case (Empty, _) => true
case (_, Empty) => false
case (Or(ss), _) => ss.forall(isSubspace(_, b))
case (Or(ss), _) =>
ss.forall(isSubspace(_, b))
case (Typ(tp1, _), Typ(tp2, _)) =>
isSubType(tp1, tp2) || tryDecompose1(tp1) || tryDecompose2(tp2)
case (Typ(tp1, _), Or(ss)) =>
isSubType(tp1, tp2)
case (Typ(tp1, _), Or(ss)) => // optimization
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe improve this comment to explain what this is an optimization of?

ss.exists(isSubspace(a, _)) || tryDecompose1(tp1)
case (_, Or(_)) =>
simplify(minus(a, b)) == Empty
case (Typ(tp1, _), Kon(tp2, ss)) =>
isSubType(tp1, tp2) && isSubspace(Kon(tp2, signature(tp2).map(Typ(_, false))), b) ||
tryDecompose1(tp1)
isSubType(tp1, tp2) && isSubspace(Kon(tp2, signature(tp2).map(Typ(_, false))), b)
case (Kon(tp1, ss), Typ(tp2, _)) =>
isSubType(tp1, tp2) ||
simplify(a) == Empty ||
(isSubType(tp2, tp1) && tryDecompose1(tp1)) ||
tryDecompose2(tp2)
case (Kon(_, _), Or(_)) =>
simplify(minus(a, b)) == Empty
isSubType(tp1, tp2)
case (Kon(tp1, ss1), Kon(tp2, ss2)) =>
isEqualType(tp1, tp2) && ss1.zip(ss2).forall((isSubspace _).tupled)
case (Fun(tp1, fun, ss), Typ(tp2, _)) =>
isSubType(tp1, tp2)
case (Typ(tp2, _), Fun(tp1, fun, ss)) =>
false // approximation: assume a type can never be fully matched by an extractor
case (Kon(_, _), Fun(_, _, _)) =>
false // approximation
case (Fun(_, _, _), Kon(_, _)) =>
false // approximation
case (Fun(_, fun1, ss1), Fun(_, fun2, ss2)) =>
isEqualType(fun1, fun2) && ss1.zip(ss2).forall((isSubspace _).tupled)
}

debug.println(s"${show(a)} < ${show(b)} = $res")
Expand All @@ -169,7 +192,7 @@ trait SpaceLogic {
def tryDecompose1(tp: Type) = intersect(Or(decompose(tp)), b)
def tryDecompose2(tp: Type) = intersect(a, Or(decompose(tp)))

val res = (a, b) match {
val res: Space = (a, b) match {
case (Empty, _) | (_, Empty) => Empty
case (_, Or(ss)) => Or(ss.map(intersect(a, _)).filterConserve(_ ne Empty))
case (Or(ss), _) => Or(ss.map(intersect(_, b)).filterConserve(_ ne Empty))
Expand All @@ -193,6 +216,24 @@ trait SpaceLogic {
if (!isEqualType(tp1, tp2)) Empty
else if (ss1.zip(ss2).exists(p => simplify(intersect(p._1, p._2)) == Empty)) Empty
else Kon(tp1, ss1.zip(ss2).map((intersect _).tupled))
case (Typ(tp1, _), Fun(tp2, _, _)) =>
if (isSubType(tp1, tp2) || isSubType(tp2, tp1)) b // prefer extractor space for better approximation
else if (canDecompose(tp1)) tryDecompose1(tp1)
else Empty
case (Fun(tp1, _, _), Typ(tp2, _)) =>
if (isSubType(tp1, tp2) || isSubType(tp2, tp1)) a
else if (canDecompose(tp2)) tryDecompose2(tp2)
else Empty
case (Fun(tp1, _, _), Kon(tp2, _)) =>
if (isSubType(tp1, tp2) || isSubType(tp2, tp1)) a
else Empty
case (Kon(tp1, _), Fun(tp2, _, _)) =>
if (isSubType(tp1, tp2) || isSubType(tp2, tp1)) b
else Empty
case (Fun(tp1, fun1, ss1), Fun(tp2, fun2, ss2)) =>
if (!isEqualType(fun1, fun2)) Empty
else if (ss1.zip(ss2).exists(p => simplify(intersect(p._1, p._2)) == Empty)) Empty
else Fun(tp1, fun1, ss1.zip(ss2).map((intersect _).tupled))
}

debug.println(s"${show(a)} & ${show(b)} = ${show(res)}")
Expand Down Expand Up @@ -238,6 +279,25 @@ trait SpaceLogic {
Or(ss1.zip(ss2).map((minus _).tupled).zip(0 to ss2.length - 1).map {
case (ri, i) => Kon(tp1, ss1.updated(i, ri))
})
case (Fun(tp1, _, _), Typ(tp2, _)) =>
if (isSubType(tp1, tp2)) Empty
else a
case (Typ(tp1, _), Fun(tp2, _, _)) =>
a // approximation
case (Fun(_, _, _), Kon(_, _)) =>
a
case (Kon(_, _), Fun(_, _, _)) =>
a
case (Fun(tp1, fun1, ss1), Fun(tp2, fun2, ss2)) =>
if (!isEqualType(fun1, fun2)) a
else if (ss1.zip(ss2).exists(p => simplify(intersect(p._1, p._2)) == Empty)) a
else if (ss1.zip(ss2).forall((isSubspace _).tupled)) Empty
else
// `(_, _, _) - (Some, None, _)` becomes `(None, _, _) | (_, Some, _) | (_, _, Empty)`
Or(ss1.zip(ss2).map((minus _).tupled).zip(0 to ss2.length - 1).map {
case (ri, i) => Fun(tp1, fun1, ss1.updated(i, ri))
})

}

debug.println(s"${show(a)} - ${show(b)} = ${show(res)}")
Expand All @@ -263,6 +323,12 @@ class SpaceEngine(implicit ctx: Context) extends SpaceLogic {
import SpaceEngine._
import tpd._

private val scalaOptionClass = ctx.requiredClassRef("scala.Option".toTypeName).symbol.asClass
private val scalaSeqFactoryClass = ctx.requiredClass("scala.collection.generic.SeqFactory".toTypeName)
private val scalaListType = ctx.requiredClassRef("scala.collection.immutable.List".toTypeName)
private val scalaNilType = ctx.requiredModuleRef("scala.collection.immutable.Nil".toTermName)
private val scalaConType = ctx.requiredClassRef("scala.collection.immutable.::".toTypeName)

/** Checks if it's possible to create a trait/class which is a subtype of `tp`.
*
* - doesn't handle member collisions (will not declare a type unimplementable because of one)
Expand Down Expand Up @@ -337,11 +403,8 @@ class SpaceEngine(implicit ctx: Context) extends SpaceLogic {
}

/** Return the space that represents the pattern `pat`
*
* If roundUp is true, approximate extractors to its type,
* otherwise approximate extractors to Empty
*/
def project(pat: Tree, roundUp: Boolean = true)(implicit ctx: Context): Space = pat match {
def project(pat: Tree): Space = pat match {
case Literal(c) =>
if (c.value.isInstanceOf[Symbol])
Typ(c.value.asInstanceOf[Symbol].termRef, false)
Expand All @@ -350,20 +413,40 @@ class SpaceEngine(implicit ctx: Context) extends SpaceLogic {
case _: BackquotedIdent => Typ(pat.tpe, false)
case Ident(_) | Select(_, _) =>
Typ(pat.tpe.stripAnnots, false)
case Alternative(trees) => Or(trees.map(project(_, roundUp)))
case Alternative(trees) => Or(trees.map(project(_)))
case Bind(_, pat) => project(pat)
case UnApply(_, _, pats) =>
case UnApply(fun, _, pats) =>
if (pat.tpe.classSymbol.is(CaseClass))
// FIXME: why dealias is needed here?
Kon(pat.tpe.stripAnnots.dealias, pats.map(pat => project(pat, roundUp)))
else if (roundUp) Typ(pat.tpe.stripAnnots, false)
else Empty
Kon(pat.tpe.stripAnnots, pats.map(pat => project(pat)))
else if (fun.symbol.owner == scalaSeqFactoryClass && fun.symbol.name == nme.unapplySeq)
projectList(pats)
else if (!fun.symbol.info.finalResultType.isRef(scalaOptionClass))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is correct in general, the return type of an option-less patmat may define a def isEmpty: Boolean method, if this method return false, then the case will not be taken.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, if isEmpty returns true, not false

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I see, then it means we cannot tell from types whether an option-less is exhaustive or not. I'll retract the last commit.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, if the method is defined as def isEmpty: false then we know it will always return false :). But we should probably not encode this logic in the exhaustiveness checker until we have a real spec for option-less pattern-matching that explains all cases.

Kon(pat.tpe.stripAnnots, pats.map(pat => project(pat)))
else
Fun(pat.tpe.stripAnnots, fun.tpe, pats.map(pat => project(pat)))
case Typed(pat @ UnApply(_, _, _), _) => project(pat)
case Typed(expr, _) => Typ(expr.tpe.stripAnnots, true)
case _ =>
Empty
}


/** Space of the pattern: List(a, b, c: _*)
*/
def projectList(pats: List[Tree]): Space = {
if (pats.isEmpty) return Typ(scalaNilType, false)

val (items, zero) = if (pats.last.tpe.isRepeatedParam)
(pats.init, Typ(scalaListType.appliedTo(pats.head.tpe.widen), false))
else
(pats, Typ(scalaNilType, false))

items.foldRight[Space](zero) { (pat, acc) =>
Kon(scalaConType.appliedTo(pats.head.tpe.widen), project(pat) :: acc :: Nil)
}
}


/* Erase a type binding according to erasure semantics in pattern matching */
def erase(tp: Type): Type = {
def doErase(tp: Type): Type = tp match {
Expand All @@ -386,7 +469,7 @@ class SpaceEngine(implicit ctx: Context) extends SpaceLogic {
/** Is `tp1` a subtype of `tp2`? */
def isSubType(tp1: Type, tp2: Type): Boolean = {
// check SI-9657 and tests/patmat/gadt.scala
val res = erase(tp1) <:< erase(tp2)
val res = tp1 <:< erase(tp2)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add a comment explaining why it's correct to erase tp2 but not tp1 ?

debug.println(s"${tp1.show} <:< ${tp2.show} = $res")
res
}
Expand Down Expand Up @@ -426,14 +509,14 @@ class SpaceEngine(implicit ctx: Context) extends SpaceLogic {
case space => List(space)
}
case OrType(tp1, tp2) => List(Typ(tp1, true), Typ(tp2, true))
case _ if tp =:= ctx.definitions.BooleanType =>
case tp if tp =:= ctx.definitions.BooleanType =>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tp.isRef(defn.BooleanClass)

List(
Typ(ConstantType(Constant(true)), true),
Typ(ConstantType(Constant(false)), true)
)
case _ if tp.classSymbol.is(Enum) =>
case tp if tp.classSymbol.is(Enum) =>
children.map(sym => Typ(sym.termRef, true))
case _ =>
case tp =>
val parts = children.map { sym =>
if (sym.is(ModuleClass))
refine(tp, sym.sourceModule.termRef)
Expand Down Expand Up @@ -549,6 +632,8 @@ class SpaceEngine(implicit ctx: Context) extends SpaceLogic {

if (ctx.definitions.isTupleType(tp))
signature(tp).map(_ => "_").mkString("(", ", ", ")")
else if (sym.showFullName == "scala.collection.immutable.List")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should never do string comparisons to determine the type of something. Instead something like sym == defn.ListClass should be used, if ListClass is not defined in Definitions.scala then it should be added.

if (mergeList) "_*" else "_: List"
else if (sym.showFullName == "scala.collection.immutable.::")
if (mergeList) "_" else "List(_)"
else if (tp.classSymbol.is(CaseClass))
Expand All @@ -566,6 +651,8 @@ class SpaceEngine(implicit ctx: Context) extends SpaceLogic {
else params.map(doShow(_, true)).filter(_ != "Nil").mkString("List(", ", ", ")")
else
showType(tp) + params.map(doShow(_)).mkString("(", ", ", ")")
case Fun(tp, fun, params) =>
showType(fun) + params.map(doShow(_)).mkString("(", ", ", ")")
case Or(_) =>
throw new Exception("incorrect flatten result " + s)
}
Expand Down Expand Up @@ -664,31 +751,35 @@ class SpaceEngine(implicit ctx: Context) extends SpaceLogic {

val patternSpace = cases.map({ x =>
val space = project(x.pat)
debug.println(s"${x.pat.show} projects to ${show(space)}")
debug.println(s"${x.pat.show} ====> ${show(space)}")
space
}).reduce((a, b) => Or(List(a, b)))
val uncovered = simplify(minus(Typ(selTyp, true), patternSpace))
val uncovered = simplify(minus(Typ(selTyp, true), patternSpace), aggressive = true)

if (uncovered != Empty)
ctx.warning(PatternMatchExhaustivity(show(uncovered)), _match.pos)
ctx.warning(PatternMatchExhaustivity(show(uncovered)), sel.pos)
}

def checkRedundancy(_match: Match): Unit = {
val Match(sel, cases) = _match
// ignore selector type for now
// val selTyp = sel.tpe.widen.deAnonymize.dealias

if (cases.length == 1) return

// starts from the second, the first can't be redundant
(1 until cases.length).foreach { i =>
// in redundancy check, take guard as false, take extractor as match
// nothing in order to soundly approximate
// in redundancy check, take guard as false in order to soundly approximate
val prevs = cases.take(i).map { x =>
if (x.guard.isEmpty) project(x.pat, false)
if (x.guard.isEmpty) project(x.pat)
else Empty
}.reduce((a, b) => Or(List(a, b)))

val curr = project(cases(i).pat)

debug.println(s"---------------reachable? ${show(curr)}")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest adding a Printer for the exhaustiveness checker in https://github.com/lampepfl/dotty/blob/master/compiler/src/dotty/tools/dotc/config/Printers.scala instead of using debug here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The debug is just a rename of the exhaustivity printer :)

debug.println(s"prev: ${show(prevs)}")

if (isSubspace(curr, prevs)) {
ctx.warning(MatchCaseUnreachable(), cases(i).body.pos)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ final case class Text(text: String) extends Inline
abstract class EntityLink(val title: Inline) extends Inline { def link: LinkTo }
object EntityLink {
def apply(title: Inline, linkTo: LinkTo) = new EntityLink(title) { def link: LinkTo = linkTo }
def unapply(el: EntityLink): Option[(Inline, LinkTo)] = Some((el.title, el.link))
def unapply(el: EntityLink): Some[(Inline, LinkTo)] = Some((el.title, el.link))
}
final case class HtmlTag(data: String) extends Inline {
private val Pattern = """(?ms)\A<(/?)(.*?)[\s>].*\z""".r
Expand Down
4 changes: 4 additions & 0 deletions doc-tool/src/dotty/tools/dottydoc/staticsite/tags.scala
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,10 @@ object tags {
title

case ConstantReference(title) => title

case _ => // EmptyReference
ctx.docbase.error(s"invalid reference: $ref")
null
}
override def render(tctx: TemplateContext, nodes: LNode*): AnyRef = nodes(0).render(tctx) match {
case map: JMap[String, AnyRef] @unchecked =>
Expand Down
2 changes: 2 additions & 0 deletions doc-tool/src/dotty/tools/dottydoc/util/MemberLookup.scala
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ trait MemberLookup {
case (x :: xs, _) =>
if (xs.nonEmpty) globalLookup
else lookup(entity, packages, "scala." + query)
case (Nil, _) =>
throw new IllegalArgumentException("`query` cannot be empty")
}
}

Expand Down
17 changes: 17 additions & 0 deletions tests/patmat/dotty-unreachable.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
object Types {
abstract case class TermRef(val prefix: String, name: String) {
type ThisType = TermRef

def alts: List[TermRef] = ???
}
}

class Test {
def foo(tp: Types.TermRef): Unit = {
tp.alts.filter(_.name == "apply") match {
case Nil =>
case alt :: Nil =>
case alt =>
}
}
}
Loading