From fa957ca85edf105797cfd59c7d1a15e51f59983c Mon Sep 17 00:00:00 2001 From: HarrisL2 Date: Fri, 12 Sep 2025 10:22:37 -0400 Subject: [PATCH 01/11] Only nullfiy simple kinded types --- compiler/src/dotty/tools/dotc/core/ImplicitNullInterop.scala | 3 +++ 1 file changed, 3 insertions(+) diff --git a/compiler/src/dotty/tools/dotc/core/ImplicitNullInterop.scala b/compiler/src/dotty/tools/dotc/core/ImplicitNullInterop.scala index a72943f2128f..d4aea805b0fe 100644 --- a/compiler/src/dotty/tools/dotc/core/ImplicitNullInterop.scala +++ b/compiler/src/dotty/tools/dotc/core/ImplicitNullInterop.scala @@ -107,10 +107,13 @@ object ImplicitNullInterop { case tp: TypeRef if !tp.hasSimpleKind // We don't modify value types because they're non-nullable even in Java. || tp.symbol.isValueClass + || tp.isRef(defn.NullClass) + || tp.isRef(defn.NothingClass) // We don't modify unit types. || tp.isRef(defn.UnitClass) // We don't modify `Any` because it's already nullable. || tp.isRef(defn.AnyClass) => false + case tp: TypeParamRef if !tp.hasSimpleKind => false case _ => true // We don't nullify Java varargs at the top level. From f2af5702cc479c015ae31e6687f021c19eaf275d Mon Sep 17 00:00:00 2001 From: HarrisL2 Date: Fri, 12 Sep 2025 10:24:33 -0400 Subject: [PATCH 02/11] Add tests for nullification of higher kinded types --- tests/explicit-nulls/flexible-unpickle/Flexible_2.scala | 8 +++++++- tests/explicit-nulls/flexible-unpickle/Unsafe_1.scala | 5 +++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/tests/explicit-nulls/flexible-unpickle/Flexible_2.scala b/tests/explicit-nulls/flexible-unpickle/Flexible_2.scala index 20448d31c3aa..7fc56d1cc462 100644 --- a/tests/explicit-nulls/flexible-unpickle/Flexible_2.scala +++ b/tests/explicit-nulls/flexible-unpickle/Flexible_2.scala @@ -1,6 +1,6 @@ import unsafeNulls.Foo.* import unsafeNulls.Unsafe_1 -import unsafeNulls.{A, B, C, F, G, H, I, J, L, M, S, T, U, expects} +import unsafeNulls.{A, B, C, F, G, H, I, J, L, M, N, S, T, U, expects} import scala.reflect.Selectable.reflectiveSelectable import scala.quoted.* @@ -100,6 +100,11 @@ def Flexible_2() = val m: String = M.test(null) + // i23911 + val n1: List[Map[String, Int]] = ??? + val n2 = new N[List]() + val n3 = n2.accept[Any](n1) + // i23845 transparent inline def typeName[A]: String = ${typeNameMacro[A]} @@ -109,3 +114,4 @@ def Flexible_2() = implicit val givenS: S[A] = ??? expects(alphaTypeNameMacro[A]) } + diff --git a/tests/explicit-nulls/flexible-unpickle/Unsafe_1.scala b/tests/explicit-nulls/flexible-unpickle/Unsafe_1.scala index 6bec3dabf302..7f118d7805ad 100644 --- a/tests/explicit-nulls/flexible-unpickle/Unsafe_1.scala +++ b/tests/explicit-nulls/flexible-unpickle/Unsafe_1.scala @@ -73,6 +73,11 @@ object M { def test(input: => String): String = "foo " + input } + +class N[F[_]] { + def accept[A](arg: F[A]): Nothing = ??? +} + class S[X] object S { def show[X] = "dummyStr" } class T From a5427586f0ff134266c172d6dc51690ca7c1bd45 Mon Sep 17 00:00:00 2001 From: noti0na1 Date: Tue, 16 Sep 2025 20:22:00 +0200 Subject: [PATCH 03/11] Enhance nullification logic: ignore type annotations; ignore some special types --- .../dotty/tools/dotc/core/Definitions.scala | 4 +-- .../tools/dotc/core/ImplicitNullInterop.scala | 25 +++++++++++-------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index bb64f4c7ffa0..8906fa2fd263 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -1991,13 +1991,11 @@ class Definitions { * - the upper bound of a TypeParamRef in the current constraint */ def asContextFunctionType(tp: Type)(using Context): Type = - tp.stripTypeVar.dealias match + tp.stripNull().stripTypeVar.dealias match case tp1: TypeParamRef if ctx.typerState.constraint.contains(tp1) => asContextFunctionType(TypeComparer.bounds(tp1).hiBound) case tp1 @ PolyFunctionOf(mt: MethodType) if mt.isContextualMethod => tp1 - case tp1: FlexibleType => - asContextFunctionType(tp1.underlying) case tp1 => if tp1.typeSymbol.name.isContextFunction && isFunctionNType(tp1) then tp1 else NoType diff --git a/compiler/src/dotty/tools/dotc/core/ImplicitNullInterop.scala b/compiler/src/dotty/tools/dotc/core/ImplicitNullInterop.scala index d4aea805b0fe..cb4d0dd9b8b5 100644 --- a/compiler/src/dotty/tools/dotc/core/ImplicitNullInterop.scala +++ b/compiler/src/dotty/tools/dotc/core/ImplicitNullInterop.scala @@ -102,18 +102,19 @@ object ImplicitNullInterop { /** Should we nullify `tp` at the outermost level? */ def needsNull(tp: Type): Boolean = - if outermostLevelAlreadyNullable then false + if outermostLevelAlreadyNullable || !tp.hasSimpleKind then false else tp match - case tp: TypeRef if !tp.hasSimpleKind + case tp: TypeRef => // We don't modify value types because they're non-nullable even in Java. - || tp.symbol.isValueClass + !(tp.symbol.isValueClass + // We don't modify some special types. || tp.isRef(defn.NullClass) || tp.isRef(defn.NothingClass) - // We don't modify unit types. || tp.isRef(defn.UnitClass) - // We don't modify `Any` because it's already nullable. - || tp.isRef(defn.AnyClass) => false - case tp: TypeParamRef if !tp.hasSimpleKind => false + || tp.isRef(defn.SingletonClass) + || tp.isRef(defn.AnyValClass) + || tp.isRef(defn.AnyKindClass) + || tp.isRef(defn.AnyClass)) case _ => true // We don't nullify Java varargs at the top level. @@ -156,17 +157,19 @@ object ImplicitNullInterop { outermostLevelAlreadyNullable = true nullify(derivedAndType(tp, this(tp.tp1), this(tp.tp2))) case tp: TypeParamRef if needsNull(tp) => nullify(tp) - // In all other cases, return the type unchanged. - // In particular, if the type is a ConstantType, then we don't nullify it because it is the - // type of a final non-nullable field. case tp: ExprType => mapOver(tp) - case tp: AnnotatedType => mapOver(tp) + case tp: AnnotatedType => + // We don't nullify the annotation part. + derivedAnnotatedType(tp, this(tp.underlying), tp.annot) case tp: OrType => outermostLevelAlreadyNullable = true nullify(derivedOrType(tp, this(tp.tp1), this(tp.tp2))) case tp: RefinedType => outermostLevelAlreadyNullable = true nullify(mapOver(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 } } From e445207525ff92431ca29fb50c34c9cc1e84f36c Mon Sep 17 00:00:00 2001 From: noti0na1 Date: Tue, 16 Sep 2025 22:46:29 +0200 Subject: [PATCH 04/11] Refactor ImplicitNullInterop --- .../tools/dotc/core/ImplicitNullInterop.scala | 249 ++++++++++-------- 1 file changed, 136 insertions(+), 113 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/ImplicitNullInterop.scala b/compiler/src/dotty/tools/dotc/core/ImplicitNullInterop.scala index cb4d0dd9b8b5..0bef9dbccf8c 100644 --- a/compiler/src/dotty/tools/dotc/core/ImplicitNullInterop.scala +++ b/compiler/src/dotty/tools/dotc/core/ImplicitNullInterop.scala @@ -2,19 +2,25 @@ package dotty.tools.dotc package core import Contexts.* -import Flags.JavaDefined +import Flags.* import StdNames.nme import Symbols.* import Types.* -import dotty.tools.dotc.reporting.* -import dotty.tools.dotc.core.Decorators.i +import Decorators.i +import reporting.* -/** This module defines methods to interpret types of Java symbols, which are implicitly nullable in Java, - * as Scala types, which are explicitly nullable. +/** This module defines methods to interpret types originating from sources without explicit nulls + * (Java, and Scala code compiled without `-Yexplicit-nulls`) as Scala types with explicit nulls. + * In those sources, reference types are implicitly nullable; here we make that nullability explicit. + * + * e.g. given a Java method: `String foo(String arg) { return arg; }` + * + * After calling `nullifyMember`, Scala will see the method as: + * `def foo(arg: String | Null): String | Null` * * The transformation is (conceptually) a function `n` that adheres to the following rules: * (1) n(T) = T | Null if T is a reference type - * (2) n(T) = T if T is a value type + * (2) n(T) = T if T is a value type * (3) n(C[T]) = C[T] | Null if C is Java-defined * (4) n(C[T]) = C[n(T)] | Null if C is Scala-defined * (5) n(A|B) = n(A) | n(B) | Null @@ -29,148 +35,165 @@ import dotty.tools.dotc.core.Decorators.i * e.g. calling `get` on a `java.util.List[String]` already returns `String|Null` and not `String`, so * we don't need to write `java.util.List[String | Null]`. * - if `C` is Scala-defined, however, then we want `n(C[T]) = C[n(T)] | Null`. This is because - * `C` won't be nullified, so we need to indicate that its type argument is nullable. + * Scala-defined classes are not implicitly nullified inside their bodies, so we need to indicate that + * their type arguments are nullable when the defining source did not use explicit nulls. + * + * Why not use subtyping to nullify “exactly”? + * ------------------------------------------------- + * The symbols we nullify here are often still under construction (e.g. during classfile loading or unpickling), + * so we don't always have precise or stable type information available. Using full subtyping checks to determine + * which parts are reference types would either force types prematurely or risk cyclic initializations. Therefore, + * we use a conservative approach that targets concrete reference types without depending on precise subtype + * information. + * + * Scope and limitations + * ------------------------------------------------- + * The transformation is applied to types attached to members coming from Java and from Scala code compiled without + * explicit nulls. The implementation is intentionally conservative and does not attempt to cover the full spectrum + * of Scala types. In particular, we do not nullify type parameters or some complex type forms (e.g., match types, + * or refined types) beyond straightforward mapping; in such cases we typically recurse only into obviously safe + * positions or leave the type unchanged. * - * Notice that since the transformation is only applied to types attached to Java symbols, it doesn't need - * to handle the full spectrum of Scala types. Additionally, some kinds of symbols like constructors and - * enum instances get special treatment. + * Additionally, some kinds of symbols like constructors and enum instances get special treatment. */ -object ImplicitNullInterop { +object ImplicitNullInterop: - /** Transforms the type `tp` of Java member `sym` to be explicitly nullable. - * `tp` is needed because the type inside `sym` might not be set when this method is called. - * - * e.g. given a Java method - * String foo(String arg) { return arg; } - * - * After calling `nullifyMember`, Scala will see the method as - * - * def foo(arg: String | Null): String | Null - * - * If unsafeNulls is enabled, we can select on the return of `foo`: - * - * val len = foo("hello").length - * - * But the selection can throw an NPE if the returned value is `null`. + /** Transforms the type `tp` of a member `sym` that originates from a source without explicit nulls. + * `tp` is passed explicitly because the type stored in `sym` might not yet be set when this is called. */ - def nullifyMember(sym: Symbol, tp: Type, isEnumValueDef: Boolean)(using Context): Type = trace(i"nullifyMember ${sym}, ${tp}"){ + def nullifyMember(sym: Symbol, tp: Type, isEnumValueDef: Boolean)(using Context): Type = trace(i"nullifyMember ${sym}, ${tp}"): assert(ctx.explicitNulls) - // Some special cases when nullifying the type - if isEnumValueDef || sym.name == nme.TYPE_ // Don't nullify the `TYPE` field in every class and Java enum instances - || sym.is(Flags.ModuleVal) // Don't nullify Modules - then - tp - else if sym.name == nme.toString_ || sym.isConstructor || hasNotNullAnnot(sym) then - // 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) - else - // Otherwise, nullify everything - nullifyType(tp) - } + // Skip `TYPE`, enum values, and modules + if isEnumValueDef || sym.name == nme.TYPE_ || sym.is(Flags.ModuleVal) then + return tp - private def hasNotNullAnnot(sym: Symbol)(using Context): Boolean = - ctx.definitions.NotNullAnnots.exists(nna => sym.unforcedAnnotation(nna).isDefined) + // Skip result type for `toString`, constructors, and @NotNull methods + val skipResultType = sym.name == nme.toString_ || sym.isConstructor || hasNotNullAnnot(sym) + // Don't nullify parameter types for Scala-defined methods since those are already nullified + val skipParamTypes = sym.is(Method) && !sym.is(Flags.JavaDefined) + // Skip Given/implicit parameters + val skipCurrentLevel = sym.isOneOf(GivenOrImplicitVal) - /** If tp is a MethodType, the parameters and the inside of return type are nullified, - * but the result return type is not nullable. - * 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 ImplicitNullMap(outermostLevelAlreadyNullable = true)(tp) + val map = new ImplicitNullMap(skipResultType = skipResultType, skipParamTypes = skipParamTypes, skipCurrentLevel = skipCurrentLevel) + map(tp) - /** Nullifies a type by adding `| Null` in the relevant places. */ - private def nullifyType(tp: Type)(using Context): Type = - new ImplicitNullMap(outermostLevelAlreadyNullable = false)(tp) + private def hasNotNullAnnot(sym: Symbol)(using Context): Boolean = + ctx.definitions.NotNullAnnots.exists(nna => sym.unforcedAnnotation(nna).isDefined) - /** A type map that implements the nullification function on types. Given a Java-sourced type or an - * implicitly null type, this adds `| Null` in the right places to make the nulls explicit. + /** A type map that implements the nullification function on types. Given a Java-sourced type or a type + * coming from Scala code compiled without explicit nulls, this adds `| Null` or `FlexibleType` in the + * right places to make nullability explicit in a conservative way (without forcing incomplete symbols). * - * @param outermostLevelAlreadyNullable whether this type is already nullable at the outermost level. - * For example, `Array[String] | Null` is already nullable at the - * outermost level, but `Array[String | Null]` isn't. - * If this parameter is set to true, then the types of fields, and the return - * types of methods will not be nullified. - * This is useful for e.g. constructors, and also so that `A & B` is nullified - * to `(A & B) | Null`, instead of `(A | Null & B | Null) | Null`. + * @param skipResultType do not nullify the method result type at the outermost level (e.g. for `toString`, + * constructors, or methods annotated as not-null) + * @param skipParamTypes do not nullify parameter types for the current method (used for Scala-defined methods + * or specific parameter sections) + * @param skipCurrentLevel do not nullify at the current level (used for implicit/Given parameters, varargs, etc.) */ - private class ImplicitNullMap(var outermostLevelAlreadyNullable: Boolean)(using Context) extends TypeMap { + private class ImplicitNullMap( + var skipResultType: Boolean = false , + var skipParamTypes: Boolean = false , + var skipCurrentLevel: Boolean = false + )(using Context) extends TypeMap: + def nullify(tp: Type): Type = if ctx.flexibleTypes then FlexibleType(tp) else OrNull(tp) - /** Should we nullify `tp` at the outermost level? */ + /** Should we nullify `tp` at the outermost level? + * The symbols are still under construction, so we don't have precise information. + * We purposely do not rely on precise subtyping checks here (e.g., asking whether `tp <:< AnyRef`), + * because doing so could force incomplete symbols or trigger cycles. Instead, we conservatively + * nullify only when we can recognize a concrete reference type shape. + */ def needsNull(tp: Type): Boolean = - if outermostLevelAlreadyNullable || !tp.hasSimpleKind then false - else tp match + if skipCurrentLevel || !tp.hasSimpleKind then false + else tp.dealias match case tp: TypeRef => + val isValueOrSpecialClass = + tp.symbol.isValueClass + || tp.isRef(defn.NullClass) + || tp.isRef(defn.NullClass) + || tp.isRef(defn.NothingClass) + || tp.isRef(defn.UnitClass) + || tp.isRef(defn.SingletonClass) + || tp.isRef(defn.AnyKindClass) + || tp.isRef(defn.AnyClass) // We don't modify value types because they're non-nullable even in Java. - !(tp.symbol.isValueClass - // We don't modify some special types. - || tp.isRef(defn.NullClass) - || tp.isRef(defn.NothingClass) - || tp.isRef(defn.UnitClass) - || tp.isRef(defn.SingletonClass) - || tp.isRef(defn.AnyValClass) - || tp.isRef(defn.AnyKindClass) - || tp.isRef(defn.AnyClass)) - case _ => true + tp.symbol.isNullableClassAfterErasure && !isValueOrSpecialClass + case _ => false - // We don't nullify Java varargs at the top level. + // We don't nullify varargs (repeated parameters) 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. def tyconNeedsNull(tp: Type): Boolean = - if outermostLevelAlreadyNullable then false + if skipCurrentLevel then false else tp match case tp: TypeRef if !ctx.flexibleTypes && tp.isRef(defn.RepeatedParamClass) => false case _ => true - override def apply(tp: Type): Type = tp match { - case tp: TypeRef if needsNull(tp) => nullify(tp) + override def apply(tp: Type): Type = tp match + case tp: TypeRef if needsNull(tp) => + nullify(tp) case appTp @ AppliedType(tycon, targs) => - val oldOutermostNullable = outermostLevelAlreadyNullable - // We don't make the outmost levels of type arguments nullable if tycon is Java-defined. - // 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 savedSkipCurrentLevel = skipCurrentLevel + + // If Java-defined tycon, don't nullify outer level of type args (Java classes are fully nullified) + skipCurrentLevel = tp.classSymbol.is(JavaDefined) + val targs2 = targs.map(this) + + skipCurrentLevel = savedSkipCurrentLevel val appTp2 = derivedAppliedType(appTp, tycon, targs2) if tyconNeedsNull(tycon) then nullify(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 - outermostLevelAlreadyNullable = oldOutermostNullable - derivedLambdaType(mtp)(paramInfos2, this(mtp.resType)) - case tp: TypeAlias => mapOver(tp) - case tp: TypeBounds => 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 - nullify(derivedAndType(tp, this(tp.tp1), this(tp.tp2))) - case tp: TypeParamRef if needsNull(tp) => nullify(tp) - case tp: ExprType => mapOver(tp) + val savedSkipCurrentLevel = skipCurrentLevel + + // Skip param types for implicit/using sections + val skipThisParamList = skipParamTypes || mtp.isImplicitMethod + skipCurrentLevel = skipThisParamList + val paramInfos2 = mtp.paramInfos.map(this) + + skipCurrentLevel = skipResultType + val resType2 = this(mtp.resType) + + skipCurrentLevel = savedSkipCurrentLevel + derivedLambdaType(mtp)(paramInfos2, resType2) + case tp: TypeAlias => + mapOver(tp) + case tp: TypeBounds => + mapOver(tp) + case tp: AndOrType => + // For unions/intersections we recurse into constituents but do not force an outer `| Null` here; + // outer nullability is handled by the surrounding context. This keeps the result minimal and avoids + // duplicating `| Null` on both sides and at the outer level. + mapOver(tp) + case tp: ExprType => + mapOver(tp) case tp: AnnotatedType => // We don't nullify the annotation part. derivedAnnotatedType(tp, this(tp.underlying), tp.annot) - case tp: OrType => - outermostLevelAlreadyNullable = true - nullify(derivedOrType(tp, this(tp.tp1), this(tp.tp2))) case tp: RefinedType => - outermostLevelAlreadyNullable = true - nullify(mapOver(tp)) - // In all other cases, return the type unchanged. - // In particular, if the type is a ConstantType, then we don't nullify it because it is the - // type of a final non-nullable field. - case _ => tp - } - } -} \ No newline at end of file + val savedSkipCurrentLevel = skipCurrentLevel + + // Nullify parent at outer level; not refined members + skipCurrentLevel = true + val parent2 = this(tp.parent) + + skipCurrentLevel = false + val refinedInfo2 = this(tp.refinedInfo) + + skipCurrentLevel = savedSkipCurrentLevel + derivedRefinedType(tp, parent2, refinedInfo2) + case _ => + // 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. We also deliberately do not attempt to nullify + // complex computed types such as match types here; those remain as-is to avoid forcing + // incomplete information during symbol construction. + tp + end apply + end ImplicitNullMap \ No newline at end of file From 75dd9d42cb02729dfa4fbca9ad993ac2638e59d7 Mon Sep 17 00:00:00 2001 From: noti0na1 Date: Tue, 16 Sep 2025 23:00:26 +0200 Subject: [PATCH 05/11] Fix formatting and remove duplicate null class check --- compiler/src/dotty/tools/dotc/core/ImplicitNullInterop.scala | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/ImplicitNullInterop.scala b/compiler/src/dotty/tools/dotc/core/ImplicitNullInterop.scala index 0bef9dbccf8c..b698d31e5490 100644 --- a/compiler/src/dotty/tools/dotc/core/ImplicitNullInterop.scala +++ b/compiler/src/dotty/tools/dotc/core/ImplicitNullInterop.scala @@ -92,8 +92,8 @@ object ImplicitNullInterop: * @param skipCurrentLevel do not nullify at the current level (used for implicit/Given parameters, varargs, etc.) */ private class ImplicitNullMap( - var skipResultType: Boolean = false , - var skipParamTypes: Boolean = false , + var skipResultType: Boolean = false, + var skipParamTypes: Boolean = false, var skipCurrentLevel: Boolean = false )(using Context) extends TypeMap: @@ -112,7 +112,6 @@ object ImplicitNullInterop: val isValueOrSpecialClass = tp.symbol.isValueClass || tp.isRef(defn.NullClass) - || tp.isRef(defn.NullClass) || tp.isRef(defn.NothingClass) || tp.isRef(defn.UnitClass) || tp.isRef(defn.SingletonClass) From 3aa4b0d522231d2e2037d3dc3b3f8f8bd547fc55 Mon Sep 17 00:00:00 2001 From: noti0na1 Date: Wed, 17 Sep 2025 00:11:17 +0200 Subject: [PATCH 06/11] Always nullify type param refs from Java --- .../tools/dotc/core/ImplicitNullInterop.scala | 29 ++++++++++--------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/ImplicitNullInterop.scala b/compiler/src/dotty/tools/dotc/core/ImplicitNullInterop.scala index b698d31e5490..e163dfbf3533 100644 --- a/compiler/src/dotty/tools/dotc/core/ImplicitNullInterop.scala +++ b/compiler/src/dotty/tools/dotc/core/ImplicitNullInterop.scala @@ -68,14 +68,15 @@ object ImplicitNullInterop: if isEnumValueDef || sym.name == nme.TYPE_ || sym.is(Flags.ModuleVal) then return tp - // Skip result type for `toString`, constructors, and @NotNull methods + // Don't nullify result type for `toString`, constructors, and @NotNull methods val skipResultType = sym.name == nme.toString_ || sym.isConstructor || hasNotNullAnnot(sym) - // Don't nullify parameter types for Scala-defined methods since those are already nullified - val skipParamTypes = sym.is(Method) && !sym.is(Flags.JavaDefined) - // Skip Given/implicit parameters + // Don't nullify Given/implicit parameters val skipCurrentLevel = sym.isOneOf(GivenOrImplicitVal) - val map = new ImplicitNullMap(skipResultType = skipResultType, skipParamTypes = skipParamTypes, skipCurrentLevel = skipCurrentLevel) + val map = new ImplicitNullMap( + javaDefined = sym.is(JavaDefined), + skipResultType = skipResultType, + skipCurrentLevel = skipCurrentLevel) map(tp) private def hasNotNullAnnot(sym: Symbol)(using Context): Boolean = @@ -85,15 +86,14 @@ object ImplicitNullInterop: * coming from Scala code compiled without explicit nulls, this adds `| Null` or `FlexibleType` in the * right places to make nullability explicit in a conservative way (without forcing incomplete symbols). * + * @param javaDefined whether the type is from Java source, we always nullify type param refs from Java * @param skipResultType do not nullify the method result type at the outermost level (e.g. for `toString`, * constructors, or methods annotated as not-null) - * @param skipParamTypes do not nullify parameter types for the current method (used for Scala-defined methods - * or specific parameter sections) * @param skipCurrentLevel do not nullify at the current level (used for implicit/Given parameters, varargs, etc.) */ private class ImplicitNullMap( + val javaDefined: Boolean, var skipResultType: Boolean = false, - var skipParamTypes: Boolean = false, var skipCurrentLevel: Boolean = false )(using Context) extends TypeMap: @@ -109,6 +109,7 @@ object ImplicitNullInterop: if skipCurrentLevel || !tp.hasSimpleKind then false else tp.dealias match case tp: TypeRef => + // We don't modify value types because they're non-nullable even in Java. val isValueOrSpecialClass = tp.symbol.isValueClass || tp.isRef(defn.NullClass) @@ -117,8 +118,9 @@ object ImplicitNullInterop: || tp.isRef(defn.SingletonClass) || tp.isRef(defn.AnyKindClass) || tp.isRef(defn.AnyClass) - // We don't modify value types because they're non-nullable even in Java. - tp.symbol.isNullableClassAfterErasure && !isValueOrSpecialClass + !isValueOrSpecialClass && (javaDefined || tp.symbol.isNullableClassAfterErasure) + case tp: TypeParamRef => + javaDefined case _ => false // We don't nullify varargs (repeated parameters) at the top level. @@ -136,6 +138,8 @@ object ImplicitNullInterop: override def apply(tp: Type): Type = tp match case tp: TypeRef if needsNull(tp) => nullify(tp) + case tp: TypeParamRef if needsNull(tp) => + nullify(tp) case appTp @ AppliedType(tycon, targs) => val savedSkipCurrentLevel = skipCurrentLevel @@ -151,9 +155,8 @@ object ImplicitNullInterop: case mtp: MethodType => val savedSkipCurrentLevel = skipCurrentLevel - // Skip param types for implicit/using sections - val skipThisParamList = skipParamTypes || mtp.isImplicitMethod - skipCurrentLevel = skipThisParamList + // Don't nullify param types for implicit/using sections + skipCurrentLevel = mtp.isImplicitMethod val paramInfos2 = mtp.paramInfos.map(this) skipCurrentLevel = skipResultType From adc549be46245bfbf99e82cd3fba0e9fee791f36 Mon Sep 17 00:00:00 2001 From: noti0na1 Date: Wed, 17 Sep 2025 15:55:44 +0200 Subject: [PATCH 07/11] Try to move the flexible types to outer level --- .../tools/dotc/core/ImplicitNullInterop.scala | 40 ++++++++++++++----- .../src/dotty/tools/dotc/core/Types.scala | 2 + 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/ImplicitNullInterop.scala b/compiler/src/dotty/tools/dotc/core/ImplicitNullInterop.scala index e163dfbf3533..d120e14d5cf4 100644 --- a/compiler/src/dotty/tools/dotc/core/ImplicitNullInterop.scala +++ b/compiler/src/dotty/tools/dotc/core/ImplicitNullInterop.scala @@ -65,11 +65,15 @@ object ImplicitNullInterop: assert(ctx.explicitNulls) // Skip `TYPE`, enum values, and modules - if isEnumValueDef || sym.name == nme.TYPE_ || sym.is(Flags.ModuleVal) then + if isEnumValueDef + || sym.name == nme.TYPE_ + || sym.name == nme.getClass_ + || sym.name == nme.toString_ + || sym.is(Flags.ModuleVal) then return tp // Don't nullify result type for `toString`, constructors, and @NotNull methods - val skipResultType = sym.name == nme.toString_ || sym.isConstructor || hasNotNullAnnot(sym) + val skipResultType = sym.isConstructor || hasNotNullAnnot(sym) // Don't nullify Given/implicit parameters val skipCurrentLevel = sym.isOneOf(GivenOrImplicitVal) @@ -103,7 +107,7 @@ object ImplicitNullInterop: * The symbols are still under construction, so we don't have precise information. * We purposely do not rely on precise subtyping checks here (e.g., asking whether `tp <:< AnyRef`), * because doing so could force incomplete symbols or trigger cycles. Instead, we conservatively - * nullify only when we can recognize a concrete reference type shape. + * nullify only when we can recognize a concrete reference type or type parameters from Java. */ def needsNull(tp: Type): Boolean = if skipCurrentLevel || !tp.hasSimpleKind then false @@ -149,7 +153,7 @@ object ImplicitNullInterop: skipCurrentLevel = savedSkipCurrentLevel val appTp2 = derivedAppliedType(appTp, tycon, targs2) - if tyconNeedsNull(tycon) then nullify(appTp2) else appTp2 + if tyconNeedsNull(tycon) && tp.hasSimpleKind then nullify(appTp2) else appTp2 case ptp: PolyType => derivedLambdaType(ptp)(ptp.paramInfos, this(ptp.resType)) case mtp: MethodType => @@ -169,10 +173,17 @@ object ImplicitNullInterop: case tp: TypeBounds => mapOver(tp) case tp: AndOrType => - // For unions/intersections we recurse into constituents but do not force an outer `| Null` here; - // outer nullability is handled by the surrounding context. This keeps the result minimal and avoids - // duplicating `| Null` on both sides and at the outer level. - mapOver(tp) + // For unions/intersections we recurse into both sides. + // If both sides are nullalble, we only add `| Null` once. + // This keeps the result minimal and avoids duplicating `| Null` + // on both sides and at the outer level. + (this(tp.tp1), this(tp.tp2)) match + case (FlexibleType(_, t1), FlexibleType(_, t2)) if ctx.flexibleTypes => + FlexibleType(derivedAndOrType(tp, t1, t2)) + case (OrNull(t1), OrNull(t2)) => + OrNull(derivedAndOrType(tp, t1, t2)) + case (t1, t2) => + derivedAndOrType(tp, t1, t2) case tp: ExprType => mapOver(tp) case tp: AnnotatedType => @@ -180,16 +191,25 @@ object ImplicitNullInterop: derivedAnnotatedType(tp, this(tp.underlying), tp.annot) case tp: RefinedType => val savedSkipCurrentLevel = skipCurrentLevel + val savedSkipResultType = skipResultType - // Nullify parent at outer level; not refined members skipCurrentLevel = true val parent2 = this(tp.parent) skipCurrentLevel = false + skipResultType = false val refinedInfo2 = this(tp.refinedInfo) skipCurrentLevel = savedSkipCurrentLevel - derivedRefinedType(tp, parent2, refinedInfo2) + skipResultType = savedSkipResultType + + parent2 match + case FlexibleType(_, parent2a) if ctx.flexibleTypes => + FlexibleType(derivedRefinedType(tp, parent2a, refinedInfo2)) + case OrNull(parent2a) => + OrNull(derivedRefinedType(tp, parent2a, refinedInfo2)) + case _ => + derivedRefinedType(tp, parent2, refinedInfo2) case _ => // 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 diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index efc2e200ccc3..0e3ada25eb83 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -6253,6 +6253,8 @@ object Types extends TypeUtils { tp.derivedAndType(tp1, tp2) protected def derivedOrType(tp: OrType, tp1: Type, tp2: Type): Type = tp.derivedOrType(tp1, tp2) + protected def derivedAndOrType(tp: AndOrType, tp1: Type, tp2: Type): Type = + tp.derivedAndOrType(tp1, tp2) protected def derivedMatchType(tp: MatchType, bound: Type, scrutinee: Type, cases: List[Type]): Type = tp.derivedMatchType(bound, scrutinee, cases) protected def derivedAnnotatedType(tp: AnnotatedType, underlying: Type, annot: Annotation): Type = From 299a72a37c3c8fa2968a8f765e9da91861662755 Mon Sep 17 00:00:00 2001 From: noti0na1 Date: Wed, 17 Sep 2025 17:21:11 +0200 Subject: [PATCH 08/11] Update tests --- .../dotty/tools/dotc/CompilationTests.scala | 20 ++++++++++++---- .../flexible-unpickle/neg/Flexible_2.scala | 24 +++++++++++++++++++ .../flexible-unpickle/neg/Unsafe_1.scala | 22 +++++++++++++++++ .../{ => pos}/Flexible_2.scala | 5 ++++ .../{ => pos}/Unsafe_1.scala | 10 ++++++++ tests/explicit-nulls/pos/i23933.scala | 14 +++++++++++ tests/explicit-nulls/pos/i23936.scala | 8 +++++++ 7 files changed, 98 insertions(+), 5 deletions(-) create mode 100644 tests/explicit-nulls/flexible-unpickle/neg/Flexible_2.scala create mode 100644 tests/explicit-nulls/flexible-unpickle/neg/Unsafe_1.scala rename tests/explicit-nulls/flexible-unpickle/{ => pos}/Flexible_2.scala (93%) rename tests/explicit-nulls/flexible-unpickle/{ => pos}/Unsafe_1.scala (79%) create mode 100644 tests/explicit-nulls/pos/i23933.scala create mode 100644 tests/explicit-nulls/pos/i23936.scala diff --git a/compiler/test/dotty/tools/dotc/CompilationTests.scala b/compiler/test/dotty/tools/dotc/CompilationTests.scala index fe8ea2c502d9..4ac322f6fc50 100644 --- a/compiler/test/dotty/tools/dotc/CompilationTests.scala +++ b/compiler/test/dotty/tools/dotc/CompilationTests.scala @@ -212,8 +212,18 @@ class CompilationTests { compileFilesInDir("tests/explicit-nulls/neg", explicitNullsOptions, FileFilter.exclude(TestSources.negExplicitNullsScala2LibraryTastyExcludelisted)), compileFilesInDir("tests/explicit-nulls/flexible-types-common", explicitNullsOptions and "-Yno-flexible-types"), compileFilesInDir("tests/explicit-nulls/unsafe-common", explicitNullsOptions and "-Yno-flexible-types", FileFilter.exclude(TestSources.negExplicitNullsScala2LibraryTastyExcludelisted)), - ) - }.checkExpectedErrors() + ).checkExpectedErrors() + + locally { + val unsafeFile = compileFile("tests/explicit-nulls/flexible-unpickle/neg/Unsafe_1.scala", explicitNullsOptions without "-Yexplicit-nulls") + val flexibleFile = compileFile("tests/explicit-nulls/flexible-unpickle/neg/Flexible_2.scala", + explicitNullsOptions.withClasspath(defaultOutputDir + testGroup + "/Unsafe_1/neg/Unsafe_1")) + + flexibleFile.keepOutput.checkExpectedErrors() + + List(unsafeFile, flexibleFile).foreach(_.delete()) + } + } @Ignore @Test def explicitNullsPos: Unit = { @@ -226,9 +236,9 @@ class CompilationTests { locally { val tests = List( - compileFile("tests/explicit-nulls/flexible-unpickle/Unsafe_1.scala", explicitNullsOptions without "-Yexplicit-nulls"), - compileFile("tests/explicit-nulls/flexible-unpickle/Flexible_2.scala", explicitNullsOptions.withClasspath( - defaultOutputDir + testGroup + "/Unsafe_1/flexible-unpickle/Unsafe_1")), + compileFile("tests/explicit-nulls/flexible-unpickle/pos/Unsafe_1.scala", explicitNullsOptions without "-Yexplicit-nulls"), + compileFile("tests/explicit-nulls/flexible-unpickle/pos/Flexible_2.scala", explicitNullsOptions.withClasspath( + defaultOutputDir + testGroup + "/Unsafe_1/pos/Unsafe_1")), ).map(_.keepOutput.checkCompile()) tests.foreach(_.delete()) diff --git a/tests/explicit-nulls/flexible-unpickle/neg/Flexible_2.scala b/tests/explicit-nulls/flexible-unpickle/neg/Flexible_2.scala new file mode 100644 index 000000000000..6d210cb281a8 --- /dev/null +++ b/tests/explicit-nulls/flexible-unpickle/neg/Flexible_2.scala @@ -0,0 +1,24 @@ +import unsafe.* + +@A[String] class C + +def test = + + val ii = Bar.f[Int](null) // error + val jj = Bar.g[String](null) // error + val jj2 = Bar.g[String | Null](null) // ok + val kk = Bar.g2[String](null) // error + val kk2 = Bar.g2[String | Null](null) // ok + + val bar_x: Int = Bar.x + val bar_y: String | Null = Bar.y.replaceAll(" ","") + + def testUsingFoo(using Foo[Option]) = Bar.h(null) + + val ii2 = Bar2[String]().f(null) // error + val ii3 = Bar2[String | Null]().f(null) // ok + + val a = Bar.ff( + (x: AnyRef) => x.toString, + 42 + ) \ No newline at end of file diff --git a/tests/explicit-nulls/flexible-unpickle/neg/Unsafe_1.scala b/tests/explicit-nulls/flexible-unpickle/neg/Unsafe_1.scala new file mode 100644 index 000000000000..9a682c96663e --- /dev/null +++ b/tests/explicit-nulls/flexible-unpickle/neg/Unsafe_1.scala @@ -0,0 +1,22 @@ +package unsafe + +import scala.annotation.* + +type XtoY = [X] =>> [Y] =>> X => Y + +class Foo[T[_]] + +class A[T] extends Annotation + +object Bar: + def ff(f: AnyRef => String, g: AnyRef ?=> Int): (AnyRef => String) = ??? + var x: Int = 0 + var y: String = "" + def f[T <: Int](x: T): T = x + def g[T <: AnyRef](x: T): T = x + def g2[T >: Null <: AnyRef](x: T): T = x + def h(x: String)(using Foo[Option]): String = x + def h2(a: Foo[XtoY[String]]) = ??? + +class Bar2[T]: + def f(x: T): T = x \ No newline at end of file diff --git a/tests/explicit-nulls/flexible-unpickle/Flexible_2.scala b/tests/explicit-nulls/flexible-unpickle/pos/Flexible_2.scala similarity index 93% rename from tests/explicit-nulls/flexible-unpickle/Flexible_2.scala rename to tests/explicit-nulls/flexible-unpickle/pos/Flexible_2.scala index 7fc56d1cc462..55c5dc90ac78 100644 --- a/tests/explicit-nulls/flexible-unpickle/Flexible_2.scala +++ b/tests/explicit-nulls/flexible-unpickle/pos/Flexible_2.scala @@ -115,3 +115,8 @@ def Flexible_2() = expects(alphaTypeNameMacro[A]) } +// i23935 +opaque type ZArrow[-I, -R, +E, +O] = I => ZIO[R, E, O] +object ZArrow: + def fromZIOAttempt[I, R, E, O](f: I => ZIO[R, E, O]): ZArrow[I, R, Throwable | E, O] = + (in: I) => ZIO.attempt(f(in)).flatten \ No newline at end of file diff --git a/tests/explicit-nulls/flexible-unpickle/Unsafe_1.scala b/tests/explicit-nulls/flexible-unpickle/pos/Unsafe_1.scala similarity index 79% rename from tests/explicit-nulls/flexible-unpickle/Unsafe_1.scala rename to tests/explicit-nulls/flexible-unpickle/pos/Unsafe_1.scala index 7f118d7805ad..c35e05e4e2c4 100644 --- a/tests/explicit-nulls/flexible-unpickle/Unsafe_1.scala +++ b/tests/explicit-nulls/flexible-unpickle/pos/Unsafe_1.scala @@ -84,3 +84,13 @@ class T class U[Y](a: Y) def expects(take: (T) ?=> U[String]) = ??? +// i23935 +object ZIO: + def attempt[A](code: => A): ZIO[Any, Throwable, A] = ??? + +trait ZIO[-R, +E, +A]: + final def flatten[R1 <: R, E1 >: E, B](using A IsSubtypeOfOutput ZIO[R1, E1, B]): ZIO[R1, E1, B] = ??? + +infix sealed abstract class IsSubtypeOfOutput[-A, +B] +object IsSubtypeOfOutput: + given [A, B](using A <:< B): IsSubtypeOfOutput[A, B] = ??? diff --git a/tests/explicit-nulls/pos/i23933.scala b/tests/explicit-nulls/pos/i23933.scala new file mode 100644 index 000000000000..e8529a94c801 --- /dev/null +++ b/tests/explicit-nulls/pos/i23933.scala @@ -0,0 +1,14 @@ +enum FormatPattern: + case AsInt + case AsLong + +// some basic operations with enum: +def test = + val p1 = FormatPattern.AsInt + val p2 = FormatPattern.AsLong + val p3 = FormatPattern.valueOf("AsInt") + val p4 = FormatPattern.values(0) + val ord1 = p1.ordinal + val ord2 = p2.ordinal + val str1 = p1.toString() + val str2 = p2.toString() \ No newline at end of file diff --git a/tests/explicit-nulls/pos/i23936.scala b/tests/explicit-nulls/pos/i23936.scala new file mode 100644 index 000000000000..041e358c87f0 --- /dev/null +++ b/tests/explicit-nulls/pos/i23936.scala @@ -0,0 +1,8 @@ +//> using options -Yexplicit-nulls + +sealed abstract class IsSubtypeOfOutput[-A, +B] extends (A => B) +object IsSubtypeOfOutput: + private val instance: IsSubtypeOfOutput[Any, Any] = new IsSubtypeOfOutput[Any, Any] { def apply(a: Any): Any = a } + +sealed trait DerivationAnnotation +class Rename(name: String) extends DerivationAnnotation From 78495eaee11410de6dd6b4d14617aab3cfe55a91 Mon Sep 17 00:00:00 2001 From: noti0na1 Date: Thu, 18 Sep 2025 13:23:50 +0200 Subject: [PATCH 09/11] Skip tuple for now --- compiler/src/dotty/tools/dotc/core/ImplicitNullInterop.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/core/ImplicitNullInterop.scala b/compiler/src/dotty/tools/dotc/core/ImplicitNullInterop.scala index d120e14d5cf4..0426665eaca1 100644 --- a/compiler/src/dotty/tools/dotc/core/ImplicitNullInterop.scala +++ b/compiler/src/dotty/tools/dotc/core/ImplicitNullInterop.scala @@ -137,6 +137,7 @@ object ImplicitNullInterop: else tp match case tp: TypeRef if !ctx.flexibleTypes && tp.isRef(defn.RepeatedParamClass) => false + case tp: TypeRef if defn.isTupleClass(tp.symbol) => false case _ => true override def apply(tp: Type): Type = tp match @@ -193,7 +194,6 @@ object ImplicitNullInterop: val savedSkipCurrentLevel = skipCurrentLevel val savedSkipResultType = skipResultType - skipCurrentLevel = true val parent2 = this(tp.parent) skipCurrentLevel = false From 57235ef7a8dfe2a24c4556d05bd36f8bf42d3c35 Mon Sep 17 00:00:00 2001 From: noti0na1 Date: Mon, 22 Sep 2025 13:20:08 +0200 Subject: [PATCH 10/11] Add `-Ynullify-tasty` setting and only nullify compiled Scala if the flag is set --- compiler/src/dotty/tools/dotc/config/ScalaSettings.scala | 1 + compiler/src/dotty/tools/dotc/core/Contexts.scala | 2 ++ .../src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala | 4 ++-- compiler/test/dotty/tools/dotc/CompilationTests.scala | 6 +++--- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala index 2d048befe171..6baf037d1b6d 100644 --- a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala +++ b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala @@ -455,6 +455,7 @@ private sealed trait YSettings: val YnoKindPolymorphism: Setting[Boolean] = BooleanSetting(ForkSetting, "Yno-kind-polymorphism", "Disable kind polymorphism. (This flag has no effect)", deprecation = Deprecation.removed()) val YexplicitNulls: Setting[Boolean] = BooleanSetting(ForkSetting, "Yexplicit-nulls", "Make reference types non-nullable. Nullable types can be expressed with unions: e.g. String|Null.") val YnoFlexibleTypes: Setting[Boolean] = BooleanSetting(ForkSetting, "Yno-flexible-types", "Disable turning nullable Java return types and parameter types into flexible types, which behave like abstract types with a nullable lower bound and non-nullable upper bound.") + val YnullifyTasty: Setting[Boolean] = BooleanSetting(ForkSetting, "Ynullify-tasty", "Apply nullification to Scala code compiled without -Yexplicit-nulls, when reading from tasty.") val YsafeInitGlobal: Setting[Boolean] = BooleanSetting(ForkSetting, "Ysafe-init-global", "Check safe initialization of global objects.") val YrequireTargetName: Setting[Boolean] = BooleanSetting(ForkSetting, "Yrequire-targetName", "Warn if an operator is defined without a @targetName annotation.") val YrecheckTest: Setting[Boolean] = BooleanSetting(ForkSetting, "Yrecheck-test", "Run basic rechecking (internal test only).") diff --git a/compiler/src/dotty/tools/dotc/core/Contexts.scala b/compiler/src/dotty/tools/dotc/core/Contexts.scala index 5a0e03330ef2..3f7e930039a2 100644 --- a/compiler/src/dotty/tools/dotc/core/Contexts.scala +++ b/compiler/src/dotty/tools/dotc/core/Contexts.scala @@ -480,6 +480,8 @@ object Contexts { /** Is the flexible types option set? */ def flexibleTypes: Boolean = base.settings.YexplicitNulls.value && !base.settings.YnoFlexibleTypes.value + def nullifyTasty: Boolean = base.settings.YexplicitNulls.value && base.settings.YnullifyTasty.value + /** Is the best-effort option set? */ def isBestEffort: Boolean = base.settings.YbestEffort.value diff --git a/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala b/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala index 95c9731a0679..6c989f410840 100644 --- a/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala +++ b/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala @@ -921,10 +921,10 @@ class TreeUnpickler(reader: TastyReader, def ta = ctx.typeAssigner - // If explicit nulls is enabled, and the source file did not have explicit + // If explicit nulls and `YnullifyTasty` is enabled, and the source file did not have explicit // nulls enabled, nullify the member to allow for compatibility. def nullify(sym: Symbol) = - if (ctx.explicitNulls && ctx.flexibleTypes && !explicitNulls) then + if (ctx.nullifyTasty && !explicitNulls) then sym.info = ImplicitNullInterop.nullifyMember(sym, sym.info, sym.is(Enum)) val name = readName() diff --git a/compiler/test/dotty/tools/dotc/CompilationTests.scala b/compiler/test/dotty/tools/dotc/CompilationTests.scala index 4ac322f6fc50..602ac319f8a5 100644 --- a/compiler/test/dotty/tools/dotc/CompilationTests.scala +++ b/compiler/test/dotty/tools/dotc/CompilationTests.scala @@ -217,7 +217,7 @@ class CompilationTests { locally { val unsafeFile = compileFile("tests/explicit-nulls/flexible-unpickle/neg/Unsafe_1.scala", explicitNullsOptions without "-Yexplicit-nulls") val flexibleFile = compileFile("tests/explicit-nulls/flexible-unpickle/neg/Flexible_2.scala", - explicitNullsOptions.withClasspath(defaultOutputDir + testGroup + "/Unsafe_1/neg/Unsafe_1")) + explicitNullsOptions.and("-Ynullify-tasty").withClasspath(defaultOutputDir + testGroup + "/Unsafe_1/neg/Unsafe_1")) flexibleFile.keepOutput.checkExpectedErrors() @@ -237,8 +237,8 @@ class CompilationTests { locally { val tests = List( compileFile("tests/explicit-nulls/flexible-unpickle/pos/Unsafe_1.scala", explicitNullsOptions without "-Yexplicit-nulls"), - compileFile("tests/explicit-nulls/flexible-unpickle/pos/Flexible_2.scala", explicitNullsOptions.withClasspath( - defaultOutputDir + testGroup + "/Unsafe_1/pos/Unsafe_1")), + compileFile("tests/explicit-nulls/flexible-unpickle/pos/Flexible_2.scala", + explicitNullsOptions.and("-Ynullify-tasty").withClasspath(defaultOutputDir + testGroup + "/Unsafe_1/pos/Unsafe_1")), ).map(_.keepOutput.checkCompile()) tests.foreach(_.delete()) From 16630389d47182dc87472f46ac74250f861e87ef Mon Sep 17 00:00:00 2001 From: noti0na1 Date: Mon, 22 Sep 2025 17:26:03 +0200 Subject: [PATCH 11/11] Rename `YnullifyTasty` to `YflexifyTasty` --- compiler/src/dotty/tools/dotc/config/ScalaSettings.scala | 2 +- compiler/src/dotty/tools/dotc/core/Contexts.scala | 3 ++- compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala | 4 ++-- compiler/test/dotty/tools/dotc/CompilationTests.scala | 4 ++-- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala index 6baf037d1b6d..479e61530aa2 100644 --- a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala +++ b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala @@ -455,7 +455,7 @@ private sealed trait YSettings: val YnoKindPolymorphism: Setting[Boolean] = BooleanSetting(ForkSetting, "Yno-kind-polymorphism", "Disable kind polymorphism. (This flag has no effect)", deprecation = Deprecation.removed()) val YexplicitNulls: Setting[Boolean] = BooleanSetting(ForkSetting, "Yexplicit-nulls", "Make reference types non-nullable. Nullable types can be expressed with unions: e.g. String|Null.") val YnoFlexibleTypes: Setting[Boolean] = BooleanSetting(ForkSetting, "Yno-flexible-types", "Disable turning nullable Java return types and parameter types into flexible types, which behave like abstract types with a nullable lower bound and non-nullable upper bound.") - val YnullifyTasty: Setting[Boolean] = BooleanSetting(ForkSetting, "Ynullify-tasty", "Apply nullification to Scala code compiled without -Yexplicit-nulls, when reading from tasty.") + val YflexifyTasty: Setting[Boolean] = BooleanSetting(ForkSetting, "Yflexify-tasty", "Apply flexification to Scala code compiled without -Yexplicit-nulls, when reading from tasty.") val YsafeInitGlobal: Setting[Boolean] = BooleanSetting(ForkSetting, "Ysafe-init-global", "Check safe initialization of global objects.") val YrequireTargetName: Setting[Boolean] = BooleanSetting(ForkSetting, "Yrequire-targetName", "Warn if an operator is defined without a @targetName annotation.") val YrecheckTest: Setting[Boolean] = BooleanSetting(ForkSetting, "Yrecheck-test", "Run basic rechecking (internal test only).") diff --git a/compiler/src/dotty/tools/dotc/core/Contexts.scala b/compiler/src/dotty/tools/dotc/core/Contexts.scala index 3f7e930039a2..51b42a3eb5f5 100644 --- a/compiler/src/dotty/tools/dotc/core/Contexts.scala +++ b/compiler/src/dotty/tools/dotc/core/Contexts.scala @@ -480,7 +480,8 @@ object Contexts { /** Is the flexible types option set? */ def flexibleTypes: Boolean = base.settings.YexplicitNulls.value && !base.settings.YnoFlexibleTypes.value - def nullifyTasty: Boolean = base.settings.YexplicitNulls.value && base.settings.YnullifyTasty.value + /** Is the flexify tasty option set? */ + def flexifyTasty: Boolean = base.settings.YexplicitNulls.value && base.settings.YflexifyTasty.value /** Is the best-effort option set? */ def isBestEffort: Boolean = base.settings.YbestEffort.value diff --git a/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala b/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala index 6c989f410840..3265d98fd859 100644 --- a/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala +++ b/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala @@ -921,10 +921,10 @@ class TreeUnpickler(reader: TastyReader, def ta = ctx.typeAssigner - // If explicit nulls and `YnullifyTasty` is enabled, and the source file did not have explicit + // If explicit nulls and `Yflexify-tasty` is enabled, and the source file did not have explicit // nulls enabled, nullify the member to allow for compatibility. def nullify(sym: Symbol) = - if (ctx.nullifyTasty && !explicitNulls) then + if (ctx.flexifyTasty && !explicitNulls) then sym.info = ImplicitNullInterop.nullifyMember(sym, sym.info, sym.is(Enum)) val name = readName() diff --git a/compiler/test/dotty/tools/dotc/CompilationTests.scala b/compiler/test/dotty/tools/dotc/CompilationTests.scala index 602ac319f8a5..0a826e7089bb 100644 --- a/compiler/test/dotty/tools/dotc/CompilationTests.scala +++ b/compiler/test/dotty/tools/dotc/CompilationTests.scala @@ -217,7 +217,7 @@ class CompilationTests { locally { val unsafeFile = compileFile("tests/explicit-nulls/flexible-unpickle/neg/Unsafe_1.scala", explicitNullsOptions without "-Yexplicit-nulls") val flexibleFile = compileFile("tests/explicit-nulls/flexible-unpickle/neg/Flexible_2.scala", - explicitNullsOptions.and("-Ynullify-tasty").withClasspath(defaultOutputDir + testGroup + "/Unsafe_1/neg/Unsafe_1")) + explicitNullsOptions.and("-Yflexify-tasty").withClasspath(defaultOutputDir + testGroup + "/Unsafe_1/neg/Unsafe_1")) flexibleFile.keepOutput.checkExpectedErrors() @@ -238,7 +238,7 @@ class CompilationTests { val tests = List( compileFile("tests/explicit-nulls/flexible-unpickle/pos/Unsafe_1.scala", explicitNullsOptions without "-Yexplicit-nulls"), compileFile("tests/explicit-nulls/flexible-unpickle/pos/Flexible_2.scala", - explicitNullsOptions.and("-Ynullify-tasty").withClasspath(defaultOutputDir + testGroup + "/Unsafe_1/pos/Unsafe_1")), + explicitNullsOptions.and("-Yflexify-tasty").withClasspath(defaultOutputDir + testGroup + "/Unsafe_1/pos/Unsafe_1")), ).map(_.keepOutput.checkCompile()) tests.foreach(_.delete())