Skip to content

Commit 7d1cddb

Browse files
authored
Merge pull request #13290 from dwijnand/unreachable-literals
2 parents 0da9afd + 8776906 commit 7d1cddb

File tree

9 files changed

+165
-47
lines changed

9 files changed

+165
-47
lines changed

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

+52-45
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ trait SpaceLogic {
165165
}
166166

167167
/** Is `a` a subspace of `b`? Equivalent to `a - b == Empty`, but faster */
168-
def isSubspace(a: Space, b: Space)(using Context): Boolean = trace(s"${show(a)} < ${show(b)}", debug) {
168+
def isSubspace(a: Space, b: Space)(using Context): Boolean = trace(s"isSubspace(${show(a)}, ${show(b)})", debug) {
169169
def tryDecompose1(tp: Type) = canDecompose(tp) && isSubspace(Or(decompose(tp)), b)
170170
def tryDecompose2(tp: Type) = canDecompose(tp) && isSubspace(a, Or(decompose(tp)))
171171

@@ -212,14 +212,14 @@ trait SpaceLogic {
212212
if (isSubType(tp2, tp1)) b
213213
else if (canDecompose(tp1)) tryDecompose1(tp1)
214214
else if (isSubType(tp1, tp2)) a // problematic corner case: inheriting a case class
215-
else Empty
215+
else intersectUnrelatedAtomicTypes(tp1, tp2)
216216
case (Prod(tp1, fun, ss), Typ(tp2, _)) =>
217217
if (isSubType(tp1, tp2)) a
218218
else if (canDecompose(tp2)) tryDecompose2(tp2)
219219
else if (isSubType(tp2, tp1)) a // problematic corner case: inheriting a case class
220-
else Empty
220+
else intersectUnrelatedAtomicTypes(tp1, tp2)
221221
case (Prod(tp1, fun1, ss1), Prod(tp2, fun2, ss2)) =>
222-
if (!isSameUnapply(fun1, fun2)) Empty
222+
if (!isSameUnapply(fun1, fun2)) intersectUnrelatedAtomicTypes(tp1, tp2)
223223
else if (ss1.zip(ss2).exists(p => simplify(intersect(p._1, p._2)) == Empty)) Empty
224224
else Prod(tp1, fun1, ss1.zip(ss2).map((intersect _).tupled))
225225
}
@@ -500,14 +500,34 @@ class SpaceEngine(using Context) extends SpaceLogic {
500500
}
501501
}
502502

503+
/** Numeric literals, while being constant values of unrelated types (e.g. Char and Int),
504+
* when used in a case may end up matching at runtime, because their equals may returns true.
505+
* Because these are universally available, general purpose types, it would be good to avoid
506+
* returning false positive warnings, such as in `(c: Char) match { case 67 => ... }` emitting a
507+
* reachability warning on the case. So the type `ConstantType(Constant(67, IntTag))` is
508+
* converted to `ConstantType(Constant(67, CharTag))`. #12805 */
509+
def convertConstantType(tp: Type, pt: Type): Type = tp match
510+
case tp @ ConstantType(const) =>
511+
val converted = const.convertTo(pt)
512+
if converted == null then tp else ConstantType(converted)
513+
case _ => tp
514+
515+
/** Adapt types by performing primitive value boxing. #12805 */
516+
def maybeBox(tp1: Type, tp2: Type): Type =
517+
if tp1.classSymbol.isPrimitiveValueClass && !tp2.classSymbol.isPrimitiveValueClass then
518+
defn.boxedType(tp1).narrow
519+
else tp1
520+
503521
/** Is `tp1` a subtype of `tp2`? */
504-
def isSubType(tp1: Type, tp2: Type): Boolean = {
505-
debug.println(TypeComparer.explained(_.isSubType(tp1, tp2)))
522+
def isSubType(_tp1: Type, tp2: Type): Boolean = {
523+
val tp1 = maybeBox(convertConstantType(_tp1, tp2), tp2)
524+
//debug.println(TypeComparer.explained(_.isSubType(tp1, tp2)))
506525
val res = if (ctx.explicitNulls) {
507526
tp1 <:< tp2
508527
} else {
509528
(tp1 != constantNullType || tp2 == constantNullType) && tp1 <:< tp2
510529
}
530+
debug.println(i"$tp1 <:< $tp2 = $res")
511531
res
512532
}
513533

@@ -647,7 +667,6 @@ class SpaceEngine(using Context) extends SpaceLogic {
647667
parts.map(Typ(_, true))
648668
}
649669

650-
651670
/** Abstract sealed types, or-types, Boolean and Java enums can be decomposed */
652671
def canDecompose(tp: Type): Boolean =
653672
val res = tp.dealias match
@@ -663,7 +682,7 @@ class SpaceEngine(using Context) extends SpaceLogic {
663682
|| cls.isAllOf(JavaEnumTrait)
664683
|| tp.isRef(defn.BooleanClass)
665684
|| tp.isRef(defn.UnitClass)
666-
debug.println(s"decomposable: ${tp.show} = $res")
685+
//debug.println(s"decomposable: ${tp.show} = $res")
667686
res
668687

669688
/** Show friendly type name with current scope in mind
@@ -747,6 +766,7 @@ class SpaceEngine(using Context) extends SpaceLogic {
747766
}
748767

749768
def show(ss: Seq[Space]): String = ss.map(show).mkString(", ")
769+
750770
/** Display spaces */
751771
def show(s: Space): String = {
752772
def params(tp: Type): List[Type] = tp.classSymbol.primaryConstructor.info.firstParamTypes
@@ -885,49 +905,36 @@ class SpaceEngine(using Context) extends SpaceLogic {
885905

886906
if (!redundancyCheckable(sel)) return
887907

888-
val targetSpace =
889-
if !selTyp.classSymbol.isNullableClass then
890-
project(selTyp)
891-
else
892-
project(OrType(selTyp, constantNullType, soft = false))
893-
894-
// in redundancy check, take guard as false in order to soundly approximate
895-
val spaces = cases.map { x =>
896-
val res =
897-
if (x.guard.isEmpty) project(x.pat)
898-
else Empty
908+
val isNullable = selTyp.classSymbol.isNullableClass
909+
val targetSpace = if isNullable
910+
then project(OrType(selTyp, constantNullType, soft = false))
911+
else project(selTyp)
912+
debug.println(s"targetSpace: ${show(targetSpace)}")
899913

900-
debug.println(s"${x.pat.show} ====> ${res}")
901-
res
902-
}
903-
904-
(1 until cases.length).foreach { i =>
905-
val pat = cases(i).pat
914+
cases.iterator.zipWithIndex.foldLeft(Nil: List[Space]) { case (prevs, (CaseDef(pat, guard, _), i)) =>
915+
debug.println(i"case pattern: $pat")
906916

907-
if (pat != EmptyTree) { // rethrow case of catch uses EmptyTree
908-
val prevs = Or(spaces.take(i))
909-
val curr = project(pat)
917+
val curr = project(pat)
918+
debug.println(i"reachable? ${show(curr)}")
910919

911-
debug.println(s"---------------reachable? ${show(curr)}")
912-
debug.println(s"prev: ${show(prevs)}")
920+
val prev = simplify(Or(prevs))
921+
debug.println(s"prev: ${show(prev)}")
913922

914-
var covered = simplify(intersect(curr, targetSpace))
915-
debug.println(s"covered: $covered")
923+
val covered = simplify(intersect(curr, targetSpace))
924+
debug.println(s"covered: ${show(covered)}")
916925

917-
// `covered == Empty` may happen for primitive types with auto-conversion
918-
// see tests/patmat/reader.scala tests/patmat/byte.scala
919-
if (covered == Empty && !isNullLit(pat)) covered = curr
920-
921-
if (isSubspace(covered, prevs)) {
922-
if i == cases.length - 1
923-
&& isWildcardArg(pat)
924-
&& pat.tpe.classSymbol.isNullableClass
925-
then
926-
report.warning(MatchCaseOnlyNullWarning(), pat.srcPos)
927-
else
928-
report.warning(MatchCaseUnreachable(), pat.srcPos)
929-
}
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
933+
report.warning(MatchCaseUnreachable(), pat.srcPos)
930934
}
935+
936+
// in redundancy check, take guard as false in order to soundly approximate
937+
(if guard.isEmpty then covered else Empty) :: prevs
931938
}
932939
}
933940
}

compiler/test/dotty/tools/dotc/transform/PatmatExhaustivityTest.scala

+6-2
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ class PatmatExhaustivityTest {
4545
val baseFilePath = path.toString.stripSuffix(".scala")
4646
val checkFilePath = baseFilePath + ".check"
4747

48-
FileDiff.checkAndDump(path.toString, actualLines, checkFilePath)
48+
FileDiff.checkAndDumpOrUpdate(path.toString, actualLines, checkFilePath)
4949
}
5050

5151
/** A single test with multiple files grouped in a folder */
@@ -57,13 +57,17 @@ class PatmatExhaustivityTest {
5757
val actualLines = compile(files)
5858
val checkFilePath = s"${path}${File.separator}expected.check"
5959

60-
FileDiff.checkAndDump(path.toString, actualLines, checkFilePath)
60+
FileDiff.checkAndDumpOrUpdate(path.toString, actualLines, checkFilePath)
6161
}
6262

6363
@Test
6464
def patmatExhaustivity: Unit = {
6565
val res = Directory(testsDir).list.toList
6666
.filter(f => f.extension == "scala" || f.isDirectory)
67+
.filter { f =>
68+
val path = if f.isDirectory then f.path + "/" else f.path
69+
path.contains(Properties.testsFilter.getOrElse(""))
70+
}
6771
.map(f => if f.isDirectory then compileDir(f.jpath) else compileFile(f.jpath))
6872

6973
val failed = res.filter(!_)

compiler/test/dotty/tools/vulpix/FileDiff.scala

+17
Original file line numberDiff line numberDiff line change
@@ -63,4 +63,21 @@ object FileDiff {
6363
}
6464
}
6565

66+
def checkAndDumpOrUpdate(sourceTitle: String, actualLines: Seq[String], checkFilePath: String): Boolean = {
67+
val outFilePath = checkFilePath + ".out"
68+
FileDiff.check(sourceTitle, actualLines, checkFilePath) match {
69+
case Some(msg) if dotty.Properties.testsUpdateCheckfile =>
70+
FileDiff.dump(checkFilePath, actualLines)
71+
println("Updated checkfile: " + checkFilePath)
72+
false
73+
case Some(msg) =>
74+
FileDiff.dump(outFilePath, actualLines)
75+
println(msg)
76+
println(FileDiff.diffMessage(checkFilePath, outFilePath))
77+
false
78+
case _ =>
79+
Files.deleteIfExists(Paths.get(outFilePath))
80+
true
81+
}
82+
}
6683
}

tests/patmat/boxing.scala

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
class C {
2+
def matchUnboxed(i: Integer) = i match {
3+
case null => 0
4+
case 1 => 1
5+
case _ => 9
6+
}
7+
8+
def matchBoxed(i: Int) = i match {
9+
case C.ZERO => 0
10+
case C.ONE => 1
11+
case _ => 9
12+
}
13+
}
14+
15+
object C {
16+
final val ZERO: Integer = 0
17+
final val ONE: Integer = 1
18+
}

tests/patmat/i12805-fallout.scala

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import scala.annotation.unchecked.uncheckedVariance
2+
3+
type Untyped = Null
4+
5+
class Type
6+
7+
abstract class Tree[-T >: Untyped] {
8+
type ThisTree[T >: Untyped] <: Tree[T]
9+
10+
protected var myTpe: T @uncheckedVariance = _
11+
12+
def withType(tpe: Type): ThisTree[Type] = {
13+
val tree = this.asInstanceOf[ThisTree[Type]]
14+
tree.myTpe = tpe
15+
tree
16+
}
17+
}
18+
19+
case class Ident[-T >: Untyped]() extends Tree[T]
20+
case class DefDef[-T >: Untyped]() extends Tree[T]
21+
case class Inlined[-T >: Untyped]() extends Tree[T]
22+
case class CaseDef[-T >: Untyped]() extends Tree[T]
23+
24+
def test[T >: Untyped](tree: Tree[T], tp: Type) = tree.withType(tp) match {
25+
case Ident() => 1
26+
case DefDef() => 2
27+
case _: Inlined[_] => 3
28+
case CaseDef() => 4
29+
case _ => 5
30+
}

tests/patmat/i12805.check

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
10: Match case Unreachable
2+
16: Match case Unreachable
3+
22: Match case Unreachable

tests/patmat/i12805.scala

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import scala.language.implicitConversions
2+
3+
type Timeframe = "1m" | "2m" | "1H"
4+
type TimeframeN = 1 | 2 | 60
5+
6+
def manualConvertToN(tf: Timeframe): TimeframeN = tf match
7+
case "1m" => 1
8+
case "2m" => 2
9+
case "1H" => 60
10+
case "4H" => ??? // was: no reachability warning
11+
12+
given Conversion[Timeframe, TimeframeN] =
13+
case "1m" => 1
14+
case "2m" => 2
15+
case "1H" => 60
16+
case "4H" => ??? // was: no reachability warning
17+
18+
given Conversion[TimeframeN, Timeframe] =
19+
case 1 => "1m"
20+
case 2 => "2m"
21+
case 60 => "1H"
22+
case 240 => ??? // was: no reachability warning

tests/patmat/i12805b.check

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
4: Match case Unreachable
2+
9: Match case Unreachable
3+
14: Match case Unreachable

tests/patmat/i12805b.scala

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
def test1(a: 1 | 2) = a match
2+
case 1 => true
3+
case 2 => false
4+
case 4 => ??? // unreachable case, was: no warning
5+
6+
def test2(a: 1 | 2) = a match
7+
case 1 => true
8+
case 2 => false
9+
case _ => ??? // unreachable
10+
11+
def test3(a: 1 | 2) = a match
12+
case 1 => true
13+
case 2 => false
14+
case a if a < 0 => ??? // unreachable

0 commit comments

Comments
 (0)