From d4ae15076aee07cf2d151e8bec56b92b831987f3 Mon Sep 17 00:00:00 2001 From: noti0na1 Date: Mon, 2 May 2022 16:48:33 -0400 Subject: [PATCH 01/13] Add unsafeJavaReturn --- compiler/src/dotty/tools/dotc/Run.scala | 7 +++++-- .../src/dotty/tools/dotc/core/Contexts.scala | 19 +++++++++++++------ .../dotty/tools/dotc/core/Definitions.scala | 1 + compiler/src/dotty/tools/dotc/core/Mode.scala | 2 ++ .../tools/dotc/core/NullOpsDecorator.scala | 14 ++++++++++++++ .../src/dotty/tools/dotc/core/StdNames.scala | 1 + .../dotty/tools/dotc/typer/Applications.scala | 10 +++++++++- .../dotty/tools/dotc/typer/Synthesizer.scala | 10 +++++++--- .../src/scala/annotation/CanEqualNull.scala | 3 +++ .../runtime/stdLibPatches/language.scala | 3 +++ 10 files changed, 58 insertions(+), 12 deletions(-) create mode 100644 library/src/scala/annotation/CanEqualNull.scala diff --git a/compiler/src/dotty/tools/dotc/Run.scala b/compiler/src/dotty/tools/dotc/Run.scala index 8f0bc395879e..7f31c1ee324b 100644 --- a/compiler/src/dotty/tools/dotc/Run.scala +++ b/compiler/src/dotty/tools/dotc/Run.scala @@ -365,8 +365,11 @@ class Run(comp: Compiler, ictx: Context) extends ImplicitRunInfo with Constraint .setTyper(new Typer) .addMode(Mode.ImplicitsEnabled) .setTyperState(ctx.typerState.fresh(ctx.reporter)) - if ctx.settings.YexplicitNulls.value && !Feature.enabledBySetting(nme.unsafeNulls) then - start = start.addMode(Mode.SafeNulls) + if ctx.settings.YexplicitNulls.value then + if !Feature.enabledBySetting(nme.unsafeNulls) then + start = start.addMode(Mode.SafeNulls) + if Feature.enabledBySetting(nme.unsafeJavaReturn) then + start = start.addMode(Mode.UnsafeJavaReturn) ctx.initialize()(using start) // re-initialize the base context with start // `this` must be unchecked for safe initialization because by being passed to setRun during diff --git a/compiler/src/dotty/tools/dotc/core/Contexts.scala b/compiler/src/dotty/tools/dotc/core/Contexts.scala index 3dfafe6837d0..30d95d256a6e 100644 --- a/compiler/src/dotty/tools/dotc/core/Contexts.scala +++ b/compiler/src/dotty/tools/dotc/core/Contexts.scala @@ -642,12 +642,19 @@ object Contexts { def setProfiler(profiler: Profiler): this.type = updateStore(profilerLoc, profiler) def setNotNullInfos(notNullInfos: List[NotNullInfo]): this.type = updateStore(notNullInfosLoc, notNullInfos) def setImportInfo(importInfo: ImportInfo): this.type = - importInfo.mentionsFeature(nme.unsafeNulls) match - case Some(true) => - setMode(this.mode &~ Mode.SafeNulls) - case Some(false) if ctx.settings.YexplicitNulls.value => - setMode(this.mode | Mode.SafeNulls) - case _ => + if ctx.settings.YexplicitNulls.value then + importInfo.mentionsFeature(nme.unsafeNulls) match + case Some(true) => + setMode(this.mode &~ Mode.SafeNulls) + case Some(false) => + setMode(this.mode | Mode.SafeNulls) + case _ => + importInfo.mentionsFeature(nme.unsafeJavaReturn) match + case Some(true) => + setMode(this.mode | Mode.UnsafeJavaReturn) + case Some(false) => + setMode(this.mode &~ Mode.UnsafeJavaReturn) + case _ => updateStore(importInfoLoc, importInfo) def setTypeAssigner(typeAssigner: TypeAssigner): this.type = updateStore(typeAssignerLoc, typeAssigner) diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 3e2373d3bd4b..8a7f1c9a5988 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -989,6 +989,7 @@ class Definitions { @tu lazy val FunctionalInterfaceAnnot: ClassSymbol = requiredClass("java.lang.FunctionalInterface") @tu lazy val TargetNameAnnot: ClassSymbol = requiredClass("scala.annotation.targetName") @tu lazy val VarargsAnnot: ClassSymbol = requiredClass("scala.annotation.varargs") + @tu lazy val CanEqualNullAnnot: ClassSymbol = requiredClass("scala.annotation.CanEqualNull") @tu lazy val JavaRepeatableAnnot: ClassSymbol = requiredClass("java.lang.annotation.Repeatable") diff --git a/compiler/src/dotty/tools/dotc/core/Mode.scala b/compiler/src/dotty/tools/dotc/core/Mode.scala index d141cf7032ee..87b7ca1cbe08 100644 --- a/compiler/src/dotty/tools/dotc/core/Mode.scala +++ b/compiler/src/dotty/tools/dotc/core/Mode.scala @@ -129,4 +129,6 @@ object Mode { * Type `Null` becomes a subtype of non-primitive value types in TypeComparer. */ val RelaxedOverriding: Mode = newMode(30, "RelaxedOverriding") + + val UnsafeJavaReturn: Mode = newMode(31, "UnsafeJavaReturn") } diff --git a/compiler/src/dotty/tools/dotc/core/NullOpsDecorator.scala b/compiler/src/dotty/tools/dotc/core/NullOpsDecorator.scala index e18271772ff1..4c8fd11b3529 100644 --- a/compiler/src/dotty/tools/dotc/core/NullOpsDecorator.scala +++ b/compiler/src/dotty/tools/dotc/core/NullOpsDecorator.scala @@ -1,7 +1,9 @@ package dotty.tools.dotc package core +import Annotations._ import Contexts._ +import Symbols._ import Types._ /** Defines operations on nullable types and tree. */ @@ -42,6 +44,18 @@ object NullOpsDecorator: if ctx.explicitNulls then strip(self) else self } + def replaceOrNull(using Context): Type = + def recur(tp: Type): Type = tp match + case tp @ OrType(lhs, rhs) if rhs.isNullType => + AnnotatedType(recur(lhs), Annotation(defn.CanEqualNullAnnot)) + case tp: AndOrType => + tp.derivedAndOrType(recur(tp.tp1), recur(tp.tp2)) + case tp @ AppliedType(tycon, targs) => + tp.derivedAppliedType(tycon, targs.map(recur)) + case _ => tp + if ctx.explicitNulls then recur(self) else self + + /** Is self (after widening and dealiasing) a type of the form `T | Null`? */ def isNullableUnion(using Context): Boolean = { val stripped = self.stripNull diff --git a/compiler/src/dotty/tools/dotc/core/StdNames.scala b/compiler/src/dotty/tools/dotc/core/StdNames.scala index 9f128a71be7b..95f4b5027305 100644 --- a/compiler/src/dotty/tools/dotc/core/StdNames.scala +++ b/compiler/src/dotty/tools/dotc/core/StdNames.scala @@ -612,6 +612,7 @@ object StdNames { val unapplySeq: N = "unapplySeq" val unbox: N = "unbox" val universe: N = "universe" + val unsafeJavaReturn: N = "unsafeJavaReturn" val unsafeNulls: N = "unsafeNulls" val update: N = "update" val updateDynamic: N = "updateDynamic" diff --git a/compiler/src/dotty/tools/dotc/typer/Applications.scala b/compiler/src/dotty/tools/dotc/typer/Applications.scala index 04f5bf034ac0..31b791240fce 100644 --- a/compiler/src/dotty/tools/dotc/typer/Applications.scala +++ b/compiler/src/dotty/tools/dotc/typer/Applications.scala @@ -25,6 +25,7 @@ import reporting._ import transform.TypeUtils._ import transform.SymUtils._ import Nullables._ +import NullOpsDecorator._ import config.Feature import collection.mutable @@ -908,7 +909,14 @@ trait Applications extends Compatibility { def simpleApply(fun1: Tree, proto: FunProto)(using Context): Tree = methPart(fun1).tpe match { case funRef: TermRef => - val app = ApplyTo(tree, fun1, funRef, proto, pt) + var app = ApplyTo(tree, fun1, funRef, proto, pt) + if ctx.mode.is(Mode.UnsafeJavaReturn) then + val funSym = fun1.symbol + if funSym.is(JavaDefined) && !funSym.isConstructor then + val rtp1 = app.tpe + val rtp2 = rtp1.replaceOrNull + if rtp1 ne rtp2 then + app = app.cast(rtp2) convertNewGenericArray( widenEnumCase( postProcessByNameArgs(funRef, app).computeNullable(), diff --git a/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala b/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala index 8b0fa88ad5a9..6f245ef19948 100644 --- a/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala @@ -148,7 +148,7 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context): } /** Is an `CanEqual[cls1, cls2]` instance assumed for predefined classes `cls1`, cls2`? */ - def canComparePredefinedClasses(cls1: ClassSymbol, cls2: ClassSymbol): Boolean = + def canComparePredefinedClasses(cls1: ClassSymbol, cls2: ClassSymbol)(using Context): Boolean = def cmpWithBoxed(cls1: ClassSymbol, cls2: ClassSymbol) = cls2 == defn.NothingClass @@ -173,7 +173,8 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context): // val x: String = null.asInstanceOf[String] // if (x == null) {} // error: x is non-nullable // if (x.asInstanceOf[String|Null] == null) {} // ok - cls1 == defn.NullClass && cls1 == cls2 + if cls1 == defn.NullClass then cls1 == cls2 + else cls1 == defn.NothingClass || cls2 == defn.NothingClass else if cls1 == defn.NullClass then cls1 == cls2 || cls2.derivesFrom(defn.ObjectClass) else if cls2 == defn.NullClass then @@ -187,9 +188,12 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context): * interpret. */ def canComparePredefined(tp1: Type, tp2: Type) = + val checkCtx = if ctx.explicitNulls + && (tp1.hasAnnotation(defn.CanEqualNullAnnot) || tp2.hasAnnotation(defn.CanEqualNullAnnot)) + then ctx.retractMode(Mode.SafeNulls) else ctx tp1.classSymbols.exists(cls1 => tp2.classSymbols.exists(cls2 => - canComparePredefinedClasses(cls1, cls2))) + canComparePredefinedClasses(cls1, cls2)(using checkCtx))) formal.argTypes match case args @ (arg1 :: arg2 :: Nil) => diff --git a/library/src/scala/annotation/CanEqualNull.scala b/library/src/scala/annotation/CanEqualNull.scala new file mode 100644 index 000000000000..1a8386562c8a --- /dev/null +++ b/library/src/scala/annotation/CanEqualNull.scala @@ -0,0 +1,3 @@ +package scala.annotation + +final class CanEqualNull extends StaticAnnotation diff --git a/library/src/scala/runtime/stdLibPatches/language.scala b/library/src/scala/runtime/stdLibPatches/language.scala index 2be4861b4cc2..73e888564d06 100644 --- a/library/src/scala/runtime/stdLibPatches/language.scala +++ b/library/src/scala/runtime/stdLibPatches/language.scala @@ -128,6 +128,9 @@ object language: @compileTimeOnly("`unsafeNulls` can only be used at compile time in import statements") object unsafeNulls + @compileTimeOnly("`unsafeJavaReturn` can only be used at compile time in import statements") + object unsafeJavaReturn + @compileTimeOnly("`future` can only be used at compile time in import statements") object future From c4836fbc3181a90812dd5d8d843241a69db3339f Mon Sep 17 00:00:00 2001 From: noti0na1 Date: Tue, 3 May 2022 17:35:04 -0400 Subject: [PATCH 02/13] Fix class vals, keep annotation loonger --- compiler/src/dotty/tools/dotc/typer/Applications.scala | 5 ++++- compiler/src/dotty/tools/dotc/typer/Typer.scala | 10 +++++++++- library/src/scala/annotation/CanEqualNull.scala | 2 +- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/typer/Applications.scala b/compiler/src/dotty/tools/dotc/typer/Applications.scala index 31b791240fce..9158367eca47 100644 --- a/compiler/src/dotty/tools/dotc/typer/Applications.scala +++ b/compiler/src/dotty/tools/dotc/typer/Applications.scala @@ -912,7 +912,10 @@ trait Applications extends Compatibility { var app = ApplyTo(tree, fun1, funRef, proto, pt) if ctx.mode.is(Mode.UnsafeJavaReturn) then val funSym = fun1.symbol - if funSym.is(JavaDefined) && !funSym.isConstructor then + if funSym.is(JavaDefined) + && funSym.isTerm + && funSym.is(Method) + && !funSym.isConstructor then val rtp1 = app.tpe val rtp2 = rtp1.replaceOrNull if rtp1 ne rtp2 then diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index bb7ca762d9f2..2c1f7ef750f5 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -646,7 +646,15 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer def typeSelectOnTerm(using Context): Tree = val qual = typedExpr(tree.qualifier, shallowSelectionProto(tree.name, pt, this)) - typedSelect(tree, pt, qual).withSpan(tree.span).computeNullable() + var sel = typedSelect(tree, pt, qual).withSpan(tree.span).computeNullable() + if ctx.mode.is(Mode.UnsafeJavaReturn) && pt != AssignProto then + val sym = sel.symbol + if sym.is(JavaDefined) && sym.isTerm && !sym.is(Method) then + val stp1 = sel.tpe.widen + val stp2 = stp1.replaceOrNull + if stp1 ne stp2 then + sel = sel.cast(stp2) + sel def javaSelectOnType(qual: Tree)(using Context) = // semantic name conversion for `O$` in java code diff --git a/library/src/scala/annotation/CanEqualNull.scala b/library/src/scala/annotation/CanEqualNull.scala index 1a8386562c8a..5fd5c89eb52f 100644 --- a/library/src/scala/annotation/CanEqualNull.scala +++ b/library/src/scala/annotation/CanEqualNull.scala @@ -1,3 +1,3 @@ package scala.annotation -final class CanEqualNull extends StaticAnnotation +final class CanEqualNull extends RefiningAnnotation From 0e797c04041c7a4475760436650fefe2bf884d1d Mon Sep 17 00:00:00 2001 From: noti0na1 Date: Thu, 12 May 2022 14:01:07 -0400 Subject: [PATCH 03/13] Add comment --- .../tools/dotc/core/NullOpsDecorator.scala | 8 +++- .../unsafe-java-common/java-chain/J.java | 7 ++++ .../unsafe-java-common/java-chain/S.scala | 4 ++ .../unsafe-java-common/java-class/J.java | 42 +++++++++++++++++++ .../unsafe-java-common/java-class/S.scala | 41 ++++++++++++++++++ 5 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 tests/explicit-nulls/unsafe-java-common/java-chain/J.java create mode 100644 tests/explicit-nulls/unsafe-java-common/java-chain/S.scala create mode 100644 tests/explicit-nulls/unsafe-java-common/java-class/J.java create mode 100644 tests/explicit-nulls/unsafe-java-common/java-class/S.scala diff --git a/compiler/src/dotty/tools/dotc/core/NullOpsDecorator.scala b/compiler/src/dotty/tools/dotc/core/NullOpsDecorator.scala index 4c8fd11b3529..a6ee70fa6fc0 100644 --- a/compiler/src/dotty/tools/dotc/core/NullOpsDecorator.scala +++ b/compiler/src/dotty/tools/dotc/core/NullOpsDecorator.scala @@ -44,7 +44,12 @@ object NullOpsDecorator: if ctx.explicitNulls then strip(self) else self } + /** Strips `|Null` from the return type of a Java method, + * replacing it with a `@CanEqualNull` annotation + */ def replaceOrNull(using Context): Type = + // Since this method should only be called on types from Java, + // handling these cases is enough. def recur(tp: Type): Type = tp match case tp @ OrType(lhs, rhs) if rhs.isNullType => AnnotatedType(recur(lhs), Annotation(defn.CanEqualNullAnnot)) @@ -52,10 +57,11 @@ object NullOpsDecorator: tp.derivedAndOrType(recur(tp.tp1), recur(tp.tp2)) case tp @ AppliedType(tycon, targs) => tp.derivedAppliedType(tycon, targs.map(recur)) + case mptp: MethodOrPoly => + mptp.derivedLambdaType(resType = recur(mptp.resType)) case _ => tp if ctx.explicitNulls then recur(self) else self - /** Is self (after widening and dealiasing) a type of the form `T | Null`? */ def isNullableUnion(using Context): Boolean = { val stripped = self.stripNull diff --git a/tests/explicit-nulls/unsafe-java-common/java-chain/J.java b/tests/explicit-nulls/unsafe-java-common/java-chain/J.java new file mode 100644 index 000000000000..bd266bae13d9 --- /dev/null +++ b/tests/explicit-nulls/unsafe-java-common/java-chain/J.java @@ -0,0 +1,7 @@ +class J1 { + J2 getJ2() { return new J2(); } +} + +class J2 { + J1 getJ1() { return new J1(); } +} \ No newline at end of file diff --git a/tests/explicit-nulls/unsafe-java-common/java-chain/S.scala b/tests/explicit-nulls/unsafe-java-common/java-chain/S.scala new file mode 100644 index 000000000000..9fe5aa3f08ce --- /dev/null +++ b/tests/explicit-nulls/unsafe-java-common/java-chain/S.scala @@ -0,0 +1,4 @@ +class S { + val j: J2 = new J2() + j.getJ1().getJ2().getJ1().getJ2().getJ1().getJ2() // error +} diff --git a/tests/explicit-nulls/unsafe-java-common/java-class/J.java b/tests/explicit-nulls/unsafe-java-common/java-class/J.java new file mode 100644 index 000000000000..91f58a42ccf9 --- /dev/null +++ b/tests/explicit-nulls/unsafe-java-common/java-class/J.java @@ -0,0 +1,42 @@ +import java.util.List; + +public class JC { + + public int a; + + public String b; + + public String[] c; + + public int f1() { + return 0; + } + + public int[] f2() { + return null; + } + + public String g1() { + return null; + } + + public List g2() { + return null; + } + + public String[] g3() { + return null; + } + + public T h1() { + return null; + } + + public List h2() { + return null; + } + + public T[] h3() { + return null; + } +} \ No newline at end of file diff --git a/tests/explicit-nulls/unsafe-java-common/java-class/S.scala b/tests/explicit-nulls/unsafe-java-common/java-class/S.scala new file mode 100644 index 000000000000..3ab74166db5b --- /dev/null +++ b/tests/explicit-nulls/unsafe-java-common/java-class/S.scala @@ -0,0 +1,41 @@ +import scala.language.unsafeJavaReturn +import java.{util => ju} + +class S { + + def test[T <: AnyRef](jc: JC) = { + // val a: Int = jc.a + + // val b = jc.b // it returns String @CanEqualNull + // val b2: String = b + // val b3: String = jc.b + // val bb = jc.b == null // it's ok to compare String @CanEqualNull with Null + // val btl = jc.b.trim().length() // String @CanEqualNull is just String, unsafe selecting + + // val c = jc.c + // val cl = c.length + // val c2: Array[String] = c + val c3: Array[String] = jc.c + // val c4: Array[Int] = c.map(_.length()) + + // val f1: Int = jc.f1() + // val f2: Array[Int] = jc.f2() + // val f2n = jc.f2() == null + + // val g1: String = jc.g1() + // val g1n = jc.g1() == null + // val g1tl = jc.g1().trim().length() + + // val g2h: ju.List[String] = jc.g2() + + // val g3: Array[String] = jc.g3() + // val g3n = jc.g3() == null + // val g3m: Array[Boolean] = jc.g3().map(_ == null) + + // val h1: T = jc.h1[T]() + + // val h2: ju.List[T] = jc.h2() + + // val h3: Array[T] = jc.h3() + } +} \ No newline at end of file From 8a41011bcc7dafe3cf2e89e2b51b0ac23df3fe70 Mon Sep 17 00:00:00 2001 From: noti0na1 Date: Thu, 12 May 2022 21:03:16 -0400 Subject: [PATCH 04/13] Add a special rule for CanEqualNull --- .../dotty/tools/dotc/core/TypeComparer.scala | 8 +- .../dotty/tools/dotc/typer/Synthesizer.scala | 3 +- .../src/scala/annotation/CanEqualNull.scala | 18 +++++ .../unsafe-java-common/java-class/S.scala | 80 +++++++++++-------- 4 files changed, 72 insertions(+), 37 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index 6d79f377c84e..f66462553131 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -756,8 +756,12 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling } compareTypeBounds case tp2: AnnotatedType if tp2.isRefining => - (tp1.derivesAnnotWith(tp2.annot.sameAnnotation) || tp1.isBottomType) && - recur(tp1, tp2.parent) + // `CanEqualNull` is a special refining annotation. + // An annotated type is equivalent to the original type. + (tp1.derivesAnnotWith(tp2.annot.sameAnnotation) + || tp2.annot.matches(defn.CanEqualNullAnnot) + || tp1.isBottomType) + && recur(tp1, tp2.parent) case ClassInfo(pre2, cls2, _, _, _) => def compareClassInfo = tp1 match { case ClassInfo(pre1, cls1, _, _, _) => diff --git a/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala b/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala index 6f245ef19948..6e5570f96ed3 100644 --- a/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala @@ -173,8 +173,7 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context): // val x: String = null.asInstanceOf[String] // if (x == null) {} // error: x is non-nullable // if (x.asInstanceOf[String|Null] == null) {} // ok - if cls1 == defn.NullClass then cls1 == cls2 - else cls1 == defn.NothingClass || cls2 == defn.NothingClass + cls1 == defn.NullClass && cls1 == cls2 else if cls1 == defn.NullClass then cls1 == cls2 || cls2.derivesFrom(defn.ObjectClass) else if cls2 == defn.NullClass then diff --git a/library/src/scala/annotation/CanEqualNull.scala b/library/src/scala/annotation/CanEqualNull.scala index 5fd5c89eb52f..bb8838317f8a 100644 --- a/library/src/scala/annotation/CanEqualNull.scala +++ b/library/src/scala/annotation/CanEqualNull.scala @@ -1,3 +1,21 @@ package scala.annotation +/** An annotation makes reference types comparable to `null` in explicit nulls. + * `CanEqualNull` is a special refining annotation. An annotated type is equivalent to the original type. + * + * For example: + * ```scala + * val s1: String = ??? + * s1 == null // error + * val s2: String @CanEqualNull = ??? + * s2 == null // ok + * + * // String =:= String @CanEqualNull + * val s3: String = s2 + * val s4: String @CanEqualNull = s1 + * + * val ss: Array[String @CanEqualNull] = ?? + * ss.map(_ == null) + * ``` + */ final class CanEqualNull extends RefiningAnnotation diff --git a/tests/explicit-nulls/unsafe-java-common/java-class/S.scala b/tests/explicit-nulls/unsafe-java-common/java-class/S.scala index 3ab74166db5b..b8eb7b1cdfc2 100644 --- a/tests/explicit-nulls/unsafe-java-common/java-class/S.scala +++ b/tests/explicit-nulls/unsafe-java-common/java-class/S.scala @@ -1,41 +1,55 @@ import scala.language.unsafeJavaReturn + +import scala.annotation.CanEqualNull import java.{util => ju} class S { def test[T <: AnyRef](jc: JC) = { - // val a: Int = jc.a - - // val b = jc.b // it returns String @CanEqualNull - // val b2: String = b - // val b3: String = jc.b - // val bb = jc.b == null // it's ok to compare String @CanEqualNull with Null - // val btl = jc.b.trim().length() // String @CanEqualNull is just String, unsafe selecting - - // val c = jc.c - // val cl = c.length - // val c2: Array[String] = c - val c3: Array[String] = jc.c - // val c4: Array[Int] = c.map(_.length()) - - // val f1: Int = jc.f1() - // val f2: Array[Int] = jc.f2() - // val f2n = jc.f2() == null - - // val g1: String = jc.g1() - // val g1n = jc.g1() == null - // val g1tl = jc.g1().trim().length() - - // val g2h: ju.List[String] = jc.g2() - - // val g3: Array[String] = jc.g3() - // val g3n = jc.g3() == null - // val g3m: Array[Boolean] = jc.g3().map(_ == null) - - // val h1: T = jc.h1[T]() - - // val h2: ju.List[T] = jc.h2() - - // val h3: Array[T] = jc.h3() + val a: Int = jc.a + + val b = jc.b // it returns String @CanEqualNull + val b2: String = b + val b3: String @CanEqualNull = jc.b + val b4: String = jc.b + val bb = jc.b == null // it's ok to compare String @CanEqualNull with Null + val btl = jc.b.trim().length() // String @CanEqualNull is just String, unsafe selecting + + val c = jc.c + val cl = c.length + val c2: Array[String] = c + val c3: Array[String @CanEqualNull] @CanEqualNull = jc.c + val c4: Array[String] = jc.c + val cml: Array[Int] = c.map(_.length()) + + val f1: Int = jc.f1() + + val f21: Array[Int] @CanEqualNull = jc.f2() + val f22: Array[Int] = jc.f2() + val f2n = jc.f2() == null + + val g11: String @CanEqualNull = jc.g1() + val g12: String = jc.g1() + val g1n = jc.g1() == null + val g1tl = jc.g1().trim().length() + + val g21: ju.List[String] @CanEqualNull = jc.g2() + val g22: ju.List[String] = jc.g2() + + val g31: Array[String @CanEqualNull] @CanEqualNull = jc.g3() + val g32: Array[String] = jc.g3() + val g3n = jc.g3() == null + val g3m: Array[Boolean] = jc.g3().map(_ == null) + + val h11: T @CanEqualNull = jc.h1[T]() + val h12: T = jc.h1[T]() + val h1n = jc.h1[T]() == null + + val h21: ju.List[T] @CanEqualNull = jc.h2[T]() + val h22: ju.List[T] = jc.h2[T]() + + val h31: Array[T @CanEqualNull] @CanEqualNull = jc.h3[T]() + val h32: Array[T] = jc.h3[T]() + val h3m = jc.h3[T]().map(_ == null) } } \ No newline at end of file From 31b6dd2a2eade4a4cbc09906d7e7042308ab87a2 Mon Sep 17 00:00:00 2001 From: noti0na1 Date: Fri, 13 May 2022 14:23:25 -0400 Subject: [PATCH 05/13] Add comments --- .../src/dotty/tools/dotc/typer/Applications.scala | 2 ++ .../src/dotty/tools/dotc/typer/Synthesizer.scala | 12 +++++++++++- compiler/src/dotty/tools/dotc/typer/Typer.scala | 3 +++ .../test/dotty/tools/dotc/CompilationTests.scala | 1 + 4 files changed, 17 insertions(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/typer/Applications.scala b/compiler/src/dotty/tools/dotc/typer/Applications.scala index 9158367eca47..de829e2a8e89 100644 --- a/compiler/src/dotty/tools/dotc/typer/Applications.scala +++ b/compiler/src/dotty/tools/dotc/typer/Applications.scala @@ -911,6 +911,8 @@ trait Applications extends Compatibility { case funRef: TermRef => var app = ApplyTo(tree, fun1, funRef, proto, pt) if ctx.mode.is(Mode.UnsafeJavaReturn) then + // When UnsafeJavaReturn is enabled and the applied function is Java defined, + // we replece `| Null` with `@CanEqualNull` in the return type. val funSym = fun1.symbol if funSym.is(JavaDefined) && funSym.isTerm diff --git a/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala b/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala index 6e5570f96ed3..f453453f0793 100644 --- a/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala @@ -164,15 +164,17 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context): cmpWithBoxed(cls2, cls1) else if ctx.mode.is(Mode.SafeNulls) then // If explicit nulls is enabled, and unsafeNulls is not enabled, + // and the types don't have `@CanEqualNull` annotation, // we want to disallow comparison between Object and Null. // If we have to check whether a variable with a non-nullable type has null value // (for example, a NotNull java method returns null for some reasons), - // we can still cast it to a nullable type then compare its value. + // we can still use `eq/ne null` or cast it to a nullable type then compare its value. // // Example: // val x: String = null.asInstanceOf[String] // if (x == null) {} // error: x is non-nullable // if (x.asInstanceOf[String|Null] == null) {} // ok + // if (x eq null) {} // ok cls1 == defn.NullClass && cls1 == cls2 else if cls1 == defn.NullClass then cls1 == cls2 || cls2.derivesFrom(defn.ObjectClass) @@ -187,6 +189,14 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context): * interpret. */ def canComparePredefined(tp1: Type, tp2: Type) = + // In explicit nulls, when one of type has `@CanEqualNull` annotation, + // we use unsafe nulls semantic to check, which allows reference types + // to be compared with `Null`. + // Example: + // val s1: String = ??? + // s1 == null // error + // val s2: String @CanEqualNull = ??? + // s2 == null // ok val checkCtx = if ctx.explicitNulls && (tp1.hasAnnotation(defn.CanEqualNullAnnot) || tp2.hasAnnotation(defn.CanEqualNullAnnot)) then ctx.retractMode(Mode.SafeNulls) else ctx diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 2c1f7ef750f5..53ab8a82eb26 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -648,6 +648,9 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer val qual = typedExpr(tree.qualifier, shallowSelectionProto(tree.name, pt, this)) var sel = typedSelect(tree, pt, qual).withSpan(tree.span).computeNullable() if ctx.mode.is(Mode.UnsafeJavaReturn) && pt != AssignProto then + // When UnsafeJavaReturn is enabled and the selected member is Java defined, + // we replece `| Null` with `@CanEqualNull` in its type + // if it is not at left hand side of assignments. val sym = sel.symbol if sym.is(JavaDefined) && sym.isTerm && !sym.is(Method) then val stp1 = sel.tpe.widen diff --git a/compiler/test/dotty/tools/dotc/CompilationTests.scala b/compiler/test/dotty/tools/dotc/CompilationTests.scala index 5b18b6c81fe9..89380494112b 100644 --- a/compiler/test/dotty/tools/dotc/CompilationTests.scala +++ b/compiler/test/dotty/tools/dotc/CompilationTests.scala @@ -249,6 +249,7 @@ class CompilationTests { compileFilesInDir("tests/explicit-nulls/pos-separate", explicitNullsOptions), compileFilesInDir("tests/explicit-nulls/pos-patmat", explicitNullsOptions and "-Xfatal-warnings"), compileFilesInDir("tests/explicit-nulls/unsafe-common", explicitNullsOptions and "-language:unsafeNulls"), + compileFilesInDir("tests/explicit-nulls/unsafe-java", explicitNullsOptions), compileFile("tests/explicit-nulls/pos-special/i14682.scala", explicitNullsOptions and "-Ysafe-init"), compileFile("tests/explicit-nulls/pos-special/i14947.scala", explicitNullsOptions and "-Ytest-pickler" and "-Xprint-types"), ) From a4ec90370448668687c8900a96d14f724583601f Mon Sep 17 00:00:00 2001 From: noti0na1 Date: Fri, 13 May 2022 15:02:48 -0400 Subject: [PATCH 06/13] Rename test folder --- .../{unsafe-java-common => unsafe-java}/java-chain/J.java | 0 .../{unsafe-java-common => unsafe-java}/java-chain/S.scala | 0 .../{unsafe-java-common => unsafe-java}/java-class/J.java | 0 .../{unsafe-java-common => unsafe-java}/java-class/S.scala | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename tests/explicit-nulls/{unsafe-java-common => unsafe-java}/java-chain/J.java (100%) rename tests/explicit-nulls/{unsafe-java-common => unsafe-java}/java-chain/S.scala (100%) rename tests/explicit-nulls/{unsafe-java-common => unsafe-java}/java-class/J.java (100%) rename tests/explicit-nulls/{unsafe-java-common => unsafe-java}/java-class/S.scala (100%) diff --git a/tests/explicit-nulls/unsafe-java-common/java-chain/J.java b/tests/explicit-nulls/unsafe-java/java-chain/J.java similarity index 100% rename from tests/explicit-nulls/unsafe-java-common/java-chain/J.java rename to tests/explicit-nulls/unsafe-java/java-chain/J.java diff --git a/tests/explicit-nulls/unsafe-java-common/java-chain/S.scala b/tests/explicit-nulls/unsafe-java/java-chain/S.scala similarity index 100% rename from tests/explicit-nulls/unsafe-java-common/java-chain/S.scala rename to tests/explicit-nulls/unsafe-java/java-chain/S.scala diff --git a/tests/explicit-nulls/unsafe-java-common/java-class/J.java b/tests/explicit-nulls/unsafe-java/java-class/J.java similarity index 100% rename from tests/explicit-nulls/unsafe-java-common/java-class/J.java rename to tests/explicit-nulls/unsafe-java/java-class/J.java diff --git a/tests/explicit-nulls/unsafe-java-common/java-class/S.scala b/tests/explicit-nulls/unsafe-java/java-class/S.scala similarity index 100% rename from tests/explicit-nulls/unsafe-java-common/java-class/S.scala rename to tests/explicit-nulls/unsafe-java/java-class/S.scala From ee01fe4e8207d3f181edf7b99520e4477c6e2ed4 Mon Sep 17 00:00:00 2001 From: noti0na1 Date: Thu, 19 May 2022 16:48:05 -0400 Subject: [PATCH 07/13] Fix unary function call --- .../tools/dotc/core/NullOpsDecorator.scala | 37 ++++++++++++++++--- .../dotty/tools/dotc/typer/Applications.scala | 14 +------ .../src/dotty/tools/dotc/typer/Typer.scala | 17 ++------- .../unsafe-java/UnaryCall.scala | 4 ++ 4 files changed, 41 insertions(+), 31 deletions(-) create mode 100644 tests/explicit-nulls/unsafe-java/UnaryCall.scala diff --git a/compiler/src/dotty/tools/dotc/core/NullOpsDecorator.scala b/compiler/src/dotty/tools/dotc/core/NullOpsDecorator.scala index a6ee70fa6fc0..8b286f0df9a7 100644 --- a/compiler/src/dotty/tools/dotc/core/NullOpsDecorator.scala +++ b/compiler/src/dotty/tools/dotc/core/NullOpsDecorator.scala @@ -3,6 +3,7 @@ package core import Annotations._ import Contexts._ +import Flags._ import Symbols._ import Types._ @@ -71,10 +72,36 @@ object NullOpsDecorator: import ast.tpd._ - extension (self: Tree) + extension (tree: Tree) + // cast the type of the tree to a non-nullable type - def castToNonNullable(using Context): Tree = self.typeOpt match { - case OrNull(tp) => self.cast(tp) - case _ => self - } + def castToNonNullable(using Context): Tree = tree.typeOpt match + case OrNull(tp) => tree.cast(tp) + case _ => tree + + def tryToCastToCanEqualNull(using Context): Tree = + val sym = tree.symbol + val tp = tree.tpe + + if !ctx.mode.is(Mode.UnsafeJavaReturn) + || !sym.is(JavaDefined) + || sym.is(Package) + || !sym.isTerm + || tp.isError then + return tree + + tree match + case _: Apply if sym.is(Method) && !sym.isConstructor => + val tp2 = tp.replaceOrNull + if tp ne tp2 then + tree.cast(tp2) + else tree + case _: Select if !sym.is(Method) => + val tpw = tp.widen + val tp2 = tpw.replaceOrNull + if tpw ne tp2 then + tree.cast(tp2) + else tree + case _ => tree + end NullOpsDecorator diff --git a/compiler/src/dotty/tools/dotc/typer/Applications.scala b/compiler/src/dotty/tools/dotc/typer/Applications.scala index de829e2a8e89..1e36f0ac9140 100644 --- a/compiler/src/dotty/tools/dotc/typer/Applications.scala +++ b/compiler/src/dotty/tools/dotc/typer/Applications.scala @@ -909,19 +909,7 @@ trait Applications extends Compatibility { def simpleApply(fun1: Tree, proto: FunProto)(using Context): Tree = methPart(fun1).tpe match { case funRef: TermRef => - var app = ApplyTo(tree, fun1, funRef, proto, pt) - if ctx.mode.is(Mode.UnsafeJavaReturn) then - // When UnsafeJavaReturn is enabled and the applied function is Java defined, - // we replece `| Null` with `@CanEqualNull` in the return type. - val funSym = fun1.symbol - if funSym.is(JavaDefined) - && funSym.isTerm - && funSym.is(Method) - && !funSym.isConstructor then - val rtp1 = app.tpe - val rtp2 = rtp1.replaceOrNull - if rtp1 ne rtp2 then - app = app.cast(rtp2) + val app = ApplyTo(tree, fun1, funRef, proto, pt).tryToCastToCanEqualNull convertNewGenericArray( widenEnumCase( postProcessByNameArgs(funRef, app).computeNullable(), diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 53ab8a82eb26..9d76091e0774 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -646,18 +646,8 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer def typeSelectOnTerm(using Context): Tree = val qual = typedExpr(tree.qualifier, shallowSelectionProto(tree.name, pt, this)) - var sel = typedSelect(tree, pt, qual).withSpan(tree.span).computeNullable() - if ctx.mode.is(Mode.UnsafeJavaReturn) && pt != AssignProto then - // When UnsafeJavaReturn is enabled and the selected member is Java defined, - // we replece `| Null` with `@CanEqualNull` in its type - // if it is not at left hand side of assignments. - val sym = sel.symbol - if sym.is(JavaDefined) && sym.isTerm && !sym.is(Method) then - val stp1 = sel.tpe.widen - val stp2 = stp1.replaceOrNull - if stp1 ne stp2 then - sel = sel.cast(stp2) - sel + val sel = typedSelect(tree, pt, qual).withSpan(tree.span).computeNullable() + if pt != AssignProto then sel.tryToCastToCanEqualNull else sel def javaSelectOnType(qual: Tree)(using Context) = // semantic name conversion for `O$` in java code @@ -3690,7 +3680,8 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer } simplify(typed(etaExpand(tree, wtp, arity), pt), pt, locked) else if (wtp.paramInfos.isEmpty && isAutoApplied(tree.symbol)) - readaptSimplified(tpd.Apply(tree, Nil)) + val app = tpd.Apply(tree, Nil).tryToCastToCanEqualNull + readaptSimplified(app) else if (wtp.isImplicitMethod) err.typeMismatch(tree, pt) else diff --git a/tests/explicit-nulls/unsafe-java/UnaryCall.scala b/tests/explicit-nulls/unsafe-java/UnaryCall.scala new file mode 100644 index 000000000000..8fc535e535a6 --- /dev/null +++ b/tests/explicit-nulls/unsafe-java/UnaryCall.scala @@ -0,0 +1,4 @@ +import scala.language.unsafeJavaReturn + +val s = "foo" +val methods: Array[java.lang.reflect.Method] = s.getClass.getMethods From a9fd4247c9c6078afb12e34fd14e9f9f964eb767 Mon Sep 17 00:00:00 2001 From: noti0na1 Date: Wed, 29 Jun 2022 20:35:06 -0400 Subject: [PATCH 08/13] Fix bugs in the PR --- .../tools/dotc/core/NullOpsDecorator.scala | 10 +- .../src/dotty/tools/dotc/typer/Typer.scala | 2 +- .../unsafe-java/JavaStatic.scala | 14 +++ .../unsafe-java/UnaryCall.scala | 11 ++- .../unsafe-java/java-chain/J.java | 2 +- .../unsafe-java/java-chain/S.scala | 6 +- .../unsafe-java/java-class/J.java | 4 +- .../unsafe-java/java-class/S.scala | 97 +++++++++---------- 8 files changed, 85 insertions(+), 61 deletions(-) create mode 100644 tests/explicit-nulls/unsafe-java/JavaStatic.scala diff --git a/compiler/src/dotty/tools/dotc/core/NullOpsDecorator.scala b/compiler/src/dotty/tools/dotc/core/NullOpsDecorator.scala index 8b286f0df9a7..152cf936c762 100644 --- a/compiler/src/dotty/tools/dotc/core/NullOpsDecorator.scala +++ b/compiler/src/dotty/tools/dotc/core/NullOpsDecorator.scala @@ -6,6 +6,7 @@ import Contexts._ import Flags._ import Symbols._ import Types._ +import transform.SymUtils._ /** Defines operations on nullable types and tree. */ object NullOpsDecorator: @@ -80,23 +81,26 @@ object NullOpsDecorator: case _ => tree def tryToCastToCanEqualNull(using Context): Tree = + // return the tree directly if not at Typer phase + if !(ctx.explicitNulls && ctx.phase.isTyper) then return tree + val sym = tree.symbol val tp = tree.tpe if !ctx.mode.is(Mode.UnsafeJavaReturn) || !sym.is(JavaDefined) - || sym.is(Package) + || sym.isNoValue || !sym.isTerm || tp.isError then return tree tree match - case _: Apply if sym.is(Method) && !sym.isConstructor => + case _: Apply if sym.is(Method) => val tp2 = tp.replaceOrNull if tp ne tp2 then tree.cast(tp2) else tree - case _: Select if !sym.is(Method) => + case _: Select | _: Ident if !sym.is(Method) => val tpw = tp.widen val tp2 = tpw.replaceOrNull if tpw ne tp2 then diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 9d76091e0774..9f27b6ffd886 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -548,7 +548,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer ref(ownType).withSpan(tree.span) case _ => tree.withType(ownType) - val tree2 = toNotNullTermRef(tree1, pt) + val tree2 = toNotNullTermRef(tree1, pt).tryToCastToCanEqualNull checkLegalValue(tree2, pt) tree2 diff --git a/tests/explicit-nulls/unsafe-java/JavaStatic.scala b/tests/explicit-nulls/unsafe-java/JavaStatic.scala new file mode 100644 index 000000000000..de84fce65fd9 --- /dev/null +++ b/tests/explicit-nulls/unsafe-java/JavaStatic.scala @@ -0,0 +1,14 @@ +import language.unsafeJavaReturn + +import java.math.MathContext, MathContext._ + +val x: MathContext = DECIMAL32 +val y: MathContext = MathContext.DECIMAL32 + +import java.io.File + +val s: String = File.separator +import java.time.ZoneId + +val zids: java.util.Set[String] = ZoneId.getAvailableZoneIds +val zarr: Array[String] = ZoneId.getAvailableZoneIds.toArray(Array.empty[String | Null]) diff --git a/tests/explicit-nulls/unsafe-java/UnaryCall.scala b/tests/explicit-nulls/unsafe-java/UnaryCall.scala index 8fc535e535a6..70fc132ac8cb 100644 --- a/tests/explicit-nulls/unsafe-java/UnaryCall.scala +++ b/tests/explicit-nulls/unsafe-java/UnaryCall.scala @@ -1,4 +1,11 @@ import scala.language.unsafeJavaReturn -val s = "foo" -val methods: Array[java.lang.reflect.Method] = s.getClass.getMethods +import java.lang.reflect.Method + +def getMethods(f: String): List[Method] = + val clazz = Class.forName(f) + val methods = clazz.getMethods + if methods == null then List() + else methods.toList + +def getClass(o: AnyRef): Class[?] = o.getClass diff --git a/tests/explicit-nulls/unsafe-java/java-chain/J.java b/tests/explicit-nulls/unsafe-java/java-chain/J.java index bd266bae13d9..91dda8438a2b 100644 --- a/tests/explicit-nulls/unsafe-java/java-chain/J.java +++ b/tests/explicit-nulls/unsafe-java/java-chain/J.java @@ -4,4 +4,4 @@ class J1 { class J2 { J1 getJ1() { return new J1(); } -} \ No newline at end of file +} diff --git a/tests/explicit-nulls/unsafe-java/java-chain/S.scala b/tests/explicit-nulls/unsafe-java/java-chain/S.scala index 9fe5aa3f08ce..607db09635ac 100644 --- a/tests/explicit-nulls/unsafe-java/java-chain/S.scala +++ b/tests/explicit-nulls/unsafe-java/java-chain/S.scala @@ -1,4 +1,6 @@ -class S { +import scala.language.unsafeJavaReturn + +def f = { val j: J2 = new J2() - j.getJ1().getJ2().getJ1().getJ2().getJ1().getJ2() // error + j.getJ1().getJ2().getJ1().getJ2().getJ1().getJ2() } diff --git a/tests/explicit-nulls/unsafe-java/java-class/J.java b/tests/explicit-nulls/unsafe-java/java-class/J.java index 91f58a42ccf9..b90fffde3f22 100644 --- a/tests/explicit-nulls/unsafe-java/java-class/J.java +++ b/tests/explicit-nulls/unsafe-java/java-class/J.java @@ -1,6 +1,6 @@ import java.util.List; -public class JC { +public class J { public int a; @@ -39,4 +39,4 @@ public List h2() { public T[] h3() { return null; } -} \ No newline at end of file +} diff --git a/tests/explicit-nulls/unsafe-java/java-class/S.scala b/tests/explicit-nulls/unsafe-java/java-class/S.scala index b8eb7b1cdfc2..ff3d78f79618 100644 --- a/tests/explicit-nulls/unsafe-java/java-class/S.scala +++ b/tests/explicit-nulls/unsafe-java/java-class/S.scala @@ -3,53 +3,50 @@ import scala.language.unsafeJavaReturn import scala.annotation.CanEqualNull import java.{util => ju} -class S { - - def test[T <: AnyRef](jc: JC) = { - val a: Int = jc.a - - val b = jc.b // it returns String @CanEqualNull - val b2: String = b - val b3: String @CanEqualNull = jc.b - val b4: String = jc.b - val bb = jc.b == null // it's ok to compare String @CanEqualNull with Null - val btl = jc.b.trim().length() // String @CanEqualNull is just String, unsafe selecting - - val c = jc.c - val cl = c.length - val c2: Array[String] = c - val c3: Array[String @CanEqualNull] @CanEqualNull = jc.c - val c4: Array[String] = jc.c - val cml: Array[Int] = c.map(_.length()) - - val f1: Int = jc.f1() - - val f21: Array[Int] @CanEqualNull = jc.f2() - val f22: Array[Int] = jc.f2() - val f2n = jc.f2() == null - - val g11: String @CanEqualNull = jc.g1() - val g12: String = jc.g1() - val g1n = jc.g1() == null - val g1tl = jc.g1().trim().length() - - val g21: ju.List[String] @CanEqualNull = jc.g2() - val g22: ju.List[String] = jc.g2() - - val g31: Array[String @CanEqualNull] @CanEqualNull = jc.g3() - val g32: Array[String] = jc.g3() - val g3n = jc.g3() == null - val g3m: Array[Boolean] = jc.g3().map(_ == null) - - val h11: T @CanEqualNull = jc.h1[T]() - val h12: T = jc.h1[T]() - val h1n = jc.h1[T]() == null - - val h21: ju.List[T] @CanEqualNull = jc.h2[T]() - val h22: ju.List[T] = jc.h2[T]() - - val h31: Array[T @CanEqualNull] @CanEqualNull = jc.h3[T]() - val h32: Array[T] = jc.h3[T]() - val h3m = jc.h3[T]().map(_ == null) - } -} \ No newline at end of file +def test[T <: AnyRef](j: J) = { + val a: Int = j.a + + val b = j.b // it returns String @CanEqualNull + val b2: String = b + val b3: String @CanEqualNull = j.b + val b4: String = j.b + val bb = j.b == null // it's ok to compare String @CanEqualNull with Null + val btl = j.b.trim().length() // String @CanEqualNull is just String, unsafe selecting + + val c = j.c + val cl = c.length + val c2: Array[String] = c + val c3: Array[String @CanEqualNull] @CanEqualNull = j.c + val c4: Array[String] = j.c + val cml: Array[Int] = c.map(_.length()) + + val f1: Int = j.f1() + + val f21: Array[Int] @CanEqualNull = j.f2() + val f22: Array[Int] = j.f2() + val f2n = j.f2() == null + + val g11: String @CanEqualNull = j.g1() + val g12: String = j.g1() + val g1n = j.g1() == null + val g1tl = j.g1().trim().length() + + val g21: ju.List[String] @CanEqualNull = j.g2() + val g22: ju.List[String] = j.g2() + + val g31: Array[String @CanEqualNull] @CanEqualNull = j.g3() + val g32: Array[String] = j.g3() + val g3n = j.g3() == null + val g3m: Array[Boolean] = j.g3().map(_ == null) + + val h11: T @CanEqualNull = j.h1[T]() + val h12: T = j.h1[T]() + val h1n = j.h1[T]() == null + + val h21: ju.List[T] @CanEqualNull = j.h2[T]() + val h22: ju.List[T] = j.h2[T]() + + val h31: Array[T @CanEqualNull] @CanEqualNull = j.h3[T]() + val h32: Array[T] = j.h3[T]() + val h3m = j.h3[T]().map(_ == null) +} From 1b3fb9942db67956045018bd1c14e3cb3a99b8b1 Mon Sep 17 00:00:00 2001 From: noti0na1 Date: Wed, 29 Jun 2022 21:05:22 -0400 Subject: [PATCH 09/13] Fix comment --- library/src/scala/annotation/CanEqualNull.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/src/scala/annotation/CanEqualNull.scala b/library/src/scala/annotation/CanEqualNull.scala index bb8838317f8a..ed8cb6d582e8 100644 --- a/library/src/scala/annotation/CanEqualNull.scala +++ b/library/src/scala/annotation/CanEqualNull.scala @@ -14,7 +14,7 @@ package scala.annotation * val s3: String = s2 * val s4: String @CanEqualNull = s1 * - * val ss: Array[String @CanEqualNull] = ?? + * val ss: Array[String @CanEqualNull] = ??? * ss.map(_ == null) * ``` */ From 2a885b80de6df33ac5be7767bc107cc2a7392281 Mon Sep 17 00:00:00 2001 From: noti0na1 Date: Tue, 5 Jul 2022 14:52:03 -0400 Subject: [PATCH 10/13] Mark UnsafeJavaReturn as experimental --- compiler/src/dotty/tools/dotc/Run.scala | 2 +- compiler/src/dotty/tools/dotc/config/Feature.scala | 1 + compiler/src/dotty/tools/dotc/core/Contexts.scala | 3 ++- compiler/src/dotty/tools/dotc/core/StdNames.scala | 1 - library/src/scala/annotation/CanEqualNull.scala | 1 + library/src/scala/runtime/stdLibPatches/language.scala | 8 +++++--- tests/explicit-nulls/unsafe-java/JavaStatic.scala | 2 +- tests/explicit-nulls/unsafe-java/UnaryCall.scala | 2 +- tests/explicit-nulls/unsafe-java/java-chain/S.scala | 2 +- tests/explicit-nulls/unsafe-java/java-class/S.scala | 2 +- 10 files changed, 14 insertions(+), 10 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/Run.scala b/compiler/src/dotty/tools/dotc/Run.scala index 7f31c1ee324b..0ef2a804449a 100644 --- a/compiler/src/dotty/tools/dotc/Run.scala +++ b/compiler/src/dotty/tools/dotc/Run.scala @@ -368,7 +368,7 @@ class Run(comp: Compiler, ictx: Context) extends ImplicitRunInfo with Constraint if ctx.settings.YexplicitNulls.value then if !Feature.enabledBySetting(nme.unsafeNulls) then start = start.addMode(Mode.SafeNulls) - if Feature.enabledBySetting(nme.unsafeJavaReturn) then + if Feature.enabledBySetting(Feature.unsafeJavaReturn) then start = start.addMode(Mode.UnsafeJavaReturn) ctx.initialize()(using start) // re-initialize the base context with start diff --git a/compiler/src/dotty/tools/dotc/config/Feature.scala b/compiler/src/dotty/tools/dotc/config/Feature.scala index 4a87f5b4a537..a08badbdbe25 100644 --- a/compiler/src/dotty/tools/dotc/config/Feature.scala +++ b/compiler/src/dotty/tools/dotc/config/Feature.scala @@ -28,6 +28,7 @@ object Feature: val symbolLiterals = deprecated("symbolLiterals") val fewerBraces = experimental("fewerBraces") val saferExceptions = experimental("saferExceptions") + val unsafeJavaReturn = experimental("unsafeJavaReturn") /** Is `feature` enabled by by a command-line setting? The enabling setting is * diff --git a/compiler/src/dotty/tools/dotc/core/Contexts.scala b/compiler/src/dotty/tools/dotc/core/Contexts.scala index 30d95d256a6e..41ec9b015e66 100644 --- a/compiler/src/dotty/tools/dotc/core/Contexts.scala +++ b/compiler/src/dotty/tools/dotc/core/Contexts.scala @@ -26,6 +26,7 @@ import scala.io.Codec import collection.mutable import printing._ import config.{JavaPlatform, SJSPlatform, Platform, ScalaSettings} +import config.Feature import classfile.ReusableDataReader import StdNames.nme @@ -649,7 +650,7 @@ object Contexts { case Some(false) => setMode(this.mode | Mode.SafeNulls) case _ => - importInfo.mentionsFeature(nme.unsafeJavaReturn) match + importInfo.mentionsFeature(Feature.unsafeJavaReturn) match case Some(true) => setMode(this.mode | Mode.UnsafeJavaReturn) case Some(false) => diff --git a/compiler/src/dotty/tools/dotc/core/StdNames.scala b/compiler/src/dotty/tools/dotc/core/StdNames.scala index 95f4b5027305..9f128a71be7b 100644 --- a/compiler/src/dotty/tools/dotc/core/StdNames.scala +++ b/compiler/src/dotty/tools/dotc/core/StdNames.scala @@ -612,7 +612,6 @@ object StdNames { val unapplySeq: N = "unapplySeq" val unbox: N = "unbox" val universe: N = "universe" - val unsafeJavaReturn: N = "unsafeJavaReturn" val unsafeNulls: N = "unsafeNulls" val update: N = "update" val updateDynamic: N = "updateDynamic" diff --git a/library/src/scala/annotation/CanEqualNull.scala b/library/src/scala/annotation/CanEqualNull.scala index ed8cb6d582e8..fca1292b22af 100644 --- a/library/src/scala/annotation/CanEqualNull.scala +++ b/library/src/scala/annotation/CanEqualNull.scala @@ -18,4 +18,5 @@ package scala.annotation * ss.map(_ == null) * ``` */ +@experimental final class CanEqualNull extends RefiningAnnotation diff --git a/library/src/scala/runtime/stdLibPatches/language.scala b/library/src/scala/runtime/stdLibPatches/language.scala index 73e888564d06..63b075aeb5bb 100644 --- a/library/src/scala/runtime/stdLibPatches/language.scala +++ b/library/src/scala/runtime/stdLibPatches/language.scala @@ -60,6 +60,11 @@ object language: @compileTimeOnly("`saferExceptions` can only be used at compile time in import statements") object saferExceptions + /** Experimental support for unsafe Java return in explicit nulls + */ + @compileTimeOnly("`unsafeJavaReturn` can only be used at compile time in import statements") + object unsafeJavaReturn + end experimental /** The deprecated object contains features that are no longer officially suypported in Scala. @@ -128,9 +133,6 @@ object language: @compileTimeOnly("`unsafeNulls` can only be used at compile time in import statements") object unsafeNulls - @compileTimeOnly("`unsafeJavaReturn` can only be used at compile time in import statements") - object unsafeJavaReturn - @compileTimeOnly("`future` can only be used at compile time in import statements") object future diff --git a/tests/explicit-nulls/unsafe-java/JavaStatic.scala b/tests/explicit-nulls/unsafe-java/JavaStatic.scala index de84fce65fd9..52693545d218 100644 --- a/tests/explicit-nulls/unsafe-java/JavaStatic.scala +++ b/tests/explicit-nulls/unsafe-java/JavaStatic.scala @@ -1,4 +1,4 @@ -import language.unsafeJavaReturn +import language.experimental.unsafeJavaReturn import java.math.MathContext, MathContext._ diff --git a/tests/explicit-nulls/unsafe-java/UnaryCall.scala b/tests/explicit-nulls/unsafe-java/UnaryCall.scala index 70fc132ac8cb..553453516f9f 100644 --- a/tests/explicit-nulls/unsafe-java/UnaryCall.scala +++ b/tests/explicit-nulls/unsafe-java/UnaryCall.scala @@ -1,4 +1,4 @@ -import scala.language.unsafeJavaReturn +import scala.language.experimental.unsafeJavaReturn import java.lang.reflect.Method diff --git a/tests/explicit-nulls/unsafe-java/java-chain/S.scala b/tests/explicit-nulls/unsafe-java/java-chain/S.scala index 607db09635ac..778009f2f401 100644 --- a/tests/explicit-nulls/unsafe-java/java-chain/S.scala +++ b/tests/explicit-nulls/unsafe-java/java-chain/S.scala @@ -1,4 +1,4 @@ -import scala.language.unsafeJavaReturn +import scala.language.experimental.unsafeJavaReturn def f = { val j: J2 = new J2() diff --git a/tests/explicit-nulls/unsafe-java/java-class/S.scala b/tests/explicit-nulls/unsafe-java/java-class/S.scala index ff3d78f79618..215ae4bd895d 100644 --- a/tests/explicit-nulls/unsafe-java/java-class/S.scala +++ b/tests/explicit-nulls/unsafe-java/java-class/S.scala @@ -1,4 +1,4 @@ -import scala.language.unsafeJavaReturn +import scala.language.experimental.unsafeJavaReturn import scala.annotation.CanEqualNull import java.{util => ju} From a874a9c3c83cc03afa79e3ff5b30f23c66962433 Mon Sep 17 00:00:00 2001 From: noti0na1 Date: Tue, 5 Jul 2022 16:50:41 -0400 Subject: [PATCH 11/13] Add unsafeJavaReturn to MiMaFilters --- project/MiMaFilters.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/project/MiMaFilters.scala b/project/MiMaFilters.scala index 846856adc2c8..f1f9c3230687 100644 --- a/project/MiMaFilters.scala +++ b/project/MiMaFilters.scala @@ -11,8 +11,6 @@ object MiMaFilters { ProblemFilters.exclude[MissingClassProblem]("scala.runtime.TupleMirror"), ProblemFilters.exclude[MissingTypesProblem]("scala.Tuple$package$EmptyTuple$"), // we made the empty tuple a case object ProblemFilters.exclude[DirectMissingMethodProblem]("scala.runtime.Scala3RunTime.nnFail"), - ProblemFilters.exclude[DirectMissingMethodProblem]("scala.runtime.Scala3RunTime.nnFail"), - ProblemFilters.exclude[DirectMissingMethodProblem]("scala.runtime.LazyVals.getOffsetStatic"), // Added for #14780 ProblemFilters.exclude[DirectMissingMethodProblem]("scala.runtime.LazyVals.getOffsetStatic"), // Added for #14780 ProblemFilters.exclude[MissingFieldProblem]("scala.runtime.stdLibPatches.language.3.2-migration"), ProblemFilters.exclude[MissingFieldProblem]("scala.runtime.stdLibPatches.language.3.2"), @@ -28,6 +26,8 @@ object MiMaFilters { ProblemFilters.exclude[ReversedMissingMethodProblem]("scala.quoted.Quotes#reflectModule#SymbolMethods.typeRef"), ProblemFilters.exclude[ReversedMissingMethodProblem]("scala.quoted.Quotes#reflectModule#SymbolMethods.termRef"), ProblemFilters.exclude[ReversedMissingMethodProblem]("scala.quoted.Quotes#reflectModule#TypeTreeModule.ref"), + ProblemFilters.exclude[MissingFieldProblem]("scala.runtime.stdLibPatches.language#experimental.unsafeJavaReturn"), + ProblemFilters.exclude[MissingClassProblem]("scala.runtime.stdLibPatches.language$experimental$unsafeJavaReturn$"), ProblemFilters.exclude[MissingClassProblem]("scala.annotation.since"), ) From ad4bdc30d2192a74283533e37d2c3021436822d9 Mon Sep 17 00:00:00 2001 From: noti0na1 Date: Wed, 6 Jul 2022 14:34:27 -0400 Subject: [PATCH 12/13] Add CanEqualNull to experimentalDefinitionInLibrary --- .../tasty-inspector/stdlibExperimentalDefinitions.scala | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/run-custom-args/tasty-inspector/stdlibExperimentalDefinitions.scala b/tests/run-custom-args/tasty-inspector/stdlibExperimentalDefinitions.scala index a65a50ab54ad..f15a91b914bb 100644 --- a/tests/run-custom-args/tasty-inspector/stdlibExperimentalDefinitions.scala +++ b/tests/run-custom-args/tasty-inspector/stdlibExperimentalDefinitions.scala @@ -46,6 +46,13 @@ val experimentalDefinitionInLibrary = Set( "scala.annotation.newMain.Help$", "scala.annotation.newMain.Names", + //// New feature: CanEqualNull annotation + // Can be stabilized when language feature is stabilized. + // Needs user feedback. + // This annotation makes it possible to use `==` and `!=` to compare reference types and null in explicit nulls. + "scala.annotation.CanEqualNull", + "scala.annotation.CanEqualNull$", + //// New APIs: Mirror // Can be stabilized in 3.3.0 or later. "scala.deriving.Mirror$.fromProductTyped", // This API is a bit convoluted. We may need some more feedback before we can stabilize it. From 70bf99d01691ad03f25e9a14e1e7ee93cd2ff1c4 Mon Sep 17 00:00:00 2001 From: noti0na1 Date: Wed, 6 Jul 2022 15:42:35 -0400 Subject: [PATCH 13/13] Delete extra definition --- .../tasty-inspector/stdlibExperimentalDefinitions.scala | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/run-custom-args/tasty-inspector/stdlibExperimentalDefinitions.scala b/tests/run-custom-args/tasty-inspector/stdlibExperimentalDefinitions.scala index f15a91b914bb..dc5879a72a8b 100644 --- a/tests/run-custom-args/tasty-inspector/stdlibExperimentalDefinitions.scala +++ b/tests/run-custom-args/tasty-inspector/stdlibExperimentalDefinitions.scala @@ -51,7 +51,6 @@ val experimentalDefinitionInLibrary = Set( // Needs user feedback. // This annotation makes it possible to use `==` and `!=` to compare reference types and null in explicit nulls. "scala.annotation.CanEqualNull", - "scala.annotation.CanEqualNull$", //// New APIs: Mirror // Can be stabilized in 3.3.0 or later.