Skip to content

Commit 7da8e48

Browse files
committed
Initial UnsafeNulls PR
1 parent a3543f2 commit 7da8e48

File tree

108 files changed

+1287
-341
lines changed

Some content is hidden

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

108 files changed

+1287
-341
lines changed

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

Lines changed: 4 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -361,22 +361,6 @@ class Definitions {
361361
}
362362
def NullType: TypeRef = NullClass.typeRef
363363

364-
/** An alias for null values that originate in Java code.
365-
* This type gets special treatment in the Typer. Specifically, `UncheckedNull` can be selected through:
366-
* e.g.
367-
* ```
368-
* // x: String|Null
369-
* x.length // error: `Null` has no `length` field
370-
* // x2: String|UncheckedNull
371-
* x2.length // allowed by the Typer, but unsound (might throw NPE)
372-
* ```
373-
*/
374-
lazy val UncheckedNullAlias: TypeSymbol = {
375-
assert(ctx.explicitNulls)
376-
enterAliasType(tpnme.UncheckedNull, NullType)
377-
}
378-
def UncheckedNullAliasType: TypeRef = UncheckedNullAlias.typeRef
379-
380364
@tu lazy val ImplicitScrutineeTypeSym =
381365
newSymbol(ScalaPackageClass, tpnme.IMPLICITkw, EmptyFlags, TypeBounds.empty).entered
382366
def ImplicitScrutineeTypeRef: TypeRef = ImplicitScrutineeTypeSym.typeRef
@@ -548,14 +532,14 @@ class Definitions {
548532
@tu lazy val ClassCastExceptionClass: ClassSymbol = ctx.requiredClass("java.lang.ClassCastException")
549533
@tu lazy val ClassCastExceptionClass_stringConstructor: TermSymbol = ClassCastExceptionClass.info.member(nme.CONSTRUCTOR).suchThat(_.info.firstParamTypes match {
550534
case List(pt) =>
551-
val pt1 = if (ctx.explicitNulls) pt.stripNull() else pt
535+
val pt1 = if (ctx.explicitNulls) pt.stripNull else pt
552536
pt1.isRef(StringClass)
553537
case _ => false
554538
}).symbol.asTerm
555539
@tu lazy val ArithmeticExceptionClass: ClassSymbol = ctx.requiredClass("java.lang.ArithmeticException")
556540
@tu lazy val ArithmeticExceptionClass_stringConstructor: TermSymbol = ArithmeticExceptionClass.info.member(nme.CONSTRUCTOR).suchThat(_.info.firstParamTypes match {
557541
case List(pt) =>
558-
val pt1 = if (ctx.explicitNulls) pt.stripNull() else pt
542+
val pt1 = if (ctx.explicitNulls) pt.stripNull else pt
559543
pt1.isRef(StringClass)
560544
case _ => false
561545
}).symbol.asTerm
@@ -1444,8 +1428,8 @@ class Definitions {
14441428
// ----- Initialization ---------------------------------------------------
14451429

14461430
/** Lists core classes that don't have underlying bytecode, but are synthesized on-the-fly in every reflection universe */
1447-
@tu lazy val syntheticScalaClasses: List[TypeSymbol] = {
1448-
val synth = List(
1431+
@tu lazy val syntheticScalaClasses: List[TypeSymbol] =
1432+
List(
14491433
AnyClass,
14501434
AnyRefAlias,
14511435
AnyKindClass,
@@ -1458,9 +1442,6 @@ class Definitions {
14581442
NothingClass,
14591443
SingletonClass)
14601444

1461-
if (ctx.explicitNulls) synth :+ UncheckedNullAlias else synth
1462-
}
1463-
14641445
@tu lazy val syntheticCoreClasses: List[Symbol] = syntheticScalaClasses ++ List(
14651446
EmptyPackageVal,
14661447
OpsPackageClass)

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

Lines changed: 22 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -11,22 +11,22 @@ import NullOpsDecorator._
1111
* as Scala types, which are explicitly nullable.
1212
*
1313
* 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
14+
* (1) n(T) = T | Null if T is a reference type
1515
* (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
16+
* (3) n(C[T]) = C[T] | Null if C is Java-defined
17+
* (4) n(C[T]) = C[n(T)] | Null if C is Scala-defined
18+
* (5) n(A|B) = n(A) | n(B) | Null
1919
* (6) n(A&B) = n(A) & n(B)
2020
* (7) n((A1, ..., Am)R) = (n(A1), ..., n(Am))n(R) for a method with arguments (A1, ..., Am) and return type R
2121
* (8) n(T) = T otherwise
2222
*
2323
* 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
24+
* - if `C` is Java-defined, then `n(C[T]) = C[T] | Null`. That is, we don't recurse
25+
* on the type argument, and only add Null on the outside. This is because
2626
* `C` itself will be nullified, and in particular so will be usages of `C`'s type argument within C's body.
2727
* 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
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)] | Null`. This is because
3030
* `C` won't be nullified, so we need to indicate that its type argument is nullable.
3131
*
3232
* Notice that since the transformation is only applied to types attached to Java symbols, it doesn't need
@@ -43,10 +43,9 @@ object JavaNullInterop {
4343
*
4444
* After calling `nullifyMember`, Scala will see the method as
4545
*
46-
* def foo(arg: String|UncheckedNull): String|UncheckedNull
46+
* def foo(arg: String | Null): String | Null
4747
*
48-
* This nullability function uses `UncheckedNull` instead of vanilla `Null`, for usability.
49-
* This means that we can select on the return of `foo`:
48+
* If unsafeNulls is enabled, we can select on the return of `foo`:
5049
*
5150
* val len = foo("hello").length
5251
*
@@ -81,20 +80,20 @@ object JavaNullInterop {
8180
private def nullifyExceptReturnType(tp: Type)(implicit ctx: Context): Type =
8281
new JavaNullMap(true)(ctx)(tp)
8382

84-
/** Nullifies a Java type by adding `| UncheckedNull` in the relevant places. */
83+
/** Nullifies a Java type by adding `| Null` in the relevant places. */
8584
private def nullifyType(tp: Type)(implicit ctx: Context): Type =
8685
new JavaNullMap(false)(ctx)(tp)
8786

88-
/** A type map that implements the nullification function on types. Given a Java-sourced type, this adds `| UncheckedNull`
87+
/** A type map that implements the nullification function on types. Given a Java-sourced type, this adds `| Null`
8988
* in the right places to make the nulls explicit in Scala.
9089
*
9190
* @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.
91+
* For example, `Array[String] | Null` is already nullable at the
92+
* outermost level, but `Array[String | Null]` isn't.
9493
* If this parameter is set to true, then the types of fields, and the return
9594
* types of methods will not be nullified.
9695
* 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`.
96+
* to `(A & B) | Null`, instead of `(A | Null & B | Null) | Null`.
9897
*/
9998
private class JavaNullMap(var outermostLevelAlreadyNullable: Boolean)(implicit ctx: Context) extends TypeMap {
10099
/** Should we nullify `tp` at the outermost level? */
@@ -107,15 +106,15 @@ object JavaNullInterop {
107106
!tp.isRef(defn.AnyClass) &&
108107
// We don't nullify Java varargs at the top level.
109108
// 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`.
109+
// then its Scala signature will be `def setNames(names: (String|Null)*): Unit`.
111110
// This is because `setNames(null)` passes as argument a single-element array containing the value `null`,
112111
// and not a `null` array.
113112
!tp.isRef(defn.RepeatedParamClass)
114113
case _ => true
115114
})
116115

117116
override def apply(tp: Type): Type = tp match {
118-
case tp: TypeRef if needsNull(tp) => OrUncheckedNull(tp)
117+
case tp: TypeRef if needsNull(tp) => OrNull(tp)
119118
case appTp @ AppliedType(tycon, targs) =>
120119
val oldOutermostNullable = outermostLevelAlreadyNullable
121120
// We don't make the outmost levels of type arguments nullable if tycon is Java-defined.
@@ -125,7 +124,7 @@ object JavaNullInterop {
125124
val targs2 = targs map this
126125
outermostLevelAlreadyNullable = oldOutermostNullable
127126
val appTp2 = derivedAppliedType(appTp, tycon, targs2)
128-
if (needsNull(tycon)) OrUncheckedNull(appTp2) else appTp2
127+
if (needsNull(tycon)) OrNull(appTp2) else appTp2
129128
case ptp: PolyType =>
130129
derivedLambdaType(ptp)(ptp.paramInfos, this(ptp.resType))
131130
case mtp: MethodType =>
@@ -136,11 +135,11 @@ object JavaNullInterop {
136135
derivedLambdaType(mtp)(paramInfos2, this(mtp.resType))
137136
case tp: TypeAlias => mapOver(tp)
138137
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`.
138+
// nullify(A & B) = (nullify(A) & nullify(B)) | Null, but take care not to add
139+
// duplicate `Null`s at the outermost level inside `A` and `B`.
141140
outermostLevelAlreadyNullable = true
142-
OrUncheckedNull(derivedAndType(tp, this(tp.tp1), this(tp.tp2)))
143-
case tp: TypeParamRef if needsNull(tp) => OrUncheckedNull(tp)
141+
OrNull(derivedAndType(tp, this(tp.tp1), this(tp.tp2)))
142+
case tp: TypeParamRef if needsNull(tp) => OrNull(tp)
144143
// In all other cases, return the type unchanged.
145144
// In particular, if the type is a ConstantType, then we don't nullify it because it is the
146145
// type of a final non-nullable field.

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,4 +110,6 @@ object Mode {
110110

111111
/** Are we in a quote in a pattern? */
112112
val QuotedPattern: Mode = newMode(25, "QuotedPattern")
113+
114+
val UnsafeNullConversion: Mode = newMode(26, "UnsafeNullConversion")
113115
}
Lines changed: 50 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,86 +1,77 @@
11
package dotty.tools.dotc.core
22

3-
import dotty.tools.dotc.core.Contexts.Context
4-
import dotty.tools.dotc.core.Symbols.defn
5-
import dotty.tools.dotc.core.Types._
3+
import Contexts.Context
4+
import Symbols.defn
5+
import Types._
66

77
/** Defines operations on nullable types. */
88
object NullOpsDecorator {
99

1010
implicit class NullOps(val self: Type) {
11-
/** Is this type exactly `UncheckedNull` (no vars, aliases, refinements etc allowed)? */
12-
def isUncheckedNullType(implicit ctx: 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-
}
1911

2012
/** Syntactically strips the nullability from this type.
21-
* If the type is `T1 | ... | Tn`, and `Ti` references to `Null` (or `UncheckedNull`),
13+
* If the type is `T1 | ... | Tn`, and `Ti` references to `Null`,
2214
* then return `T1 | ... | Ti-1 | Ti+1 | ... | Tn`.
2315
* 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
2616
*/
27-
def stripNull(onlyUncheckedNull: Boolean = false)(implicit ctx: 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-
}
17+
def stripNull(using Context): Type = {
18+
def strip(tp: Type): Type =
19+
val tpWiden = tp.widenDealias
20+
val tpStriped = tpWiden match {
21+
case tp @ OrType(lhs, rhs) =>
22+
val llhs = strip(lhs)
23+
val rrhs = strip(rhs)
24+
if rrhs.isNullType then llhs
25+
else if llhs.isNullType then rrhs
26+
else tp.derivedOrType(llhs, rrhs)
27+
case tp @ AndType(tp1, tp2) =>
28+
// We cannot `tp.derivedAndType(strip(tp1), strip(tp2))` directly,
29+
// since `stripNull((A | Null) & B)` would produce the wrong
30+
// result `(A & B) | Null`.
31+
val tp1s = strip(tp1)
32+
val tp2s = strip(tp2)
33+
if (tp1s ne tp1) && (tp2s ne tp2) then
34+
tp.derivedAndType(tp1s, tp2s)
35+
else tp
36+
case tp => tp
37+
}
38+
if tpStriped ne tpWiden then tpStriped else tp
5239

53-
val self1 = self.widenDealias
54-
val stripped = strip(self1)
55-
if (stripped ne self1) stripped else self
40+
strip(self)
5641
}
5742

58-
/** Like `stripNull`, but removes only the `UncheckedNull`s. */
59-
def stripUncheckedNull(implicit ctx: 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(implicit ctx: Context): Type = {
43+
def stripAllNulls(using Context): Type = {
6744
object RemoveNulls extends TypeMap {
68-
override def apply(tp: Type): Type = mapOver(tp.stripNull(true))
45+
override def apply(tp: Type): Type =
46+
mapOver(tp.widenTermRefExpr.stripNull)
6947
}
7048
val rem = RemoveNulls(self)
71-
if (rem ne self) rem else self
49+
if rem ne self then rem else self
7250
}
7351

7452
/** Is self (after widening and dealiasing) a type of the form `T | Null`? */
75-
def isNullableUnion(implicit ctx: Context): Boolean = {
76-
val stripped = self.stripNull()
53+
def isNullableUnion(using Context): Boolean = {
54+
val stripped = self.stripNull
7755
stripped ne self
7856
}
7957

80-
/** Is self (after widening and dealiasing) a type of the form `T | UncheckedNull`? */
81-
def isUncheckedNullableUnion(implicit ctx: Context): Boolean = {
82-
val stripped = self.stripNull(true)
83-
stripped ne self
58+
/** Can the type have null value after erasure?
59+
*/
60+
def hasNullAfterErasure(using Context): Boolean = {
61+
self match {
62+
case tp: ClassInfo => tp.cls.isNullableClassAfterErasure
63+
case tp: TypeProxy => tp.underlying.hasNullAfterErasure
64+
case OrType(lhs, rhs) =>
65+
lhs.hasNullAfterErasure || rhs.hasNullAfterErasure
66+
case _ =>
67+
self <:< defn.ObjectType
68+
}
8469
}
70+
71+
/** Can we convert a tree with type `self` to type `pt` unsafely.
72+
*/
73+
def isUnsafeConvertable(pt: Type)(using Context): Boolean =
74+
(self.isNullType && pt.hasNullAfterErasure) ||
75+
(self.stripAllNulls <:< pt.stripAllNulls)
8576
}
8677
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,6 @@ object StdNames {
197197
final val Nothing: N = "Nothing"
198198
final val NotNull: N = "NotNull"
199199
final val Null: N = "Null"
200-
final val UncheckedNull: N = "UncheckedNull"
201200
final val Object: N = "Object"
202201
final val Product: N = "Product"
203202
final val PartialFunction: N = "PartialFunction"
@@ -609,6 +608,7 @@ object StdNames {
609608
val unbox: N = "unbox"
610609
val unitExpr: N = "unitExpr"
611610
val universe: N = "universe"
611+
val unsafeNulls: N = "unsafeNulls"
612612
val update: N = "update"
613613
val updateDynamic: N = "updateDynamic"
614614
val using: N = "using"

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

Lines changed: 11 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -729,12 +729,10 @@ object Types {
729729
go(l).meet(go(r), pre, safeIntersection = ctx.base.pendingMemberSearches.contains(name))
730730

731731
def goOr(tp: OrType) = tp match {
732-
case OrUncheckedNull(tp1) =>
733-
// Selecting `name` from a type `T|UncheckedNull` is like selecting `name` from `T`.
734-
// This can throw at runtime, but we trade soundness for usability.
735-
// We need to strip `UncheckedNull` from both the type and the prefix so that
736-
// `pre <: tp` continues to hold.
737-
tp1.findMember(name, pre.stripUncheckedNull, required, excluded)
732+
case OrNull(tp1) if config.Feature.enabled(nme.unsafeNulls) =>
733+
// Selecting `name` from a type `T | Null` is like selecting `name` from `T`, if
734+
// unsafeNulls is enabled. This can throw at runtime, but we trade soundness for usability.
735+
tp1.findMember(name, pre.stripNull, required, excluded)
738736
case _ =>
739737
// we need to keep the invariant that `pre <: tp`. Branch `union-types-narrow-prefix`
740738
// achieved that by narrowing `pre` to each alternative, but it led to merge errors in
@@ -979,7 +977,12 @@ object Types {
979977
*/
980978
def matches(that: Type)(implicit ctx: Context): Boolean = {
981979
record("matches")
982-
ctx.typeComparer.matchesType(this, that, relaxed = !ctx.phase.erasedTypes)
980+
ctx.typeComparer.matchesType(this, that, relaxed = !ctx.phase.erasedTypes) ||
981+
(ctx.explicitNulls &&
982+
// TODO: optimize, for example, add a parameter to ignore Null type?
983+
ctx.typeComparer.matchesType(
984+
this.stripAllNulls, that.stripAllNulls,
985+
relaxed = !ctx.phase.erasedTypes))
983986
}
984987

985988
/** This is the same as `matches` except that it also matches => T with T and
@@ -3029,25 +3032,7 @@ object Types {
30293032
OrType(tp, defn.NullType)
30303033
def unapply(tp: Type)(using Context): Option[Type] =
30313034
if (ctx.explicitNulls) {
3032-
val tp1 = tp.stripNull()
3033-
if tp1 ne tp then Some(tp1) else None
3034-
}
3035-
else None
3036-
}
3037-
3038-
/** An extractor object to pattern match against a Java-nullable union.
3039-
* e.g.
3040-
*
3041-
* (tp: Type) match
3042-
* case OrUncheckedNull(tp1) => // tp had the form `tp1 | UncheckedNull`
3043-
* case _ => // tp was not a Java-nullable union
3044-
*/
3045-
object OrUncheckedNull {
3046-
def apply(tp: Type)(using Context) =
3047-
OrType(tp, defn.UncheckedNullAliasType)
3048-
def unapply(tp: Type)(using Context): Option[Type] =
3049-
if (ctx.explicitNulls) {
3050-
val tp1 = tp.stripUncheckedNull
3035+
val tp1 = tp.stripNull
30513036
if tp1 ne tp then Some(tp1) else None
30523037
}
30533038
else None

0 commit comments

Comments
 (0)