Skip to content

Commit 73967d5

Browse files
authored
Fix #17467: Limit isNullable widening to stable TermRefs; remove under explicit nulls. (#17470)
The Scala language specification has a peculiar clause about the nullness of singleton types of the form `path.type`. It says that `Null <:< path.type` if the *underlying* type `U` of `path` is nullable itself. The previous implementation of that rule was overly broad, as it indiscrimately widened all types. This resulted in problematic subtyping relationships like `Null <:< "foo"`. We do not widen anymore. Instead, we specifically handle `TermRef`s of stable members, which are how dotc represents singleton types. We also have a rule for `Null <:< null`, which is necessary for pattern matching exhaustivity to keep working in the presence of nulls. --- #### Under explicit nulls, remove the rule Null <:< x.type. The specified rule that `Null <:< x.type` when the underlying type `U` of `x` is nullable is dubious to begin with. Under explicit nulls, it becomes decidedly out of place. We now disable that rule under `-Yexplicit-nulls`.
2 parents 6a6ee08 + 3945e96 commit 73967d5

File tree

6 files changed

+158
-2
lines changed

6 files changed

+158
-2
lines changed

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -943,16 +943,24 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
943943
// However, `null` can always be a value of `T` for Java side.
944944
// So the best solution here is to let `Null` be a subtype of non-primitive
945945
// value types temporarily.
946-
def isNullable(tp: Type): Boolean = tp.widenDealias match
946+
def isNullable(tp: Type): Boolean = tp.dealias match
947947
case tp: TypeRef =>
948948
val tpSym = tp.symbol
949949
ctx.mode.is(Mode.RelaxedOverriding) && !tpSym.isPrimitiveValueClass ||
950950
tpSym.isNullableClass
951+
case tp: TermRef =>
952+
// https://scala-lang.org/files/archive/spec/2.13/03-types.html#singleton-types
953+
// A singleton type is of the form p.type. Where p is a path pointing to a value which conforms to
954+
// scala.AnyRef [Scala 3: which scala.Null conforms to], the type denotes the set of values consisting
955+
// of null and the value denoted by p (i.e., the value v for which v eq p). [Otherwise,] the type
956+
// denotes the set consisting of only the value denoted by p.
957+
!ctx.explicitNulls && isNullable(tp.underlying) && tp.isStable
951958
case tp: RefinedOrRecType => isNullable(tp.parent)
952959
case tp: AppliedType => isNullable(tp.tycon)
953960
case AndType(tp1, tp2) => isNullable(tp1) && isNullable(tp2)
954961
case OrType(tp1, tp2) => isNullable(tp1) || isNullable(tp2)
955962
case AnnotatedType(tp1, _) => isNullable(tp1)
963+
case ConstantType(c) => c.tag == Constants.NullTag
956964
case _ => false
957965
val sym1 = tp1.symbol
958966
(sym1 eq NothingClass) && tp2.isValueTypeOrLambda ||

tests/explicit-nulls/neg/i17467.check

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
-- [E007] Type Mismatch Error: tests/explicit-nulls/neg/i17467.scala:4:22 ----------------------------------------------
2+
4 | val a2: a1.type = null // error
3+
| ^^^^
4+
| Found: Null
5+
| Required: (a1 : String)
6+
| Note that implicit conversions were not tried because the result of an implicit conversion
7+
| must be more specific than (a1 : String)
8+
|
9+
| longer explanation available when compiling with `-explain`
10+
-- [E007] Type Mismatch Error: tests/explicit-nulls/neg/i17467.scala:7:22 ----------------------------------------------
11+
7 | val b2: b1.type = null // error
12+
| ^^^^
13+
| Found: Null
14+
| Required: (b1 : String | Null)
15+
| Note that implicit conversions were not tried because the result of an implicit conversion
16+
| must be more specific than (b1 : String | Null)
17+
|
18+
| longer explanation available when compiling with `-explain`
19+
-- [E172] Type Error: tests/explicit-nulls/neg/i17467.scala:8:28 -------------------------------------------------------
20+
8 | summon[Null <:< b1.type] // error
21+
| ^
22+
| Cannot prove that Null <:< (b1 : String | Null).
23+
-- [E007] Type Mismatch Error: tests/explicit-nulls/neg/i17467.scala:14:22 ---------------------------------------------
24+
14 | val c2: c1.type = null // error
25+
| ^^^^
26+
| Found: Null
27+
| Required: (c1 : Null)
28+
| Note that implicit conversions were not tried because the result of an implicit conversion
29+
| must be more specific than (c1 : Null)
30+
|
31+
| longer explanation available when compiling with `-explain`

tests/explicit-nulls/neg/i17467.scala

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
object Test:
2+
def test(): Unit =
3+
val a1: String = "foo"
4+
val a2: a1.type = null // error
5+
6+
val b1: String | Null = "foo"
7+
val b2: b1.type = null // error
8+
summon[Null <:< b1.type] // error
9+
10+
/* The following would be sound, but it would require a specific subtyping
11+
* rule (and implementation code) for debatable value. So it is an error.
12+
*/
13+
val c1: Null = null
14+
val c2: c1.type = null // error
15+
end test
16+
end Test

