Skip to content

Commit 72acaef

Browse files
authored
Merge pull request #13975 from noti0na1/explicit-nulls-more-relaxed-override
Fix override containing type param in explicit nulls
2 parents 5061804 + e78f449 commit 72acaef

File tree

9 files changed

+99
-45
lines changed

9 files changed

+99
-45
lines changed

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -712,6 +712,9 @@ object Contexts {
712712

713713
def withNotNullInfos(infos: List[NotNullInfo]): Context =
714714
if c.notNullInfos eq infos then c else c.fresh.setNotNullInfos(infos)
715+
716+
def relaxedOverrideContext: Context =
717+
c.withModeBits(c.mode &~ Mode.SafeNulls | Mode.RelaxedOverriding)
715718
end ops
716719

717720
// TODO: Fix issue when converting ModeChanges and FreshModeChanges to extension givens

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,4 +124,9 @@ object Mode {
124124
* This mode forces expansion of inline calls in those positions even during typing.
125125
*/
126126
val ForceInline: Mode = newMode(29, "ForceInline")
127+
128+
/** This mode is enabled when we check Java overriding in explicit nulls.
129+
* Type `Null` becomes a subtype of non-primitive value types in TypeComparer.
130+
*/
131+
val RelaxedOverriding: Mode = newMode(30, "RelaxedOverriding")
127132
}

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

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -766,14 +766,24 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
766766

767767
isSubType(hi1, tp2, approx.addLow) || compareGADT || tryLiftedToThis1
768768
case _ =>
769-
def isNullable(tp: Type): Boolean = tp.widenDealias match {
770-
case tp: TypeRef => tp.symbol.isNullableClass
769+
// `Mode.RelaxedOverriding` is only enabled when checking Java overriding
770+
// in explicit nulls, and `Null` becomes a bottom type, which allows
771+
// `T | Null` being a subtype of `T`.
772+
// A type variable `T` from Java is translated to `T >: Nothing <: Any`.
773+
// However, `null` can always be a value of `T` for Java side.
774+
// So the best solution here is to let `Null` be a subtype of non-primitive
775+
// value types temporarily.
776+
def isNullable(tp: Type): Boolean = tp.widenDealias match
777+
case tp: TypeRef =>
778+
val tpSym = tp.symbol
779+
ctx.mode.is(Mode.RelaxedOverriding) && !tpSym.isPrimitiveValueClass ||
780+
tpSym.isNullableClass
771781
case tp: RefinedOrRecType => isNullable(tp.parent)
772782
case tp: AppliedType => isNullable(tp.tycon)
773783
case AndType(tp1, tp2) => isNullable(tp1) && isNullable(tp2)
774784
case OrType(tp1, tp2) => isNullable(tp1) || isNullable(tp2)
775785
case _ => false
776-
}
786+
777787
val sym1 = tp1.symbol
778788
(sym1 eq NothingClass) && tp2.isValueTypeOrLambda ||
779789
(sym1 eq NullClass) && isNullable(tp2)

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1112,8 +1112,8 @@ object Types {
11121112
*/
11131113
def matches(that: Type)(using Context): Boolean = {
11141114
record("matches")
1115-
withoutMode(Mode.SafeNulls)(
1116-
TypeComparer.matchesType(this, that, relaxed = !ctx.phase.erasedTypes))
1115+
val overrideCtx = if ctx.explicitNulls then ctx.relaxedOverrideContext else ctx
1116+
TypeComparer.matchesType(this, that, relaxed = !ctx.phase.erasedTypes)(using overrideCtx)
11171117
}
11181118

11191119
/** This is the same as `matches` except that it also matches => T with T and

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

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package transform
55
import core._
66
import Flags._, Symbols._, Contexts._, Scopes._, Decorators._, Types.Type
77
import NameKinds.DefaultGetterName
8+
import NullOpsDecorator._
89
import collection.mutable
910
import collection.immutable.BitSet
1011
import scala.annotation.tailrec
@@ -216,14 +217,12 @@ object OverridingPairs:
216217
)
217218
else
218219
// releaxed override check for explicit nulls if one of the symbols is Java defined,
219-
// force `Null` being a subtype of reference types during override checking
220-
val relaxedCtxForNulls =
221-
if ctx.explicitNulls && (member.is(JavaDefined) || other.is(JavaDefined)) then
222-
ctx.retractMode(Mode.SafeNulls)
223-
else ctx
220+
// force `Null` to be a subtype of non-primitive value types during override checking.
221+
val overrideCtx = if ctx.explicitNulls && (member.is(JavaDefined) || other.is(JavaDefined))
222+
then ctx.relaxedOverrideContext else ctx
224223
member.name.is(DefaultGetterName) // default getters are not checked for compatibility
225224
|| memberTp.overrides(otherTp,
226225
member.matchNullaryLoosely || other.matchNullaryLoosely || fallBack
227-
)(using relaxedCtxForNulls)
226+
)(using overrideCtx)
228227

229228
end OverridingPairs

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

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import Names._
1313
import StdNames._
1414
import NameOps._
1515
import NameKinds._
16+
import NullOpsDecorator._
1617
import ResolveSuper._
1718
import reporting.IllegalSuperAccessor
1819

@@ -111,15 +112,12 @@ object ResolveSuper {
111112
val otherTp = other.asSeenFrom(base.typeRef).info
112113
val accTp = acc.asSeenFrom(base.typeRef).info
113114
// Since the super class can be Java defined,
114-
// we use releaxed overriding check for explicit nulls if one of the symbols is Java defined.
115-
// This forces `Null` being a subtype of reference types during override checking.
116-
val relaxedCtxForNulls =
117-
if ctx.explicitNulls && (sym.is(JavaDefined) || acc.is(JavaDefined)) then
118-
ctx.retractMode(Mode.SafeNulls)
119-
else ctx
120-
if (!(otherTp.overrides(accTp, matchLoosely = true)(using relaxedCtxForNulls)))
115+
// we use relaxed overriding check for explicit nulls if one of the symbols is Java defined.
116+
// This forces `Null` to be a subtype of non-primitive value types during override checking.
117+
val overrideCtx = if ctx.explicitNulls && (sym.is(JavaDefined) || acc.is(JavaDefined))
118+
then ctx.relaxedOverrideContext else ctx
119+
if !otherTp.overrides(accTp, matchLoosely = true)(using overrideCtx) then
121120
report.error(IllegalSuperAccessor(base, memberName, targetName, acc, accTp, other.symbol, otherTp), base.srcPos)
122-
123121
bcs = bcs.tail
124122
}
125123
assert(sym.exists, i"cannot rebind $acc, ${acc.targetName} $memberName")
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// Unboxed option type using unions + null + opaque.
2+
// Relies on the fact that Null is not a subtype of AnyRef.
3+
// Test suggested by Sébastien Doeraene.
4+
5+
object Nullables {
6+
opaque type Nullable[+A <: AnyRef] = A | Null // disjoint by construction!
7+
8+
object Nullable:
9+
def apply[A <: AnyRef](x: A | Null): Nullable[A] = x
10+
11+
def some[A <: AnyRef](x: A): Nullable[A] = x
12+
def none: Nullable[Nothing] = null
13+
14+
extension [A <: AnyRef](x: Nullable[A])
15+
def isEmpty: Boolean = x == null
16+
def get: A | Null = x
17+
18+
extension [A <: AnyRef, B <: AnyRef](x: Nullable[A])
19+
def flatMap(f: A => Nullable[B]): Nullable[B] =
20+
if (x == null) null
21+
else f(x)
22+
23+
def map(f: A => B): Nullable[B] = x.flatMap(f)
24+
25+
def test1 =
26+
val s1: Nullable[String] = Nullable("hello")
27+
val s2: Nullable[String] = "world"
28+
val s3: Nullable[String] = Nullable.none
29+
val s4: Nullable[String] = null
30+
31+
s1.isEmpty
32+
s1.flatMap((x) => true)
33+
34+
assert(s2 != null)
35+
}
36+
37+
def test2 =
38+
import Nullables._
39+
40+
val s1: Nullable[String] = Nullable("hello")
41+
val s2: Nullable[String] = Nullable.none
42+
val s3: Nullable[String] = null // error: don't leak nullable union
43+
44+
s1.isEmpty
45+
s1.flatMap((x) => Nullable(true))
46+
47+
assert(s2 == null) // error

tests/explicit-nulls/pos/opaque-nullable.scala

Lines changed: 0 additions & 26 deletions
This file was deleted.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Testing relaxed overriding check for explicit nulls.
2+
// The relaxed check is only enabled if one of the members is Java defined.
3+
4+
import java.util.Comparator
5+
6+
class C1[T <: AnyRef] extends Ordering[T]:
7+
override def compare(o1: T, o2: T): Int = 0
8+
9+
// The following overriding is not allowed, because `compare`
10+
// has already been declared in Scala class `Ordering`.
11+
// class C2[T <: AnyRef] extends Ordering[T]:
12+
// override def compare(o1: T | Null, o2: T | Null): Int = 0
13+
14+
class D1[T <: AnyRef] extends Comparator[T]:
15+
override def compare(o1: T, o2: T): Int = 0
16+
17+
class D2[T <: AnyRef] extends Comparator[T]:
18+
override def compare(o1: T | Null, o2: T | Null): Int = 0

0 commit comments

Comments
 (0)