Skip to content

Commit dd79d4d

Browse files
committed
New UnsafeNulls Implementation
1 parent 95fa9cb commit dd79d4d

File tree

140 files changed

+1791
-572
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

140 files changed

+1791
-572
lines changed

compiler/src/dotty/tools/dotc/ast/tpd.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -946,7 +946,7 @@ object tpd extends Trees.Instance[Type] with TypedTreeInfo {
946946

947947
/** The current tree applied to given type argument list: `tree[targs(0), ..., targs(targs.length - 1)]` */
948948
def appliedToTypeTrees(targs: List[Tree])(using Context): Tree =
949-
if (targs.isEmpty) tree else TypeApply(tree, targs)
949+
if targs.isEmpty then tree else TypeApply(tree, targs)
950950

951951
/** Apply to `()` unless tree's widened type is parameterless */
952952
def ensureApplied(using Context): Tree =

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

+8-28
Original file line numberDiff line numberDiff line change
@@ -452,27 +452,11 @@ class Definitions {
452452
ScalaPackageClass, tpnme.Nothing, AbstractFinal, List(AnyType))
453453
def NothingType: TypeRef = NothingClass.typeRef
454454
@tu lazy val NullClass: ClassSymbol = {
455-
val parent = if (ctx.explicitNulls) AnyType else ObjectType
455+
val parent = if ctx.explicitNulls then AnyType else ObjectType
456456
enterCompleteClassSymbol(ScalaPackageClass, tpnme.Null, AbstractFinal, parent :: Nil)
457457
}
458458
def NullType: TypeRef = NullClass.typeRef
459459

460-
/** An alias for null values that originate in Java code.
461-
* This type gets special treatment in the Typer. Specifically, `UncheckedNull` can be selected through:
462-
* e.g.
463-
* ```
464-
* // x: String|Null
465-
* x.length // error: `Null` has no `length` field
466-
* // x2: String|UncheckedNull
467-
* x2.length // allowed by the Typer, but unsound (might throw NPE)
468-
* ```
469-
*/
470-
lazy val UncheckedNullAlias: TypeSymbol = {
471-
assert(ctx.explicitNulls)
472-
enterAliasType(tpnme.UncheckedNull, NullType)
473-
}
474-
def UncheckedNullAliasType: TypeRef = UncheckedNullAlias.typeRef
475-
476460
@tu lazy val ImplicitScrutineeTypeSym =
477461
newPermanentSymbol(ScalaPackageClass, tpnme.IMPLICITkw, EmptyFlags, TypeBounds.empty).entered
478462
def ImplicitScrutineeTypeRef: TypeRef = ImplicitScrutineeTypeSym.typeRef
@@ -633,7 +617,7 @@ class Definitions {
633617
@tu lazy val StringModule: Symbol = StringClass.linkedClass
634618
@tu lazy val String_+ : TermSymbol = enterMethod(StringClass, nme.raw.PLUS, methOfAny(StringType), Final)
635619
@tu lazy val String_valueOf_Object: Symbol = StringModule.info.member(nme.valueOf).suchThat(_.info.firstParamTypes match {
636-
case List(pt) => pt.isAny || pt.isAnyRef
620+
case List(pt) => pt.isAny || pt.stripNull.isAnyRef
637621
case _ => false
638622
}).symbol
639623

@@ -645,15 +629,13 @@ class Definitions {
645629
@tu lazy val ClassCastExceptionClass: ClassSymbol = requiredClass("java.lang.ClassCastException")
646630
@tu lazy val ClassCastExceptionClass_stringConstructor: TermSymbol = ClassCastExceptionClass.info.member(nme.CONSTRUCTOR).suchThat(_.info.firstParamTypes match {
647631
case List(pt) =>
648-
val pt1 = if (ctx.explicitNulls) pt.stripNull() else pt
649-
pt1.isRef(StringClass)
632+
pt.stripNull.isRef(StringClass)
650633
case _ => false
651634
}).symbol.asTerm
652635
@tu lazy val ArithmeticExceptionClass: ClassSymbol = requiredClass("java.lang.ArithmeticException")
653636
@tu lazy val ArithmeticExceptionClass_stringConstructor: TermSymbol = ArithmeticExceptionClass.info.member(nme.CONSTRUCTOR).suchThat(_.info.firstParamTypes match {
654637
case List(pt) =>
655-
val pt1 = if (ctx.explicitNulls) pt.stripNull() else pt
656-
pt1.isRef(StringClass)
638+
pt.stripNull.isRef(StringClass)
657639
case _ => false
658640
}).symbol.asTerm
659641

@@ -1236,7 +1218,8 @@ class Definitions {
12361218
idx == name.length || name(idx).isDigit && digitsOnlyAfter(name, idx + 1)
12371219

12381220
def isBottomClass(cls: Symbol): Boolean =
1239-
if (ctx.explicitNulls && !ctx.phase.erasedTypes) cls == NothingClass
1221+
if ctx.mode.is(Mode.SafeNulls) && !ctx.phase.erasedTypes
1222+
then cls == NothingClass
12401223
else isBottomClassAfterErasure(cls)
12411224

12421225
def isBottomClassAfterErasure(cls: Symbol): Boolean = cls == NothingClass || cls == NullClass
@@ -1700,8 +1683,8 @@ class Definitions {
17001683
// ----- Initialization ---------------------------------------------------
17011684

17021685
/** Lists core classes that don't have underlying bytecode, but are synthesized on-the-fly in every reflection universe */
1703-
@tu lazy val syntheticScalaClasses: List[TypeSymbol] = {
1704-
val synth = List(
1686+
@tu lazy val syntheticScalaClasses: List[TypeSymbol] =
1687+
List(
17051688
AnyClass,
17061689
MatchableClass,
17071690
AnyRefAlias,
@@ -1715,9 +1698,6 @@ class Definitions {
17151698
NothingClass,
17161699
SingletonClass)
17171700

1718-
if (ctx.explicitNulls) synth :+ UncheckedNullAlias else synth
1719-
}
1720-
17211701
@tu lazy val syntheticCoreClasses: List[Symbol] = syntheticScalaClasses ++ List(
17221702
EmptyPackageVal,
17231703
OpsPackageClass)

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

+32-31
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,34 @@
1-
package dotty.tools.dotc.core
1+
package dotty.tools.dotc
2+
package core
23

3-
import dotty.tools.dotc.core.Contexts._
4-
import dotty.tools.dotc.core.Flags.JavaDefined
5-
import dotty.tools.dotc.core.StdNames.{jnme, nme}
6-
import dotty.tools.dotc.core.Symbols._
7-
import dotty.tools.dotc.core.Types._
4+
import config.Feature._
5+
import Contexts._
6+
import Flags.JavaDefined
87
import NullOpsDecorator._
8+
import StdNames.nme
9+
import Symbols._
10+
import Types._
911

1012
/** This module defines methods to interpret types of Java symbols, which are implicitly nullable in Java,
1113
* as Scala types, which are explicitly nullable.
1214
*
1315
* The transformation is (conceptually) a function `n` that adheres to the following rules:
14-
* (1) n(T) = T|UncheckedNull if T is a reference type
16+
* (1) n(T) = T | Null if T is a reference type
1517
* (2) n(T) = T if T is a value type
16-
* (3) n(C[T]) = C[T]|UncheckedNull if C is Java-defined
17-
* (4) n(C[T]) = C[n(T)]|UncheckedNull if C is Scala-defined
18-
* (5) n(A|B) = n(A)|n(B)|UncheckedNull
18+
* (3) n(C[T]) = C[T] | Null if C is Java-defined
19+
* (4) n(C[T]) = C[n(T)] | Null if C is Scala-defined
20+
* (5) n(A|B) = n(A) | n(B) | Null
1921
* (6) n(A&B) = n(A) & n(B)
2022
* (7) n((A1, ..., Am)R) = (n(A1), ..., n(Am))n(R) for a method with arguments (A1, ..., Am) and return type R
2123
* (8) n(T) = T otherwise
2224
*
2325
* Treatment of generics (rules 3 and 4):
24-
* - if `C` is Java-defined, then `n(C[T]) = C[T]|UncheckedNull`. That is, we don't recurse
25-
* on the type argument, and only add UncheckedNull on the outside. This is because
26+
* - if `C` is Java-defined, then `n(C[T]) = C[T] | Null`. That is, we don't recurse
27+
* on the type argument, and only add Null on the outside. This is because
2628
* `C` itself will be nullified, and in particular so will be usages of `C`'s type argument within C's body.
2729
* e.g. calling `get` on a `java.util.List[String]` already returns `String|Null` and not `String`, so
28-
* we don't need to write `java.util.List[String|Null]`.
29-
* - if `C` is Scala-defined, however, then we want `n(C[T]) = C[n(T)]|UncheckedNull`. This is because
30+
* we don't need to write `java.util.List[String | Null]`.
31+
* - if `C` is Scala-defined, however, then we want `n(C[T]) = C[n(T)] | Null`. This is because
3032
* `C` won't be nullified, so we need to indicate that its type argument is nullable.
3133
*
3234
* Notice that since the transformation is only applied to types attached to Java symbols, it doesn't need
@@ -43,10 +45,9 @@ object JavaNullInterop {
4345
*
4446
* After calling `nullifyMember`, Scala will see the method as
4547
*
46-
* def foo(arg: String|UncheckedNull): String|UncheckedNull
48+
* def foo(arg: String | Null): String | Null
4749
*
48-
* This nullability function uses `UncheckedNull` instead of vanilla `Null`, for usability.
49-
* This means that we can select on the return of `foo`:
50+
* If unsafeNulls is enabled, we can select on the return of `foo`:
5051
*
5152
* val len = foo("hello").length
5253
*
@@ -57,10 +58,10 @@ object JavaNullInterop {
5758
assert(sym.is(JavaDefined), "can only nullify java-defined members")
5859

5960
// Some special cases when nullifying the type
60-
if (isEnumValueDef || sym.name == nme.TYPE_)
61+
if isEnumValueDef || sym.name == nme.TYPE_ then
6162
// Don't nullify the `TYPE` field in every class and Java enum instances
6263
tp
63-
else if (sym.name == nme.toString_ || sym.isConstructor || hasNotNullAnnot(sym))
64+
else if sym.name == nme.toString_ || sym.isConstructor || hasNotNullAnnot(sym) then
6465
// Don't nullify the return type of the `toString` method.
6566
// Don't nullify the return type of constructors.
6667
// Don't nullify the return type of methods with a not-null annotation.
@@ -81,20 +82,20 @@ object JavaNullInterop {
8182
private def nullifyExceptReturnType(tp: Type)(using Context): Type =
8283
new JavaNullMap(true)(tp)
8384

84-
/** Nullifies a Java type by adding `| UncheckedNull` in the relevant places. */
85+
/** Nullifies a Java type by adding `| Null` in the relevant places. */
8586
private def nullifyType(tp: Type)(using Context): Type =
8687
new JavaNullMap(false)(tp)
8788

88-
/** A type map that implements the nullification function on types. Given a Java-sourced type, this adds `| UncheckedNull`
89+
/** A type map that implements the nullification function on types. Given a Java-sourced type, this adds `| Null`
8990
* in the right places to make the nulls explicit in Scala.
9091
*
9192
* @param outermostLevelAlreadyNullable whether this type is already nullable at the outermost level.
92-
* For example, `Array[String]|UncheckedNull` is already nullable at the
93-
* outermost level, but `Array[String|UncheckedNull]` isn't.
93+
* For example, `Array[String] | Null` is already nullable at the
94+
* outermost level, but `Array[String | Null]` isn't.
9495
* If this parameter is set to true, then the types of fields, and the return
9596
* types of methods will not be nullified.
9697
* This is useful for e.g. constructors, and also so that `A & B` is nullified
97-
* to `(A & B) | UncheckedNull`, instead of `(A|UncheckedNull & B|UncheckedNull) | UncheckedNull`.
98+
* to `(A & B) | Null`, instead of `(A | Null & B | Null) | Null`.
9899
*/
99100
private class JavaNullMap(var outermostLevelAlreadyNullable: Boolean)(using Context) extends TypeMap {
100101
/** Should we nullify `tp` at the outermost level? */
@@ -107,15 +108,15 @@ object JavaNullInterop {
107108
!tp.isRef(defn.AnyClass) &&
108109
// We don't nullify Java varargs at the top level.
109110
// Example: if `setNames` is a Java method with signature `void setNames(String... names)`,
110-
// then its Scala signature will be `def setNames(names: (String|UncheckedNull)*): Unit`.
111+
// then its Scala signature will be `def setNames(names: (String|Null)*): Unit`.
111112
// This is because `setNames(null)` passes as argument a single-element array containing the value `null`,
112113
// and not a `null` array.
113114
!tp.isRef(defn.RepeatedParamClass)
114115
case _ => true
115116
})
116117

117118
override def apply(tp: Type): Type = tp match {
118-
case tp: TypeRef if needsNull(tp) => OrUncheckedNull(tp)
119+
case tp: TypeRef if needsNull(tp) => OrNull(tp)
119120
case appTp @ AppliedType(tycon, targs) =>
120121
val oldOutermostNullable = outermostLevelAlreadyNullable
121122
// We don't make the outmost levels of type arguments nullable if tycon is Java-defined.
@@ -125,7 +126,7 @@ object JavaNullInterop {
125126
val targs2 = targs map this
126127
outermostLevelAlreadyNullable = oldOutermostNullable
127128
val appTp2 = derivedAppliedType(appTp, tycon, targs2)
128-
if (needsNull(tycon)) OrUncheckedNull(appTp2) else appTp2
129+
if needsNull(tycon) then OrNull(appTp2) else appTp2
129130
case ptp: PolyType =>
130131
derivedLambdaType(ptp)(ptp.paramInfos, this(ptp.resType))
131132
case mtp: MethodType =>
@@ -136,11 +137,11 @@ object JavaNullInterop {
136137
derivedLambdaType(mtp)(paramInfos2, this(mtp.resType))
137138
case tp: TypeAlias => mapOver(tp)
138139
case tp: AndType =>
139-
// nullify(A & B) = (nullify(A) & nullify(B)) | UncheckedNull, but take care not to add
140-
// duplicate `UncheckedNull`s at the outermost level inside `A` and `B`.
140+
// nullify(A & B) = (nullify(A) & nullify(B)) | Null, but take care not to add
141+
// duplicate `Null`s at the outermost level inside `A` and `B`.
141142
outermostLevelAlreadyNullable = true
142-
OrUncheckedNull(derivedAndType(tp, this(tp.tp1), this(tp.tp2)))
143-
case tp: TypeParamRef if needsNull(tp) => OrUncheckedNull(tp)
143+
OrNull(derivedAndType(tp, this(tp.tp1), this(tp.tp2)))
144+
case tp: TypeParamRef if needsNull(tp) => OrNull(tp)
144145
// In all other cases, return the type unchanged.
145146
// In particular, if the type is a ConstantType, then we don't nullify it because it is the
146147
// type of a final non-nullable field.

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ object Mode {
120120
/** Are we resolving a TypeTest node? */
121121
val InTypeTest: Mode = newMode(27, "InTypeTest")
122122

123-
/** Are we enforcing null safety */
123+
/** Are we enforcing null safety? */
124124
val SafeNulls = newMode(28, "SafeNulls")
125125

126126
/** We are typing the body of the condition of an `inline if` or the scrutinee of an `inline match`
Original file line numberDiff line numberDiff line change
@@ -1,86 +1,62 @@
1-
package dotty.tools.dotc.core
1+
package dotty.tools.dotc
2+
package core
23

3-
import dotty.tools.dotc.core.Contexts._
4-
import dotty.tools.dotc.core.Symbols.defn
5-
import dotty.tools.dotc.core.Types._
4+
import ast.Trees._
5+
import Contexts._
6+
import Symbols.defn
7+
import Types._
68

7-
/** Defines operations on nullable types. */
8-
object NullOpsDecorator {
9-
10-
extension (self: Type) {
11-
/** Is this type exactly `UncheckedNull` (no vars, aliases, refinements etc allowed)? */
12-
def isUncheckedNullType(using Context): Boolean = {
13-
assert(ctx.explicitNulls)
14-
// We can't do `self == defn.UncheckedNull` because when trees are unpickled new references
15-
// to `UncheckedNull` could be created that are different from `defn.UncheckedNull`.
16-
// Instead, we compare the symbol.
17-
self.isDirectRef(defn.UncheckedNullAlias)
18-
}
9+
/** Defines operations on nullable types and tree. */
10+
object NullOpsDecorator:
1911

12+
extension (self: Type)
2013
/** Syntactically strips the nullability from this type.
21-
* If the type is `T1 | ... | Tn`, and `Ti` references to `Null` (or `UncheckedNull`),
14+
* If the type is `T1 | ... | Tn`, and `Ti` references to `Null`,
2215
* then return `T1 | ... | Ti-1 | Ti+1 | ... | Tn`.
2316
* If this type isn't (syntactically) nullable, then returns the type unchanged.
24-
*
25-
* @param onlyUncheckedNull whether we only remove `UncheckedNull`, the default value is false
17+
* The type will not be changed if explicit-nulls is not enabled.
2618
*/
27-
def stripNull(onlyUncheckedNull: Boolean = false)(using Context): Type = {
28-
assert(ctx.explicitNulls)
29-
30-
def isNull(tp: Type) =
31-
if (onlyUncheckedNull) tp.isUncheckedNullType
32-
else tp.isNullType
33-
34-
def strip(tp: Type): Type = tp match {
35-
case tp @ OrType(lhs, rhs) =>
36-
val llhs = strip(lhs)
37-
val rrhs = strip(rhs)
38-
if (isNull(rrhs)) llhs
39-
else if (isNull(llhs)) rrhs
40-
else tp.derivedOrType(llhs, rrhs)
41-
case tp @ AndType(tp1, tp2) =>
42-
// We cannot `tp.derivedAndType(strip(tp1), strip(tp2))` directly,
43-
// since `stripNull((A | Null) & B)` would produce the wrong
44-
// result `(A & B) | Null`.
45-
val tp1s = strip(tp1)
46-
val tp2s = strip(tp2)
47-
if((tp1s ne tp1) && (tp2s ne tp2))
48-
tp.derivedAndType(tp1s, tp2s)
49-
else tp
50-
case _ => tp
51-
}
52-
53-
val self1 = self.widenDealias
54-
val stripped = strip(self1)
55-
if (stripped ne self1) stripped else self
56-
}
57-
58-
/** Like `stripNull`, but removes only the `UncheckedNull`s. */
59-
def stripUncheckedNull(using Context): Type = self.stripNull(true)
60-
61-
/** Collapses all `UncheckedNull` unions within this type, and not just the outermost ones (as `stripUncheckedNull` does).
62-
* e.g. (Array[String|UncheckedNull]|UncheckedNull).stripUncheckedNull => Array[String|UncheckedNull]
63-
* (Array[String|UncheckedNull]|UncheckedNull).stripAllUncheckedNull => Array[String]
64-
* If no `UncheckedNull` unions are found within the type, then returns the input type unchanged.
65-
*/
66-
def stripAllUncheckedNull(using Context): Type = {
67-
object RemoveNulls extends TypeMap {
68-
override def apply(tp: Type): Type = mapOver(tp.stripNull(true))
69-
}
70-
val rem = RemoveNulls(self)
71-
if (rem ne self) rem else self
19+
def stripNull(using Context): Type = {
20+
def strip(tp: Type): Type =
21+
val tpWiden = tp.widenDealias
22+
val tpStripped = tpWiden match {
23+
case tp @ OrType(lhs, rhs) =>
24+
val llhs = strip(lhs)
25+
val rrhs = strip(rhs)
26+
if rrhs.isNullType then llhs
27+
else if llhs.isNullType then rrhs
28+
else tp.derivedOrType(llhs, rrhs)
29+
case tp @ AndType(tp1, tp2) =>
30+
// We cannot `tp.derivedAndType(strip(tp1), strip(tp2))` directly,
31+
// since `stripNull((A | Null) & B)` would produce the wrong
32+
// result `(A & B) | Null`.
33+
val tp1s = strip(tp1)
34+
val tp2s = strip(tp2)
35+
if (tp1s ne tp1) && (tp2s ne tp2) then
36+
tp.derivedAndType(tp1s, tp2s)
37+
else tp
38+
case tp @ TypeBounds(lo, hi) =>
39+
tp.derivedTypeBounds(strip(lo), strip(hi))
40+
case tp => tp
41+
}
42+
if tpStripped ne tpWiden then tpStripped else tp
43+
44+
if ctx.explicitNulls then strip(self) else self
7245
}
7346

7447
/** Is self (after widening and dealiasing) a type of the form `T | Null`? */
7548
def isNullableUnion(using Context): Boolean = {
76-
val stripped = self.stripNull()
49+
val stripped = self.stripNull
7750
stripped ne self
7851
}
52+
end extension
7953

80-
/** Is self (after widening and dealiasing) a type of the form `T | UncheckedNull`? */
81-
def isUncheckedNullableUnion(using Context): Boolean = {
82-
val stripped = self.stripNull(true)
83-
stripped ne self
54+
import ast.tpd._
55+
56+
extension (self: Tree)
57+
// cast the type of the tree to a non-nullable type
58+
def castToNonNullable(using Context): Tree = self.typeOpt match {
59+
case OrNull(tp) => self.cast(tp)
60+
case _ => self
8461
}
85-
}
86-
}
62+
end NullOpsDecorator

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

-1
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,6 @@ object StdNames {
199199
final val Nothing: N = "Nothing"
200200
final val NotNull: N = "NotNull"
201201
final val Null: N = "Null"
202-
final val UncheckedNull: N = "UncheckedNull"
203202
final val Object: N = "Object"
204203
final val FromJavaObject: N = "<FromJavaObject>"
205204
final val Product: N = "Product"

0 commit comments

Comments
 (0)