tests/neg/i17467.check

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
-- [E007] Type Mismatch Error: tests/neg/i17467.scala:6:20 -------------------------------------------------------------
2+
6 | val b1: "foo" = null // error
3+
| ^^^^
4+
| Found: Null
5+
| Required: ("foo" : String)
6+
| Note that implicit conversions were not tried because the result of an implicit conversion
7+
| must be more specific than ("foo" : String)
8+
|
9+
| longer explanation available when compiling with `-explain`
10+
-- [E007] Type Mismatch Error: tests/neg/i17467.scala:9:22 -------------------------------------------------------------
11+
9 | val c2: c1.type = null // error
12+
| ^^^^
13+
| Found: Null
14+
| Required: (c1 : ("foo" : String))
15+
| Note that implicit conversions were not tried because the result of an implicit conversion
16+
| must be more specific than (c1 : ("foo" : String))
17+
|
18+
| longer explanation available when compiling with `-explain`
19+
-- [E007] Type Mismatch Error: tests/neg/i17467.scala:17:22 ------------------------------------------------------------
20+
17 | val e2: e1.type = null // error
21+
| ^^^^
22+
| Found: Null
23+
| Required: (e1 : MyNonNullable)
24+
| Note that implicit conversions were not tried because the result of an implicit conversion
25+
| must be more specific than (e1 : MyNonNullable)
26+
|
27+
| longer explanation available when compiling with `-explain`
28+
-- [E172] Type Error: tests/neg/i17467.scala:19:26 ---------------------------------------------------------------------
29+
19 | summon[Null <:< "foo"] // error
30+
| ^
31+
| Cannot prove that Null <:< ("foo" : String).
32+
-- [E007] Type Mismatch Error: tests/neg/i17467.scala:21:23 ------------------------------------------------------------
33+
21 | val f1: Mod.type = null // error
34+
| ^^^^
35+
| Found: Null
36+
| Required: Test.Mod.type
37+
| Note that implicit conversions were not tried because the result of an implicit conversion
38+
| must be more specific than Test.Mod.type
39+
|
40+
| longer explanation available when compiling with `-explain`
41+
-- [E083] Type Error: tests/neg/i17467.scala:24:12 ---------------------------------------------------------------------
42+
24 | val g2: g1.type = null // error // error
43+
| ^^^^^^^
44+
| (g1 : AnyRef) is not a valid singleton type, since it is not an immutable path
45+
|
46+
| longer explanation available when compiling with `-explain`
47+
-- [E007] Type Mismatch Error: tests/neg/i17467.scala:24:22 ------------------------------------------------------------
48+
24 | val g2: g1.type = null // error // error
49+
| ^^^^
50+
| Found: Null
51+
| Required: (g1 : AnyRef)
52+
| Note that implicit conversions were not tried because the result of an implicit conversion
53+
| must be more specific than (g1 : AnyRef)
54+
|
55+
| longer explanation available when compiling with `-explain`
56+
-- [E007] Type Mismatch Error: tests/neg/i17467.scala:33:24 ------------------------------------------------------------
57+
33 | def me: this.type = null // error
58+
| ^^^^
59+
| Found: Null
60+
| Required: (Bar.this : Test.Bar)
61+
| Note that implicit conversions were not tried because the result of an implicit conversion
62+
| must be more specific than (Bar.this : Test.Bar)
63+
|
64+
| longer explanation available when compiling with `-explain`

tests/neg/i17467.scala

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
object Test:
2+
def test(): Unit =
3+
val a1: String = "foo"
4+
val a2: a1.type = null // OK
5+
6+
val b1: "foo" = null // error
7+
8+
val c1: "foo" = "foo"
9+
val c2: c1.type = null // error
10+
11+
type MyNullable = String
12+
val d1: MyNullable = "foo"
13+
val d2: d1.type = null // OK
14+
15+
type MyNonNullable = Int
16+
val e1: MyNonNullable = 5
17+
val e2: e1.type = null // error
18+
19+
summon[Null <:< "foo"] // error
20+
21+
val f1: Mod.type = null // error
22+
23+
var g1: AnyRef = "foo"
24+
val g2: g1.type = null // error // error
25+
26+
val h1: Null = null
27+
val h2: h1.type = null
28+
end test
29+
30+
object Mod
31+
32+
class Bar:
33+
def me: this.type = null // error
34+
end Test

tests/run/t6443b.scala

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ trait A {
22
type D >: Null <: C
33
def foo(d: D)(d2: d.type): Unit
44
trait C {
5-
def bar: Unit = foo(null)(null)
5+
def bar: Unit = {
6+
val nul = null
7+
foo(nul)(nul)
8+
}
69
}
710
}
811
object B extends A {

0 commit comments

Comments
 (0)