Skip to content

Commit a90d826

Browse files
committed
Relax comparision between null and reference types in explicit nulls
1 parent 9dce045 commit a90d826

File tree

12 files changed

+152
-131
lines changed

12 files changed

+152
-131
lines changed

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,12 @@ class PatternMatcher extends MiniPhase {
5656
if !inInlinedCode then
5757
// check exhaustivity and unreachability
5858
SpaceEngine.checkExhaustivity(tree)
59-
SpaceEngine.checkRedundancy(tree)
59+
// With explcit nulls, even if the selector type is non-nullable,
60+
// we still need to consider the possibility of null value,
61+
// so we use the after-erasure nullability for the target space
62+
// for consistent runtime behavior.
63+
// For example, `val x: String = ???; x match { case null => }` should not be unreachable.
64+
withoutMode(Mode.SafeNulls)(SpaceEngine.checkRedundancy(tree))
6065

6166
translated.ensureConforms(matchType)
6267
}

compiler/src/dotty/tools/dotc/typer/Synthesizer.scala

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -175,18 +175,6 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context):
175175
cmpWithBoxed(cls1, cls2)
176176
else if cls2.isPrimitiveValueClass then
177177
cmpWithBoxed(cls2, cls1)
178-
else if ctx.mode.is(Mode.SafeNulls) then
179-
// If explicit nulls is enabled, and unsafeNulls is not enabled,
180-
// we want to disallow comparison between Object and Null.
181-
// If we have to check whether a variable with a non-nullable type has null value
182-
// (for example, a NotNull java method returns null for some reasons),
183-
// we can still cast it to a nullable type then compare its value.
184-
//
185-
// Example:
186-
// val x: String = null.asInstanceOf[String]
187-
// if (x == null) {} // error: x is non-nullable
188-
// if (x.asInstanceOf[String|Null] == null) {} // ok
189-
cls1 == defn.NullClass && cls1 == cls2
190178
else if cls1 == defn.NullClass then
191179
cls1 == cls2 || cls2.derivesFrom(defn.ObjectClass)
192180
else if cls2 == defn.NullClass then

docs/_docs/reference/experimental/explicit-nulls.md

