Skip to content

Commit

Permalink
Fix #17467: Limit isNullable widening to stable TermRefs.
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
sjrd committed May 11, 2023
1 parent d103f8c commit 88b4949
Show file tree
Hide file tree
Showing 3 changed files with 44 additions and 2 deletions.
10 changes: 9 additions & 1 deletion compiler/src/dotty/tools/dotc/core/TypeComparer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -901,16 +901,24 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
// However, `null` can always be a value of `T` for Java side.
// So the best solution here is to let `Null` be a subtype of non-primitive
// value types temporarily.
def isNullable(tp: Type): Boolean = tp.widenDealias match
def isNullable(tp: Type): Boolean = tp.dealias match
case tp: TypeRef =>
val tpSym = tp.symbol
ctx.mode.is(Mode.RelaxedOverriding) && !tpSym.isPrimitiveValueClass ||
tpSym.isNullableClass
case tp: TermRef =>
// https://scala-lang.org/files/archive/spec/2.13/03-types.html#singleton-types
// A singleton type is of the form p.type. Where p is a path pointing to a value which conforms to
// scala.AnyRef [Scala 3: which scala.Null conforms to], the type denotes the set of values consisting
// of null and the value denoted by p (i.e., the value v for which v eq p). [Otherwise,] the type
// denotes the set consisting of only the value denoted by p.
isNullable(tp.underlying) && tp.isStable
case tp: RefinedOrRecType => isNullable(tp.parent)
case tp: AppliedType => isNullable(tp.tycon)
case AndType(tp1, tp2) => isNullable(tp1) && isNullable(tp2)
case OrType(tp1, tp2) => isNullable(tp1) || isNullable(tp2)
case AnnotatedType(tp1, _) => isNullable(tp1)
case ConstantType(c) => c.tag == Constants.NullTag
case _ => false
val sym1 = tp1.symbol
(sym1 eq NothingClass) && tp2.isValueTypeOrLambda ||
Expand Down
31 changes: 31 additions & 0 deletions tests/neg/i17467.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
object Test:
def test(): Unit =
val a1: String = "foo"
val a2: a1.type = null // OK

val b1: "foo" = null // error

val c1: "foo" = "foo"
val c2: c1.type = null // error

type MyNullable = String
val d1: MyNullable = "foo"
val d2: d1.type = null // OK

type MyNonNullable = Int
val e1: MyNonNullable = 5
val e2: e1.type = null // error

summon[Null <:< "foo"] // error

val f1: Mod.type = null // error

var g1: AnyRef = "foo"
val g2: g1.type = null // error // error
end test

object Mod

class Bar:
def me: this.type = null // error
end Test
5 changes: 4 additions & 1 deletion tests/run/t6443b.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ trait A {
type D >: Null <: C
def foo(d: D)(d2: d.type): Unit
trait C {
def bar: Unit = foo(null)(null)
def bar: Unit = {
val nul = null
foo(nul)(nul)
}
}
}
object B extends A {
Expand Down

0 comments on commit 88b4949

Please sign in to comment.