Skip to content

Commit 08b0fec

Browse files
committed
Add flexible types for explicit nulls
1 parent 32afee9 commit 08b0fec

File tree

73 files changed

+541
-158
lines changed

Some content is hidden

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

73 files changed

+541
-158
lines changed

compiler/src/dotty/tools/dotc/config/ScalaSettings.scala

+1
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,7 @@ private sealed trait YSettings:
417417
// Experimental language features
418418
val YnoKindPolymorphism: Setting[Boolean] = BooleanSetting(ForkSetting, "Yno-kind-polymorphism", "Disable kind polymorphism.")
419419
val YexplicitNulls: Setting[Boolean] = BooleanSetting(ForkSetting, "Yexplicit-nulls", "Make reference types non-nullable. Nullable types can be expressed with unions: e.g. String|Null.")
420+
val YnoFlexibleTypes: Setting[Boolean] = BooleanSetting(ForkSetting, "Yno-flexible-types", "Disable turning nullable Java return types and parameter types into flexible types, which behave like abstract types with a nullable lower bound and non-nullable upper bound.")
420421
val YcheckInit: Setting[Boolean] = BooleanSetting(ForkSetting, "Ysafe-init", "Ensure safe initialization of objects.")
421422
val YcheckInitGlobal: Setting[Boolean] = BooleanSetting(ForkSetting, "Ysafe-init-global", "Check safe initialization of global objects.")
422423
val YrequireTargetName: Setting[Boolean] = BooleanSetting(ForkSetting, "Yrequire-targetName", "Warn if an operator is defined without a @targetName annotation.")

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