Lines changed: 4 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -90,26 +90,10 @@ More details can be found in [safe initialization](../other-new-features/safe-in
9090

9191
## Equality
9292

93-
We don't allow the double-equal (`==` and `!=`) and reference (`eq` and `ne`) comparison between
94-
`AnyRef` and `Null` anymore, since a variable with a non-nullable type cannot have `null` as value.
95-
`null` can only be compared with `Null`, nullable union (`T | Null`), or `Any` type.
96-
97-
For some reason, if we really want to compare `null` with non-null values, we have to provide a type hint (e.g. `: Any`).
98-
99-
```scala
100-
val x: String = ???
101-
val y: String | Null = ???
102-
103-
x == null // error: Values of types String and Null cannot be compared with == or !=
104-
x eq null // error
105-
"hello" == null // error
106-
107-
y == null // ok
108-
y == x // ok
109-
110-
(x: String | Null) == null // ok
111-
(x: Any) == null // ok
112-
```
93+
We still allow the double-equal (`==` and `!=`), reference (`eq` and `ne`) comparison,
94+
and pattern matching between `Null` and reference types.
95+
Even if a type is non-nullable, we still need to consider the possibility of `null` value
96+
caused by the Java methods or uninitialized values.
11397

11498
## Java Interoperability
11599

tests/explicit-nulls/neg/equal1.scala

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
// Test what can be compared for equality against null.
2-
class Foo {
2+
3+
case class VC(x: Int) extends AnyVal
4+
5+
def test =
36
// Null itself
47
val x0: Null = null
58
x0 != x0
@@ -9,21 +12,21 @@ class Foo {
912
null == null
1013
null != null
1114

12-
// Non-nullable types: error
15+
// Non-nullable types: OK.
1316
val x1: String = "hello"
14-
x1 != null // error
15-
x1 == null // error
16-
null == x1 // error
17-
null != x1 // error
18-
x1 == x0 // error
19-
x0 != x1 // error
20-
x1.asInstanceOf[String|Null] == null
21-
x1.asInstanceOf[String|Null] == x0
17+
x1 != null
18+
x1 == null
19+
null == x1
20+
null != x1
21+
x1 == x0
22+
x0 != x1
23+
x1.asInstanceOf[String | Null] == null
24+
x1.asInstanceOf[String | Null] == x0
2225
x1.asInstanceOf[Any] == null
2326
x1.asInstanceOf[Any] == x0
2427

2528
// Nullable types: OK
26-
val x2: String|Null = null
29+
val x2: String | Null = null
2730
x2 == null
2831
null == x2
2932
x2 == x0
@@ -41,4 +44,10 @@ class Foo {
4144
null == false // error
4245
'a' == null // error
4346
null == 'b' // error
44-
}
47+
48+
// Nullable value types: OK.
49+
val x3: Int | Null = null
50+
x3 == null
51+
null == x3
52+
x3 == x0
53+
x3 != x0

tests/explicit-nulls/neg/equal2.scala

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,17 @@
1-
// Test that we can't compare for equality `null` with classes.
2-
// This rule is for both regular classes and value classes.
1+
// Test that we can compare values of regular classes against null,
2+
// but not values of value classes.
33

44
class Foo(x: Int)
55
class Bar(x: Int) extends AnyVal
66

77
class Test {
88
locally {
99
val foo: Foo = new Foo(15)
10-
foo == null // error: Values of types Null and Foo cannot be compared
11-
null == foo // error
12-
foo != null // error
13-
null != foo // error
10+
foo == null
11+
null == foo
12+
foo != null
13+
null != foo
1414

15-
// To test against null, make the type nullable.
1615
val foo2: Foo | Null = foo
1716
// ok
1817
foo2 == null

tests/explicit-nulls/neg/flow-match.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@
33
object MatchTest {
44
def f6(s: String | Null): String = s match {
55
case s2 => s2 // error
6-
case null => "other" // error
6+
case null => "other"
77
case s3 => s3
88
}
99

1010
def f7(s: String | Null): String = s match {
1111
case null => "other"
12-
case null => "other" // error
12+
case null => "other"
1313
case s3 => s3
1414
}
1515
}
Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,21 @@
11
// Test we are correctly striping nulls from nullable unions.
22

3-
class Foo {
3+
class Foo:
44

55
class B1
66
class B2
7-
locally {
7+
8+
locally:
89
val x: (Null | String) | Null | (B1 | (Null | B2)) = ???
9-
if (x != null) {
10+
if x != null then
1011
val _: String | B1 | B2 = x // ok: can remove all nullable unions
11-
}
12-
}
1312

14-
locally {
13+
locally:
1514
val x: (Null | String) & (Null | B1) = ???
16-
if (x != null) {
15+
if x != null then
1716
val _: String & B1 = x // ok: can remove null from embedded intersection
18-
}
19-
}
2017

21-
locally {
18+
locally:
2219
val x: (Null | B1) & B2 = ???
23-
if (x != null) {} // error: the type of x is not a nullable union, so we cannot remove the Null
24-
}
25-
}
20+
if x != null then
21+
val _: B1 & B2 = x // error: the type of x is not a nullable union, so we cannot remove the Null
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
//> using options -Xfatal-warnings
2+
3+
class Foo:
4+
5+
val s: String = ???
6+
7+
s match
8+
case s: String => 100
9+
case _ => 200 // error: unreachable case except for null
10+
11+
s match
12+
case s: String => 100
13+
case null => 200
14+
15+
s match
16+
case null => 100
17+
case _ => 200
18+
19+
val s2: String | Null = ???
20+
21+
s2 match
22+
case s2: String => 100
23+
case _ => 200 // error: unreachable case except for null
24+
25+
s2 match
26+
case s2: String => 100
27+
case null => 200
28+
29+
s2 match
30+
case null => 200
31+
case s2: String => 100
32+
33+
sealed trait Animal
34+
case class Dog(name: String) extends Animal
35+
case object Cat extends Animal
36+
37+
val a: Animal = ???
38+
a match
39+
case Dog(name) => 100
40+
case Cat => 200
41+
case _ => 300 // error: unreachable case except for null
42+
43+
val a2: Animal | Null = ???
44+
a2 match
45+
case Dog(_) => 100
46+
case Cat => 200
47+
case _ => 300 // error: unreachable case except for null
48+
49+
a2 match
50+
case Dog(_) => 100
51+
case Cat => 200
52+
case null => 300 // ok

tests/explicit-nulls/unsafe-common/unsafe-equal.scala renamed to tests/explicit-nulls/pos/anyref-equal-nulls.scala

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@ class S {
1111
null == s1
1212
null != s1
1313

14-
s2 == null // error
15-
s2 != null // error
16-
null == s2 // error
17-
null != s2 // error
14+
s2 == null
15+
s2 != null
16+
null == s2
17+
null != s2
1818

1919
s1 == s2
2020
s1 != s2
@@ -27,21 +27,21 @@ class S {
2727
null != n
2828

2929
s1 == n
30-
s2 == n // error
30+
s2 == n
3131
n != s1
32-
n != s2 // error
32+
n != s2
3333
}
3434

3535
locally {
36-
ss1 == null // error
37-
ss1 != null // error
38-
null == ss1 // error
39-
null != ss1 // error
40-
41-
ss1 == n // error
42-
ss1 != n // error
43-
n == ss1 // error
44-
n != ss1 // error
36+
ss1 == null
37+
ss1 != null
38+
null == ss1
39+
null != ss1
40+
41+
ss1 == n
42+
ss1 != n
43+
n == ss1
44+
n != ss1
4545

4646
ss1 == ss2
4747
ss2 != ss1

tests/explicit-nulls/pos/pattern-matching.scala

Lines changed: 0 additions & 38 deletions
This file was deleted.
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
object Test:
2+
3+
def main(args: Array[String]): Unit =
4+
5+
val s: String = null.asInstanceOf[String]
6+
7+
val r1 = s match
8+
case s: String => 100
9+
case _ => 200
10+
assert(r1 == 200)
11+
12+
val r2 = s match
13+
case s: String => 100
14+
case null => 200
15+
assert(r2 == 200)
16+
17+
val r3 = s match
18+
case null => 100
19+
case _ => 200
20+
assert(r3 == 100)
21+
22+
val s2: String | Null = null
23+
24+
val r4 = s2 match
25+
case s2: String => 100
26+
case _ => 200
27+
assert(r4 == 200)
28+
29+
val r5 = s2 match
30+
case s2: String => 100
31+
case null => 200
32+
assert(r5 == 200)
33+
34+
val r6 = s2 match
35+
case null => 200
36+
case s2: String => 100
37+
assert(r6 == 200)

tests/explicit-nulls/unsafe-common/unsafe-match-null.scala

Lines changed: 0 additions & 11 deletions
This file was deleted.

0 commit comments

Comments
 (0)