Skip to content

Commit da2e666

Browse files
committed
Initial UnsafeNulls PR
1 parent f4dfc7d commit da2e666

File tree

108 files changed

+1283
-338
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

+1283
-338
lines changed

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

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

366-
/** An alias for null values that originate in Java code.
367-
* This type gets special treatment in the Typer. Specifically, `UncheckedNull` can be selected through:
368-
* e.g.
369-
* ```
370-
* // x: String|Null
371-
* x.length // error: `Null` has no `length` field
372-
* // x2: String|UncheckedNull
373-
* x2.length // allowed by the Typer, but unsound (might throw NPE)
374-
* ```
375-
*/
376-
lazy val UncheckedNullAlias: TypeSymbol = {
377-
assert(ctx.explicitNulls)
378-
enterAliasType(tpnme.UncheckedNull, NullType)
379-
}
380-
def UncheckedNullAliasType: TypeRef = UncheckedNullAlias.typeRef
381-
382366
@tu lazy val ImplicitScrutineeTypeSym =
383367
newPermanentSymbol(ScalaPackageClass, tpnme.IMPLICITkw, EmptyFlags, TypeBounds.empty).entered
384368
def ImplicitScrutineeTypeRef: TypeRef = ImplicitScrutineeTypeSym.typeRef
@@ -552,14 +536,14 @@ class Definitions {
552536
@tu lazy val ClassCastExceptionClass: ClassSymbol = requiredClass("java.lang.ClassCastException")
553537
@tu lazy val ClassCastExceptionClass_stringConstructor: TermSymbol = ClassCastExceptionClass.info.member(nme.CONSTRUCTOR).suchThat(_.info.firstParamTypes match {
554538
case List(pt) =>
555-
val pt1 = if (ctx.explicitNulls) pt.stripNull() else pt
539+
val pt1 = if (ctx.explicitNulls) pt.stripNull else pt
556540
pt1.isRef(StringClass)
557541
case _ => false
558542
}).symbol.asTerm
559543
@tu lazy val ArithmeticExceptionClass: ClassSymbol = requiredClass("java.lang.ArithmeticException")
560544
@tu lazy val ArithmeticExceptionClass_stringConstructor: TermSymbol = ArithmeticExceptionClass.info.member(nme.CONSTRUCTOR).suchThat(_.info.firstParamTypes match {
561545
case List(pt) =>
562-
val pt1 = if (ctx.explicitNulls) pt.stripNull() else pt
546+
val pt1 = if (ctx.explicitNulls) pt.stripNull else pt
563547
pt1.isRef(StringClass)
564548
case _ => false
565549
}).symbol.asTerm
@@ -1504,8 +1488,8 @@ class Definitions {
15041488
// ----- Initialization ---------------------------------------------------
15051489

15061490
/** Lists core classes that don't have underlying bytecode, but are synthesized on-the-fly in every reflection universe */
1507-
@tu lazy val syntheticScalaClasses: List[TypeSymbol] = {
1508-
val synth = List(
1491+
@tu lazy val syntheticScalaClasses: List[TypeSymbol] =
1492+
List(
15091493
AnyClass,
15101494
AnyRefAlias,
15111495
AnyKindClass,
@@ -1518,9 +1502,6 @@ class Definitions {
15181502
NothingClass,
15191503
SingletonClass)
15201504

1521-
if (ctx.explicitNulls) synth :+ UncheckedNullAlias else synth
1522-
}
1523-
15241505
@tu lazy val syntheticCoreClasses: List[Symbol] = syntheticScalaClasses ++ List(
15251506
EmptyPackageVal,
15261507
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)(using Context): Type =
8281
new JavaNullMap(true)(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)(using Context): Type =
8685
new JavaNullMap(false)(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)(using 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
@@ -115,4 +115,6 @@ object Mode {
115115

116116
/** Are we in a quote in a pattern? */
117117
val QuotedPattern: Mode = newMode(25, "QuotedPattern")
118+
119+
val UnsafeNullConversion: Mode = newMode(26, "UnsafeNullConversion")
118120
}
Lines changed: 49 additions & 58 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._
4-
import dotty.tools.dotc.core.Symbols.defn
5-
import dotty.tools.dotc.core.Types._
3+
import Contexts.{Context, ctx}
4+
import Symbols.defn
5+
import Types._
66

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

1010
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-
}
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)(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-
}
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(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 = {
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`? */
7553
def isNullableUnion(using Context): Boolean = {
76-
val stripped = self.stripNull()
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(using 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"
@@ -608,6 +607,7 @@ object StdNames {
608607
val unapplySeq: N = "unapplySeq"
609608
val unbox: N = "unbox"
610609
val universe: N = "universe"
610+
val unsafeNulls: N = "unsafeNulls"
611611
val update: N = "update"
612612
val updateDynamic: N = "updateDynamic"
613613
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
@@ -738,12 +738,10 @@ object Types {
738738
go(l).meet(go(r), pre, safeIntersection = ctx.base.pendingMemberSearches.contains(name))
739739

740740
def goOr(tp: OrType) = tp match {
741-
case OrUncheckedNull(tp1) =>
742-
// Selecting `name` from a type `T|UncheckedNull` is like selecting `name` from `T`.
743-
// This can throw at runtime, but we trade soundness for usability.
744-
// We need to strip `UncheckedNull` from both the type and the prefix so that
745-
// `pre <: tp` continues to hold.
746-
tp1.findMember(name, pre.stripUncheckedNull, required, excluded)
741+
case OrNull(tp1) if config.Feature.enabled(nme.unsafeNulls) =>
742+
// Selecting `name` from a type `T | Null` is like selecting `name` from `T`, if
743+
// unsafeNulls is enabled. This can throw at runtime, but we trade soundness for usability.
744+
tp1.findMember(name, pre.stripNull, required, excluded)
747745
case _ =>
748746
// we need to keep the invariant that `pre <: tp`. Branch `union-types-narrow-prefix`
749747
// achieved that by narrowing `pre` to each alternative, but it led to merge errors in
@@ -988,7 +986,12 @@ object Types {
988986
*/
989987
def matches(that: Type)(using Context): Boolean = {
990988
record("matches")
991-
ctx.typeComparer.matchesType(this, that, relaxed = !ctx.phase.erasedTypes)
989+
ctx.typeComparer.matchesType(this, that, relaxed = !ctx.phase.erasedTypes) ||
990+
(ctx.explicitNulls &&
991+
// TODO: optimize, for example, add a parameter to ignore Null type?
992+
ctx.typeComparer.matchesType(
993+
this.stripAllNulls, that.stripAllNulls,
994+
relaxed = !ctx.phase.erasedTypes))
992995
}
993996

994997
/** This is the same as `matches` except that it also matches => T with T and
@@ -3053,25 +3056,7 @@ object Types {
30533056
OrType(tp, defn.NullType)
30543057
def unapply(tp: Type)(using Context): Option[Type] =
30553058
if (ctx.explicitNulls) {
3056-
val tp1 = tp.stripNull()
3057-
if tp1 ne tp then Some(tp1) else None
3058-
}
3059-
else None
3060-
}
3061-
3062-
/** An extractor object to pattern match against a Java-nullable union.
3063-
* e.g.
3064-
*
3065-
* (tp: Type) match
3066-
* case OrUncheckedNull(tp1) => // tp had the form `tp1 | UncheckedNull`
3067-
* case _ => // tp was not a Java-nullable union
3068-
*/
3069-
object OrUncheckedNull {
3070-
def apply(tp: Type)(using Context) =
3071-
OrType(tp, defn.UncheckedNullAliasType)
3072-
def unapply(tp: Type)(using Context): Option[Type] =
3073-
if (ctx.explicitNulls) {
3074-
val tp1 = tp.stripUncheckedNull
3059+
val tp1 = tp.stripNull
30753060
if tp1 ne tp then Some(tp1) else None
30763061
}
30773062
else None

0 commit comments

Comments
 (0)