Skip to content

Relax comparison between Null and reference types in explicit nulls #19258

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

Closed
wants to merge 1 commit into from
Closed
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
7 changes: 6 additions & 1 deletion compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,12 @@ class PatternMatcher extends MiniPhase {
if !inInlinedCode then
// check exhaustivity and unreachability
SpaceEngine.checkExhaustivity(tree)
SpaceEngine.checkRedundancy(tree)
// With explcit nulls, even if the selector type is non-nullable,
// we still need to consider the possibility of null value,
// so we use the after-erasure nullability for space operations
// to achieve consistent runtime behavior.
// For example, `val x: String = ???; x match { case null => }` should not be unreachable.
withoutMode(Mode.SafeNulls)(SpaceEngine.checkRedundancy(tree))

translated.ensureConforms(matchType)
}
Expand Down
12 changes: 0 additions & 12 deletions compiler/src/dotty/tools/dotc/typer/Synthesizer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -175,18 +175,6 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context):
cmpWithBoxed(cls1, cls2)
else if cls2.isPrimitiveValueClass then
cmpWithBoxed(cls2, cls1)
else if ctx.mode.is(Mode.SafeNulls) then
// If explicit nulls is enabled, and unsafeNulls is not enabled,
// we want to disallow comparison between Object and Null.
// If we have to check whether a variable with a non-nullable type has null value
// (for example, a NotNull java method returns null for some reasons),
// we can still cast it to a nullable type then compare its value.
//
// Example:
// val x: String = null.asInstanceOf[String]
// if (x == null) {} // error: x is non-nullable
// if (x.asInstanceOf[String|Null] == null) {} // ok
cls1 == defn.NullClass && cls1 == cls2
else if cls1 == defn.NullClass then
cls1 == cls2 || cls2.derivesFrom(defn.ObjectClass)
else if cls2 == defn.NullClass then
Expand Down
24 changes: 4 additions & 20 deletions docs/_docs/reference/experimental/explicit-nulls.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,26 +90,10 @@ More details can be found in [safe initialization](../other-new-features/safe-in

## Equality

We don't allow the double-equal (`==` and `!=`) and reference (`eq` and `ne`) comparison between
`AnyRef` and `Null` anymore, since a variable with a non-nullable type cannot have `null` as value.
`null` can only be compared with `Null`, nullable union (`T | Null`), or `Any` type.

For some reason, if we really want to compare `null` with non-null values, we have to provide a type hint (e.g. `: Any`).

```scala
val x: String = ???
val y: String | Null = ???

x == null // error: Values of types String and Null cannot be compared with == or !=
x eq null // error
"hello" == null // error

y == null // ok
y == x // ok

(x: String | Null) == null // ok
(x: Any) == null // ok
```
We still allow the double-equal (`==` and `!=`), reference (`eq` and `ne`) comparison,
and pattern matching between `Null` and reference types.
Even if a type is non-nullable, we still need to consider the possibility of `null` value
caused by the Java methods or uninitialized values.

## Java Interoperability

Expand Down
33 changes: 21 additions & 12 deletions tests/explicit-nulls/neg/equal1.scala
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
// Test what can be compared for equality against null.
class Foo {

case class VC(x: Int) extends AnyVal

def test =
// Null itself
val x0: Null = null
x0 != x0
Expand All @@ -9,21 +12,21 @@ class Foo {
null == null
null != null

// Non-nullable types: error
// Non-nullable types: OK.
val x1: String = "hello"
x1 != null // error
x1 == null // error
null == x1 // error
null != x1 // error
x1 == x0 // error
x0 != x1 // error
x1.asInstanceOf[String|Null] == null
x1.asInstanceOf[String|Null] == x0
x1 != null
x1 == null
null == x1
null != x1
x1 == x0
x0 != x1
x1.asInstanceOf[String | Null] == null
x1.asInstanceOf[String | Null] == x0
x1.asInstanceOf[Any] == null
x1.asInstanceOf[Any] == x0

// Nullable types: OK
val x2: String|Null = null
val x2: String | Null = null
x2 == null
null == x2
x2 == x0
Expand All @@ -41,4 +44,10 @@ class Foo {
null == false // error
'a' == null // error
null == 'b' // error
}

// Nullable value types: OK.
val x3: Int | Null = null
x3 == null
null == x3
x3 == x0
x3 != x0
13 changes: 6 additions & 7 deletions tests/explicit-nulls/neg/equal2.scala
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
// Test that we can't compare for equality `null` with classes.
// This rule is for both regular classes and value classes.
// Test that we can compare values of regular classes against null,
// but not values of value classes.

class Foo(x: Int)
class Bar(x: Int) extends AnyVal

class Test {
locally {
val foo: Foo = new Foo(15)
foo == null // error: Values of types Null and Foo cannot be compared
null == foo // error
foo != null // error
null != foo // error
foo == null
null == foo
foo != null
null != foo

// To test against null, make the type nullable.
val foo2: Foo | Null = foo
// ok
foo2 == null
Expand Down
4 changes: 2 additions & 2 deletions tests/explicit-nulls/neg/flow-match.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@
object MatchTest {
def f6(s: String | Null): String = s match {
case s2 => s2 // error
case null => "other" // error
case null => "other"
case s3 => s3
}

def f7(s: String | Null): String = s match {
case null => "other"
case null => "other" // error
case null => "other"
case s3 => s3
}
}
22 changes: 9 additions & 13 deletions tests/explicit-nulls/neg/flow-strip-null.scala
Original file line number Diff line number Diff line change
@@ -1,25 +1,21 @@
// Test we are correctly striping nulls from nullable unions.

class Foo {
class Foo:

class B1
class B2
locally {

locally:
val x: (Null | String) | Null | (B1 | (Null | B2)) = ???
if (x != null) {
if x != null then
val _: String | B1 | B2 = x // ok: can remove all nullable unions
}
}

locally {
locally:
val x: (Null | String) & (Null | B1) = ???
if (x != null) {
if x != null then
val _: String & B1 = x // ok: can remove null from embedded intersection
}
}

locally {
locally:
val x: (Null | B1) & B2 = ???
if (x != null) {} // error: the type of x is not a nullable union, so we cannot remove the Null
}
}
if x != null then
val _: B1 & B2 = x // error: the type of x is not a nullable union, so we cannot remove the Null
52 changes: 52 additions & 0 deletions tests/explicit-nulls/neg/pattern-matching.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
//> using options -Xfatal-warnings

class Foo:

val s: String = ???

s match
case s: String => 100
case _ => 200 // error: unreachable case except for null

s match
case s: String => 100
case null => 200

s match
case null => 100
case _ => 200

val s2: String | Null = ???

s2 match
case s2: String => 100
case _ => 200 // error: unreachable case except for null

s2 match
case s2: String => 100
case null => 200

s2 match
case null => 200
case s2: String => 100

sealed trait Animal
case class Dog(name: String) extends Animal
case object Cat extends Animal

val a: Animal = ???
a match
case Dog(name) => 100
case Cat => 200
case _ => 300 // error: unreachable case except for null

val a2: Animal | Null = ???
a2 match
case Dog(_) => 100
case Cat => 200
case _ => 300 // error: unreachable case except for null

a2 match
case Dog(_) => 100
case Cat => 200
case null => 300 // ok
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ class S {
null == s1
null != s1

s2 == null // error
s2 != null // error
null == s2 // error
null != s2 // error
s2 == null
s2 != null
null == s2
null != s2

s1 == s2
s1 != s2
Expand All @@ -27,21 +27,21 @@ class S {
null != n

s1 == n
s2 == n // error
s2 == n
n != s1
n != s2 // error
n != s2
}

locally {
ss1 == null // error
ss1 != null // error
null == ss1 // error
null != ss1 // error

ss1 == n // error
ss1 != n // error
n == ss1 // error
n != ss1 // error
ss1 == null
ss1 != null
null == ss1
null != ss1

ss1 == n
ss1 != n
n == ss1
n != ss1

ss1 == ss2
ss2 != ss1
Expand Down
38 changes: 0 additions & 38 deletions tests/explicit-nulls/pos/pattern-matching.scala

This file was deleted.

37 changes: 37 additions & 0 deletions tests/explicit-nulls/run/pattern-matching.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
object Test:

def main(args: Array[String]): Unit =

val s: String = null.asInstanceOf[String]

val r1 = s match
case s: String => 100
case _ => 200
assert(r1 == 200)

val r2 = s match
case s: String => 100
case null => 200
assert(r2 == 200)

val r3 = s match
case null => 100
case _ => 200
assert(r3 == 100)

val s2: String | Null = null

val r4 = s2 match
case s2: String => 100
case _ => 200
assert(r4 == 200)

val r5 = s2 match
case s2: String => 100
case null => 200
assert(r5 == 200)

val r6 = s2 match
case null => 200
case s2: String => 100
assert(r6 == 200)
11 changes: 0 additions & 11 deletions tests/explicit-nulls/unsafe-common/unsafe-match-null.scala

This file was deleted.