Skip to content

Commit 2007ad2

Browse files
committed
Fix #5067: handle scrutinee of bottom type in pattern matcher
When desugaring pattern matching code for expressions where the matched value has type `Null` or `Nothing`, we used to generate code that's type-incorrect. Example: ``` val Some(x) = null ``` got desugared into ``` val x: Nothing = matchResult1[Nothing]: { case val x1: Null @unchecked = null: Null @unchecked if x1.ne(null) then { case val x: Nothing = x1.value.asInstanceOf[Nothing] return[matchResult1] x: Nothing } else () return[matchResult1] throw new MatchError(x1) } ``` There were two problems here: 1) `x1.ne(null)` 2) `x1.value` In both cases, we're trying to invoke methods that don't exist for type `Nothing` (and #2 doesn't exist for `Null`). This commits changes the desugaring so that 1) is solved by adding an ascription, if needed: (x1: AnyRef).ne(null) 2) is added by generating throw-away but type-correct code that never executes: `throw null`
1 parent 58b8aaa commit 2007ad2

File tree

7 files changed

+90
-7
lines changed

7 files changed

+90
-7
lines changed

compiler/src/dotty/tools/dotc/ast/tpd.scala

+9-3
Original file line numberDiff line numberDiff line change
@@ -889,9 +889,15 @@ object tpd extends Trees.Instance[Type] with TypedTreeInfo {
889889
else Erasure.Boxing.adaptToType(tree, tp)
890890

891891
/** `tree ne null` (might need a cast to be type correct) */
892-
def testNotNull(implicit ctx: Context): Tree =
893-
tree.ensureConforms(defn.ObjectType)
894-
.select(defn.Object_ne).appliedTo(Literal(Constant(null)))
892+
def testNotNull(implicit ctx: Context): Tree = {
893+
val receiver = if (!defn.isSubtypeOfBottom(tree.tpe)) tree.ensureConforms(defn.ObjectType)
894+
else {
895+
// If the receiver is of type `Nothing` or `Null`, add an ascription so that the selection
896+
// succeeds: e.g. `null.ne(null)` doesn't type, but `(null: AnyRef).ne(null)` does.
897+
Typed(tree, TypeTree(defn.AnyRefType))
898+
}
899+
receiver.select(defn.Object_ne).appliedTo(Literal(Constant(null)))
900+
}
895901

896902
/** If inititializer tree is `_', the default value of its type,
897903
* otherwise the tree itself.

compiler/src/dotty/tools/dotc/core/Definitions.scala

+5
Original file line numberDiff line numberDiff line change
@@ -1004,6 +1004,11 @@ class Definitions {
10041004
def isBottomType(tp: Type): Boolean =
10051005
tp.derivesFrom(NothingClass) || tp.derivesFrom(NullClass)
10061006

1007+
/** Is `tp` a subtype of `Nothing` or `Null`? Unlike `isBottomType`, this uses subtyping instead of inheritance. */
1008+
def isSubtypeOfBottom(tp: Type): Boolean = {
1009+
tp.frozen_<:<(NothingType) || tp.frozen_<:<(NullType)
1010+
}
1011+
10071012
/** Is a function class.
10081013
* - FunctionXXL
10091014
* - FunctionN for N >= 0

compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala

+10-4
Original file line numberDiff line numberDiff line change
@@ -335,10 +335,16 @@ object PatternMatcher {
335335
patternPlan(casted, pat, onSuccess)
336336
})
337337
case UnApply(extractor, implicits, args) =>
338-
val mt @ MethodType(_) = extractor.tpe.widen
339-
var unapp = extractor.appliedTo(ref(scrutinee).ensureConforms(mt.paramInfos.head))
340-
if (implicits.nonEmpty) unapp = unapp.appliedToArgs(implicits)
341-
val unappPlan = unapplyPlan(unapp, args)
338+
val unappPlan = if (!defn.isSubtypeOfBottom(scrutinee.info)) {
339+
val mt @ MethodType(_) = extractor.tpe.widen
340+
var unapp = extractor.appliedTo(ref(scrutinee).ensureConforms(mt.paramInfos.head))
341+
if (implicits.nonEmpty) unapp = unapp.appliedToArgs(implicits)
342+
unapplyPlan(unapp, args)
343+
} else {
344+
// Generate a throwaway but type-correct plan.
345+
// This plan will never execute because it'll be guarded by a `NonNullTest`.
346+
ResultPlan(tpd.Throw(tpd.Literal(Constant(null))))
347+
}
342348
if (scrutinee.info.isNotNull || nonNull(scrutinee)) unappPlan
343349
else TestPlan(NonNullTest, scrutinee, tree.span, unappPlan)
344350
case Bind(name, body) =>

tests/run/i5067.check

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
matches null literal
2+
matches null literal
3+
not implemented

tests/run/i5067.scala

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Test that we correctly handle scrutinees with type `Null` or `Nothing`.
2+
object Test {
3+
def main(args: Array[String]): Unit = {
4+
null match {
5+
case Some(_) => println("matches Some")
6+
case (_, _) => println("matches Pair")
7+
case null => println("matches null literal")
8+
}
9+
10+
type X = Null
11+
(null: X) match {
12+
case Some(_) => println("matches Some")
13+
case (_, _) => println("matches Pair")
14+
case null => println("matches null literal")
15+
}
16+
17+
type Y = Nothing
18+
try {
19+
(??? : Y) match {
20+
case _ => println("matches anything")
21+
}
22+
} catch {
23+
case e: NotImplementedError => println("not implemented")
24+
}
25+
}
26+
}

tests/run/i5067b.check

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
match error
2+
match error nested
3+
not implemented error

tests/run/i5067b.scala

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Test that we correctly handle scrutinees with type `Null` or `Nothing`.
2+
object Test {
3+
def main(args: Array[String]): Unit = {
4+
class B[T] {}
5+
object B {
6+
def unapply[T](x: Any): Option[B[T]] = None
7+
}
8+
try {
9+
val B(_) = null
10+
} catch {
11+
case e: MatchError => println("match error")
12+
}
13+
14+
null match {
15+
case null =>
16+
try {
17+
null match {
18+
case Some(_) => ()
19+
}
20+
} catch {
21+
case e: MatchError => println("match error nested")
22+
}
23+
}
24+
25+
try {
26+
??? match {
27+
case (_, _) => ()
28+
case _ => ()
29+
}
30+
} catch {
31+
case e: NotImplementedError => println("not implemented error")
32+
}
33+
}
34+
}

0 commit comments

Comments
 (0)