+4-2
Original file line numberDiff line numberDiff line change
@@ -696,9 +696,11 @@ trait ConstraintHandling {
696696
tp.rebind(tp.parent.hardenUnions)
697697
case tp: HKTypeLambda =>
698698
tp.derivedLambdaType(resType = tp.resType.hardenUnions)
699+
case tp: FlexibleType =>
700+
tp.derivedFlexibleType(tp.hi.hardenUnions)
699701
case tp: OrType =>
700-
val tp1 = tp.stripNull
701-
if tp1 ne tp then tp.derivedOrType(tp1.hardenUnions, defn.NullType)
702+
val tp1 = tp.stripNull(stripFlexibleTypes = false)
703+
if tp1 ne tp then tp.derivedOrType(tp1.hardenUnions, defn.NullType, soft = false)
702704
else tp.derivedOrType(tp.tp1.hardenUnions, tp.tp2.hardenUnions, soft = false)
703705
case _ =>
704706
tp

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

+3
Original file line numberDiff line numberDiff line change
@@ -472,6 +472,9 @@ object Contexts {
472472
/** Is the explicit nulls option set? */
473473
def explicitNulls: Boolean = base.settings.YexplicitNulls.value
474474

475+
/** Is the flexible types option set? */
476+
def flexibleTypes: Boolean = base.settings.YexplicitNulls.value && !base.settings.YnoFlexibleTypes.value
477+
475478
/** A fresh clone of this context embedded in this context. */
476479
def fresh: FreshContext = freshOver(this)
477480

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

+3-3
Original file line numberDiff line numberDiff line change
@@ -648,7 +648,7 @@ class Definitions {
648648
@tu lazy val StringModule: Symbol = StringClass.linkedClass
649649
@tu lazy val String_+ : TermSymbol = enterMethod(StringClass, nme.raw.PLUS, methOfAny(StringType), Final)
650650
@tu lazy val String_valueOf_Object: Symbol = StringModule.info.member(nme.valueOf).suchThat(_.info.firstParamTypes match {
651-
case List(pt) => pt.isAny || pt.stripNull.isAnyRef
651+
case List(pt) => pt.isAny || pt.stripNull().isAnyRef
652652
case _ => false
653653
}).symbol
654654

@@ -660,13 +660,13 @@ class Definitions {
660660
@tu lazy val ClassCastExceptionClass: ClassSymbol = requiredClass("java.lang.ClassCastException")
661661
@tu lazy val ClassCastExceptionClass_stringConstructor: TermSymbol = ClassCastExceptionClass.info.member(nme.CONSTRUCTOR).suchThat(_.info.firstParamTypes match {
662662
case List(pt) =>
663-
pt.stripNull.isRef(StringClass)
663+
pt.stripNull().isRef(StringClass)
664664
case _ => false
665665
}).symbol.asTerm
666666
@tu lazy val ArithmeticExceptionClass: ClassSymbol = requiredClass("java.lang.ArithmeticException")
667667
@tu lazy val ArithmeticExceptionClass_stringConstructor: TermSymbol = ArithmeticExceptionClass.info.member(nme.CONSTRUCTOR).suchThat(_.info.firstParamTypes match {
668668
case List(pt) =>
669-
pt.stripNull.isRef(StringClass)
669+
pt.stripNull().isRef(StringClass)
670670
case _ => false
671671
}).symbol.asTerm
672672

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

+17-13
Original file line numberDiff line numberDiff line change
@@ -78,11 +78,11 @@ object JavaNullInterop {
7878
* but the result type is not nullable.
7979
*/
8080
private def nullifyExceptReturnType(tp: Type)(using Context): Type =
81-
new JavaNullMap(true)(tp)
81+
new JavaNullMap(outermostLevelAlreadyNullable = true)(tp)
8282

8383
/** Nullifies a Java type by adding `| Null` in the relevant places. */
8484
private def nullifyType(tp: Type)(using Context): Type =
85-
new JavaNullMap(false)(tp)
85+
new JavaNullMap(outermostLevelAlreadyNullable = false)(tp)
8686

8787
/** A type map that implements the nullification function on types. Given a Java-sourced type, this adds `| Null`
8888
* in the right places to make the nulls explicit in Scala.
@@ -96,25 +96,29 @@ object JavaNullInterop {
9696
* to `(A & B) | Null`, instead of `(A | Null & B | Null) | Null`.
9797
*/
9898
private class JavaNullMap(var outermostLevelAlreadyNullable: Boolean)(using Context) extends TypeMap {
99+
def nullify(tp: Type): Type = if ctx.flexibleTypes then FlexibleType(tp) else OrNull(tp)
100+
99101
/** Should we nullify `tp` at the outermost level? */
100102
def needsNull(tp: Type): Boolean =
101-
!outermostLevelAlreadyNullable && (tp match {
102-
case tp: TypeRef =>
103+
if outermostLevelAlreadyNullable then false
104+
else tp match
105+
case tp: TypeRef if
103106
// We don't modify value types because they're non-nullable even in Java.
104-
!tp.symbol.isValueClass &&
107+
tp.symbol.isValueClass
108+
// We don't modify unit types.
109+
|| tp.isRef(defn.UnitClass)
105110
// We don't modify `Any` because it's already nullable.
106-
!tp.isRef(defn.AnyClass) &&
111+
|| tp.isRef(defn.AnyClass)
107112
// We don't nullify Java varargs at the top level.
108113
// Example: if `setNames` is a Java method with signature `void setNames(String... names)`,
109114
// then its Scala signature will be `def setNames(names: (String|Null)*): Unit`.
110115
// This is because `setNames(null)` passes as argument a single-element array containing the value `null`,
111116
// and not a `null` array.
112-
!tp.isRef(defn.RepeatedParamClass)
117+
|| !ctx.flexibleTypes && tp.isRef(defn.RepeatedParamClass) => false
113118
case _ => true
114-
})
115119

116120
override def apply(tp: Type): Type = tp match {
117-
case tp: TypeRef if needsNull(tp) => OrNull(tp)
121+
case tp: TypeRef if needsNull(tp) => nullify(tp)
118122
case appTp @ AppliedType(tycon, targs) =>
119123
val oldOutermostNullable = outermostLevelAlreadyNullable
120124
// We don't make the outmost levels of type arguments nullable if tycon is Java-defined.
@@ -124,7 +128,7 @@ object JavaNullInterop {
124128
val targs2 = targs map this
125129
outermostLevelAlreadyNullable = oldOutermostNullable
126130
val appTp2 = derivedAppliedType(appTp, tycon, targs2)
127-
if needsNull(tycon) then OrNull(appTp2) else appTp2
131+
if needsNull(tycon) then nullify(appTp2) else appTp2
128132
case ptp: PolyType =>
129133
derivedLambdaType(ptp)(ptp.paramInfos, this(ptp.resType))
130134
case mtp: MethodType =>
@@ -138,12 +142,12 @@ object JavaNullInterop {
138142
// nullify(A & B) = (nullify(A) & nullify(B)) | Null, but take care not to add
139143
// duplicate `Null`s at the outermost level inside `A` and `B`.
140144
outermostLevelAlreadyNullable = true
141-
OrNull(derivedAndType(tp, this(tp.tp1), this(tp.tp2)))
142-
case tp: TypeParamRef if needsNull(tp) => OrNull(tp)
145+
nullify(derivedAndType(tp, this(tp.tp1), this(tp.tp2)))
146+
case tp: TypeParamRef if needsNull(tp) => nullify(tp)
143147
// In all other cases, return the type unchanged.
144148
// In particular, if the type is a ConstantType, then we don't nullify it because it is the
145149
// type of a final non-nullable field.
146150
case _ => tp
147151
}
148152
}
149-
}
153+
}

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

+5-2
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ object NullOpsDecorator:
1414
* If this type isn't (syntactically) nullable, then returns the type unchanged.
1515
* The type will not be changed if explicit-nulls is not enabled.
1616
*/
17-
def stripNull(using Context): Type = {
17+
def stripNull(stripFlexibleTypes: Boolean = true)(using Context): Type = {
1818
def strip(tp: Type): Type =
1919
val tpWiden = tp.widenDealias
2020
val tpStripped = tpWiden match {
@@ -33,6 +33,9 @@ object NullOpsDecorator:
3333
if (tp1s ne tp1) && (tp2s ne tp2) then
3434
tp.derivedAndType(tp1s, tp2s)
3535
else tp
36+
case tp: FlexibleType =>
37+
val hi1 = strip(tp.hi)
38+
if stripFlexibleTypes then hi1 else tp.derivedFlexibleType(hi1)
3639
case tp @ TypeBounds(lo, hi) =>
3740
tp.derivedTypeBounds(strip(lo), strip(hi))
3841
case tp => tp
@@ -44,7 +47,7 @@ object NullOpsDecorator:
4447

4548
/** Is self (after widening and dealiasing) a type of the form `T | Null`? */
4649
def isNullableUnion(using Context): Boolean = {
47-
val stripped = self.stripNull
50+
val stripped = self.stripNull()
4851
stripped ne self
4952
}
5053
end extension

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

+4-4
Original file line numberDiff line numberDiff line change
@@ -562,11 +562,11 @@ class OrderingConstraint(private val boundsMap: ParamBounds,
562562
val underlying1 = recur(tp.underlying)
563563
if underlying1 ne tp.underlying then underlying1 else tp
564564
case CapturingType(parent, refs) =>
565-
val parent1 = recur(parent)
566-
if parent1 ne parent then tp.derivedCapturingType(parent1, refs) else tp
565+
tp.derivedCapturingType(recur(parent), refs)
566+
case tp: FlexibleType =>
567+
tp.derivedFlexibleType(recur(tp.hi))
567568
case tp: AnnotatedType =>
568-
val parent1 = recur(tp.parent)
569-
if parent1 ne tp.parent then tp.derivedAnnotatedType(parent1, tp.annot) else tp
569+
tp.derivedAnnotatedType(recur(tp.parent), tp.annot)
570570
case _ =>
571571
val tp1 = tp.dealiasKeepAnnots
572572
if tp1 ne tp then

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ trait PatternTypeConstrainer { self: TypeComparer =>
163163
}
164164
}
165165

166-
def dealiasDropNonmoduleRefs(tp: Type) = tp.dealias match {
166+
def dealiasDropNonmoduleRefs(tp: Type): Type = tp.dealias match {
167167
case tp: TermRef =>
168168
// we drop TermRefs that don't have a class symbol, as they can't
169169
// meaningfully participate in GADT reasoning and just get in the way.
@@ -172,6 +172,7 @@ trait PatternTypeConstrainer { self: TypeComparer =>
172172
// additional trait - argument-less enum cases desugar to vals.
173173
// See run/enum-Tree.scala.
174174
if tp.classSymbol.exists then tp else tp.info
175+
case tp: FlexibleType => dealiasDropNonmoduleRefs(tp.underlying)
175176
case tp => tp
176177
}
177178

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

+1
Original file line numberDiff line numberDiff line change
@@ -541,6 +541,7 @@ class TypeApplications(val self: Type) extends AnyVal {
541541
*/
542542
final def argInfos(using Context): List[Type] = self.stripped match
543543
case AppliedType(tycon, args) => args
544+
case tp: FlexibleType => tp.underlying.argInfos
544545
case _ => Nil
545546

546547
/** If this is an encoding of a function type, return its arguments, otherwise return Nil.

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

+6
Original file line numberDiff line numberDiff line change
@@ -864,6 +864,8 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
864864
false
865865
}
866866
compareClassInfo
867+
case tp2: FlexibleType =>
868+
recur(tp1, tp2.lo)
867869
case _ =>
868870
fourthTry
869871
}
@@ -1059,6 +1061,8 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
10591061
case tp1: ExprType if ctx.phaseId > gettersPhase.id =>
10601062
// getters might have converted T to => T, need to compensate.
10611063
recur(tp1.widenExpr, tp2)
1064+
case tp1: FlexibleType =>
1065+
recur(tp1.hi, tp2)
10621066
case _ =>
10631067
false
10641068
}
@@ -3437,6 +3441,8 @@ class MatchReducer(initctx: Context) extends TypeComparer(initctx) {
34373441
isConcrete(tp1.underlying)
34383442
case tp1: AndOrType =>
34393443
isConcrete(tp1.tp1) && isConcrete(tp1.tp2)
3444+
case tp1: FlexibleType =>
3445+
isConcrete(tp1.hi)
34403446
case _ =>
34413447
val tp2 = tp1.stripped.stripLazyRef
34423448
(tp2 ne tp) && isConcrete(tp2)

0 commit comments

Comments
 (0)