-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Restore unreachable warnings by converting literals #13290
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
Changes from 6 commits
d17d0dc
5fb2ac0
a945326
202b63e
4b0f32f
be658f6
8776906
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -165,7 +165,7 @@ trait SpaceLogic { | |
} | ||
|
||
/** Is `a` a subspace of `b`? Equivalent to `a - b == Empty`, but faster */ | ||
def isSubspace(a: Space, b: Space)(using Context): Boolean = trace(s"${show(a)} < ${show(b)}", debug) { | ||
def isSubspace(a: Space, b: Space)(using Context): Boolean = trace(s"isSubspace(${show(a)}, ${show(b)})", debug) { | ||
def tryDecompose1(tp: Type) = canDecompose(tp) && isSubspace(Or(decompose(tp)), b) | ||
def tryDecompose2(tp: Type) = canDecompose(tp) && isSubspace(a, Or(decompose(tp))) | ||
|
||
|
@@ -212,14 +212,14 @@ trait SpaceLogic { | |
if (isSubType(tp2, tp1)) b | ||
else if (canDecompose(tp1)) tryDecompose1(tp1) | ||
else if (isSubType(tp1, tp2)) a // problematic corner case: inheriting a case class | ||
else Empty | ||
else intersectUnrelatedAtomicTypes(tp1, tp2) | ||
case (Prod(tp1, fun, ss), Typ(tp2, _)) => | ||
if (isSubType(tp1, tp2)) a | ||
else if (canDecompose(tp2)) tryDecompose2(tp2) | ||
else if (isSubType(tp2, tp1)) a // problematic corner case: inheriting a case class | ||
else Empty | ||
else intersectUnrelatedAtomicTypes(tp1, tp2) | ||
case (Prod(tp1, fun1, ss1), Prod(tp2, fun2, ss2)) => | ||
if (!isSameUnapply(fun1, fun2)) Empty | ||
if (!isSameUnapply(fun1, fun2)) intersectUnrelatedAtomicTypes(tp1, tp2) | ||
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)) | ||
} | ||
|
@@ -503,14 +503,34 @@ class SpaceEngine(using Context) extends SpaceLogic { | |
} | ||
} | ||
|
||
/** Numeric literals, while being constant values of unrelated types (e.g. Char and Int), | ||
* when used in a case may end up matching at runtime, because their equals may returns true. | ||
* Because these are universally available, general purpose types, it would be good to avoid | ||
* returning false positive warnings, such as in `(c: Char) match { case 67 => ... }` emitting a | ||
* reachability warning on the case. So the type `ConstantType(Constant(67, IntTag))` is | ||
* converted to `ConstantType(Constant(67, CharTag))`. #12805 */ | ||
def convertConstantType(tp: Type, pt: Type): Type = tp match | ||
case tp @ ConstantType(const) => | ||
val converted = const.convertTo(pt) | ||
if converted == null then tp else ConstantType(converted) | ||
case _ => tp | ||
|
||
/** Adapt types by performing primitive value boxing. #12805 */ | ||
def maybeBox(tp1: Type, tp2: Type): Type = | ||
if tp1.classSymbol.isPrimitiveValueClass && !tp2.classSymbol.isPrimitiveValueClass then | ||
defn.boxedType(tp1).narrow | ||
else tp1 | ||
|
||
/** Is `tp1` a subtype of `tp2`? */ | ||
def isSubType(tp1: Type, tp2: Type): Boolean = { | ||
debug.println(TypeComparer.explained(_.isSubType(tp1, tp2))) | ||
def isSubType(_tp1: Type, tp2: Type): Boolean = { | ||
val tp1 = maybeBox(convertConstantType(_tp1, tp2), tp2) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am wondering if it's possible to write a helper method def isSubtypeWithConversion(tp: Type, pt: Type): Boolean = ... The protocol of the method will be simpler, the code in tp1 <:< tp2 || isSubtypeWithConversion(tp1, tp2) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Where does that leave the explicitNulls code? And what does that code even do? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was thinking of keeping the code for explicitNulls as it's --- duplicate There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I might leave that for another rotation on the code, so I can get the other fixes in. |
||
//debug.println(TypeComparer.explained(_.isSubType(tp1, tp2))) | ||
val res = if (ctx.explicitNulls) { | ||
tp1 <:< tp2 | ||
} else { | ||
(tp1 != constantNullType || tp2 == constantNullType) && tp1 <:< tp2 | ||
} | ||
debug.println(i"$tp1 <:< $tp2 = $res") | ||
res | ||
} | ||
|
||
|
@@ -650,7 +670,6 @@ class SpaceEngine(using Context) extends SpaceLogic { | |
parts.map(Typ(_, true)) | ||
} | ||
|
||
|
||
/** Abstract sealed types, or-types, Boolean and Java enums can be decomposed */ | ||
def canDecompose(tp: Type): Boolean = | ||
val res = tp.dealias match | ||
|
@@ -666,7 +685,7 @@ class SpaceEngine(using Context) extends SpaceLogic { | |
|| cls.isAllOf(JavaEnumTrait) | ||
|| tp.isRef(defn.BooleanClass) | ||
|| tp.isRef(defn.UnitClass) | ||
debug.println(s"decomposable: ${tp.show} = $res") | ||
//debug.println(s"decomposable: ${tp.show} = $res") | ||
res | ||
|
||
/** Show friendly type name with current scope in mind | ||
|
@@ -750,6 +769,7 @@ class SpaceEngine(using Context) extends SpaceLogic { | |
} | ||
|
||
def show(ss: Seq[Space]): String = ss.map(show).mkString(", ") | ||
|
||
/** Display spaces */ | ||
def show(s: Space): String = { | ||
def params(tp: Type): List[Type] = tp.classSymbol.primaryConstructor.info.firstParamTypes | ||
|
@@ -894,32 +914,33 @@ class SpaceEngine(using Context) extends SpaceLogic { | |
else | ||
project(OrType(selTyp, constantNullType, soft = false)) | ||
|
||
debug.println(s"targetSpace: ${show(targetSpace)}") | ||
|
||
// in redundancy check, take guard as false in order to soundly approximate | ||
val spaces = cases.map { x => | ||
val res = | ||
if (x.guard.isEmpty) project(x.pat) | ||
else Empty | ||
|
||
debug.println(s"${x.pat.show} ====> ${res}") | ||
debug.println(s"${x.pat.show} ====> ${show(res)}") | ||
res | ||
} | ||
|
||
(1 until cases.length).foreach { i => | ||
val pat = cases(i).pat | ||
|
||
if (pat != EmptyTree) { // rethrow case of catch uses EmptyTree | ||
val prevs = Or(spaces.take(i)) | ||
val curr = project(pat) | ||
val prevs = Or(spaces.take(i)) | ||
if (pat != EmptyTree // rethrow case of catch uses EmptyTree | ||
&& simplify(intersect(prevs, targetSpace)) != Empty | ||
// it's required that at one of the previous cases is reachable (its intersected Space isn't Empty) | ||
// because if all the previous cases are unreachable then case i can't be unreachable | ||
) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor: maybe use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The case There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh, this isn't a performance improvement, this is correctness! As explored with Seth there's two levels of unreachable: there's "this case can never be reached for this scrutinee" and there's "this case is never reached, because it's covered by something previous". We're only doing the second, but in order to do that properly we need to make sure that the previous, including the first(!), is actually possibly reached. Personally I think the first level should be covered by typing rather than here. But it's also possible, with aliases and casting, for passing code to throw warnings - so it's a bit of a judgement call. Anyways, rethinking this code, I can make this cheaper by reusing and accumulating, so I avoid re-doing lots of intersecting and simplifying. |
||
val curr = project(pat) // TODO(dnw) reuse `spaces(i)` & avoid re-computing? Or is mutability present? | ||
dwijnand marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
debug.println(s"---------------reachable? ${show(curr)}") | ||
debug.println(s"prev: ${show(prevs)}") | ||
|
||
var covered = simplify(intersect(curr, targetSpace)) | ||
debug.println(s"covered: $covered") | ||
|
||
// `covered == Empty` may happen for primitive types with auto-conversion | ||
// see tests/patmat/reader.scala tests/patmat/byte.scala | ||
if (covered == Empty && !isNullLit(pat)) covered = curr | ||
val covered = simplify(intersect(curr, targetSpace)) | ||
debug.println(s"covered: ${show(covered)}") | ||
|
||
if (isSubspace(covered, prevs)) { | ||
if i == cases.length - 1 | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
class C { | ||
def matchUnboxed(i: Integer) = i match { | ||
case null => 0 | ||
case 1 => 1 | ||
case _ => 9 | ||
} | ||
|
||
def matchBoxed(i: Int) = i match { | ||
case C.ZERO => 0 | ||
case C.ONE => 1 | ||
case _ => 9 | ||
} | ||
} | ||
|
||
object C { | ||
final val ZERO: Integer = 0 | ||
final val ONE: Integer = 1 | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
import scala.annotation.unchecked.uncheckedVariance | ||
|
||
type Untyped = Null | ||
|
||
class Type | ||
|
||
abstract class Tree[-T >: Untyped] { | ||
type ThisTree[T >: Untyped] <: Tree[T] | ||
|
||
protected var myTpe: T @uncheckedVariance = _ | ||
|
||
def withType(tpe: Type): ThisTree[Type] = { | ||
val tree = this.asInstanceOf[ThisTree[Type]] | ||
tree.myTpe = tpe | ||
tree | ||
} | ||
} | ||
|
||
case class Ident[-T >: Untyped]() extends Tree[T] | ||
case class DefDef[-T >: Untyped]() extends Tree[T] | ||
case class Inlined[-T >: Untyped]() extends Tree[T] | ||
case class CaseDef[-T >: Untyped]() extends Tree[T] | ||
|
||
def test[T >: Untyped](tree: Tree[T], tp: Type) = tree.withType(tp) match { | ||
case Ident() => 1 | ||
case DefDef() => 2 | ||
case _: Inlined[_] => 3 | ||
case CaseDef() => 4 | ||
case _ => 5 | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
10: Match case Unreachable | ||
16: Match case Unreachable | ||
22: Match case Unreachable |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import scala.language.implicitConversions | ||
|
||
type Timeframe = "1m" | "2m" | "1H" | ||
type TimeframeN = 1 | 2 | 60 | ||
|
||
def manualConvertToN(tf: Timeframe): TimeframeN = tf match | ||
case "1m" => 1 | ||
case "2m" => 2 | ||
case "1H" => 60 | ||
case "4H" => ??? // was: no reachability warning | ||
|
||
given Conversion[Timeframe, TimeframeN] = | ||
case "1m" => 1 | ||
case "2m" => 2 | ||
case "1H" => 60 | ||
case "4H" => ??? // was: no reachability warning | ||
|
||
given Conversion[TimeframeN, Timeframe] = | ||
case 1 => "1m" | ||
case 2 => "2m" | ||
case 60 => "1H" | ||
case 240 => ??? // was: no reachability warning |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
4: Match case Unreachable | ||
9: Match case Unreachable | ||
14: Match case Unreachable |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
def test1(a: 1 | 2) = a match | ||
case 1 => true | ||
case 2 => false | ||
case 4 => ??? // unreachable case, was: no warning | ||
|
||
def test2(a: 1 | 2) = a match | ||
case 1 => true | ||
case 2 => false | ||
case _ => ??? // unreachable | ||
|
||
def test3(a: 1 | 2) = a match | ||
case 1 => true | ||
case 2 => false | ||
case a if a < 0 => ??? // unreachable |
Uh oh!
There was an error while loading. Please reload this page.