diff --git a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala index c06aa304ef72..597307e708e6 100644 --- a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala +++ b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala @@ -379,6 +379,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. Flexible types essentially circumvent explicit nulls and force something resembling the old type system for Java interop.") val YcheckInit: Setting[Boolean] = BooleanSetting("-Ysafe-init", "Ensure safe initialization of objects") val YrequireTargetName: Setting[Boolean] = BooleanSetting("-Yrequire-targetName", "Warn if an operator is defined without a @targetName annotation") val YrecheckTest: Setting[Boolean] = BooleanSetting("-Yrecheck-test", "Run basic rechecking (internal test only)") diff --git a/compiler/src/dotty/tools/dotc/core/CheckRealizable.scala b/compiler/src/dotty/tools/dotc/core/CheckRealizable.scala index a61701eee2d7..0917b65ea1a2 100644 --- a/compiler/src/dotty/tools/dotc/core/CheckRealizable.scala +++ b/compiler/src/dotty/tools/dotc/core/CheckRealizable.scala @@ -122,6 +122,7 @@ class CheckRealizable(using Context) { case tp: TypeProxy => isConcrete(tp.underlying) case tp: AndType => isConcrete(tp.tp1) && isConcrete(tp.tp2) case tp: OrType => isConcrete(tp.tp1) && isConcrete(tp.tp2) + case tp: FlexibleType => isConcrete(tp.underlying) case _ => false } if (!isConcrete(tp)) NotConcrete diff --git a/compiler/src/dotty/tools/dotc/core/Contexts.scala b/compiler/src/dotty/tools/dotc/core/Contexts.scala index e0e43169820a..1e6d0057c166 100644 --- a/compiler/src/dotty/tools/dotc/core/Contexts.scala +++ b/compiler/src/dotty/tools/dotc/core/Contexts.scala @@ -453,6 +453,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.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 60fc4a4274e0..772497203bfb 100644 --- a/compiler/src/dotty/tools/dotc/core/JavaNullInterop.scala +++ b/compiler/src/dotty/tools/dotc/core/JavaNullInterop.scala @@ -63,10 +63,10 @@ object JavaNullInterop { // Don't nullify the return type of the `toString` method. // Don't nullify the return type of constructors. // Don't nullify the return type of methods with a not-null annotation. - nullifyExceptReturnType(tp) + nullifyExceptReturnType(tp, sym.owner.isClass) else // Otherwise, nullify everything - nullifyType(tp) + nullifyType(tp, sym.owner.isClass) } private def hasNotNullAnnot(sym: Symbol)(using Context): Boolean = @@ -77,12 +77,12 @@ object JavaNullInterop { * If tp is a type of a field, the inside of the type is nullified, * but the result type is not nullable. */ - private def nullifyExceptReturnType(tp: Type)(using Context): Type = - new JavaNullMap(true)(tp) + private def nullifyExceptReturnType(tp: Type, ownerIsClass: Boolean)(using Context): Type = + if ctx.flexibleTypes /*&& ownerIsClass*/ then new JavaFlexibleMap(true)(tp) else new JavaNullMap(true)(tp) // FLEX PARAMS /** Nullifies a Java type by adding `| Null` in the relevant places. */ - private def nullifyType(tp: Type)(using Context): Type = - new JavaNullMap(false)(tp) + private def nullifyType(tp: Type, ownerIsClass: Boolean)(using Context): Type = + if ctx.flexibleTypes /*&& ownerIsClass*/ then new JavaFlexibleMap(false)(tp) else new JavaNullMap(false)(tp) // FLEX PARAMS /** 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. @@ -146,4 +146,63 @@ object JavaNullInterop { case _ => tp } } + + /** + * Flexible types + */ + + private class JavaFlexibleMap(var outermostLevelAlreadyNullable: Boolean)(using Context) extends TypeMap { + /** Should we nullify `tp` at the outermost level? */ + def needsFlexible(tp: Type): Boolean = + !outermostLevelAlreadyNullable && (tp match { + case tp: TypeRef => + // We don't modify value types because they're non-nullable even in Java. + !tp.symbol.isValueClass && + // We don't modify `Any` because it's already nullable. + !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 + }) + + override def apply(tp: Type): Type = tp match { + case tp: TypeRef if needsFlexible(tp) => + //println(Thread.currentThread().getStackTrace()(3).getMethodName()) + FlexibleType(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. + // This is because Java classes are _all_ nullified, so both `java.util.List[String]` and + // `java.util.List[String|Null]` contain nullable elements. + outermostLevelAlreadyNullable = tp.classSymbol.is(JavaDefined) + val targs2 = targs map this + outermostLevelAlreadyNullable = oldOutermostNullable + val appTp2 = derivedAppliedType(appTp, tycon, targs2) + if needsFlexible(tycon) then FlexibleType(appTp2) else appTp2 + case ptp: PolyType => + derivedLambdaType(ptp)(ptp.paramInfos, this(ptp.resType)) + case mtp: MethodType => + val oldOutermostNullable = outermostLevelAlreadyNullable + outermostLevelAlreadyNullable = false + val paramInfos2 = mtp.paramInfos map this /*new JavaNullMap(outermostLevelAlreadyNullable)*/ // FLEX PARAMS + outermostLevelAlreadyNullable = oldOutermostNullable + derivedLambdaType(mtp)(paramInfos2, this(mtp.resType)) + case tp: TypeAlias => mapOver(tp) + case tp: AndType => + // 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 + FlexibleType(derivedAndType(tp, this(tp.tp1), this(tp.tp2))) + case tp: TypeParamRef if needsFlexible(tp) => + FlexibleType(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 + } + } } diff --git a/compiler/src/dotty/tools/dotc/core/NullOpsDecorator.scala b/compiler/src/dotty/tools/dotc/core/NullOpsDecorator.scala index e18271772ff1..2ede75f5cec5 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(tp1) => strip(tp1) 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 faea30390d2b..92b982bb7042 100644 --- a/compiler/src/dotty/tools/dotc/core/OrderingConstraint.scala +++ b/compiler/src/dotty/tools/dotc/core/OrderingConstraint.scala @@ -560,6 +560,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 5e8a960608e6..614813a2f6d9 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 => @@ -175,7 +176,7 @@ trait PatternTypeConstrainer { self: TypeComparer => case tp => tp } - dealiasDropNonmoduleRefs(scrut) match { + dealiasDropNonmoduleRefs(scrut.stripFlexible) match { case OrType(scrut1, scrut2) => either(constrainPatternType(pat, scrut1), constrainPatternType(pat, scrut2)) case AndType(scrut1, scrut2) => diff --git a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala index aa97435d64bb..6ced82ebdb55 100644 --- a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala +++ b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala @@ -2243,6 +2243,9 @@ object SymDenotations { case CapturingType(parent, refs) => tp.derivedCapturingType(recur(parent), refs) + case tp: FlexibleType => + recur(tp.underlying) + case tp: TypeProxy => def computeTypeProxy = { val superTp = tp.superType diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index 6857e3da38ed..b4642f02179e 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -382,6 +382,10 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling case OrType(tp21, tp22) => if (tp21.stripTypeVar eq tp22.stripTypeVar) recur(tp1, tp21) else secondTry + // tp1 <: Flex(T) = T|N..T + // iff tp1 <: T|N + case tp2: FlexibleType => + recur(tp1, tp2.lo) case TypeErasure.ErasedValueType(tycon1, underlying2) => def compareErasedValueType = tp1 match { case TypeErasure.ErasedValueType(tycon2, underlying1) => @@ -530,7 +534,10 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling hardenTypeVars(tp2) res - + // invariant: tp2 is NOT a FlexibleType + // is Flex(T) <: tp2? + case tp1: FlexibleType => + recur(tp1.underlying, tp2) case CapturingType(parent1, refs1) => if tp2.isAny then true else if subCaptures(refs1, tp2.captureSet, frozenConstraint).isOK && sameBoxed(tp1, tp2, refs1) @@ -2509,53 +2516,73 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling /** Try to distribute `&` inside type, detect and handle conflicts * @pre !(tp1 <: tp2) && !(tp2 <:< tp1) -- these cases were handled before */ - private def distributeAnd(tp1: Type, tp2: Type): Type = tp1 match { - case tp1 @ AppliedType(tycon1, args1) => - tp2 match { - case AppliedType(tycon2, args2) - if tycon1.typeSymbol == tycon2.typeSymbol && tycon1 =:= tycon2 => - val jointArgs = glbArgs(args1, args2, tycon1.typeParams) - if (jointArgs.forall(_.exists)) (tycon1 & tycon2).appliedTo(jointArgs) - else NoType - case _ => - NoType - } - case tp1: RefinedType => - // opportunistically merge same-named refinements - // this does not change anything semantically (i.e. merging or not merging - // gives =:= types), but it keeps the type smaller. - tp2 match { - case tp2: RefinedType if tp1.refinedName == tp2.refinedName => - val jointInfo = Denotations.infoMeet(tp1.refinedInfo, tp2.refinedInfo, safeIntersection = false) - if jointInfo.exists then - tp1.derivedRefinedType(tp1.parent & tp2.parent, tp1.refinedName, jointInfo) - else + private def distributeAnd(tp1: Type, tp2: Type): Type = { + var ft1 = false + var ft2 = false + def recur(tp1: Type, tp2: Type): Type = tp1 match { + case tp1 @ FlexibleType(tp) => + // Hack -- doesn't generalise to other intersection/union types + // but covers a common special case for pattern matching + ft1 = true + recur(tp, tp2) + case tp1 @ AppliedType(tycon1, args1) => + tp2 match { + case AppliedType(tycon2, args2) + if tycon1.typeSymbol == tycon2.typeSymbol && tycon1 =:= tycon2 => + val jointArgs = glbArgs(args1, args2, tycon1.typeParams) + if (jointArgs.forall(_.exists)) (tycon1 & tycon2).appliedTo(jointArgs) + else { + NoType + } + case FlexibleType(tp) => + // Hack from above + ft2 = true + recur(tp1, tp) + case _ => NoType - case _ => - NoType - } - case tp1: RecType => - tp1.rebind(distributeAnd(tp1.parent, tp2)) - case ExprType(rt1) => - tp2 match { - case ExprType(rt2) => - ExprType(rt1 & rt2) - case _ => - NoType - } - case tp1: TypeVar if tp1.isInstantiated => - tp1.underlying & tp2 - case CapturingType(parent1, refs1) => - if subCaptures(tp2.captureSet, refs1, frozen = true).isOK - && tp1.isBoxedCapturing == tp2.isBoxedCapturing - then - parent1 & tp2 - else - tp1.derivedCapturingType(parent1 & tp2, refs1) - case tp1: AnnotatedType if !tp1.isRefining => - tp1.underlying & tp2 - case _ => - NoType + } + + // if result exists and is not notype, maybe wrap result in flex based on whether seen flex on both sides + case tp1: RefinedType => + // opportunistically merge same-named refinements + // this does not change anything semantically (i.e. merging or not merging + // gives =:= types), but it keeps the type smaller. + tp2 match { + case tp2: RefinedType if tp1.refinedName == tp2.refinedName => + val jointInfo = Denotations.infoMeet(tp1.refinedInfo, tp2.refinedInfo, safeIntersection = false) + if jointInfo.exists then + tp1.derivedRefinedType(tp1.parent & tp2.parent, tp1.refinedName, jointInfo) + else + NoType + case _ => + NoType + } + case tp1: RecType => + tp1.rebind(recur(tp1.parent, tp2)) + case ExprType(rt1) => + tp2 match { + case ExprType(rt2) => + ExprType(rt1 & rt2) + case _ => + NoType + } + case tp1: TypeVar if tp1.isInstantiated => + tp1.underlying & tp2 + case CapturingType(parent1, refs1) => + if subCaptures(tp2.captureSet, refs1, frozen = true).isOK + && tp1.isBoxedCapturing == tp2.isBoxedCapturing + then + parent1 & tp2 + else + tp1.derivedCapturingType(parent1 & tp2, refs1) + case tp1: AnnotatedType if !tp1.isRefining => + tp1.underlying & tp2 + case _ => + NoType + } + // if flex on both sides, return flex type + val ret = recur(tp1, tp2) + if (ft1 && ft2) then FlexibleType(ret) else ret } /** Try to distribute `|` inside type, detect and handle conflicts diff --git a/compiler/src/dotty/tools/dotc/core/TypeErasure.scala b/compiler/src/dotty/tools/dotc/core/TypeErasure.scala index 9bcb3eca36bb..3bdaf9cee352 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeErasure.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeErasure.scala @@ -311,6 +311,8 @@ object TypeErasure { repr1.orElse(repr2) else NoSymbol + case tp: FlexibleType => + arrayUpperBound(tp.underlying) case _ => NoSymbol @@ -337,6 +339,8 @@ object TypeErasure { isGenericArrayElement(tp.tp1, isScala2) && isGenericArrayElement(tp.tp2, isScala2) case tp: OrType => isGenericArrayElement(tp.tp1, isScala2) || isGenericArrayElement(tp.tp2, isScala2) + case tp: FlexibleType => + isGenericArrayElement(tp.underlying, isScala2) case _ => false } } @@ -526,6 +530,7 @@ object TypeErasure { case tp: TypeProxy => hasStableErasure(tp.translucentSuperType) case tp: AndType => hasStableErasure(tp.tp1) && hasStableErasure(tp.tp2) case tp: OrType => hasStableErasure(tp.tp1) && hasStableErasure(tp.tp2) + case _: FlexibleType => false case _ => false } @@ -622,6 +627,7 @@ class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConst erasePolyFunctionApply(refinedInfo) case RefinedType(parent, nme.apply, refinedInfo: MethodType) if defn.isErasedFunctionType(parent) => eraseErasedFunctionApply(refinedInfo) + case FlexibleType(tp) => this(tp) case tp: TypeProxy => this(tp.underlying) case tp @ AndType(tp1, tp2) => diff --git a/compiler/src/dotty/tools/dotc/core/TypeOps.scala b/compiler/src/dotty/tools/dotc/core/TypeOps.scala index 6809e4b9083c..ebed947f3959 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeOps.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeOps.scala @@ -52,7 +52,6 @@ object TypeOps: Stats.record("asSeenFrom skolem prefix required") case _ => } - new AsSeenFromMap(pre, cls).apply(tp) } @@ -237,6 +236,7 @@ object TypeOps: if tp1.isBottomType && (tp1 frozen_<:< tp2) then orBaseClasses(tp2) else if tp2.isBottomType && (tp2 frozen_<:< tp1) then orBaseClasses(tp1) else intersect(orBaseClasses(tp1), orBaseClasses(tp2)) + case FlexibleType(tp1) => orBaseClasses(tp1) case _ => tp.baseClasses /** The minimal set of classes in `cs` which derive all other classes in `cs` */ diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index fe0fc8a6dc2d..2713468da672 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -363,6 +363,7 @@ object Types { 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 tp: FlexibleType => tp.underlying.unusableForInference || tp.lo.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 @@ -687,13 +688,17 @@ object Types { }) case tp: TypeRef => tp.denot match { - case d: ClassDenotation => d.findMember(name, pre, required, excluded) + case d: ClassDenotation => + d.findMember(name, pre, required, excluded) case d => go(d.info) } + case tp: FlexibleType => + go(tp.underlying) case tp: AppliedType => tp.tycon match { case tc: TypeRef => - if (tc.symbol.isClass) go(tc) + if (tc.symbol.isClass) then + go(tc) else { val normed = tp.tryNormalize go(if (normed.exists) normed else tp.superType) @@ -1363,6 +1368,7 @@ object Types { case tp: ExprType => tp.resType.atoms case tp: OrType => tp.atoms // `atoms` overridden in OrType case tp: AndType => tp.tp1.atoms & tp.tp2.atoms + case tp: FlexibleType => tp.underlying.atoms case tp: TypeRef if tp.symbol.is(ModuleClass) => // The atom of a module class is the module itself, // this corresponds to the special case in TypeComparer @@ -1593,6 +1599,10 @@ object Types { /** The type with given denotation, reduced if possible. */ def select(name: Name, denot: Denotation)(using Context): Type = + /*println("select:") + println(name) + println(denot) + println(NamedType(this, name, denot).reduceProjection) // "method get" for level12.get(0)*/ NamedType(this, name, denot).reduceProjection /** The type , reduced if possible */ @@ -1676,7 +1686,9 @@ object Types { } /** Is this (an alias of) the `scala.Null` type? */ - final def isNullType(using Context) = isRef(defn.NullClass) + final def isNullType(using Context) = { + isRef(defn.NullClass) + } /** Is this (an alias of) the `scala.Nothing` type? */ final def isNothingType(using Context) = isRef(defn.NothingClass) @@ -2403,6 +2415,7 @@ object Types { private def argDenot(param: TypeSymbol)(using Context): Denotation = { val cls = param.owner val args = prefix.baseType(cls).argInfos + //throw new RuntimeException("") val typeParams = cls.typeParams def concretize(arg: Type, tparam: TypeSymbol) = arg match { @@ -2429,6 +2442,7 @@ object Types { } else { if (!ctx.reporter.errorsReported) + //throw RuntimeException("") throw TypeError( em"""bad parameter reference $this at ${ctx.phase} |the parameter is ${param.showLocated} but the prefix $prefix @@ -3286,8 +3300,29 @@ object Types { } } + // --- FlexibleType ----------------------------------------------------------------- + + case class FlexibleType(tp: Type) extends CachedGroundType with ValueType { + def hi(using Context) = { + this.tp + } + def lo(using Context) = { + OrNull(this.tp) + } + override def show(using Context) = "FlexibleType("+tp.show+")" + def underlying(using Context) : Type = this.tp + def derivedFlexibleType(under: Type)(using Context): Type = + FlexibleType(under) + override def computeHash(bs: Binders): Int = doHash(bs, tp) + override def toString = "FlexibleType(%s)".format(tp) + //override def hash = NotCached + } + // --- AndType/OrType --------------------------------------------------------------- + // -Vprint:all, -Vprint:typer, -Xprint-types + // use obj.show to pretty-print struct + abstract class AndOrType extends CachedGroundType with ValueType { def isAnd: Boolean def tp1: Type @@ -5650,6 +5685,8 @@ object Types { 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) @@ -5771,6 +5808,9 @@ object Types { 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)) @@ -6059,6 +6099,14 @@ object Types { 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) => @@ -6200,6 +6248,9 @@ object Types { 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 8a396921f32b..ce3ee9163808 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) { 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/sbt/ExtractAPI.scala b/compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala index f54baeb7256c..35f9395da725 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/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 4bc012b5b226..e5b79229d3a0 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -4102,6 +4102,17 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer case closure(Nil, id @ Ident(nme.ANON_FUN), _) if defn.isFunctionType(wtp) && !defn.isFunctionType(pt) => pt match { + case FlexibleType(tp) => tp match { + // recurse once inside + case SAMType(sam) + if wtp <:< sam.toFunctionType(isJava = pt.classSymbol.is(JavaDefined)) => + // was ... && isFullyDefined(pt, ForceDegree.flipBottom) + // but this prevents case blocks from implementing polymorphic partial functions, + // since we do not know the result parameter a priori. Have to wait until the + // body is typechecked. + return toSAM(tree) + case _ => + } case SAMType(sam) if wtp <:< sam.toFunctionType(isJava = pt.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 ed531aa404c2..2f8a57f66440 100644 --- a/compiler/test/dotty/tools/dotc/CompilationTests.scala +++ b/compiler/test/dotty/tools/dotc/CompilationTests.scala @@ -266,6 +266,20 @@ class CompilationTests { compileFilesInDir("tests/explicit-nulls/run", explicitNullsOptions) }.checkRuns() + // Flexible types tests + @Test def flexibleTypesRun: Unit = { + implicit val testGroup: TestGroup = TestGroup("flexibleTypesRun") + compileFilesInDir("tests/flexible-types/run", flexibleTypesOptions) + }.checkRuns() + + @Test def flexibleTypesPos: Unit = { + implicit val testGroup: TestGroup = TestGroup("flexibleTypesPos") + aggregateTests( + compileFilesInDir("tests/flexible-types/pos", flexibleTypesOptions), + compileFilesInDir("tests/flexible-types/pos-separate", flexibleTypesOptions) + ) + }.checkCompile() + // initialization tests @Test def checkInit: Unit = { implicit val testGroup: TestGroup = TestGroup("checkInit") diff --git a/compiler/test/dotty/tools/vulpix/TestConfiguration.scala b/compiler/test/dotty/tools/vulpix/TestConfiguration.scala index 5d2992b50a09..b4e47e143234 100644 --- a/compiler/test/dotty/tools/vulpix/TestConfiguration.scala +++ b/compiler/test/dotty/tools/vulpix/TestConfiguration.scala @@ -20,7 +20,7 @@ object TestConfiguration { // "-Yscala2-unpickler", s"${Properties.scalaLibrary}", "-Yno-deep-subtypes", "-Yno-double-bindings", - "-Yforce-sbt-phases", + //"-Yforce-sbt-phases", "-Xsemanticdb", "-Xverify-signatures" ) @@ -63,7 +63,7 @@ object TestConfiguration { val yCheckOptions = Array("-Ycheck:all") - val commonOptions = Array("-indent") ++ checkOptions ++ noCheckOptions ++ yCheckOptions + val commonOptions = Array("-indent") ++ checkOptions ++ noCheckOptions// ++ yCheckOptions val defaultOptions = TestFlags(basicClasspath, commonOptions) val unindentOptions = TestFlags(basicClasspath, Array("-no-indent") ++ checkOptions ++ noCheckOptions ++ yCheckOptions) val withCompilerOptions = @@ -92,6 +92,8 @@ object TestConfiguration { /** Enables explicit nulls */ 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/tasty/src/dotty/tools/tasty/TastyFormat.scala b/tasty/src/dotty/tools/tasty/TastyFormat.scala index 226fc14acb39..66ba92e35e46 100644 --- a/tasty/src/dotty/tools/tasty/TastyFormat.scala +++ b/tasty/src/dotty/tools/tasty/TastyFormat.scala @@ -585,6 +585,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/flexible-types/flexible-types.iml b/tests/flexible-types/flexible-types.iml new file mode 100644 index 000000000000..8c9f1e01b3d7 --- /dev/null +++ b/tests/flexible-types/flexible-types.iml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/tests/flexible-types/neg/array.scala b/tests/flexible-types/neg/array.scala new file mode 100644 index 000000000000..83e39cc80929 --- /dev/null +++ b/tests/flexible-types/neg/array.scala @@ -0,0 +1,44 @@ +// Test array with nulls. + +class ArrayWithNulls { + def test1 = { + // A non-nullable array of non-nullable strings + val a1: Array[String] = Array("hello") + val s1: String = a1(0) + val s2: String | Null = a1(0) + val a2: Array[String] = Array() + + // Array type is non-nullable + val b1: Array[String] = null // error + val b2: Array[Int] = null // error + } + + def test2 = { + // A nullable array of non-nullable strings + val a1: Array[String] | Null = null + val a2: Array[String] | Null = Array() + val a3: Array[String] | Null = Array("") + val a4: Array[String] | Null = Array("", null) // error + + // A non-nullable array of nullable strings + val b1: Array[String | Null] = Array() + val b2: Array[String | Null] = Array(null) + val b3: Array[String | Null] = Array("") + val b4: Array[String | Null] = Array("", null) + val b5: Array[String | Null] = null // error + + val s1: String = b1(0) // error + val s2: String | Null = b1(0) + + // A nullable array of nullable strings + val c1: Array[String | Null] | Null = Array() + } + + def test3 = { + val a1: Array[String] = Array() + + val a2: Array[String] | Null = a1 + + val a3: Array[String | Null] = a1 // error + } +} diff --git a/tests/flexible-types/pos-separate/interop-enum-src/Day_1.java b/tests/flexible-types/pos-separate/interop-enum-src/Day_1.java new file mode 100644 index 000000000000..b5d96e446fa8 --- /dev/null +++ b/tests/flexible-types/pos-separate/interop-enum-src/Day_1.java @@ -0,0 +1,6 @@ + +public enum Day_1 { + SUN, + MON, + TUE +} diff --git a/tests/flexible-types/pos-separate/interop-enum-src/Planet_2.java b/tests/flexible-types/pos-separate/interop-enum-src/Planet_2.java new file mode 100644 index 000000000000..c46e92d13bea --- /dev/null +++ b/tests/flexible-types/pos-separate/interop-enum-src/Planet_2.java @@ -0,0 +1,19 @@ +public enum Planet_2 { + 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_2(double mass, double radius) { + this.mass = mass; + this.radius = radius; + } + private double mass() { return mass; } + private double radius() { return radius; } +} diff --git a/tests/flexible-types/pos-separate/interop-enum-src/S_3.scala b/tests/flexible-types/pos-separate/interop-enum-src/S_3.scala new file mode 100644 index 000000000000..3c5c8cd451ae --- /dev/null +++ b/tests/flexible-types/pos-separate/interop-enum-src/S_3.scala @@ -0,0 +1,6 @@ + +// Verify that enum values aren't nullified. +class S { + val d: Day_1 = Day_1.MON + val p: Planet_2 = Planet_2.MARS +} diff --git a/tests/flexible-types/pos/i7883.scala b/tests/flexible-types/pos/i7883.scala new file mode 100644 index 000000000000..7938c92dce1e --- /dev/null +++ b/tests/flexible-types/pos/i7883.scala @@ -0,0 +1,16 @@ +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 + } + + def headUnsafe(s: String, r: Regex): Option[(String, String)] = + import scala.language.unsafeNulls + s.trim match { + case r(hd, tl) => Some((hd, tl)) + case _ => None + } +} \ No newline at end of file diff --git a/tests/flexible-types/pos/i8981.scala b/tests/flexible-types/pos/i8981.scala new file mode 100644 index 000000000000..f72b508cf64e --- /dev/null +++ b/tests/flexible-types/pos/i8981.scala @@ -0,0 +1 @@ +class Foo extends javax.swing.JPanel \ No newline at end of file diff --git a/tests/flexible-types/pos/instanceof-nothing.scala b/tests/flexible-types/pos/instanceof-nothing.scala new file mode 100644 index 000000000000..ef5fc4ede841 --- /dev/null +++ b/tests/flexible-types/pos/instanceof-nothing.scala @@ -0,0 +1,26 @@ +// Check that calling `asInstanceOf[Nothing]` throws a ClassCastException. +// In particular, the compiler needs access to the right method to throw +// the exception, and identifying the method uses some explicit nulls related +// logic (see ClassCastExceptionClass in Definitions.scala). + +object Test { + def main(args: Array[String]): Unit = { + val x: String = "hello" + try { + val y: Nothing = x.asInstanceOf[Nothing] + assert(false) + } catch { + case e: ClassCastException => + // ok + } + + val n: Null = null + try { + val y: Nothing = n.asInstanceOf[Nothing] + assert(false) + } catch { + case e: ClassCastException => + // ok + } + } +} diff --git a/tests/flexible-types/pos/interop-array-src/J.java b/tests/flexible-types/pos/interop-array-src/J.java new file mode 100644 index 000000000000..741c3739b296 --- /dev/null +++ b/tests/flexible-types/pos/interop-array-src/J.java @@ -0,0 +1,13 @@ +class J { + void foo1(String[] ss) {} + + String[] foo2() { + return new String[]{""}; + } + + void bar1(int[] is) {} + + int[] bar2() { + return new int[]{0}; + } +} diff --git a/tests/flexible-types/pos/interop-array-src/S.scala b/tests/flexible-types/pos/interop-array-src/S.scala new file mode 100644 index 000000000000..6741ad50e351 --- /dev/null +++ b/tests/flexible-types/pos/interop-array-src/S.scala @@ -0,0 +1,25 @@ +class S { + + val j = new J() + + def f = { + val x1: Array[String|Null]|Null = ??? + j.foo1(x1) // error: expected Array[String | Null] but got Array[String] + + val x2: Array[String | Null] = ??? + j.foo1(x2) // ok + j.foo1(null) // ok + + val y1: Array[String] = j.foo2() // error + val y2: Array[String | Null] = j.foo2() // error: expected Array[String | Null] but got Array[String] + val y3: Array[String | Null] | Null = j.foo2() + } + + def g = { + val x1: Array[Int] = ??? + j.bar1(x1) // ok + + val y1: Array[Int] = j.bar2() // error + val y2: Array[Int] | Null = j.bar2() + } +} diff --git a/tests/flexible-types/pos/interop-compare-src/J.java b/tests/flexible-types/pos/interop-compare-src/J.java new file mode 100644 index 000000000000..fcd07e47764c --- /dev/null +++ b/tests/flexible-types/pos/interop-compare-src/J.java @@ -0,0 +1,5 @@ +class J { + public String foo(String s) { + return s; + } +} diff --git a/tests/flexible-types/pos/interop-compare-src/S.scala b/tests/flexible-types/pos/interop-compare-src/S.scala new file mode 100644 index 000000000000..3624a92180c6 --- /dev/null +++ b/tests/flexible-types/pos/interop-compare-src/S.scala @@ -0,0 +1,10 @@ +@main def main() = { + val j: J = new J + if (j.foo(null) == null) { + println("null") + } + val x = j.foo("a") + if (x == null) { + println("null") + } +} diff --git a/tests/flexible-types/pos/interop-constructor-src/J.java b/tests/flexible-types/pos/interop-constructor-src/J.java new file mode 100644 index 000000000000..b1590d50023e --- /dev/null +++ b/tests/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/flexible-types/pos/interop-constructor-src/S.scala b/tests/flexible-types/pos/interop-constructor-src/S.scala new file mode 100644 index 000000000000..3defd73f3945 --- /dev/null +++ b/tests/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/flexible-types/pos/interop-constructor.scala b/tests/flexible-types/pos/interop-constructor.scala new file mode 100644 index 000000000000..4ebfaa752b3a --- /dev/null +++ b/tests/flexible-types/pos/interop-constructor.scala @@ -0,0 +1,7 @@ +// Test that constructors have a non-nullable return type. + +class Foo { + val x: java.lang.String = new java.lang.String() + val y: java.util.Date = new java.util.Date() + val v = new java.util.Vector[String](null /*stands for Collection*/) +} diff --git a/tests/flexible-types/pos/interop-enum-src/Day.java b/tests/flexible-types/pos/interop-enum-src/Day.java new file mode 100644 index 000000000000..55dca0783931 --- /dev/null +++ b/tests/flexible-types/pos/interop-enum-src/Day.java @@ -0,0 +1,6 @@ + +public enum Day { + SUN, + MON, + TUE +} diff --git a/tests/flexible-types/pos/interop-enum-src/Planet.java b/tests/flexible-types/pos/interop-enum-src/Planet.java new file mode 100644 index 000000000000..287aed6aecc5 --- /dev/null +++ b/tests/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/flexible-types/pos/interop-enum-src/S.scala b/tests/flexible-types/pos/interop-enum-src/S.scala new file mode 100644 index 000000000000..75e4654869a4 --- /dev/null +++ b/tests/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/flexible-types/pos/interop-generics/J.java b/tests/flexible-types/pos/interop-generics/J.java new file mode 100644 index 000000000000..4bbdbd4cf319 --- /dev/null +++ b/tests/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/flexible-types/pos/interop-generics/S.scala b/tests/flexible-types/pos/interop-generics/S.scala new file mode 100644 index 000000000000..a5d885d7e664 --- /dev/null +++ b/tests/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/flexible-types/pos/interop-java-varargs-src/Names.java b/tests/flexible-types/pos/interop-java-varargs-src/Names.java new file mode 100644 index 000000000000..e46b406749ce --- /dev/null +++ b/tests/flexible-types/pos/interop-java-varargs-src/Names.java @@ -0,0 +1,4 @@ + +class Names { + static void setNames(String... names) {} +} diff --git a/tests/flexible-types/pos/interop-java-varargs-src/S.scala b/tests/flexible-types/pos/interop-java-varargs-src/S.scala new file mode 100644 index 000000000000..e867202e506d --- /dev/null +++ b/tests/flexible-types/pos/interop-java-varargs-src/S.scala @@ -0,0 +1,19 @@ +// Test that nullification can handle Java varargs. +// For varargs, the element type is nullified, but the top level argument isn't. + +class S { + // Pass an empty array. + Names.setNames() + + // Pass a singleton array with null as an element. + Names.setNames(null) + + // Pass a singleton array. + Names.setNames("name1") + + // Multiple arguments. + Names.setNames("name1", "name2", "name3", "name4") + + // Multiple arguments, some null. + Names.setNames(null, null, "hello", "world", null) +} diff --git a/tests/flexible-types/pos/interop-java-varargs.scala b/tests/flexible-types/pos/interop-java-varargs.scala new file mode 100644 index 000000000000..46dc388d02af --- /dev/null +++ b/tests/flexible-types/pos/interop-java-varargs.scala @@ -0,0 +1,22 @@ +import java.nio.file.* +import java.nio.file.Paths + +class S { + + // Paths.get is a Java method with two arguments, where the second one + // is a varargs: https://docs.oracle.com/javase/8/docs/api/java/nio/file/Paths.html + // static Path get(String first, String... more) + // The Scala compiler converts this signature into + // def get(first: String | Null, more: (String | Null)*) + + // Test that we can avoid providing the varargs argument altogether. + Paths.get("out") + + // Test with one argument in the varargs. + Paths.get("home", "src") + Paths.get("home", null) + + // Test multiple arguments in the varargs. + Paths.get("home", "src", "compiler", "src") + Paths.get("home", null, null, null) +} diff --git a/tests/flexible-types/pos/interop-match-src/J.java b/tests/flexible-types/pos/interop-match-src/J.java new file mode 100644 index 000000000000..16f99f082ebb --- /dev/null +++ b/tests/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/flexible-types/pos/interop-match-src/S.scala b/tests/flexible-types/pos/interop-match-src/S.scala new file mode 100644 index 000000000000..49e8852226ff --- /dev/null +++ b/tests/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/flexible-types/pos/interop-method-src/J.java b/tests/flexible-types/pos/interop-method-src/J.java new file mode 100644 index 000000000000..1b7ea514e4b2 --- /dev/null +++ b/tests/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/flexible-types/pos/interop-method-src/S.scala b/tests/flexible-types/pos/interop-method-src/S.scala new file mode 100644 index 000000000000..403c86bc4c06 --- /dev/null +++ b/tests/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/flexible-types/pos/interop-ortype-src/J.java b/tests/flexible-types/pos/interop-ortype-src/J.java new file mode 100644 index 000000000000..b0d767bccf3e --- /dev/null +++ b/tests/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/flexible-types/pos/interop-ortype-src/S.scala b/tests/flexible-types/pos/interop-ortype-src/S.scala new file mode 100644 index 000000000000..af3b44ab29a7 --- /dev/null +++ b/tests/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/flexible-types/pos/interop-poly-src/J.java b/tests/flexible-types/pos/interop-poly-src/J.java new file mode 100644 index 000000000000..a0d5c109605e --- /dev/null +++ b/tests/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/flexible-types/pos/interop-poly-src/S.scala b/tests/flexible-types/pos/interop-poly-src/S.scala new file mode 100644 index 000000000000..8aed9e99b689 --- /dev/null +++ b/tests/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/flexible-types/pos/interop-propagate.scala b/tests/flexible-types/pos/interop-propagate.scala new file mode 100644 index 000000000000..9be5ed26d8e3 --- /dev/null +++ b/tests/flexible-types/pos/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) + val level2: ArrayList[String] = ll.get(0).get(0) + val level3: String = ll.get(0).get(0).get(0) + + val lb = new ArrayList[ArrayList[ArrayList[String]]] + val levelA = lb.get(0) + val levelB = lb.get(0).get(0) + val levelC = lb.get(0).get(0).get(0) + + val x = levelA.get(0) + val y = levelB.get(0) + val z: String = levelA.get(0).get(0) +} diff --git a/tests/flexible-types/pos/interop-sam-src/J.java b/tests/flexible-types/pos/interop-sam-src/J.java new file mode 100644 index 000000000000..336e252aa861 --- /dev/null +++ b/tests/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/flexible-types/pos/interop-sam-src/S.scala b/tests/flexible-types/pos/interop-sam-src/S.scala new file mode 100644 index 000000000000..c0da89163018 --- /dev/null +++ b/tests/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/flexible-types/pos/interop-static-src/J.java b/tests/flexible-types/pos/interop-static-src/J.java new file mode 100644 index 000000000000..a233d9662950 --- /dev/null +++ b/tests/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/flexible-types/pos/interop-static-src/S.scala b/tests/flexible-types/pos/interop-static-src/S.scala new file mode 100644 index 000000000000..61694434d018 --- /dev/null +++ b/tests/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/flexible-types/pos/override-java-object-arg-src/J.java b/tests/flexible-types/pos/override-java-object-arg-src/J.java new file mode 100644 index 000000000000..efcb630b7b6c --- /dev/null +++ b/tests/flexible-types/pos/override-java-object-arg-src/J.java @@ -0,0 +1,10 @@ + +// Copy of https://docs.oracle.com/javase/7/docs/api/javax/management/NotificationListener.html + +class Notification {}; + +interface NotificationListener { + + void handleNotification(Notification notification, Object handback); + +} diff --git a/tests/flexible-types/pos/override-java-object-arg-src/S.scala b/tests/flexible-types/pos/override-java-object-arg-src/S.scala new file mode 100644 index 000000000000..757a3b6b1235 --- /dev/null +++ b/tests/flexible-types/pos/override-java-object-arg-src/S.scala @@ -0,0 +1,33 @@ +// This test is like tests/pos/override-java-object-arg.scala, except that +// here we load the Java code from source, as opposed to a class file. +// In this case, the Java 'Object' type is turned into 'AnyRef', not 'Any'. + +class S { + + def bar(): Unit = { + val listener = new NotificationListener() { + override def handleNotification(n: Notification|Null, emitter: Object|Null): Unit = { + } + } + + val listener2 = new NotificationListener() { + override def handleNotification(n: Notification|Null, emitter: AnyRef|Null): Unit = { + } + } + + val listener3 = new NotificationListener() { + override def handleNotification(n: Notification|Null, emitter: Object): Unit = { + } + } + + val listener4 = new NotificationListener() { + override def handleNotification(n: Notification, emitter: Object|Null): Unit = { + } + } + + val listener5 = new NotificationListener() { + override def handleNotification(n: Notification, emitter: Object): Unit = { + } + } + } +} diff --git a/tests/flexible-types/pos/override-java-object-arg.scala b/tests/flexible-types/pos/override-java-object-arg.scala new file mode 100644 index 000000000000..8c5a76e15a6c --- /dev/null +++ b/tests/flexible-types/pos/override-java-object-arg.scala @@ -0,0 +1,43 @@ +// When we load a Java class file, if a java method has an argument with type +// 'Object', it (the method argument) gets loaded by Dotty as 'Any' (as opposed to 'AnyRef'). +// This is pre-explicit-nulls behaviour. +// There is special logic in the type comparer that allows that method to be overridden +// with a corresponding argument with type 'AnyRef | Null' (or `Object | Null`). +// This test verifies that we can continue to override such methods, except that in +// the explicit nulls world we override with 'AnyRef|Null'. + +import javax.management.{Notification, NotificationEmitter, NotificationListener} + +class Foo { + + def bar(): Unit = { + val listener = new NotificationListener() { + // The second argument in the base interface is loaded with type 'Any', but we override + // it with 'AnyRef|Null'. + override def handleNotification(n: Notification|Null, emitter: Object|Null): Unit = { + } + } + + val listener2 = new NotificationListener() { + // The second argument in the base interface is loaded with type 'Any', but we override + // it with 'AnyRef|Null'. + override def handleNotification(n: Notification|Null, emitter: AnyRef|Null): Unit = { + } + } + + val listener3 = new NotificationListener() { + override def handleNotification(n: Notification|Null, emitter: Object): Unit = { + } + } + + val listener4 = new NotificationListener() { + override def handleNotification(n: Notification, emitter: Object|Null): Unit = { + } + } + + val listener5 = new NotificationListener() { + override def handleNotification(n: Notification, emitter: Object): Unit = { + } + } + } +} diff --git a/tests/flexible-types/pos/override-java-varargs/J.java b/tests/flexible-types/pos/override-java-varargs/J.java new file mode 100644 index 000000000000..24313aad2241 --- /dev/null +++ b/tests/flexible-types/pos/override-java-varargs/J.java @@ -0,0 +1,4 @@ +abstract class J { + abstract void foo(String... x); + abstract void bar(String x, String... y); +} \ No newline at end of file diff --git a/tests/flexible-types/pos/override-java-varargs/S.scala b/tests/flexible-types/pos/override-java-varargs/S.scala new file mode 100644 index 000000000000..bb98c86b455c --- /dev/null +++ b/tests/flexible-types/pos/override-java-varargs/S.scala @@ -0,0 +1,14 @@ +class S1 extends J { + override def foo(x: (String | Null)*): Unit = ??? + override def bar(x: String | Null, y: (String | Null)*): Unit = ??? +} + +class S2 extends J { + override def foo(x: String*): Unit = ??? + override def bar(x: String | Null, y: String*): Unit = ??? +} + +class S3 extends J { + override def foo(x: String*): Unit = ??? + override def bar(x: String, y: String*): Unit = ??? +} diff --git a/tests/flexible-types/pos/override-java/J1.java b/tests/flexible-types/pos/override-java/J1.java new file mode 100644 index 000000000000..0c66c26fdea9 --- /dev/null +++ b/tests/flexible-types/pos/override-java/J1.java @@ -0,0 +1,4 @@ +abstract class J1 { + abstract void foo1(String x); + abstract String foo2(); +} \ No newline at end of file diff --git a/tests/flexible-types/pos/override-java/J2.java b/tests/flexible-types/pos/override-java/J2.java new file mode 100644 index 000000000000..8ff04d59f54f --- /dev/null +++ b/tests/flexible-types/pos/override-java/J2.java @@ -0,0 +1,9 @@ +import java.util.List; + +abstract class J2 { + abstract void bar1(List xs); + abstract void bar2(List xss); + + abstract List bar3(); + abstract List bar4(); +} \ No newline at end of file diff --git a/tests/flexible-types/pos/override-java/S1.scala b/tests/flexible-types/pos/override-java/S1.scala new file mode 100644 index 000000000000..01a95c8e0ef7 --- /dev/null +++ b/tests/flexible-types/pos/override-java/S1.scala @@ -0,0 +1,9 @@ +class S1a extends J1 { + override def foo1(x: String | Null): Unit = ??? + override def foo2(): String | Null = ??? +} + +class S1b extends J1 { + override def foo1(x: String): Unit = ??? + override def foo2(): String = ??? +} \ No newline at end of file diff --git a/tests/flexible-types/pos/override-java/S2.scala b/tests/flexible-types/pos/override-java/S2.scala new file mode 100644 index 000000000000..ec440ca8f150 --- /dev/null +++ b/tests/flexible-types/pos/override-java/S2.scala @@ -0,0 +1,25 @@ +import java.util.List + +class S2a extends J2 { + override def bar1(xs: List[String] | Null): Unit = ??? + override def bar2(xss: List[Array[String | Null]] | Null): Unit = ??? + + override def bar3(): List[String] | Null = ??? + override def bar4(): List[Array[String | Null]] | Null = ??? +} + +class S2b extends J2 { + override def bar1(xs: List[String]): Unit = ??? + override def bar2(xss: List[Array[String | Null]]): Unit = ??? + + override def bar3(): List[String] = ??? + override def bar4(): List[Array[String | Null]] = ??? +} + +class S2c extends J2 { + override def bar1(xs: List[String]): Unit = ??? + override def bar2(xss: List[Array[String]]): Unit = ??? + + override def bar3(): List[String] = ??? + override def bar4(): List[Array[String]] = ??? +} \ No newline at end of file diff --git a/tests/flexible-types/pos/override-type-params.scala b/tests/flexible-types/pos/override-type-params.scala new file mode 100644 index 000000000000..7f59409a4c3c --- /dev/null +++ b/tests/flexible-types/pos/override-type-params.scala @@ -0,0 +1,18 @@ +// Testing relaxed overriding check for explicit nulls. +// The relaxed check is only enabled if one of the members is Java defined. + +import java.util.Comparator + +class C1[T <: AnyRef] extends Ordering[T]: + override def compare(o1: T, o2: T): Int = 0 + +// The following overriding is not allowed, because `compare` +// has already been declared in Scala class `Ordering`. +// class C2[T <: AnyRef] extends Ordering[T]: +// override def compare(o1: T | Null, o2: T | Null): Int = 0 + +class D1[T <: AnyRef] extends Comparator[T]: + override def compare(o1: T, o2: T): Int = 0 + +class D2[T <: AnyRef] extends Comparator[T]: + override def compare(o1: T | Null, o2: T | Null): Int = 0 diff --git a/tests/flexible-types/run/array/J.java b/tests/flexible-types/run/array/J.java new file mode 100644 index 000000000000..6fb5d625b872 --- /dev/null +++ b/tests/flexible-types/run/array/J.java @@ -0,0 +1,4 @@ +public class J { + public J(java.lang.String s) { + } +} diff --git a/tests/flexible-types/run/array/Test.scala b/tests/flexible-types/run/array/Test.scala new file mode 100644 index 000000000000..b37329cacbce --- /dev/null +++ b/tests/flexible-types/run/array/Test.scala @@ -0,0 +1,31 @@ +/* +class Foo { + def err(msg: String): Nothing = { + throw new RuntimeException("Hello") + } + def retTypeNothing(): String = { + val y: String|Null = ??? + if (y == null) err("y is null!") + y + } +} +*/ + + +object Test { + def main(args: Array[String]): Unit = { + val i : Integer = new Integer(3) // Constructor with non-ref arg + val s1 : String | Null = new String("abc") // Constructor with ref arg + val s2 : String = new String("abc") // Constructor with ref arg, not null + val s3 = s1.nn.substring(0,1).substring(0,1) + val s4 = s2.substring(0,1).substring(0,1) + val s5 = s4.startsWith(s4) + // s1.substring(0,1) // error + val j : J = new J("") + println(s4) + //val f : Foo = new Foo("x") + //f.err("Hello") + //val l : List[String] = Java.returnsNull(); + //val j : J = new J + } +} diff --git a/tests/flexible-types/run/erasure.scala b/tests/flexible-types/run/erasure.scala new file mode 100644 index 000000000000..1f43bde49a2e --- /dev/null +++ b/tests/flexible-types/run/erasure.scala @@ -0,0 +1,8 @@ +object Test { + def main(args: Array[String]): Unit = { + val v: Vector[String | Null] = Vector("a", "b") + val v2: Vector[String] = Vector("a", "b") + println(v) + println(v2) + } +} diff --git a/tests/flexible-types/run/generic-java-array-src/JA.java b/tests/flexible-types/run/generic-java-array-src/JA.java new file mode 100644 index 000000000000..ccca309d4f49 --- /dev/null +++ b/tests/flexible-types/run/generic-java-array-src/JA.java @@ -0,0 +1,13 @@ +class JA { + public static T get(T[] arr) { + return arr[0]; + } + + public static int getInt(int[] arr) { + return arr[0]; + } + + public static boolean getBool(boolean[] arr) { + return arr[0]; + } +} diff --git a/tests/flexible-types/run/generic-java-array-src/Test.scala b/tests/flexible-types/run/generic-java-array-src/Test.scala new file mode 100644 index 000000000000..26e4886d499b --- /dev/null +++ b/tests/flexible-types/run/generic-java-array-src/Test.scala @@ -0,0 +1,21 @@ +object Test { + def main(args: Array[String]): Unit = { + // This test shows that if we have a Java method that takes a generic array, + // then on the Scala side we'll need to pass a nullable array. + // i.e. with explicit nulls the previously-implicit cast becomes an explicit + // type annotation. + val x = new Array[Integer|Null](1) + x(0) = 10 + println(JA.get(x)) + + // However, if the Java method takes an array that's explicitly of a value type, + // then we can pass a non-nullable array from the Scala side. + val intArr = new Array[Int](1) + intArr(0) = 20 + println(JA.getInt(intArr)) + + val boolArr = new Array[Boolean](1) + boolArr(0) = true + println(JA.getBool(boolArr)) + } +} diff --git a/tests/flexible-types/run/i11332.scala b/tests/flexible-types/run/i11332.scala new file mode 100644 index 000000000000..73fb48839c16 --- /dev/null +++ b/tests/flexible-types/run/i11332.scala @@ -0,0 +1,22 @@ +// scalajs: --skip +import scala.language.unsafeNulls + +import java.lang.invoke._, MethodType.methodType + +// A copy of tests/run/i11332.scala +// to test the bootstrap minimisation which failed +// (because bootstrap runs under explicit nulls) +class Foo: + def neg(x: Int): Int = -x + +object Test: + def main(args: Array[String]): Unit = + val l = MethodHandles.lookup() + val self = new Foo() + + val res4 = { + l // explicit chain method call - previously derivedSelect broke the type + .findVirtual(classOf[Foo], "neg", methodType(classOf[Int], classOf[Int])) + .invokeExact(self, 4): Int + } + assert(-4 == res4) diff --git a/tests/flexible-types/run/instanceof-nothing.scala b/tests/flexible-types/run/instanceof-nothing.scala new file mode 100644 index 000000000000..ef5fc4ede841 --- /dev/null +++ b/tests/flexible-types/run/instanceof-nothing.scala @@ -0,0 +1,26 @@ +// Check that calling `asInstanceOf[Nothing]` throws a ClassCastException. +// In particular, the compiler needs access to the right method to throw +// the exception, and identifying the method uses some explicit nulls related +// logic (see ClassCastExceptionClass in Definitions.scala). + +object Test { + def main(args: Array[String]): Unit = { + val x: String = "hello" + try { + val y: Nothing = x.asInstanceOf[Nothing] + assert(false) + } catch { + case e: ClassCastException => + // ok + } + + val n: Null = null + try { + val y: Nothing = n.asInstanceOf[Nothing] + assert(false) + } catch { + case e: ClassCastException => + // ok + } + } +} diff --git a/tests/flexible-types/run/interop-unsound-src/J.java b/tests/flexible-types/run/interop-unsound-src/J.java new file mode 100644 index 000000000000..e06b22c3bae2 --- /dev/null +++ b/tests/flexible-types/run/interop-unsound-src/J.java @@ -0,0 +1,17 @@ + +class JavaBox { + T contents; + + JavaBox(T contents) { this.contents = contents; } +} + +class Forwarder { + + static > void putInJavaBox(T box, String s) { + box.contents = s; + } + + static > void putInScalaBox(T box, String s) { + box.setContents(s); + } +} diff --git a/tests/flexible-types/run/interop-unsound-src/S.scala b/tests/flexible-types/run/interop-unsound-src/S.scala new file mode 100644 index 000000000000..2e5eca0c1e5b --- /dev/null +++ b/tests/flexible-types/run/interop-unsound-src/S.scala @@ -0,0 +1,33 @@ +// An example that shows that the nullability transform is unsound. + +class ScalaBox[T](init: T) { + var contents: T = init + + def setContents(c: T): Unit = { + contents = c + } +} + +object Test { + + def main(args: Array[String]): Unit = { + val jb: JavaBox[String] = new JavaBox("hello") + val sb: ScalaBox[String] = ScalaBox("world") + + Forwarder.putInJavaBox(jb, null) // not unsound, becase JavaBox is java-defined + // so the contents fields will have a nullable + // type + + Forwarder.putInScalaBox(sb, null) // this is unsound, because ScalaBox + // should contain only Strings, but we added + // a null + + try { + sb.contents.length + assert(false) + } catch { + case ex: NullPointerException => + // expected + } + } +}