From 3e7d5ae4e324eed2f30c5b4483203745c034f01b Mon Sep 17 00:00:00 2001 From: noti0na1 Date: Thu, 30 Nov 2023 14:26:01 +0100 Subject: [PATCH] Add experimental flexible types feature on top of explicit nulls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). Co-authored-by: Ondřej Lhoták Co-authored-by: Evan Girardin --- .../tools/dotc/config/ScalaSettings.scala | 1 + .../src/dotty/tools/dotc/core/Contexts.scala | 3 ++ .../tools/dotc/core/JavaNullInterop.scala | 30 ++++++----- .../tools/dotc/core/NullOpsDecorator.scala | 7 +++ .../tools/dotc/core/OrderingConstraint.scala | 3 ++ .../dotc/core/PatternTypeConstrainer.scala | 4 +- .../dotty/tools/dotc/core/TypeComparer.scala | 29 ++++++---- .../src/dotty/tools/dotc/core/Types.scala | 54 +++++++++++++++++++ .../tools/dotc/core/tasty/TreePickler.scala | 3 ++ .../tools/dotc/core/tasty/TreeUnpickler.scala | 2 + .../tools/dotc/printing/PlainPrinter.scala | 2 + .../src/dotty/tools/dotc/sbt/ExtractAPI.scala | 2 + .../dotty/tools/dotc/typer/Applications.scala | 5 +- .../dotty/tools/dotc/typer/Nullables.scala | 14 +++-- .../src/dotty/tools/dotc/typer/Typer.scala | 15 +++--- .../dotty/tools/dotc/CompilationTests.scala | 22 ++++++++ .../tools/vulpix/TestConfiguration.scala | 4 ++ .../scala/runtime/stdLibPatches/Predef.scala | 1 + tasty/src/dotty/tools/tasty/TastyFormat.scala | 1 + .../flexible-types/common/i7883.scala | 9 ++++ .../common/interop-propagate.scala | 18 +++++++ .../flexible-types/common/java-call/J.java | 28 ++++++++++ .../flexible-types/common/java-call/S.scala | 37 +++++++++++++ .../flexible-types/common/java-chain/J.java | 7 +++ .../flexible-types/common/java-chain/S.scala | 4 ++ .../common/java-varargs-src/J.java | 3 ++ .../common/java-varargs-src/S.scala | 20 +++++++ .../flexible-types/common/unsafe-chain.scala | 9 ++++ .../common/unsafe-implicit.scala | 10 ++++ .../common/unsafe-java-varargs.scala | 38 +++++++++++++ .../common/unsafe-select-type-member.scala | 7 +++ .../pos/applied-type-in-java/J.java | 3 ++ .../pos/applied-type-in-java/S.scala | 14 +++++ .../flexible-types/pos/equal/J.java | 9 ++++ .../flexible-types/pos/equal/S.scala | 17 ++++++ .../pos/interop-constructor-src/J.java | 6 +++ .../pos/interop-constructor-src/S.scala | 6 +++ .../pos/interop-enum-src/Day.java | 6 +++ .../pos/interop-enum-src/Planet.java | 19 +++++++ .../pos/interop-enum-src/S.scala | 6 +++ .../pos/interop-generics/J.java | 13 +++++ .../pos/interop-generics/S.scala | 9 ++++ .../pos/interop-match-src/J.java | 5 ++ .../pos/interop-match-src/S.scala | 6 +++ .../pos/interop-method-src/J.java | 5 ++ .../pos/interop-method-src/S.scala | 10 ++++ .../pos/interop-ortype-src/J.java | 3 ++ .../pos/interop-ortype-src/S.scala | 7 +++ .../pos/interop-poly-src/J.java | 29 ++++++++++ .../pos/interop-poly-src/S.scala | 37 +++++++++++++ .../flexible-types/pos/interop-sam-src/J.java | 22 ++++++++ .../pos/interop-sam-src/S.scala | 19 +++++++ .../pos/interop-static-src/J.java | 5 ++ .../pos/interop-static-src/S.scala | 6 +++ .../flexible-types/pos/match-null.scala | 5 ++ .../pos/sam-parameter-javadefined/injava.java | 6 +++ .../sam-parameter-javadefined/sam-test.scala | 22 ++++++++ 57 files changed, 650 insertions(+), 37 deletions(-) create mode 100644 tests/explicit-nulls/flexible-types/common/i7883.scala create mode 100644 tests/explicit-nulls/flexible-types/common/interop-propagate.scala create mode 100644 tests/explicit-nulls/flexible-types/common/java-call/J.java create mode 100644 tests/explicit-nulls/flexible-types/common/java-call/S.scala create mode 100644 tests/explicit-nulls/flexible-types/common/java-chain/J.java create mode 100644 tests/explicit-nulls/flexible-types/common/java-chain/S.scala create mode 100644 tests/explicit-nulls/flexible-types/common/java-varargs-src/J.java create mode 100644 tests/explicit-nulls/flexible-types/common/java-varargs-src/S.scala create mode 100644 tests/explicit-nulls/flexible-types/common/unsafe-chain.scala create mode 100644 tests/explicit-nulls/flexible-types/common/unsafe-implicit.scala create mode 100644 tests/explicit-nulls/flexible-types/common/unsafe-java-varargs.scala create mode 100644 tests/explicit-nulls/flexible-types/common/unsafe-select-type-member.scala create mode 100644 tests/explicit-nulls/flexible-types/pos/applied-type-in-java/J.java create mode 100644 tests/explicit-nulls/flexible-types/pos/applied-type-in-java/S.scala create mode 100644 tests/explicit-nulls/flexible-types/pos/equal/J.java create mode 100644 tests/explicit-nulls/flexible-types/pos/equal/S.scala create mode 100644 tests/explicit-nulls/flexible-types/pos/interop-constructor-src/J.java create mode 100644 tests/explicit-nulls/flexible-types/pos/interop-constructor-src/S.scala create mode 100644 tests/explicit-nulls/flexible-types/pos/interop-enum-src/Day.java create mode 100644 tests/explicit-nulls/flexible-types/pos/interop-enum-src/Planet.java create mode 100644 tests/explicit-nulls/flexible-types/pos/interop-enum-src/S.scala create mode 100644 tests/explicit-nulls/flexible-types/pos/interop-generics/J.java create mode 100644 tests/explicit-nulls/flexible-types/pos/interop-generics/S.scala create mode 100644 tests/explicit-nulls/flexible-types/pos/interop-match-src/J.java create mode 100644 tests/explicit-nulls/flexible-types/pos/interop-match-src/S.scala create mode 100644 tests/explicit-nulls/flexible-types/pos/interop-method-src/J.java create mode 100644 tests/explicit-nulls/flexible-types/pos/interop-method-src/S.scala create mode 100644 tests/explicit-nulls/flexible-types/pos/interop-ortype-src/J.java create mode 100644 tests/explicit-nulls/flexible-types/pos/interop-ortype-src/S.scala create mode 100644 tests/explicit-nulls/flexible-types/pos/interop-poly-src/J.java create mode 100644 tests/explicit-nulls/flexible-types/pos/interop-poly-src/S.scala create mode 100644 tests/explicit-nulls/flexible-types/pos/interop-sam-src/J.java create mode 100644 tests/explicit-nulls/flexible-types/pos/interop-sam-src/S.scala create mode 100644 tests/explicit-nulls/flexible-types/pos/interop-static-src/J.java create mode 100644 tests/explicit-nulls/flexible-types/pos/interop-static-src/S.scala create mode 100644 tests/explicit-nulls/flexible-types/pos/match-null.scala create mode 100644 tests/explicit-nulls/flexible-types/pos/sam-parameter-javadefined/injava.java create mode 100644 tests/explicit-nulls/flexible-types/pos/sam-parameter-javadefined/sam-test.scala diff --git a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala index a71f28f49410..5277bf200e20 100644 --- a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala +++ b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala @@ -410,6 +410,7 @@ private sealed trait YSettings: // Experimental language features val YnoKindPolymorphism: Setting[Boolean] = BooleanSetting("-Yno-kind-polymorphism", "Disable kind polymorphism.") val YexplicitNulls: Setting[Boolean] = BooleanSetting("-Yexplicit-nulls", "Make reference types non-nullable. Nullable types can be expressed with unions: e.g. String|Null.") + val YflexibleTypes: Setting[Boolean] = BooleanSetting("-Yflexible-types", "Make Java return types and parameter types use flexible types, which have a nullable lower bound and non-null upper bound.") val YcheckInit: Setting[Boolean] = BooleanSetting("-Ysafe-init", "Ensure safe initialization of objects.") val YcheckInitGlobal: Setting[Boolean] = BooleanSetting("-Ysafe-init-global", "Check safe initialization of global objects.") val YrequireTargetName: Setting[Boolean] = BooleanSetting("-Yrequire-targetName", "Warn if an operator is defined without a @targetName annotation.") diff --git a/compiler/src/dotty/tools/dotc/core/Contexts.scala b/compiler/src/dotty/tools/dotc/core/Contexts.scala index dec73f7243e8..8cf8326caba5 100644 --- a/compiler/src/dotty/tools/dotc/core/Contexts.scala +++ b/compiler/src/dotty/tools/dotc/core/Contexts.scala @@ -472,6 +472,9 @@ object Contexts { /** Is the explicit nulls option set? */ def explicitNulls: Boolean = base.settings.YexplicitNulls.value + /** Is the flexible types option set? */ + def flexibleTypes: Boolean = base.settings.YexplicitNulls.value && base.settings.YflexibleTypes.value + /** A fresh clone of this context embedded in this context. */ def fresh: FreshContext = freshOver(this) diff --git a/compiler/src/dotty/tools/dotc/core/JavaNullInterop.scala b/compiler/src/dotty/tools/dotc/core/JavaNullInterop.scala index 6244923cfb52..8e1a7624918b 100644 --- a/compiler/src/dotty/tools/dotc/core/JavaNullInterop.scala +++ b/compiler/src/dotty/tools/dotc/core/JavaNullInterop.scala @@ -78,11 +78,11 @@ object JavaNullInterop { * but the result type is not nullable. */ private def nullifyExceptReturnType(tp: Type)(using Context): Type = - new JavaNullMap(true)(tp) + new JavaNullMap(outermostLevelAlreadyNullable = true)(tp) /** Nullifies a Java type by adding `| Null` in the relevant places. */ private def nullifyType(tp: Type)(using Context): Type = - new JavaNullMap(false)(tp) + new JavaNullMap(outermostLevelAlreadyNullable = false)(tp) /** A type map that implements the nullification function on types. Given a Java-sourced type, this adds `| Null` * in the right places to make the nulls explicit in Scala. @@ -96,25 +96,29 @@ object JavaNullInterop { * to `(A & B) | Null`, instead of `(A | Null & B | Null) | Null`. */ private class JavaNullMap(var outermostLevelAlreadyNullable: Boolean)(using Context) extends TypeMap { + def nullify(tp: Type): Type = if ctx.flexibleTypes then FlexibleType(tp) else OrNull(tp) + /** Should we nullify `tp` at the outermost level? */ def needsNull(tp: Type): Boolean = - !outermostLevelAlreadyNullable && (tp match { + !(outermostLevelAlreadyNullable || (tp match { case tp: TypeRef => // We don't modify value types because they're non-nullable even in Java. - !tp.symbol.isValueClass && + tp.symbol.isValueClass + // We don't modify unit types. + || tp.isRef(defn.UnitClass) // We don't modify `Any` because it's already nullable. - !tp.isRef(defn.AnyClass) && + || tp.isRef(defn.AnyClass) // We don't nullify Java varargs at the top level. // Example: if `setNames` is a Java method with signature `void setNames(String... names)`, // then its Scala signature will be `def setNames(names: (String|Null)*): Unit`. // This is because `setNames(null)` passes as argument a single-element array containing the value `null`, // and not a `null` array. - !tp.isRef(defn.RepeatedParamClass) - case _ => true - }) + || !ctx.flexibleTypes && tp.isRef(defn.RepeatedParamClass) + case _ => false + })) override def apply(tp: Type): Type = tp match { - case tp: TypeRef if needsNull(tp) => OrNull(tp) + case tp: TypeRef if needsNull(tp) => nullify(tp) case appTp @ AppliedType(tycon, targs) => val oldOutermostNullable = outermostLevelAlreadyNullable // We don't make the outmost levels of type arguments nullable if tycon is Java-defined. @@ -124,7 +128,7 @@ object JavaNullInterop { val targs2 = targs map this outermostLevelAlreadyNullable = oldOutermostNullable val appTp2 = derivedAppliedType(appTp, tycon, targs2) - if needsNull(tycon) then OrNull(appTp2) else appTp2 + if needsNull(tycon) then nullify(appTp2) else appTp2 case ptp: PolyType => derivedLambdaType(ptp)(ptp.paramInfos, this(ptp.resType)) case mtp: MethodType => @@ -138,12 +142,12 @@ object JavaNullInterop { // nullify(A & B) = (nullify(A) & nullify(B)) | Null, but take care not to add // duplicate `Null`s at the outermost level inside `A` and `B`. outermostLevelAlreadyNullable = true - OrNull(derivedAndType(tp, this(tp.tp1), this(tp.tp2))) - case tp: TypeParamRef if needsNull(tp) => OrNull(tp) + nullify(derivedAndType(tp, this(tp.tp1), this(tp.tp2))) + case tp: TypeParamRef if needsNull(tp) => nullify(tp) // In all other cases, return the type unchanged. // In particular, if the type is a ConstantType, then we don't nullify it because it is the // type of a final non-nullable field. case _ => tp } } -} +} \ No newline at end of file diff --git a/compiler/src/dotty/tools/dotc/core/NullOpsDecorator.scala b/compiler/src/dotty/tools/dotc/core/NullOpsDecorator.scala index 4f22f9d31e36..19baacfc70bd 100644 --- a/compiler/src/dotty/tools/dotc/core/NullOpsDecorator.scala +++ b/compiler/src/dotty/tools/dotc/core/NullOpsDecorator.scala @@ -8,6 +8,12 @@ import Types.* object NullOpsDecorator: extension (self: Type) + def stripFlexible(using Context): Type = { + self match { + case FlexibleType(tp) => tp + case _ => self + } + } /** Syntactically strips the nullability from this type. * If the type is `T1 | ... | Tn`, and `Ti` references to `Null`, * then return `T1 | ... | Ti-1 | Ti+1 | ... | Tn`. @@ -33,6 +39,7 @@ object NullOpsDecorator: if (tp1s ne tp1) && (tp2s ne tp2) then tp.derivedAndType(tp1s, tp2s) else tp + case tp: FlexibleType => tp.hi case tp @ TypeBounds(lo, hi) => tp.derivedTypeBounds(strip(lo), strip(hi)) case tp => tp diff --git a/compiler/src/dotty/tools/dotc/core/OrderingConstraint.scala b/compiler/src/dotty/tools/dotc/core/OrderingConstraint.scala index e11ac26ef93c..bc9659ab794e 100644 --- a/compiler/src/dotty/tools/dotc/core/OrderingConstraint.scala +++ b/compiler/src/dotty/tools/dotc/core/OrderingConstraint.scala @@ -564,6 +564,9 @@ class OrderingConstraint(private val boundsMap: ParamBounds, case CapturingType(parent, refs) => val parent1 = recur(parent) if parent1 ne parent then tp.derivedCapturingType(parent1, refs) else tp + case tp: FlexibleType => + val underlying = recur(tp.underlying) + if underlying ne tp.underlying then tp.derivedFlexibleType(underlying) else tp case tp: AnnotatedType => val parent1 = recur(tp.parent) if parent1 ne tp.parent then tp.derivedAnnotatedType(parent1, tp.annot) else tp diff --git a/compiler/src/dotty/tools/dotc/core/PatternTypeConstrainer.scala b/compiler/src/dotty/tools/dotc/core/PatternTypeConstrainer.scala index 4e3596ea8814..3ab50d561fcf 100644 --- a/compiler/src/dotty/tools/dotc/core/PatternTypeConstrainer.scala +++ b/compiler/src/dotty/tools/dotc/core/PatternTypeConstrainer.scala @@ -10,6 +10,7 @@ import Contexts.ctx import dotty.tools.dotc.reporting.trace import config.Feature.migrateTo3 import config.Printers.* +import dotty.tools.dotc.core.NullOpsDecorator.stripFlexible trait PatternTypeConstrainer { self: TypeComparer => @@ -163,7 +164,7 @@ trait PatternTypeConstrainer { self: TypeComparer => } } - def dealiasDropNonmoduleRefs(tp: Type) = tp.dealias match { + def dealiasDropNonmoduleRefs(tp: Type): Type = tp.dealias match { case tp: TermRef => // we drop TermRefs that don't have a class symbol, as they can't // meaningfully participate in GADT reasoning and just get in the way. @@ -172,6 +173,7 @@ trait PatternTypeConstrainer { self: TypeComparer => // additional trait - argument-less enum cases desugar to vals. // See run/enum-Tree.scala. if tp.classSymbol.exists then tp else tp.info + case FlexibleType(tp) => dealiasDropNonmoduleRefs(tp) case tp => tp } diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index 76236d635182..6d831e6a3f1b 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -23,6 +23,7 @@ import reporting.trace import annotation.constructorOnly import cc.* import NameKinds.WildcardParamName +import NullOpsDecorator.stripFlexible /** Provides methods to compare types. */ @@ -524,7 +525,6 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling constraint = constraint.hardenTypeVars(tp2) res - case tp1 @ CapturingType(parent1, refs1) => def compareCapturing = if tp2.isAny then true @@ -863,6 +863,8 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling false } compareClassInfo + case tp2: FlexibleType => + recur(tp1, tp2.lo) case _ => fourthTry } @@ -1058,6 +1060,8 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling case tp1: ExprType if ctx.phaseId > gettersPhase.id => // getters might have converted T to => T, need to compensate. recur(tp1.widenExpr, tp2) + case tp1: FlexibleType => + recur(tp1.hi, tp2) case _ => false } @@ -2499,15 +2503,18 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling NoType } - private def andTypeGen(tp1: Type, tp2: Type, op: (Type, Type) => Type, - original: (Type, Type) => Type = _ & _, isErased: Boolean = ctx.erasedTypes): Type = trace(s"andTypeGen(${tp1.show}, ${tp2.show})", subtyping, show = true) { - val t1 = distributeAnd(tp1, tp2) - if (t1.exists) t1 - else { - val t2 = distributeAnd(tp2, tp1) - if (t2.exists) t2 - else if (isErased) erasedGlb(tp1, tp2) - else liftIfHK(tp1, tp2, op, original, _ | _) + private def andTypeGen(tp1orig: Type, tp2orig: Type, op: (Type, Type) => Type, + original: (Type, Type) => Type = _ & _, isErased: Boolean = ctx.erasedTypes): Type = trace(s"andTypeGen(${tp1orig.show}, ${tp2orig.show})", subtyping, show = true) { + val tp1 = tp1orig.stripFlexible + val tp2 = tp2orig.stripFlexible + val ret = { + val t1 = distributeAnd(tp1, tp2) + if (t1.exists) t1 + else { + val t2 = distributeAnd(tp2, tp1) + if (t2.exists) t2 + else if (isErased) erasedGlb(tp1, tp2) + else liftIfHK(tp1, tp2, op, original, _ | _) // The ` | ` on variances is needed since variances are associated with bounds // not lambdas. Example: // @@ -2517,7 +2524,9 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling // // Here, `F` is treated as bivariant in `O`. That is, only bivariant implementation // of `F` are allowed. See neg/hk-variance2s.scala test. + } } + if(tp1orig.isInstanceOf[FlexibleType] && tp2orig.isInstanceOf[FlexibleType]) FlexibleType(ret) else ret } /** Form a normalized conjunction of two types. diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index e5c166f28d78..0264b214bef1 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -343,6 +343,7 @@ object Types extends TypeUtils { /** Is this type guaranteed not to have `null` as a value? */ final def isNotNull(using Context): Boolean = this match { case tp: ConstantType => tp.value.value != null + case tp: FlexibleType => false case tp: ClassInfo => !tp.cls.isNullableClass && tp.cls != defn.NothingClass case tp: AppliedType => tp.superType.isNotNull case tp: TypeBounds => tp.lo.isNotNull @@ -372,6 +373,7 @@ object Types extends TypeUtils { case AppliedType(tycon, args) => tycon.unusableForInference || args.exists(_.unusableForInference) case RefinedType(parent, _, rinfo) => parent.unusableForInference || rinfo.unusableForInference case TypeBounds(lo, hi) => lo.unusableForInference || hi.unusableForInference + case FlexibleType(underlying) => underlying.unusableForInference case tp: AndOrType => tp.tp1.unusableForInference || tp.tp2.unusableForInference case tp: LambdaType => tp.resultType.unusableForInference || tp.paramInfos.exists(_.unusableForInference) case WildcardType(optBounds) => optBounds.unusableForInference @@ -3396,6 +3398,40 @@ object Types extends TypeUtils { } } + // --- FlexibleType ----------------------------------------------------------------- + + /* Represents a nullable type coming from Java code in a similar way to Platform Types + * in Kotlin. A FlexibleType(T) generally behaves like an abstract type with bad bounds + * T|Null .. T, so that T|Null <: FlexibleType(T) <: T. + */ + case class FlexibleType(original: Type, lo: Type, hi: Type) extends CachedProxyType with ValueType { + def underlying(using Context): Type = original + + override def superType(using Context): Type = hi + + def derivedFlexibleType(original: Type)(using Context): Type = + if this.original eq original then this else FlexibleType(original) + + override def computeHash(bs: Binders): Int = doHash(bs, original) + + override final def baseClasses(using Context): List[ClassSymbol] = original.baseClasses + } + + object FlexibleType { + def apply(original: Type)(using Context): FlexibleType = original match { + case ft: FlexibleType => ft + case _ => + val hi = original.stripNull + val lo = if hi eq original then OrNull(hi) else original + new FlexibleType(original, lo, hi) + } + + def unapply(tp: Type)(using Context): Option[Type] = tp match { + case ft: FlexibleType => Some(ft.original) + case _ => None + } + } + // --- AndType/OrType --------------------------------------------------------------- abstract class AndOrType extends CachedGroundType with ValueType { @@ -5694,6 +5730,8 @@ object Types extends TypeUtils { samClass(tp.underlying) case tp: AnnotatedType => samClass(tp.underlying) + case tp: FlexibleType => + samClass(tp.superType) case _ => NoSymbol @@ -5824,6 +5862,8 @@ object Types extends TypeUtils { tp.derivedJavaArrayType(elemtp) protected def derivedExprType(tp: ExprType, restpe: Type): Type = tp.derivedExprType(restpe) + protected def derivedFlexibleType(tp: FlexibleType, under: Type): Type = + tp.derivedFlexibleType(under) // note: currying needed because Scala2 does not support param-dependencies protected def derivedLambdaType(tp: LambdaType)(formals: List[tp.PInfo], restpe: Type): Type = tp.derivedLambdaType(tp.paramNames, formals, restpe) @@ -5947,6 +5987,9 @@ object Types extends TypeUtils { case tp: OrType => derivedOrType(tp, this(tp.tp1), this(tp.tp2)) + case tp: FlexibleType => + derivedFlexibleType(tp, this(tp.underlying)) + case tp: MatchType => val bound1 = this(tp.bound) val scrut1 = atVariance(0)(this(tp.scrutinee)) @@ -6234,6 +6277,14 @@ object Types extends TypeUtils { if (underlying.isExactlyNothing) underlying else tp.derivedAnnotatedType(underlying, annot) } + override protected def derivedFlexibleType(tp: FlexibleType, underlying: Type): Type = + underlying match { + case Range(lo, hi) => + range(tp.derivedFlexibleType(lo), tp.derivedFlexibleType(hi)) + case _ => + if (underlying.isExactlyNothing) underlying + else tp.derivedFlexibleType(underlying) + } override protected def derivedCapturingType(tp: Type, parent: Type, refs: CaptureSet): Type = parent match // TODO ^^^ handle ranges in capture sets as well case Range(lo, hi) => @@ -6375,6 +6426,9 @@ object Types extends TypeUtils { case tp: TypeVar => this(x, tp.underlying) + case tp: FlexibleType => + this(x, tp.underlying) + case ExprType(restpe) => this(x, restpe) diff --git a/compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala b/compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala index dafd6c2e8daa..667eddfbe7bf 100644 --- a/compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala +++ b/compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala @@ -269,6 +269,9 @@ class TreePickler(pickler: TastyPickler, attributes: Attributes) { case tpe: OrType => writeByte(ORtype) withLength { pickleType(tpe.tp1, richTypes); pickleType(tpe.tp2, richTypes) } + case tpe: FlexibleType => + writeByte(FLEXIBLEtype) + withLength { pickleType(tpe.underlying, richTypes) } case tpe: ExprType => writeByte(BYNAMEtype) pickleType(tpe.underlying) diff --git a/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala b/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala index d4271d5bffaf..a1502b32867e 100644 --- a/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala +++ b/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala @@ -430,6 +430,8 @@ class TreeUnpickler(reader: TastyReader, readTypeRef() match { case binder: LambdaType => binder.paramRefs(readNat()) } + case FLEXIBLEtype => + FlexibleType(readType()) } assert(currentAddr == end, s"$start $currentAddr $end ${astTagToString(tag)}") result diff --git a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala index 7fed5bc97f35..83b1edcbf2e6 100644 --- a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala @@ -287,6 +287,8 @@ class PlainPrinter(_ctx: Context) extends Printer { case AnnotatedType(tpe, annot) => if annot.symbol == defn.InlineParamAnnot || annot.symbol == defn.ErasedParamAnnot then toText(tpe) else toTextLocal(tpe) ~ " " ~ toText(annot) + case FlexibleType(tpe) => + "FlexibleType(" ~ toText(tpe) ~ ")" case tp: TypeVar => def toTextCaret(tp: Type) = if printDebug then toTextLocal(tp) ~ Str("^") else toText(tp) if (tp.isInstantiated) diff --git a/compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala b/compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala index dafb44d525e4..d3307e651233 100644 --- a/compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala +++ b/compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala @@ -565,6 +565,8 @@ private class ExtractAPICollector(using Context) extends ThunkHolder { case tp: OrType => val s = combineApiTypes(apiType(tp.tp1), apiType(tp.tp2)) withMarker(s, orMarker) + case tp: FlexibleType => + apiType(tp.underlying) case ExprType(resultType) => withMarker(apiType(resultType), byNameMarker) case MatchType(bound, scrut, cases) => diff --git a/compiler/src/dotty/tools/dotc/typer/Applications.scala b/compiler/src/dotty/tools/dotc/typer/Applications.scala index 004b21ce4fb5..78ae41025095 100644 --- a/compiler/src/dotty/tools/dotc/typer/Applications.scala +++ b/compiler/src/dotty/tools/dotc/typer/Applications.scala @@ -643,14 +643,15 @@ trait Applications extends Compatibility { missingArg(n) } - if (formal.isRepeatedParam) + val formal1 = formal.stripFlexible + if (formal1.isRepeatedParam) args match { case arg :: Nil if isVarArg(arg) => addTyped(arg) case (arg @ Typed(Literal(Constant(null)), _)) :: Nil if ctx.isAfterTyper => addTyped(arg) case _ => - val elemFormal = formal.widenExpr.argTypesLo.head + val elemFormal = formal1.widenExpr.argTypesLo.head val typedArgs = harmonic(harmonizeArgs, elemFormal) { args.map { arg => diff --git a/compiler/src/dotty/tools/dotc/typer/Nullables.scala b/compiler/src/dotty/tools/dotc/typer/Nullables.scala index cc3fac3a6ffd..5783b1fc2280 100644 --- a/compiler/src/dotty/tools/dotc/typer/Nullables.scala +++ b/compiler/src/dotty/tools/dotc/typer/Nullables.scala @@ -33,20 +33,24 @@ object Nullables: && hi.isValueType // We cannot check if hi is nullable, because it can cause cyclic reference. + private def nullifiedHi(lo: Type, hi: Type)(using Context): Type = + if needNullifyHi(lo, hi) then + if ctx.flexibleTypes then FlexibleType(hi) else OrNull(hi) + else hi + /** Create a nullable type bound * If lo is `Null`, `| Null` is added to hi */ def createNullableTypeBounds(lo: Type, hi: Type)(using Context): TypeBounds = - val newHi = if needNullifyHi(lo, hi) then OrType(hi, defn.NullType, soft = false) else hi - TypeBounds(lo, newHi) + TypeBounds(lo, nullifiedHi(lo, hi)) /** Create a nullable type bound tree * If lo is `Null`, `| Null` is added to hi */ def createNullableTypeBoundsTree(lo: Tree, hi: Tree, alias: Tree = EmptyTree)(using Context): TypeBoundsTree = - val hiTpe = hi.typeOpt - val newHi = if needNullifyHi(lo.typeOpt, hiTpe) then TypeTree(OrType(hiTpe, defn.NullType, soft = false)) else hi - TypeBoundsTree(lo, newHi, alias) + val hiTpe = nullifiedHi(lo.typeOpt, hi.typeOpt) + val hiTree = if(hiTpe eq hi.typeOpt) hi else TypeTree(hiTpe) + TypeBoundsTree(lo, hiTree, alias) /** A set of val or var references that are known to be not null, plus a set of * variable references that are not known (anymore) to be not null diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index a1ef6c0b2f25..1fd8a7717681 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -969,13 +969,15 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer // A sequence argument `xs: _*` can be either a `Seq[T]` or an `Array[_ <: T]`, // irrespective of whether the method we're calling is a Java or Scala method, // so the expected type is the union `Seq[T] | Array[_ <: T]`. - val ptArg = + val pt1 = pt.stripFlexible + val ptArg0 = // FIXME(#8680): Quoted patterns do not support Array repeated arguments if ctx.mode.isQuotedPattern then - pt.translateFromRepeated(toArray = false, translateWildcard = true) + pt1.translateFromRepeated(toArray = false, translateWildcard = true) else - pt.translateFromRepeated(toArray = false, translateWildcard = true) - | pt.translateFromRepeated(toArray = true, translateWildcard = true) + pt1.translateFromRepeated(toArray = false, translateWildcard = true) + | pt1.translateFromRepeated(toArray = true, translateWildcard = true) + val ptArg = if pt1 eq pt then ptArg0 else FlexibleType(ptArg0) val expr0 = typedExpr(tree.expr, ptArg) val expr1 = if ctx.explicitNulls && (!ctx.mode.is(Mode.Pattern)) then if expr0.tpe.isNullType then @@ -4257,10 +4259,11 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer } // convert function literal to SAM closure + val pt1 = pt.stripFlexible tree match { case closure(Nil, id @ Ident(nme.ANON_FUN), _) - if defn.isFunctionNType(wtp) && !defn.isFunctionNType(pt) => - pt match { + if defn.isFunctionNType(wtp) && !defn.isFunctionNType(pt1) => + pt1 match { case SAMType(samMeth, samParent) if wtp <:< samMeth.toFunctionType(isJava = samParent.classSymbol.is(JavaDefined)) => // was ... && isFullyDefined(pt, ForceDegree.flipBottom) diff --git a/compiler/test/dotty/tools/dotc/CompilationTests.scala b/compiler/test/dotty/tools/dotc/CompilationTests.scala index 41b8de0d6138..6a775ec62528 100644 --- a/compiler/test/dotty/tools/dotc/CompilationTests.scala +++ b/compiler/test/dotty/tools/dotc/CompilationTests.scala @@ -212,6 +212,28 @@ class CompilationTests { compileFilesInDir("tests/explicit-nulls/run", defaultOptions and "-Yexplicit-nulls") }.checkRuns() + // Flexible types tests + // @Test def flexibleTypesRun: Unit = { + // implicit val testGroup: TestGroup = TestGroup("flexibleTypesRun") + // compileFilesInDir("tests/flexible-types/run", flexibleTypesOptions) + // }.checkRuns() + + @Test def flexibleTypesNeg: Unit = { + implicit val testGroup: TestGroup = TestGroup("flexibleTypesNeg") + aggregateTests( + // compileFilesInDir("tests/explicit-nulls/flexible-types/neg", defaultOptions and "-Yexplicit-nulls"), + compileFilesInDir("tests/explicit-nulls/flexible-types/common", defaultOptions and "-Yexplicit-nulls"), + ) + }.checkExpectedErrors() + + @Test def flexibleTypesPos: Unit = { + implicit val testGroup: TestGroup = TestGroup("flexibleTypesPos") + aggregateTests( + compileFilesInDir("tests/explicit-nulls/flexible-types/pos", flexibleTypesOptions), + compileFilesInDir("tests/explicit-nulls/flexible-types/common", flexibleTypesOptions), + ) + }.checkCompile() + // initialization tests @Test def checkInitGlobal: Unit = { implicit val testGroup: TestGroup = TestGroup("checkInitGlobal") diff --git a/compiler/test/dotty/tools/vulpix/TestConfiguration.scala b/compiler/test/dotty/tools/vulpix/TestConfiguration.scala index 04be00fe921e..cc5d40be1244 100644 --- a/compiler/test/dotty/tools/vulpix/TestConfiguration.scala +++ b/compiler/test/dotty/tools/vulpix/TestConfiguration.scala @@ -87,6 +87,10 @@ object TestConfiguration { val picklingWithCompilerOptions = picklingOptions.withClasspath(withCompilerClasspath).withRunClasspath(withCompilerClasspath) + val explicitNullsOptions = defaultOptions and "-Yexplicit-nulls" + + val flexibleTypesOptions = explicitNullsOptions and "-Yflexible-types" + /** Default target of the generated class files */ private def defaultTarget: String = { import scala.util.Properties.isJavaAtLeast diff --git a/library/src/scala/runtime/stdLibPatches/Predef.scala b/library/src/scala/runtime/stdLibPatches/Predef.scala index 09feaf11c31d..f9508d30e710 100644 --- a/library/src/scala/runtime/stdLibPatches/Predef.scala +++ b/library/src/scala/runtime/stdLibPatches/Predef.scala @@ -61,4 +61,5 @@ object Predef: inline def ne(inline y: AnyRef | Null): Boolean = !(x eq y) + extension (inline opt: Option.type) inline def fromNullable[T](t: T|Null): Option[T] = Option(t).asInstanceOf end Predef diff --git a/tasty/src/dotty/tools/tasty/TastyFormat.scala b/tasty/src/dotty/tools/tasty/TastyFormat.scala index 6ceb82f011f4..ee0050d20a88 100644 --- a/tasty/src/dotty/tools/tasty/TastyFormat.scala +++ b/tasty/src/dotty/tools/tasty/TastyFormat.scala @@ -604,6 +604,7 @@ object TastyFormat { final val MATCHtype = 190 final val MATCHtpt = 191 final val MATCHCASEtype = 192 + final val FLEXIBLEtype = 193 final val HOLE = 255 diff --git a/tests/explicit-nulls/flexible-types/common/i7883.scala b/tests/explicit-nulls/flexible-types/common/i7883.scala new file mode 100644 index 000000000000..9ee92553b60d --- /dev/null +++ b/tests/explicit-nulls/flexible-types/common/i7883.scala @@ -0,0 +1,9 @@ +import scala.util.matching.Regex + +object Test extends App { + def head(s: String, r: Regex): Option[(String, String)] = + s.trim match { + case r(hd, tl) => Some((hd, tl)) // error // error // error + case _ => None + } +} \ No newline at end of file diff --git a/tests/explicit-nulls/flexible-types/common/interop-propagate.scala b/tests/explicit-nulls/flexible-types/common/interop-propagate.scala new file mode 100644 index 000000000000..40eb12dd287c --- /dev/null +++ b/tests/explicit-nulls/flexible-types/common/interop-propagate.scala @@ -0,0 +1,18 @@ + class Foo { + import java.util.ArrayList + + // Test that type mapping works with flexible types. + val ll: ArrayList[ArrayList[ArrayList[String]]] = new ArrayList[ArrayList[ArrayList[String]]] + val level1: ArrayList[ArrayList[String]] = ll.get(0) // error + val level2: ArrayList[String] = ll.get(0).get(0) // error + val level3: String = ll.get(0).get(0).get(0) // error + + val lb = new ArrayList[ArrayList[ArrayList[String]]] + val levelA = lb.get(0) + val levelB = lb.get(0).get(0) // error + val levelC = lb.get(0).get(0).get(0) // error + + val x = levelA.get(0) // error + val y = levelB.get(0) + val z: String = levelA.get(0).get(0) // error +} diff --git a/tests/explicit-nulls/flexible-types/common/java-call/J.java b/tests/explicit-nulls/flexible-types/common/java-call/J.java new file mode 100644 index 000000000000..7adc03f6898e --- /dev/null +++ b/tests/explicit-nulls/flexible-types/common/java-call/J.java @@ -0,0 +1,28 @@ +public class J { + + public class K {} + + public J self() { + return this; + } + + public String f1() { + return ""; + } + + public int f2() { + return 0; + } + + public K f3() { + return null; + } + + public T g1() { + return null; + } +} + +class J2 { + public T x = null; +} \ No newline at end of file diff --git a/tests/explicit-nulls/flexible-types/common/java-call/S.scala b/tests/explicit-nulls/flexible-types/common/java-call/S.scala new file mode 100644 index 000000000000..e5f9abe70d5f --- /dev/null +++ b/tests/explicit-nulls/flexible-types/common/java-call/S.scala @@ -0,0 +1,37 @@ +// Check Java calls have been cast to non-nullable. + +val j: J = new J + +val jj = j.self() + +val s1: String = j.f1() // error + +val s1n: String | Null = j.f1() + +val i1: Int = j.f2() + +val k: jj.K = jj.f3() // error // error + +val s2: String = j.g1[String]() // error + +val s2n: String | Null = j.g1[String]() + +val s3: String = j.g1[String | Null]() // error + +val s3n: String | Null = j.g1[String | Null]() + +val i2: Int = j.g1[Int]() // error + +val a1: Any = j.g1[Any]() + +val ar1: AnyRef = j.g1[AnyRef]() // error + +val n1: Null = j.g1[Null]() + +def clo1[T]: T = j.g1[T]() // error + +def clo3[T >: Null <: AnyRef | Null]: T = j.g1[T]() + +def testJ2[T]: T = + val j2: J2[T] = new J2 + j2.x // error diff --git a/tests/explicit-nulls/flexible-types/common/java-chain/J.java b/tests/explicit-nulls/flexible-types/common/java-chain/J.java new file mode 100644 index 000000000000..bd266bae13d9 --- /dev/null +++ b/tests/explicit-nulls/flexible-types/common/java-chain/J.java @@ -0,0 +1,7 @@ +class J1 { + J2 getJ2() { return new J2(); } +} + +class J2 { + J1 getJ1() { return new J1(); } +} \ No newline at end of file diff --git a/tests/explicit-nulls/flexible-types/common/java-chain/S.scala b/tests/explicit-nulls/flexible-types/common/java-chain/S.scala new file mode 100644 index 000000000000..9fe5aa3f08ce --- /dev/null +++ b/tests/explicit-nulls/flexible-types/common/java-chain/S.scala @@ -0,0 +1,4 @@ +class S { + val j: J2 = new J2() + j.getJ1().getJ2().getJ1().getJ2().getJ1().getJ2() // error +} diff --git a/tests/explicit-nulls/flexible-types/common/java-varargs-src/J.java b/tests/explicit-nulls/flexible-types/common/java-varargs-src/J.java new file mode 100644 index 000000000000..21ba08be66c9 --- /dev/null +++ b/tests/explicit-nulls/flexible-types/common/java-varargs-src/J.java @@ -0,0 +1,3 @@ +abstract class J { + abstract void foo(String... x); +} \ No newline at end of file diff --git a/tests/explicit-nulls/flexible-types/common/java-varargs-src/S.scala b/tests/explicit-nulls/flexible-types/common/java-varargs-src/S.scala new file mode 100644 index 000000000000..87b73ab9f9ea --- /dev/null +++ b/tests/explicit-nulls/flexible-types/common/java-varargs-src/S.scala @@ -0,0 +1,20 @@ +class S { + val j: J = ??? + + j.foo() + j.foo("") + j.foo(null) + j.foo("", "") + j.foo("", null, "") + + val arg1: Array[String] = ??? + val arg2: Array[String | Null] = ??? + val arg3: Array[String] | Null = ??? + val arg4: Array[String | Null] | Null = ??? + + j.foo(arg1: _*) + j.foo(arg2: _*) + // TODO: fix for flexible types + j.foo(arg3: _*) // error + j.foo(arg4: _*) // error +} \ No newline at end of file diff --git a/tests/explicit-nulls/flexible-types/common/unsafe-chain.scala b/tests/explicit-nulls/flexible-types/common/unsafe-chain.scala new file mode 100644 index 000000000000..f087e22d50f9 --- /dev/null +++ b/tests/explicit-nulls/flexible-types/common/unsafe-chain.scala @@ -0,0 +1,9 @@ +// Test that we can select through "| Null" is unsafeNulls is enabled (unsoundly). + +class Foo { + import java.util.ArrayList + import java.util.Iterator + + val x3 = new ArrayList[ArrayList[ArrayList[String]]]() + val x4: Int = x3.get(0).get(0).get(0).length() // error +} diff --git a/tests/explicit-nulls/flexible-types/common/unsafe-implicit.scala b/tests/explicit-nulls/flexible-types/common/unsafe-implicit.scala new file mode 100644 index 000000000000..4bbba8f11cab --- /dev/null +++ b/tests/explicit-nulls/flexible-types/common/unsafe-implicit.scala @@ -0,0 +1,10 @@ +class S { + locally { + // OfType Implicits + + import java.nio.charset.StandardCharsets + import scala.io.Codec + + val c: Codec = StandardCharsets.UTF_8 // error + } +} \ No newline at end of file diff --git a/tests/explicit-nulls/flexible-types/common/unsafe-java-varargs.scala b/tests/explicit-nulls/flexible-types/common/unsafe-java-varargs.scala new file mode 100644 index 000000000000..8e61f5763391 --- /dev/null +++ b/tests/explicit-nulls/flexible-types/common/unsafe-java-varargs.scala @@ -0,0 +1,38 @@ +import java.nio.file.Paths + +def test1 = { + Paths.get("") + Paths.get("", null) + Paths.get("", "") + Paths.get("", "", null) + + val x1: String = ??? + val x2: String | Null = ??? + + Paths.get("", x1) + Paths.get("", x2) +} + +def test2 = { + val xs1: Seq[String] = ??? + val xs2: Seq[String | Null] = ??? + val xs3: Seq[String | Null] | Null = ??? + val xs4: Seq[String] | Null = ??? + + val ys1: Array[String] = ??? + val ys2: Array[String | Null] = ??? + val ys3: Array[String | Null] | Null = ??? + val ys4: Array[String] | Null = ??? + + Paths.get("", xs1: _*) + Paths.get("", xs2: _*) + Paths.get("", xs3: _*) // error + Paths.get("", xs4: _*) // error + + Paths.get("", ys1: _*) + Paths.get("", ys2: _*) + Paths.get("", ys3: _*) // error + Paths.get("", ys4: _*) // error + + Paths.get("", null: _*) // error +} \ No newline at end of file diff --git a/tests/explicit-nulls/flexible-types/common/unsafe-select-type-member.scala b/tests/explicit-nulls/flexible-types/common/unsafe-select-type-member.scala new file mode 100644 index 000000000000..ddd402545edb --- /dev/null +++ b/tests/explicit-nulls/flexible-types/common/unsafe-select-type-member.scala @@ -0,0 +1,7 @@ +import java.util.ArrayList + +def f[T]: ArrayList[T] = { + val cz = Class.forName("java.util.ArrayList") + val o = cz.newInstance() // error: T of Class[?] | Null + o.asInstanceOf[ArrayList[T]] +} \ No newline at end of file diff --git a/tests/explicit-nulls/flexible-types/pos/applied-type-in-java/J.java b/tests/explicit-nulls/flexible-types/pos/applied-type-in-java/J.java new file mode 100644 index 000000000000..c85a921a81b9 --- /dev/null +++ b/tests/explicit-nulls/flexible-types/pos/applied-type-in-java/J.java @@ -0,0 +1,3 @@ +public class J { + public J j = this; +} \ No newline at end of file diff --git a/tests/explicit-nulls/flexible-types/pos/applied-type-in-java/S.scala b/tests/explicit-nulls/flexible-types/pos/applied-type-in-java/S.scala new file mode 100644 index 000000000000..8ff50ab63840 --- /dev/null +++ b/tests/explicit-nulls/flexible-types/pos/applied-type-in-java/S.scala @@ -0,0 +1,14 @@ +def test1[T](x: J[T]): J[T] = + x match { + case y: J[_] => y + } + +def test2[T](x: J[T]): J[T] = + x match { + case y: J[_] => y.j + } + +def test3[T](x: J[T]): J[T] = + x.j match { + case y: J[_] => y.j + } \ No newline at end of file diff --git a/tests/explicit-nulls/flexible-types/pos/equal/J.java b/tests/explicit-nulls/flexible-types/pos/equal/J.java new file mode 100644 index 000000000000..2a79059c99ef --- /dev/null +++ b/tests/explicit-nulls/flexible-types/pos/equal/J.java @@ -0,0 +1,9 @@ +public class J { + public String f() { + return ""; + } + + public T g() { + return null; + } +} \ No newline at end of file diff --git a/tests/explicit-nulls/flexible-types/pos/equal/S.scala b/tests/explicit-nulls/flexible-types/pos/equal/S.scala new file mode 100644 index 000000000000..f36dfad977b8 --- /dev/null +++ b/tests/explicit-nulls/flexible-types/pos/equal/S.scala @@ -0,0 +1,17 @@ +// Check Java calls have been cast to non-nullable. + +def test[T <: AnyRef] = + val j: J = new J + + val s = j.f() + val sn = s == null + val sn2 = s != null + val seqn = s eq null + val seqn2 = null eq s + + + val t = j.g[T]() + val tn = t == null + val tn2 = t != null + val teqn = t eq null + val teqn2 = null eq t \ No newline at end of file diff --git a/tests/explicit-nulls/flexible-types/pos/interop-constructor-src/J.java b/tests/explicit-nulls/flexible-types/pos/interop-constructor-src/J.java new file mode 100644 index 000000000000..b1590d50023e --- /dev/null +++ b/tests/explicit-nulls/flexible-types/pos/interop-constructor-src/J.java @@ -0,0 +1,6 @@ +class J { + private String s; + + J(String x) { this.s = x; } + J(String x, String y, String z) {} +} diff --git a/tests/explicit-nulls/flexible-types/pos/interop-constructor-src/S.scala b/tests/explicit-nulls/flexible-types/pos/interop-constructor-src/S.scala new file mode 100644 index 000000000000..3defd73f3945 --- /dev/null +++ b/tests/explicit-nulls/flexible-types/pos/interop-constructor-src/S.scala @@ -0,0 +1,6 @@ + +class S { + val x1: J = new J("hello") + val x2: J = new J(null) + val x3: J = new J(null, null, null) +} diff --git a/tests/explicit-nulls/flexible-types/pos/interop-enum-src/Day.java b/tests/explicit-nulls/flexible-types/pos/interop-enum-src/Day.java new file mode 100644 index 000000000000..55dca0783931 --- /dev/null +++ b/tests/explicit-nulls/flexible-types/pos/interop-enum-src/Day.java @@ -0,0 +1,6 @@ + +public enum Day { + SUN, + MON, + TUE +} diff --git a/tests/explicit-nulls/flexible-types/pos/interop-enum-src/Planet.java b/tests/explicit-nulls/flexible-types/pos/interop-enum-src/Planet.java new file mode 100644 index 000000000000..287aed6aecc5 --- /dev/null +++ b/tests/explicit-nulls/flexible-types/pos/interop-enum-src/Planet.java @@ -0,0 +1,19 @@ +public enum Planet { + MERCURY (3.303e+23, 2.4397e6), + VENUS (4.869e+24, 6.0518e6), + EARTH (5.976e+24, 6.37814e6), + MARS (6.421e+23, 3.3972e6), + JUPITER (1.9e+27, 7.1492e7), + SATURN (5.688e+26, 6.0268e7), + URANUS (8.686e+25, 2.5559e7), + NEPTUNE (1.024e+26, 2.4746e7); + + private final double mass; // in kilograms + private final double radius; // in meters + Planet(double mass, double radius) { + this.mass = mass; + this.radius = radius; + } + private double mass() { return mass; } + private double radius() { return radius; } +} diff --git a/tests/explicit-nulls/flexible-types/pos/interop-enum-src/S.scala b/tests/explicit-nulls/flexible-types/pos/interop-enum-src/S.scala new file mode 100644 index 000000000000..75e4654869a4 --- /dev/null +++ b/tests/explicit-nulls/flexible-types/pos/interop-enum-src/S.scala @@ -0,0 +1,6 @@ + +// Verify that enum values aren't nullified. +class S { + val d: Day = Day.MON + val p: Planet = Planet.MARS +} diff --git a/tests/explicit-nulls/flexible-types/pos/interop-generics/J.java b/tests/explicit-nulls/flexible-types/pos/interop-generics/J.java new file mode 100644 index 000000000000..4bbdbd4cf319 --- /dev/null +++ b/tests/explicit-nulls/flexible-types/pos/interop-generics/J.java @@ -0,0 +1,13 @@ + +class I {} + +class J { + I foo(T x) { + return new I(); + } + + I[] bar(T x) { + Object[] r = new Object[]{new I()}; + return (I[]) r; + } +} diff --git a/tests/explicit-nulls/flexible-types/pos/interop-generics/S.scala b/tests/explicit-nulls/flexible-types/pos/interop-generics/S.scala new file mode 100644 index 000000000000..a5d885d7e664 --- /dev/null +++ b/tests/explicit-nulls/flexible-types/pos/interop-generics/S.scala @@ -0,0 +1,9 @@ +class S { + val j = new J() + // Check that nullable and non-nullable work + val x: I[String] | Null = j.foo("hello") + val y: Array[I[String] | Null] | Null = j.bar[String](null) + val x2: I[String] = j.foo("hello") + val y2: Array[I[String] | Null] = j.bar[String](null) + val y3: Array[I[String]] = j.bar[String](null) +} diff --git a/tests/explicit-nulls/flexible-types/pos/interop-match-src/J.java b/tests/explicit-nulls/flexible-types/pos/interop-match-src/J.java new file mode 100644 index 000000000000..16f99f082ebb --- /dev/null +++ b/tests/explicit-nulls/flexible-types/pos/interop-match-src/J.java @@ -0,0 +1,5 @@ +class J { + public J j = this; + //public J bar() { return null; } +} + diff --git a/tests/explicit-nulls/flexible-types/pos/interop-match-src/S.scala b/tests/explicit-nulls/flexible-types/pos/interop-match-src/S.scala new file mode 100644 index 000000000000..49e8852226ff --- /dev/null +++ b/tests/explicit-nulls/flexible-types/pos/interop-match-src/S.scala @@ -0,0 +1,6 @@ +class S[T] { + def bar(x: J[T]): J[T] = x.j match { + case y: J[_] => y.j + } +} + diff --git a/tests/explicit-nulls/flexible-types/pos/interop-method-src/J.java b/tests/explicit-nulls/flexible-types/pos/interop-method-src/J.java new file mode 100644 index 000000000000..1b7ea514e4b2 --- /dev/null +++ b/tests/explicit-nulls/flexible-types/pos/interop-method-src/J.java @@ -0,0 +1,5 @@ + +class J { + String foo(String x) { return null; } + static String fooStatic(String x) { return null; } +} diff --git a/tests/explicit-nulls/flexible-types/pos/interop-method-src/S.scala b/tests/explicit-nulls/flexible-types/pos/interop-method-src/S.scala new file mode 100644 index 000000000000..403c86bc4c06 --- /dev/null +++ b/tests/explicit-nulls/flexible-types/pos/interop-method-src/S.scala @@ -0,0 +1,10 @@ + +class S { + + val j = new J() + j.foo(null) // ok: argument is nullable + val s: String = j.foo("hello") // error: return type is nullable + + J.fooStatic(null) // ok: argument is nullable + val s2: String = J.fooStatic("hello") // error: return type is nullable +} diff --git a/tests/explicit-nulls/flexible-types/pos/interop-ortype-src/J.java b/tests/explicit-nulls/flexible-types/pos/interop-ortype-src/J.java new file mode 100644 index 000000000000..b0d767bccf3e --- /dev/null +++ b/tests/explicit-nulls/flexible-types/pos/interop-ortype-src/J.java @@ -0,0 +1,3 @@ +class J { + public static T foo(T t) { return null; } +} diff --git a/tests/explicit-nulls/flexible-types/pos/interop-ortype-src/S.scala b/tests/explicit-nulls/flexible-types/pos/interop-ortype-src/S.scala new file mode 100644 index 000000000000..af3b44ab29a7 --- /dev/null +++ b/tests/explicit-nulls/flexible-types/pos/interop-ortype-src/S.scala @@ -0,0 +1,7 @@ +// Tests that member finding works on (Flex(T) | S) +class S { + def foo(a: J|String) = (a match { + case x: J => J.foo(x: J) + case y: String => "" + }).asInstanceOf[J] +} diff --git a/tests/explicit-nulls/flexible-types/pos/interop-poly-src/J.java b/tests/explicit-nulls/flexible-types/pos/interop-poly-src/J.java new file mode 100644 index 000000000000..a0d5c109605e --- /dev/null +++ b/tests/explicit-nulls/flexible-types/pos/interop-poly-src/J.java @@ -0,0 +1,29 @@ +import java.util.*; + +class JavaCat { + T prop; +} + +class J { + static ScalaCat getScalaCat() { + return null; + } + + static JavaCat getJavaCat() { + return null; + } + + static List getListOfStringArray() { + List as = new ArrayList(); + as.add(new String[1]); + return as; + } + + static List[] getArrayOfStringList() { + return (List[]) new List[1]; + } + + static List[]> getComplexStrings() { + return new ArrayList[]>(); + } +} diff --git a/tests/explicit-nulls/flexible-types/pos/interop-poly-src/S.scala b/tests/explicit-nulls/flexible-types/pos/interop-poly-src/S.scala new file mode 100644 index 000000000000..8aed9e99b689 --- /dev/null +++ b/tests/explicit-nulls/flexible-types/pos/interop-poly-src/S.scala @@ -0,0 +1,37 @@ +// Test the handling of generics by the nullability transform. +// There are two classes here: JavaCat is Java-defined, and ScalaCat +// is Scala-defined. + +class ScalaCat[T] {} + +class Test { + // It's safe to return a JavaCat[String]|Null (no inner |Null), + // because JavaCat, being a Java class, _already_ nullifies its + // fields. + val jc: JavaCat[String]|Null = J.getJavaCat[String]() + val jc2: JavaCat[String] = J.getJavaCat[String]() + // ScalaCat is Scala-defined, so we need the inner |Null. + val sc: ScalaCat[String|Null]|Null = J.getScalaCat[String]() + val sc2: ScalaCat[String]|Null = J.getScalaCat[String]() + val sc3: ScalaCat[String|Null] = J.getScalaCat[String]() + val sc4: ScalaCat[String] = J.getScalaCat[String]() + + import java.util.List + + val las: List[Array[String|Null]]|Null = J.getListOfStringArray() + val las2: List[Array[String|Null]] = J.getListOfStringArray() + val las3: List[Array[String]]|Null = J.getListOfStringArray() + val las4: List[Array[String]] = J.getListOfStringArray() + val als: Array[List[String]|Null]|Null = J.getArrayOfStringList() + val als2: Array[List[String]|Null] = J.getArrayOfStringList() + val als3: Array[List[String]]|Null = J.getArrayOfStringList() + val als4: Array[List[String]] = J.getArrayOfStringList() + val css: List[Array[List[Array[String|Null]]|Null]]|Null = J.getComplexStrings() + val css2: List[Array[List[Array[String]]|Null]]|Null = J.getComplexStrings() + val css3: List[Array[List[Array[String|Null]]]]|Null = J.getComplexStrings() + val css4: List[Array[List[Array[String|Null]]|Null]] = J.getComplexStrings() + val css5: List[Array[List[Array[String|Null]]]] = J.getComplexStrings() + val css6: List[Array[List[Array[String]]]]|Null = J.getComplexStrings() + val css7: List[Array[List[Array[String]]|Null]] = J.getComplexStrings() + val css8: List[Array[List[Array[String]]]] = J.getComplexStrings() +} diff --git a/tests/explicit-nulls/flexible-types/pos/interop-sam-src/J.java b/tests/explicit-nulls/flexible-types/pos/interop-sam-src/J.java new file mode 100644 index 000000000000..336e252aa861 --- /dev/null +++ b/tests/explicit-nulls/flexible-types/pos/interop-sam-src/J.java @@ -0,0 +1,22 @@ +import java.util.function.*; + +@FunctionalInterface +interface SAMJava1 { + public String[] f(String x); +} + +@FunctionalInterface +interface SAMJava2 { + public void f(int x); +} + +class J { + public void g1(SAMJava1 s) { + } + + public void g2(SAMJava2 s) { + } + + public void h1(Function s) { + } +} \ No newline at end of file diff --git a/tests/explicit-nulls/flexible-types/pos/interop-sam-src/S.scala b/tests/explicit-nulls/flexible-types/pos/interop-sam-src/S.scala new file mode 100644 index 000000000000..c0da89163018 --- /dev/null +++ b/tests/explicit-nulls/flexible-types/pos/interop-sam-src/S.scala @@ -0,0 +1,19 @@ +def m = { + val j: J = ??? + + def f1(x: String | Null): Array[String | Null] | Null = null + + def f2(i: Int): Unit = () + + j.g1(f1) + j.g1((_: String | Null) => null) + j.g1(null) + + j.g2(f2) + j.g2((_: Int) => ()) + j.g2(null) + + j.h1(f1) + j.h1((_: String | Null) => null) + j.h1(null) +} \ No newline at end of file diff --git a/tests/explicit-nulls/flexible-types/pos/interop-static-src/J.java b/tests/explicit-nulls/flexible-types/pos/interop-static-src/J.java new file mode 100644 index 000000000000..a233d9662950 --- /dev/null +++ b/tests/explicit-nulls/flexible-types/pos/interop-static-src/J.java @@ -0,0 +1,5 @@ + +class J { + static int foo(String s) { return 42; } + static String bar(int i) { return null; } +} diff --git a/tests/explicit-nulls/flexible-types/pos/interop-static-src/S.scala b/tests/explicit-nulls/flexible-types/pos/interop-static-src/S.scala new file mode 100644 index 000000000000..61694434d018 --- /dev/null +++ b/tests/explicit-nulls/flexible-types/pos/interop-static-src/S.scala @@ -0,0 +1,6 @@ +class S { + // Java static methods are also nullified + val x: Int = J.foo(null) + val y: String | Null = J.bar(0) + val y2: String = J.bar(0) +} diff --git a/tests/explicit-nulls/flexible-types/pos/match-null.scala b/tests/explicit-nulls/flexible-types/pos/match-null.scala new file mode 100644 index 000000000000..4cdf7632fa35 --- /dev/null +++ b/tests/explicit-nulls/flexible-types/pos/match-null.scala @@ -0,0 +1,5 @@ +def test1 = + val s: String = ??? + s.trim() match + case _: String => + case null => diff --git a/tests/explicit-nulls/flexible-types/pos/sam-parameter-javadefined/injava.java b/tests/explicit-nulls/flexible-types/pos/sam-parameter-javadefined/injava.java new file mode 100644 index 000000000000..28925b3c492a --- /dev/null +++ b/tests/explicit-nulls/flexible-types/pos/sam-parameter-javadefined/injava.java @@ -0,0 +1,6 @@ +class injava { + static void overloaded(Runnable r) {} + static void overloaded(int i) {} + + static void notoverloaded(Runnable r) {} +} diff --git a/tests/explicit-nulls/flexible-types/pos/sam-parameter-javadefined/sam-test.scala b/tests/explicit-nulls/flexible-types/pos/sam-parameter-javadefined/sam-test.scala new file mode 100644 index 000000000000..552f1f01ba6e --- /dev/null +++ b/tests/explicit-nulls/flexible-types/pos/sam-parameter-javadefined/sam-test.scala @@ -0,0 +1,22 @@ +def foo = { + def unit: Unit = () + + injava.overloaded({ () => unit } : Runnable ) + injava.overloaded({ () => unit } ) + + injava.notoverloaded({ () => unit } : Runnable ) + injava.notoverloaded({ () => unit } ) + + val list = new java.util.Vector[Int]() + java.util.Collections.sort[Int](list, { (a,b) => a - b } : java.util.Comparator[Int] ) + java.util.Collections.sort[Int](list, { (a,b) => a - b }) + + new Thread({ () => unit } : Runnable ) + new Thread({ () => unit } ) + + val cf = new java.util.concurrent.CompletableFuture[String] + cf.handle[Unit]({ + case (string, null) => unit + case (string, throwable) => unit + }) +}