Skip to content

Commit 6a346af

Browse files
evangirardinolhotak
authored andcommitted
Add experimental flexible types feature on top of explicit nulls
Enabled by -Yflexible-types with -Yexplicit-nulls. A flexible type T! is a non-denotable type such that T <: T! <: T|Null and T|Null <: T! <: T. Here we patch return types and parameter types of Java methods and fields to use flexible types. This is unsound and kills subtyping transitivity but makes interop with Java play more nicely with the explicit nulls experimental feature (i.e. fewer nullability casts). Also adds a few tests for flexible types, mostly lifted from the explicit nulls tests.
1 parent c629090 commit 6a346af

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

+1069
-60
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,7 @@ private sealed trait YSettings:
381381
// Experimental language features
382382
val YnoKindPolymorphism: Setting[Boolean] = BooleanSetting("-Yno-kind-polymorphism", "Disable kind polymorphism.")
383383
val YexplicitNulls: Setting[Boolean] = BooleanSetting("-Yexplicit-nulls", "Make reference types non-nullable. Nullable types can be expressed with unions: e.g. String|Null.")
384+
val YflexibleTypes: Setting[Boolean] = BooleanSetting("-Yflexible-types", "Make Java return types and parameter types use flexible types. Flexible types essentially circumvent explicit nulls and force something resembling the old type system for Java interop.")
384385
val YcheckInit: Setting[Boolean] = BooleanSetting("-Ysafe-init", "Ensure safe initialization of objects.")
385386
val YcheckInitGlobal: Setting[Boolean] = BooleanSetting("-Ysafe-init-global", "Check safe initialization of global objects.")
386387
val YrequireTargetName: Setting[Boolean] = BooleanSetting("-Yrequire-targetName", "Warn if an operator is defined without a @targetName annotation.")

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ class CheckRealizable(using Context) {
122122
case tp: TypeProxy => isConcrete(tp.underlying)
123123
case tp: AndType => isConcrete(tp.tp1) && isConcrete(tp.tp2)
124124
case tp: OrType => isConcrete(tp.tp1) && isConcrete(tp.tp2)
125+
case tp: FlexibleType => isConcrete(tp.underlying)
125126
case _ => false
126127
}
127128
if (!isConcrete(tp)) NotConcrete

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -453,6 +453,9 @@ object Contexts {
453453
/** Is the explicit nulls option set? */
454454
def explicitNulls: Boolean = base.settings.YexplicitNulls.value
455455

456+
/** Is the flexible types option set? */
457+
def flexibleTypes: Boolean = base.settings.YflexibleTypes.value
458+
456459
/** A fresh clone of this context embedded in this context. */
457460
def fresh: FreshContext = freshOver(this)
458461

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

Lines changed: 65 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,10 @@ object JavaNullInterop {
6363
// Don't nullify the return type of the `toString` method.
6464
// Don't nullify the return type of constructors.
6565
// Don't nullify the return type of methods with a not-null annotation.
66-
nullifyExceptReturnType(tp)
66+
nullifyExceptReturnType(tp, sym.owner.isClass)
6767
else
6868
// Otherwise, nullify everything
69-
nullifyType(tp)
69+
nullifyType(tp, sym.owner.isClass)
7070
}
7171

7272
private def hasNotNullAnnot(sym: Symbol)(using Context): Boolean =
@@ -77,12 +77,12 @@ object JavaNullInterop {
7777
* If tp is a type of a field, the inside of the type is nullified,
7878
* but the result type is not nullable.
7979
*/
80-
private def nullifyExceptReturnType(tp: Type)(using Context): Type =
81-
new JavaNullMap(true)(tp)
80+
private def nullifyExceptReturnType(tp: Type, ownerIsClass: Boolean)(using Context): Type =
81+
if ctx.flexibleTypes /*&& ownerIsClass*/ then new JavaFlexibleMap(true)(tp) else new JavaNullMap(true)(tp) // FLEX PARAMS
8282

8383
/** Nullifies a Java type by adding `| Null` in the relevant places. */
84-
private def nullifyType(tp: Type)(using Context): Type =
85-
new JavaNullMap(false)(tp)
84+
private def nullifyType(tp: Type, ownerIsClass: Boolean)(using Context): Type =
85+
if ctx.flexibleTypes /*&& ownerIsClass*/ then new JavaFlexibleMap(false)(tp) else new JavaNullMap(false)(tp) // FLEX PARAMS
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.
@@ -146,4 +146,63 @@ object JavaNullInterop {
146146
case _ => tp
147147
}
148148
}
149+
150+
/**
151+
* Flexible types
152+
*/
153+
154+
private class JavaFlexibleMap(var outermostLevelAlreadyNullable: Boolean)(using Context) extends TypeMap {
155+
/** Should we nullify `tp` at the outermost level? */
156+
def needsFlexible(tp: Type): Boolean =
157+
!outermostLevelAlreadyNullable && (tp match {
158+
case tp: TypeRef =>
159+
// We don't modify value types because they're non-nullable even in Java.
160+
!tp.symbol.isValueClass &&
161+
// We don't modify `Any` because it's already nullable.
162+
!tp.isRef(defn.AnyClass) &&
163+
// We don't nullify Java varargs at the top level.
164+
// Example: if `setNames` is a Java method with signature `void setNames(String... names)`,
165+
// then its Scala signature will be `def setNames(names: (String|Null)*): Unit`.
166+
// This is because `setNames(null)` passes as argument a single-element array containing the value `null`,
167+
// and not a `null` array.
168+
!tp.isRef(defn.RepeatedParamClass)
169+
case _ => true
170+
})
171+
172+
override def apply(tp: Type): Type = tp match {
173+
case tp: TypeRef if needsFlexible(tp) =>
174+
//println(Thread.currentThread().getStackTrace()(3).getMethodName())
175+
FlexibleType(tp)
176+
case appTp @ AppliedType(tycon, targs) =>
177+
val oldOutermostNullable = outermostLevelAlreadyNullable
178+
// We don't make the outmost levels of type arguments nullable if tycon is Java-defined.
179+
// This is because Java classes are _all_ nullified, so both `java.util.List[String]` and
180+
// `java.util.List[String|Null]` contain nullable elements.
181+
outermostLevelAlreadyNullable = tp.classSymbol.is(JavaDefined)
182+
val targs2 = targs map this
183+
outermostLevelAlreadyNullable = oldOutermostNullable
184+
val appTp2 = derivedAppliedType(appTp, tycon, targs2)
185+
if needsFlexible(tycon) then FlexibleType(appTp2) else appTp2
186+
case ptp: PolyType =>
187+
derivedLambdaType(ptp)(ptp.paramInfos, this(ptp.resType))
188+
case mtp: MethodType =>
189+
val oldOutermostNullable = outermostLevelAlreadyNullable
190+
outermostLevelAlreadyNullable = false
191+
val paramInfos2 = mtp.paramInfos map this /*new JavaNullMap(outermostLevelAlreadyNullable)*/ // FLEX PARAMS
192+
outermostLevelAlreadyNullable = oldOutermostNullable
193+
derivedLambdaType(mtp)(paramInfos2, this(mtp.resType))
194+
case tp: TypeAlias => mapOver(tp)
195+
case tp: AndType =>
196+
// nullify(A & B) = (nullify(A) & nullify(B)) | Null, but take care not to add
197+
// duplicate `Null`s at the outermost level inside `A` and `B`.
198+
outermostLevelAlreadyNullable = true
199+
FlexibleType(derivedAndType(tp, this(tp.tp1), this(tp.tp2)))
200+
case tp: TypeParamRef if needsFlexible(tp) =>
201+
FlexibleType(tp)
202+
// In all other cases, return the type unchanged.
203+
// In particular, if the type is a ConstantType, then we don't nullify it because it is the
204+
// type of a final non-nullable field.
205+
case _ => tp
206+
}
207+
}
149208
}

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ import Types._
88
object NullOpsDecorator:
99

