From 0c5fba30235e44c4f536b088fa12723fc5f63cf5 Mon Sep 17 00:00:00 2001 From: Evan Girardin Date: Wed, 26 Apr 2023 02:49:31 -0400 Subject: [PATCH 1/3] Add experimental flexible types feature on top of explicit nulls Enabled by -Yflexible-types with -Yexplicit-nulls. A flexible type T! is a non-denotable type such that T <: T! <: T|Null and T|Null <: T! <: T. Here we patch return types and parameter types of Java methods and fields to use flexible types. This is unsound and kills subtyping transitivity but makes interop with Java play more nicely with the explicit nulls experimental feature (i.e. fewer nullability casts). Also adds a few tests for flexible types, mostly lifted from the explicit nulls tests. --- .../tools/dotc/config/ScalaSettings.scala | 1 + .../tools/dotc/core/CheckRealizable.scala | 1 + .../src/dotty/tools/dotc/core/Contexts.scala | 3 + .../tools/dotc/core/JavaNullInterop.scala | 71 +++++++++- .../tools/dotc/core/NullOpsDecorator.scala | 7 + .../tools/dotc/core/OrderingConstraint.scala | 3 + .../dotc/core/PatternTypeConstrainer.scala | 3 +- .../tools/dotc/core/SymDenotations.scala | 3 + .../dotty/tools/dotc/core/TypeComparer.scala | 121 +++++++++++------- .../dotty/tools/dotc/core/TypeErasure.scala | 6 + .../src/dotty/tools/dotc/core/TypeOps.scala | 2 +- .../src/dotty/tools/dotc/core/Types.scala | 57 ++++++++- .../tools/dotc/core/tasty/TreePickler.scala | 3 + .../src/dotty/tools/dotc/sbt/ExtractAPI.scala | 2 + .../src/dotty/tools/dotc/typer/Typer.scala | 11 ++ .../dotty/tools/dotc/CompilationTests.scala | 14 ++ .../tools/vulpix/TestConfiguration.scala | 6 +- tasty/src/dotty/tools/tasty/TastyFormat.scala | 1 + tests/flexible-types/flexible-types.iml | 11 ++ tests/flexible-types/neg/array.scala | 44 +++++++ .../pos-separate/interop-enum-src/Day_1.java | 6 + .../interop-enum-src/Planet_2.java | 19 +++ .../pos-separate/interop-enum-src/S_3.scala | 6 + tests/flexible-types/pos/i7883.scala | 16 +++ tests/flexible-types/pos/i8981.scala | 1 + .../pos/instanceof-nothing.scala | 26 ++++ .../pos/interop-array-src/J.java | 13 ++ .../pos/interop-array-src/S.scala | 25 ++++ .../pos/interop-compare-src/J.java | 5 + .../pos/interop-compare-src/S.scala | 10 ++ .../pos/interop-constructor-src/J.java | 6 + .../pos/interop-constructor-src/S.scala | 6 + .../pos/interop-constructor.scala | 7 + .../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-java-varargs-src/Names.java | 4 + .../pos/interop-java-varargs-src/S.scala | 19 +++ .../pos/interop-java-varargs.scala | 22 ++++ .../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 ++++++ .../pos/interop-propagate.scala | 18 +++ .../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 + .../pos/override-java-object-arg-src/J.java | 10 ++ .../pos/override-java-object-arg-src/S.scala | 33 +++++ .../pos/override-java-object-arg.scala | 43 +++++++ .../pos/override-java-varargs/J.java | 4 + .../pos/override-java-varargs/S.scala | 14 ++ .../flexible-types/pos/override-java/J1.java | 4 + .../flexible-types/pos/override-java/J2.java | 9 ++ .../flexible-types/pos/override-java/S1.scala | 9 ++ .../flexible-types/pos/override-java/S2.scala | 25 ++++ .../pos/override-type-params.scala | 18 +++ tests/flexible-types/run/array/J.java | 4 + tests/flexible-types/run/array/S.scala | 30 +++++ tests/flexible-types/run/erasure.scala | 8 ++ .../run/generic-java-array-src/JA.java | 13 ++ .../run/generic-java-array-src/Test.scala | 21 +++ tests/flexible-types/run/i11332.scala | 22 ++++ .../run/instanceof-nothing.scala | 26 ++++ .../run/interop-unsound-src/J.java | 17 +++ .../run/interop-unsound-src/S.scala | 33 +++++ 73 files changed, 1069 insertions(+), 60 deletions(-) create mode 100644 tests/flexible-types/flexible-types.iml create mode 100644 tests/flexible-types/neg/array.scala create mode 100644 tests/flexible-types/pos-separate/interop-enum-src/Day_1.java create mode 100644 tests/flexible-types/pos-separate/interop-enum-src/Planet_2.java create mode 100644 tests/flexible-types/pos-separate/interop-enum-src/S_3.scala create mode 100644 tests/flexible-types/pos/i7883.scala create mode 100644 tests/flexible-types/pos/i8981.scala create mode 100644 tests/flexible-types/pos/instanceof-nothing.scala create mode 100644 tests/flexible-types/pos/interop-array-src/J.java create mode 100644 tests/flexible-types/pos/interop-array-src/S.scala create mode 100644 tests/flexible-types/pos/interop-compare-src/J.java create mode 100644 tests/flexible-types/pos/interop-compare-src/S.scala create mode 100644 tests/flexible-types/pos/interop-constructor-src/J.java create mode 100644 tests/flexible-types/pos/interop-constructor-src/S.scala create mode 100644 tests/flexible-types/pos/interop-constructor.scala create mode 100644 tests/flexible-types/pos/interop-enum-src/Day.java create mode 100644 tests/flexible-types/pos/interop-enum-src/Planet.java create mode 100644 tests/flexible-types/pos/interop-enum-src/S.scala create mode 100644 tests/flexible-types/pos/interop-generics/J.java create mode 100644 tests/flexible-types/pos/interop-generics/S.scala create mode 100644 tests/flexible-types/pos/interop-java-varargs-src/Names.java create mode 100644 tests/flexible-types/pos/interop-java-varargs-src/S.scala create mode 100644 tests/flexible-types/pos/interop-java-varargs.scala create mode 100644 tests/flexible-types/pos/interop-match-src/J.java create mode 100644 tests/flexible-types/pos/interop-match-src/S.scala create mode 100644 tests/flexible-types/pos/interop-method-src/J.java create mode 100644 tests/flexible-types/pos/interop-method-src/S.scala create mode 100644 tests/flexible-types/pos/interop-ortype-src/J.java create mode 100644 tests/flexible-types/pos/interop-ortype-src/S.scala create mode 100644 tests/flexible-types/pos/interop-poly-src/J.java create mode 100644 tests/flexible-types/pos/interop-poly-src/S.scala create mode 100644 tests/flexible-types/pos/interop-propagate.scala create mode 100644 tests/flexible-types/pos/interop-sam-src/J.java create mode 100644 tests/flexible-types/pos/interop-sam-src/S.scala create mode 100644 tests/flexible-types/pos/interop-static-src/J.java create mode 100644 tests/flexible-types/pos/interop-static-src/S.scala create mode 100644 tests/flexible-types/pos/override-java-object-arg-src/J.java create mode 100644 tests/flexible-types/pos/override-java-object-arg-src/S.scala create mode 100644 tests/flexible-types/pos/override-java-object-arg.scala create mode 100644 tests/flexible-types/pos/override-java-varargs/J.java create mode 100644 tests/flexible-types/pos/override-java-varargs/S.scala create mode 100644 tests/flexible-types/pos/override-java/J1.java create mode 100644 tests/flexible-types/pos/override-java/J2.java create mode 100644 tests/flexible-types/pos/override-java/S1.scala create mode 100644 tests/flexible-types/pos/override-java/S2.scala create mode 100644 tests/flexible-types/pos/override-type-params.scala create mode 100644 tests/flexible-types/run/array/J.java create mode 100644 tests/flexible-types/run/array/S.scala create mode 100644 tests/flexible-types/run/erasure.scala create mode 100644 tests/flexible-types/run/generic-java-array-src/JA.java create mode 100644 tests/flexible-types/run/generic-java-array-src/Test.scala create mode 100644 tests/flexible-types/run/i11332.scala create mode 100644 tests/flexible-types/run/instanceof-nothing.scala create mode 100644 tests/flexible-types/run/interop-unsound-src/J.java create mode 100644 tests/flexible-types/run/interop-unsound-src/S.scala 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/S.scala b/tests/flexible-types/run/array/S.scala new file mode 100644 index 000000000000..70dbeae99a23 --- /dev/null +++ b/tests/flexible-types/run/array/S.scala @@ -0,0 +1,30 @@ +/* +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 + } +} +*/ + + + +@main def main() = { + 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 + } + } +} From 7fd70984279231118fb11a4a25cbf230a32abdfe Mon Sep 17 00:00:00 2001 From: Evan Girardin Date: Mon, 1 May 2023 14:21:42 -0400 Subject: [PATCH 2/3] Fix incorrectly-named run test for flexible types --- tests/flexible-types/run/array/S.scala | 30 ---------------------- tests/flexible-types/run/array/Test.scala | 31 +++++++++++++++++++++++ 2 files changed, 31 insertions(+), 30 deletions(-) delete mode 100644 tests/flexible-types/run/array/S.scala create mode 100644 tests/flexible-types/run/array/Test.scala diff --git a/tests/flexible-types/run/array/S.scala b/tests/flexible-types/run/array/S.scala deleted file mode 100644 index 70dbeae99a23..000000000000 --- a/tests/flexible-types/run/array/S.scala +++ /dev/null @@ -1,30 +0,0 @@ -/* -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 - } -} -*/ - - - -@main def main() = { - 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/array/Test.scala b/tests/flexible-types/run/array/Test.scala new file mode 100644 index 000000000000..f82059696b5f --- /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() = { + 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 + } +} From 5e9451f5b0327c33b7d74441e119700c9cb7be98 Mon Sep 17 00:00:00 2001 From: Evan Girardin Date: Mon, 1 May 2023 15:20:45 -0400 Subject: [PATCH 3/3] Ditto --- tests/flexible-types/run/array/Test.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/flexible-types/run/array/Test.scala b/tests/flexible-types/run/array/Test.scala index f82059696b5f..b37329cacbce 100644 --- a/tests/flexible-types/run/array/Test.scala +++ b/tests/flexible-types/run/array/Test.scala @@ -13,7 +13,7 @@ class Foo { object Test { - def main() = { + 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