Skip to content
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

Fix TypeTest exhaustivity check #12059

Merged
merged 3 commits into from
Apr 12, 2021
Merged
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
1 change: 1 addition & 0 deletions compiler/src/dotty/tools/dotc/core/Definitions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -768,6 +768,7 @@ class Definitions {
@tu lazy val ClassTagModule_apply: Symbol = ClassTagModule.requiredMethod(nme.apply)

@tu lazy val TypeTestClass: ClassSymbol = requiredClass("scala.reflect.TypeTest")
@tu lazy val TypeTest_unapply: Symbol = TypeTestClass.requiredMethod(nme.unapply)
@tu lazy val TypeTestModule_identity: Symbol = TypeTestClass.companionModule.requiredMethod(nme.identity)

@tu lazy val QuotedExprClass: ClassSymbol = requiredClass("scala.quoted.Expr")
Expand Down
145 changes: 75 additions & 70 deletions compiler/src/dotty/tools/dotc/transform/patmat/Space.scala
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import ProtoTypes._
import transform.SymUtils._
import reporting._
import config.Printers.{exhaustivity => debug}
import util.SrcPos
import util.{SrcPos, NoSourcePosition}
import NullOpsDecorator._
import collection.mutable

Expand Down Expand Up @@ -69,7 +69,7 @@ case object Empty extends Space
case class Typ(tp: Type, decomposed: Boolean = true) extends Space

/** Space representing an extractor pattern */
case class Prod(tp: Type, unappTp: TermRef, params: List[Space], full: Boolean) extends Space
case class Prod(tp: Type, unappTp: TermRef, params: List[Space]) extends Space

/** Union of spaces */
case class Or(spaces: List[Space]) extends Space
Expand Down Expand Up @@ -107,6 +107,9 @@ trait SpaceLogic {
/** Get components of decomposable types */
def decompose(tp: Type): List[Space]

/** Whether the extractor covers the given type */
def covers(unapp: TermRef, scrutineeTp: Type): Boolean

/** Display space in string format */
def show(sp: Space): String

Expand All @@ -118,8 +121,8 @@ trait SpaceLogic {
* This reduces noise in counterexamples.
*/
def simplify(space: Space, aggressive: Boolean = false)(using Context): Space = trace(s"simplify ${show(space)}, aggressive = $aggressive --> ", debug, x => show(x.asInstanceOf[Space]))(space match {
case Prod(tp, fun, spaces, full) =>
val sp = Prod(tp, fun, spaces.map(simplify(_)), full)
case Prod(tp, fun, spaces) =>
val sp = Prod(tp, fun, spaces.map(simplify(_)))
if (sp.params.contains(Empty)) Empty
else if (canDecompose(tp) && decompose(tp).isEmpty) Empty
else sp
Expand Down Expand Up @@ -151,14 +154,14 @@ trait SpaceLogic {

/** Flatten space to get rid of `Or` for pretty print */
def flatten(space: Space)(using Context): Seq[Space] = space match {
case Prod(tp, fun, spaces, full) =>
case Prod(tp, fun, spaces) =>
val ss = LazyList(spaces: _*).map(flatten)

ss.foldLeft(LazyList(Nil : List[Space])) { (acc, flat) =>
for { sps <- acc; s <- flat }
yield sps :+ s
}.map { sps =>
Prod(tp, fun, sps, full)
Prod(tp, fun, sps)
}

case Or(spaces) =>
Expand All @@ -184,12 +187,13 @@ trait SpaceLogic {
ss.exists(isSubspace(a, _)) || tryDecompose1(tp1)
case (_, Or(_)) =>
simplify(minus(a, b)) == Empty
case (Prod(tp1, _, _, _), Typ(tp2, _)) =>
case (Prod(tp1, _, _), Typ(tp2, _)) =>
isSubType(tp1, tp2)
case (Typ(tp1, _), Prod(tp2, fun, ss)) =>
isSubType(tp1, tp2)
case (Typ(tp1, _), Prod(tp2, fun, ss, full)) =>
// approximation: a type can never be fully matched by a partial extractor
full && isSubType(tp1, tp2) && isSubspace(Prod(tp2, fun, signature(fun, tp2, ss.length).map(Typ(_, false)), full), b)
case (Prod(_, fun1, ss1, _), Prod(_, fun2, ss2, _)) =>
&& covers(fun, tp1)
&& isSubspace(Prod(tp2, fun, signature(fun, tp2, ss.length).map(Typ(_, false))), b)
case (Prod(_, fun1, ss1), Prod(_, fun2, ss2)) =>
isSameUnapply(fun1, fun2) && ss1.zip(ss2).forall((isSubspace _).tupled)
}
}
Expand All @@ -209,28 +213,20 @@ trait SpaceLogic {
else if (canDecompose(tp1)) tryDecompose1(tp1)
else if (canDecompose(tp2)) tryDecompose2(tp2)
else intersectUnrelatedAtomicTypes(tp1, tp2)
case (Typ(tp1, _), Prod(tp2, fun, ss, true)) =>
case (Typ(tp1, _), Prod(tp2, fun, ss)) =>
if (isSubType(tp2, tp1)) b
else if (isSubType(tp1, tp2)) a // problematic corner case: inheriting a case class
else if (canDecompose(tp1)) tryDecompose1(tp1)
else Empty
case (Typ(tp1, _), Prod(tp2, _, _, false)) =>
if (isSubType(tp1, tp2) || isSubType(tp2, tp1)) b // prefer extractor space for better approximation
else if (canDecompose(tp1)) tryDecompose1(tp1)
else if (isSubType(tp1, tp2)) a // problematic corner case: inheriting a case class
else Empty
case (Prod(tp1, fun, ss, true), Typ(tp2, _)) =>
case (Prod(tp1, fun, ss), Typ(tp2, _)) =>
if (isSubType(tp1, tp2)) a
else if (isSubType(tp2, tp1)) a // problematic corner case: inheriting a case class
else if (canDecompose(tp2)) tryDecompose2(tp2)
else Empty
case (Prod(tp1, _, _, false), Typ(tp2, _)) =>
if (isSubType(tp1, tp2) || isSubType(tp2, tp1)) a
else if (canDecompose(tp2)) tryDecompose2(tp2)
else if (isSubType(tp2, tp1)) a // problematic corner case: inheriting a case class
else Empty
case (Prod(tp1, fun1, ss1, full), Prod(tp2, fun2, ss2, _)) =>
case (Prod(tp1, fun1, ss1), Prod(tp2, fun2, ss2)) =>
if (!isSameUnapply(fun1, fun2)) Empty
else if (ss1.zip(ss2).exists(p => simplify(intersect(p._1, p._2)) == Empty)) Empty
else Prod(tp1, fun1, ss1.zip(ss2).map((intersect _).tupled), full)
else Prod(tp1, fun1, ss1.zip(ss2).map((intersect _).tupled))
}
}

Expand All @@ -247,27 +243,31 @@ trait SpaceLogic {
else if (canDecompose(tp1)) tryDecompose1(tp1)
else if (canDecompose(tp2)) tryDecompose2(tp2)
else a
case (Typ(tp1, _), Prod(tp2, fun, ss, true)) =>
case (Typ(tp1, _), Prod(tp2, fun, ss)) =>
// rationale: every instance of `tp1` is covered by `tp2(_)`
if (isSubType(tp1, tp2)) minus(Prod(tp1, fun, signature(fun, tp1, ss.length).map(Typ(_, false)), true), b)
else if (canDecompose(tp1)) tryDecompose1(tp1)
else a
if isSubType(tp1, tp2) && covers(fun, tp1) then
minus(Prod(tp1, fun, signature(fun, tp1, ss.length).map(Typ(_, false))), b)
else if canDecompose(tp1) then
tryDecompose1(tp1)
else
a
case (_, Or(ss)) =>
ss.foldLeft(a)(minus)
case (Or(ss), _) =>
Or(ss.map(minus(_, b)))
case (Prod(tp1, fun, ss, true), Typ(tp2, _)) =>
// uncovered corner case: tp2 :< tp1
if (isSubType(tp1, tp2)) Empty
else if (simplify(a) == Empty) Empty
else if (canDecompose(tp2)) tryDecompose2(tp2)
else a
case (Prod(tp1, _, _, false), Typ(tp2, _)) =>
if (isSubType(tp1, tp2)) Empty
else a
case (Typ(tp1, _), Prod(tp2, _, _, false)) =>
a // approximation
case (Prod(tp1, fun1, ss1, full), Prod(tp2, fun2, ss2, _)) =>
case (Prod(tp1, fun, ss), Typ(tp2, _)) =>
// uncovered corner case: tp2 :< tp1, may happen when inheriting case class
if (isSubType(tp1, tp2))
Empty
else if (simplify(a) == Empty)
Empty
else if (canDecompose(tp2))
tryDecompose2(tp2)
else if (isSubType(tp2, tp1) &&covers(fun, tp2))
minus(a, Prod(tp1, fun, signature(fun, tp1, ss.length).map(Typ(_, false))))
else
a
case (Prod(tp1, fun1, ss1), Prod(tp2, fun2, ss2)) =>
if (!isSameUnapply(fun1, fun2)) return a

val range = (0 until ss1.size).toList
Expand All @@ -282,40 +282,36 @@ trait SpaceLogic {
else if cache.forall(sub => isSubspace(sub, Empty)) then Empty
else
// `(_, _, _) - (Some, None, _)` becomes `(None, _, _) | (_, Some, _) | (_, _, Empty)`
Or(range.map { i => Prod(tp1, fun1, ss1.updated(i, sub(i)), full) })
Or(range.map { i => Prod(tp1, fun1, ss1.updated(i, sub(i))) })
}
}
}

object SpaceEngine {

/** Is the unapply irrefutable?
/** Is the unapply or unapplySeq irrefutable?
* @param unapp The unapply function reference
*/
def isIrrefutableUnapply(unapp: tpd.Tree, patSize: Int)(using Context): Boolean = {
val unappResult = unapp.tpe.widen.finalResultType
unappResult.isRef(defn.SomeClass) ||
unappResult <:< ConstantType(Constant(true)) ||
(unapp.symbol.is(Synthetic) && unapp.symbol.owner.linkedClass.is(Case)) || // scala2 compatibility
(patSize != -1 && productArity(unappResult) == patSize) || {
val isEmptyTp = extractorMemberType(unappResult, nme.isEmpty, unapp.srcPos)
def isIrrefutable(unapp: TermRef)(using Context): Boolean = {
val unappResult = unapp.widen.finalResultType
unappResult.isRef(defn.SomeClass)
|| unappResult <:< ConstantType(Constant(true)) // only for unapply
|| (unapp.symbol.is(Synthetic) && unapp.symbol.owner.linkedClass.is(Case)) // scala2 compatibility
|| unapplySeqTypeElemTp(unappResult).exists // only for unapplySeq
|| productArity(unappResult) > 0
|| {
val isEmptyTp = extractorMemberType(unappResult, nme.isEmpty, NoSourcePosition)
isEmptyTp <:< ConstantType(Constant(false))
}
}

/** Is the unapplySeq irrefutable?
* @param unapp The unapplySeq function reference
/** Is the unapply or unapplySeq irrefutable?
* @param unapp The unapply function tree
*/
def isIrrefutableUnapplySeq(unapp: tpd.Tree, patSize: Int)(using Context): Boolean = {
val unappResult = unapp.tpe.widen.finalResultType
unappResult.isRef(defn.SomeClass) ||
(unapp.symbol.is(Synthetic) && unapp.symbol.owner.linkedClass.is(Case)) || // scala2 compatibility
unapplySeqTypeElemTp(unappResult).exists ||
isProductSeqMatch(unappResult, patSize) ||
{
val isEmptyTp = extractorMemberType(unappResult, nme.isEmpty, unapp.srcPos)
isEmptyTp <:< ConstantType(Constant(false))
}
def isIrrefutable(unapp: tpd.Tree)(using Context): Boolean = {
val fun1 = tpd.funPart(unapp)
val funRef = fun1.tpe.asInstanceOf[TermRef]
isIrrefutable(funRef)
}
}

Expand Down Expand Up @@ -396,12 +392,12 @@ class SpaceEngine(using Context) extends SpaceLogic {
else {
val (arity, elemTp, resultTp) = unapplySeqInfo(fun.tpe.widen.finalResultType, fun.srcPos)
if (elemTp.exists)
Prod(erase(pat.tpe.stripAnnots), funRef, projectSeq(pats) :: Nil, isIrrefutableUnapplySeq(fun, pats.size))
Prod(erase(pat.tpe.stripAnnots), funRef, projectSeq(pats) :: Nil)
else
Prod(erase(pat.tpe.stripAnnots), funRef, pats.take(arity - 1).map(project) :+ projectSeq(pats.drop(arity - 1)), isIrrefutableUnapplySeq(fun, pats.size))
Prod(erase(pat.tpe.stripAnnots), funRef, pats.take(arity - 1).map(project) :+ projectSeq(pats.drop(arity - 1)))
}
else
Prod(erase(pat.tpe.stripAnnots), funRef, pats.map(project), isIrrefutableUnapply(fun, pats.length))
Prod(erase(pat.tpe.stripAnnots), funRef, pats.map(project))

case Typed(pat @ UnApply(_, _, _), _) =>
project(pat)
Expand Down Expand Up @@ -509,7 +505,7 @@ class SpaceEngine(using Context) extends SpaceLogic {
val unapplyTp = scalaConsType.classSymbol.companionModule.termRef.select(nme.unapply)
items.foldRight[Space](zero) { (pat, acc) =>
val consTp = scalaConsType.appliedTo(pats.head.tpe.widen)
Prod(consTp, unapplyTp, project(pat) :: acc :: Nil, true)
Prod(consTp, unapplyTp, project(pat) :: acc :: Nil)
}
}

Expand All @@ -525,7 +521,9 @@ class SpaceEngine(using Context) extends SpaceLogic {
}

def isSameUnapply(tp1: TermRef, tp2: TermRef): Boolean =
tp1.prefix.isStable && tp2.prefix.isStable && tp1 =:= tp2
// always assume two TypeTest[S, T].unapply are the same if they are equal in types
(tp1.prefix.isStable && tp2.prefix.isStable || tp1.symbol == defn.TypeTest_unapply)
&& tp1 =:= tp2

/** Parameter types of the case class type `tp`. Adapted from `unapplyPlan` in patternMatcher */
def signature(unapp: TermRef, scrutineeTp: Type, argLen: Int): List[Type] = {
Expand Down Expand Up @@ -558,7 +556,7 @@ class SpaceEngine(using Context) extends SpaceLogic {
// Case unapplySeq:
// 1. return the type `List[T]` where `T` is the element type of the unapplySeq return type `Seq[T]`

val resTp = mt.finalResultType
val resTp = mt.instantiate(scrutineeTp :: Nil).finalResultType

val sig =
if (resTp.isRef(defn.BooleanClass))
Expand Down Expand Up @@ -591,6 +589,13 @@ class SpaceEngine(using Context) extends SpaceLogic {
sig.map(_.annotatedToRepeated)
}

/** Whether the extractor covers the given type */
def covers(unapp: TermRef, scrutineeTp: Type): Boolean =
SpaceEngine.isIrrefutable(unapp) || unapp.symbol == defn.TypeTest_unapply && {
val AppliedType(_, _ :: tp :: Nil) = unapp.prefix.widen.dealias
scrutineeTp <:< tp
}

/** Decompose a type into subspaces -- assume the type can be decomposed */
def decompose(tp: Type): List[Space] =
tp.dealias match {
Expand Down Expand Up @@ -710,7 +715,7 @@ class SpaceEngine(using Context) extends SpaceLogic {
def impossible: Nothing = throw new AssertionError("`satisfiable` only accepts flattened space.")

def genConstraint(space: Space): List[(Type, Type)] = space match {
case Prod(tp, unappTp, ss, _) =>
case Prod(tp, unappTp, ss) =>
val tps = signature(unappTp, tp, ss.length)
ss.zip(tps).flatMap {
case (sp : Prod, tp) => sp.tp -> tp :: genConstraint(sp)
Expand Down Expand Up @@ -772,7 +777,7 @@ class SpaceEngine(using Context) extends SpaceLogic {
showType(tp) + params(tp).map(_ => "_").mkString("(", ", ", ")")
else if (decomposed) "_: " + showType(tp, showTypeArgs = true)
else "_"
case Prod(tp, fun, params, _) =>
case Prod(tp, fun, params) =>
if (ctx.definitions.isTupleType(tp))
"(" + params.map(doShow(_)).mkString(", ") + ")"
else if (tp.isRef(scalaConsType.symbol))
Expand Down
4 changes: 2 additions & 2 deletions compiler/src/dotty/tools/dotc/typer/Checking.scala
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import NameKinds.DefaultGetterName
import NameOps._
import SymDenotations.{NoCompleter, NoDenotation}
import Applications.unapplyArgs
import transform.patmat.SpaceEngine.isIrrefutableUnapply
import transform.patmat.SpaceEngine.isIrrefutable
import config.Feature._
import config.SourceVersion._

Expand Down Expand Up @@ -739,7 +739,7 @@ trait Checking {
recur(pat1, pt)
case UnApply(fn, _, pats) =>
check(pat, pt) &&
(isIrrefutableUnapply(fn, pats.length) || fail(pat, pt)) && {
(isIrrefutable(fn) || fail(pat, pt)) && {
val argPts = unapplyArgs(fn.tpe.widen.finalResultType, fn, pats, pat.srcPos)
pats.corresponds(argPts)(recur)
}
Expand Down
4 changes: 2 additions & 2 deletions library/src/scala/reflect/TypeTest.scala
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package scala.reflect

/** A `TypeTest[S, T] contains the logic needed to know at runtime if a value of
* type `S` can be downcasted to `T`.
* type `S` is an instance of `T`.
*
* If a pattern match is performed on a term of type `s: S` that is uncheckable with `s.isInstanceOf[T]` and
* the pattern are of the form:
Expand All @@ -12,7 +12,7 @@ package scala.reflect
@scala.annotation.implicitNotFound(msg = "No TypeTest available for [${S}, ${T}]")
trait TypeTest[-S, T] extends Serializable:

/** A TypeTest[S, T] can serve as an extractor that matches only S of type T.
/** A TypeTest[S, T] can serve as an extractor that matches if and only if S of type T.
*
* The compiler tries to turn unchecked type tests in pattern matches into checked ones
* by wrapping a `(_: T)` type pattern as `tt(_: T)`, where `tt` is the `TypeTest[S, T]` instance.
Expand Down
1 change: 1 addition & 0 deletions tests/patmat/i12020.check
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
19: Pattern Match Exhaustivity: _: TypeDef
22 changes: 22 additions & 0 deletions tests/patmat/i12020.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import scala.quoted.*

def qwe(using Quotes) = {
import quotes.reflect.*

def ko_1(param: ValDef | TypeDef) =
param match {
case _: ValDef =>
case _: TypeDef =>
}

def ko_2(params: List[ValDef] | List[TypeDef]) =
params.map {
case x: ValDef =>
case y: TypeDef =>
}

def ko_3(param: ValDef | TypeDef) =
param match {
case _: ValDef =>
}
}
5 changes: 5 additions & 0 deletions tests/patmat/i12026.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
def test[A, B](a: A|B)(using reflect.TypeTest[Any, A], reflect.TypeTest[Any, B]) =
a match {
case a: A =>
case b: B =>
}
5 changes: 5 additions & 0 deletions tests/patmat/i12026b.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
def test[A, B](a: A|B)(tta: reflect.TypeTest[Any, A], ttb: reflect.TypeTest[Any, B]) =
a match {
case tta(a: A) =>
case ttb(b: B) =>
}
2 changes: 1 addition & 1 deletion tests/patmat/i2363.check
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
15: Pattern Match Exhaustivity: List(_, _*)
21: Pattern Match Exhaustivity: _: Expr
21: Pattern Match Exhaustivity: _: IntExpr, _: BooleanExpr
4 changes: 2 additions & 2 deletions tests/patmat/irrefutable.check
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
22: Pattern Match Exhaustivity: _: Base
65: Pattern Match Exhaustivity: _: M
22: Pattern Match Exhaustivity: _: A, _: B, C(_, _)
65: Pattern Match Exhaustivity: ExM(_, _)
2 changes: 1 addition & 1 deletion tests/patmat/optionless.check
Original file line number Diff line number Diff line change
@@ -1 +1 @@
28: Pattern Match Exhaustivity: _: Tree
28: Pattern Match Exhaustivity: Ident(_)
2 changes: 1 addition & 1 deletion tests/patmat/patmat-extractor.check
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
13: Pattern Match Exhaustivity: _: Node
13: Pattern Match Exhaustivity: NodeA(_), NodeB(_), NodeC(_)
15: Match case Unreachable