1010
extension (self: Type)
11+
def stripFlexible(using Context): Type = {
12+
self match {
13+
case FlexibleType(tp) => tp
14+
case _ => self
15+
}
16+
}
1117
/** Syntactically strips the nullability from this type.
1218
* If the type is `T1 | ... | Tn`, and `Ti` references to `Null`,
1319
* then return `T1 | ... | Ti-1 | Ti+1 | ... | Tn`.
@@ -33,6 +39,7 @@ object NullOpsDecorator:
3339
if (tp1s ne tp1) && (tp2s ne tp2) then
3440
tp.derivedAndType(tp1s, tp2s)
3541
else tp
42+
case tp @ FlexibleType(tp1) => strip(tp1)
3643
case tp @ TypeBounds(lo, hi) =>
3744
tp.derivedTypeBounds(strip(lo), strip(hi))
3845
case tp => tp

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -560,6 +560,9 @@ class OrderingConstraint(private val boundsMap: ParamBounds,
560560
case CapturingType(parent, refs) =>
561561
val parent1 = recur(parent)
562562
if parent1 ne parent then tp.derivedCapturingType(parent1, refs) else tp
563+
case tp: FlexibleType =>
564+
val underlying = recur(tp.underlying)
565+
if underlying ne tp.underlying then tp.derivedFlexibleType(underlying) else tp
563566
case tp: AnnotatedType =>
564567
val parent1 = recur(tp.parent)
565568
if parent1 ne tp.parent then tp.derivedAnnotatedType(parent1, tp.annot) else tp

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import Contexts.ctx
1010
import dotty.tools.dotc.reporting.trace
1111
import config.Feature.migrateTo3
1212
import config.Printers._
13+
import dotty.tools.dotc.core.NullOpsDecorator.stripFlexible
1314

1415
trait PatternTypeConstrainer { self: TypeComparer =>
1516

@@ -175,7 +176,7 @@ trait PatternTypeConstrainer { self: TypeComparer =>
175176
case tp => tp
176177
}
177178

178-
dealiasDropNonmoduleRefs(scrut) match {
179+
dealiasDropNonmoduleRefs(scrut.stripFlexible) match {
179180
case OrType(scrut1, scrut2) =>
180181
either(constrainPatternType(pat, scrut1), constrainPatternType(pat, scrut2))
181182
case AndType(scrut1, scrut2) =>

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2268,6 +2268,9 @@ object SymDenotations {
22682268
case CapturingType(parent, refs) =>
22692269
tp.derivedCapturingType(recur(parent), refs)
22702270

2271+
case tp: FlexibleType =>
2272+
recur(tp.underlying)
2273+
22712274
case tp: TypeProxy =>
22722275
def computeTypeProxy = {
22732276
val superTp = tp.superType

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

Lines changed: 74 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,10 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
383383
case OrType(tp21, tp22) =>
384384
if (tp21.stripTypeVar eq tp22.stripTypeVar) recur(tp1, tp21)
385385
else secondTry
386+
// tp1 <: Flex(T) = T|N..T
387+
// iff tp1 <: T|N
388+
case tp2: FlexibleType =>
389+
recur(tp1, tp2.lo)
386390
case TypeErasure.ErasedValueType(tycon1, underlying2) =>
387391
def compareErasedValueType = tp1 match {
388392
case TypeErasure.ErasedValueType(tycon2, underlying1) =>
@@ -531,7 +535,10 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
531535
hardenTypeVars(tp2)
532536

533537
res
534-
538+
// invariant: tp2 is NOT a FlexibleType
539+
// is Flex(T) <: tp2?
540+
case tp1: FlexibleType =>
541+
recur(tp1.underlying, tp2)
535542
case CapturingType(parent1, refs1) =>
536543
if tp2.isAny then true
537544
else if subCaptures(refs1, tp2.captureSet, frozenConstraint).isOK && sameBoxed(tp1, tp2, refs1)
@@ -2545,53 +2552,73 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
25452552
/** Try to distribute `&` inside type, detect and handle conflicts
25462553
* @pre !(tp1 <: tp2) && !(tp2 <:< tp1) -- these cases were handled before
25472554
*/
2548-
private def distributeAnd(tp1: Type, tp2: Type): Type = tp1 match {
2549-
case tp1 @ AppliedType(tycon1, args1) =>
2550-
tp2 match {
2551-
case AppliedType(tycon2, args2)
2552-
if tycon1.typeSymbol == tycon2.typeSymbol && tycon1 =:= tycon2 =>
2553-
val jointArgs = glbArgs(args1, args2, tycon1.typeParams)
2554-
if (jointArgs.forall(_.exists)) (tycon1 & tycon2).appliedTo(jointArgs)
2555-
else NoType
2556-
case _ =>
2557-
NoType
2558-
}
2559-
case tp1: RefinedType =>
2560-
// opportunistically merge same-named refinements
2561-
// this does not change anything semantically (i.e. merging or not merging
2562-
// gives =:= types), but it keeps the type smaller.
2563-
tp2 match {
2564-
case tp2: RefinedType if tp1.refinedName == tp2.refinedName =>
2565-
val jointInfo = Denotations.infoMeet(tp1.refinedInfo, tp2.refinedInfo, safeIntersection = false)
2566-
if jointInfo.exists then
2567-
tp1.derivedRefinedType(tp1.parent & tp2.parent, tp1.refinedName, jointInfo)
2568-
else
2555+
private def distributeAnd(tp1: Type, tp2: Type): Type = {
2556+
var ft1 = false
2557+
var ft2 = false
2558+
def recur(tp1: Type, tp2: Type): Type = tp1 match {
2559+
case tp1 @ FlexibleType(tp) =>
2560+
// Hack -- doesn't generalise to other intersection/union types
2561+
// but covers a common special case for pattern matching
2562+
ft1 = true
2563+
recur(tp, tp2)
2564+
case tp1 @ AppliedType(tycon1, args1) =>
2565+
tp2 match {
2566+
case AppliedType(tycon2, args2)
2567+
if tycon1.typeSymbol == tycon2.typeSymbol && tycon1 =:= tycon2 =>
2568+
val jointArgs = glbArgs(args1, args2, tycon1.typeParams)
2569+
if (jointArgs.forall(_.exists)) (tycon1 & tycon2).appliedTo(jointArgs)
2570+
else {
2571+
NoType
2572+
}
2573+
case FlexibleType(tp) =>
2574+
// Hack from above
2575+
ft2 = true
2576+
recur(tp1, tp)
2577+
case _ =>
25692578
NoType
2570-
case _ =>
2571-
NoType
2572-
}
2573-
case tp1: RecType =>
2574-
tp1.rebind(distributeAnd(tp1.parent, tp2))
2575-
case ExprType(rt1) =>
2576-
tp2 match {
2577-
case ExprType(rt2) =>
2578-
ExprType(rt1 & rt2)
2579-
case _ =>
2580-
NoType
2581-
}
2582-
case tp1: TypeVar if tp1.isInstantiated =>
2583-
tp1.underlying & tp2
2584-
case CapturingType(parent1, refs1) =>
2585-
if subCaptures(tp2.captureSet, refs1, frozen = true).isOK
2586-
&& tp1.isBoxedCapturing == tp2.isBoxedCapturing
2587-
then
2588-
parent1 & tp2
2589-
else
2590-
tp1.derivedCapturingType(parent1 & tp2, refs1)
2591-
case tp1: AnnotatedType if !tp1.isRefining =>
2592-
tp1.underlying & tp2
2593-
case _ =>
2594-
NoType
2579+
}
2580+
2581+
// if result exists and is not notype, maybe wrap result in flex based on whether seen flex on both sides
2582+
case tp1: RefinedType =>
2583+
// opportunistically merge same-named refinements
2584+
// this does not change anything semantically (i.e. merging or not merging
2585+
// gives =:= types), but it keeps the type smaller.
2586+
tp2 match {
2587+
case tp2: RefinedType if tp1.refinedName == tp2.refinedName =>
2588+
val jointInfo = Denotations.infoMeet(tp1.refinedInfo, tp2.refinedInfo, safeIntersection = false)
2589+
if jointInfo.exists then
2590+
tp1.derivedRefinedType(tp1.parent & tp2.parent, tp1.refinedName, jointInfo)
2591+
else
2592+
NoType
2593+
case _ =>
2594+
NoType
2595+
}
2596+
case tp1: RecType =>
2597+
tp1.rebind(recur(tp1.parent, tp2))
2598+
case ExprType(rt1) =>
2599+
tp2 match {
2600+
case ExprType(rt2) =>
2601+
ExprType(rt1 & rt2)
2602+
case _ =>
2603+
NoType
2604+
}
2605+
case tp1: TypeVar if tp1.isInstantiated =>
2606+
tp1.underlying & tp2
2607+
case CapturingType(parent1, refs1) =>
2608+
if subCaptures(tp2.captureSet, refs1, frozen = true).isOK
2609+
&& tp1.isBoxedCapturing == tp2.isBoxedCapturing
2610+
then
2611+
parent1 & tp2
2612+
else
2613+
tp1.derivedCapturingType(parent1 & tp2, refs1)
2614+
case tp1: AnnotatedType if !tp1.isRefining =>
2615+
tp1.underlying & tp2
2616+
case _ =>
2617+
NoType
2618+
}
2619+
// if flex on both sides, return flex type
2620+
val ret = recur(tp1, tp2)
2621+
if (ft1 && ft2) then FlexibleType(ret) else ret
25952622
}
25962623

25972624
/** Try to distribute `|` inside type, detect and handle conflicts

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,8 @@ object TypeErasure {
311311
repr1.orElse(repr2)
312312
else
313313
NoSymbol
314+
case tp: FlexibleType =>
315+
arrayUpperBound(tp.underlying)
314316
case _ =>
315317
NoSymbol
316318

@@ -337,6 +339,8 @@ object TypeErasure {
337339
isGenericArrayElement(tp.tp1, isScala2) && isGenericArrayElement(tp.tp2, isScala2)
338340
case tp: OrType =>
339341
isGenericArrayElement(tp.tp1, isScala2) || isGenericArrayElement(tp.tp2, isScala2)
342+
case tp: FlexibleType =>
343+
isGenericArrayElement(tp.underlying, isScala2)
340344
case _ => false
341345
}
342346
}
@@ -526,6 +530,7 @@ object TypeErasure {
526530
case tp: TypeProxy => hasStableErasure(tp.translucentSuperType)
527531
case tp: AndType => hasStableErasure(tp.tp1) && hasStableErasure(tp.tp2)
528532
case tp: OrType => hasStableErasure(tp.tp1) && hasStableErasure(tp.tp2)
533+
case _: FlexibleType => false
529534
case _ => false
530535
}
531536

@@ -622,6 +627,7 @@ class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConst
622627
erasePolyFunctionApply(refinedInfo)
623628
case RefinedType(parent, nme.apply, refinedInfo: MethodType) if defn.isErasedFunctionType(parent) =>
624629
eraseErasedFunctionApply(refinedInfo)
630+
case FlexibleType(tp) => this(tp)
625631
case tp: TypeProxy =>
626632
this(tp.underlying)
627633
case tp @ AndType(tp1, tp2) =>

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@ object TypeOps:
5252
Stats.record("asSeenFrom skolem prefix required")
5353
case _ =>
5454
}
55-
5655
new AsSeenFromMap(pre, cls).apply(tp)
5756
}
5857

@@ -237,6 +236,7 @@ object TypeOps:
237236
if tp1.isBottomType && (tp1 frozen_<:< tp2) then orBaseClasses(tp2)
238237
else if tp2.isBottomType && (tp2 frozen_<:< tp1) then orBaseClasses(tp1)
239238
else intersect(orBaseClasses(tp1), orBaseClasses(tp2))
239+
case FlexibleType(tp1) => orBaseClasses(tp1)
240240
case _ => tp.baseClasses
241241

242242
/** The minimal set of classes in `cs` which derive all other classes in `cs` */

0 commit comments

Comments
 (0)