From 4bcb5ff86efee1945d182cac2b27af8e80ead3af Mon Sep 17 00:00:00 2001 From: noti0na1 Date: Tue, 15 Jul 2025 15:47:00 +0200 Subject: [PATCH 1/6] Add `nullTrackable` annotation to force tracking mutale fields --- .../src/dotty/tools/dotc/core/Definitions.scala | 1 + .../src/dotty/tools/dotc/typer/Nullables.scala | 17 ++++++++++++----- .../src/scala/annotation/nullTrackable.scala | 3 +++ .../pos/force-track-var-fields.scala | 14 ++++++++++++++ 4 files changed, 30 insertions(+), 5 deletions(-) create mode 100644 library/src/scala/annotation/nullTrackable.scala create mode 100644 tests/explicit-nulls/pos/force-track-var-fields.scala diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 386bae0f68c2..01c2eed2fd01 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -1101,6 +1101,7 @@ class Definitions { @tu lazy val RetainsByNameAnnot: ClassSymbol = requiredClass("scala.annotation.retainsByName") @tu lazy val PublicInBinaryAnnot: ClassSymbol = requiredClass("scala.annotation.publicInBinary") @tu lazy val WitnessNamesAnnot: ClassSymbol = requiredClass("scala.annotation.internal.WitnessNames") + @tu lazy val NullTrackableAnnot: ClassSymbol = requiredClass("scala.annotation.nullTrackable") @tu lazy val JavaRepeatableAnnot: ClassSymbol = requiredClass("java.lang.annotation.Repeatable") diff --git a/compiler/src/dotty/tools/dotc/typer/Nullables.scala b/compiler/src/dotty/tools/dotc/typer/Nullables.scala index 86b9a337e69a..97b6aff57ab3 100644 --- a/compiler/src/dotty/tools/dotc/typer/Nullables.scala +++ b/compiler/src/dotty/tools/dotc/typer/Nullables.scala @@ -186,15 +186,22 @@ object Nullables: * Check `usedOutOfOrder` to see the explaination and example of "out of order". * See more examples in `tests/explicit-nulls/neg/var-ref-in-closure.scala`. */ - def isTracked(ref: TermRef)(using Context) = + def isTracked(ref: TermRef)(using Context) = // true + val sym = ref.symbol + + def isNullTrackableField: Boolean = + ref.prefix.isStable + && sym.isField + && sym.hasAnnotation(defn.NullTrackableAnnot) + + // println(s"isTracked: $ref, usedOutOfOrder = ${ref.usedOutOfOrder}, isStable = ${ref.isStable}, span = ${ref.symbol.span}, assignmentSpans = ${ctx.compilationUnit.assignmentSpans.get(ref.symbol.span.start)}") ref.isStable - || { val sym = ref.symbol - val unit = ctx.compilationUnit + || isNullTrackableField + || { val unit = ctx.compilationUnit !ref.usedOutOfOrder && sym.span.exists && (unit ne NoCompilationUnit) // could be null under -Ytest-pickler - && unit.assignmentSpans.contains(sym.span.start) - } + && unit.assignmentSpans.contains(sym.span.start) } /** The nullability context to be used after a case that matches pattern `pat`. * If `pat` is `null`, this will assert that the selector `sel` is not null afterwards. diff --git a/library/src/scala/annotation/nullTrackable.scala b/library/src/scala/annotation/nullTrackable.scala new file mode 100644 index 000000000000..535bcc85b068 --- /dev/null +++ b/library/src/scala/annotation/nullTrackable.scala @@ -0,0 +1,3 @@ +package scala.annotation + +final class nullTrackable extends StaticAnnotation \ No newline at end of file diff --git a/tests/explicit-nulls/pos/force-track-var-fields.scala b/tests/explicit-nulls/pos/force-track-var-fields.scala new file mode 100644 index 000000000000..5c4c2d8e93c2 --- /dev/null +++ b/tests/explicit-nulls/pos/force-track-var-fields.scala @@ -0,0 +1,14 @@ +import scala.annotation.nullTrackable + +class A: + @nullTrackable var s: String | Null = null + def getS: String = + if s == null then s = "" + s + +def test(a: A): String = + if a.s == null then + a.s = "" + a.s + else + a.s \ No newline at end of file From 5a4f04f20f4f49390a02148756e5365d263968f1 Mon Sep 17 00:00:00 2001 From: noti0na1 Date: Wed, 16 Jul 2025 13:11:58 +0200 Subject: [PATCH 2/6] Fix stdlib compiling --- library/src/scala/annotation/nullTrackable.scala | 9 ++++++++- project/Build.scala | 2 ++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/library/src/scala/annotation/nullTrackable.scala b/library/src/scala/annotation/nullTrackable.scala index 535bcc85b068..d10024de80ee 100644 --- a/library/src/scala/annotation/nullTrackable.scala +++ b/library/src/scala/annotation/nullTrackable.scala @@ -1,3 +1,10 @@ package scala.annotation -final class nullTrackable extends StaticAnnotation \ No newline at end of file +/** An annotation that can be used to mark a mutable field as trackable for nullability. + * With explicit nulls, a normal mutable field can be tracked for nullability by flow typing, + * since it can be updated to a null value at the same time. + * This annotation will force the compiler to track the field for nullability, as long as the + * prefix is a stable path. + * See `tests/explicit-nulls/pos/force-track-var-fields.scala` for an example. + */ +final class nullTrackable extends StaticAnnotation diff --git a/project/Build.scala b/project/Build.scala index 5f75d156f5a4..e8a5bee8fa71 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -1141,6 +1141,7 @@ object Build { file(s"${baseDirectory.value}/src/scala/annotation/init.scala"), file(s"${baseDirectory.value}/src/scala/annotation/unroll.scala"), file(s"${baseDirectory.value}/src/scala/annotation/targetName.scala"), + file(s"${baseDirectory.value}/src/scala/annotation/nullTrackable.scala"), file(s"${baseDirectory.value}/src/scala/deriving/Mirror.scala"), file(s"${baseDirectory.value}/src/scala/compiletime/package.scala"), file(s"${baseDirectory.value}/src/scala/quoted/Type.scala"), @@ -1278,6 +1279,7 @@ object Build { file(s"${baseDirectory.value}/src/scala/annotation/init.scala"), file(s"${baseDirectory.value}/src/scala/annotation/unroll.scala"), file(s"${baseDirectory.value}/src/scala/annotation/targetName.scala"), + file(s"${baseDirectory.value}/src/scala/annotation/nullTrackable.scala"), file(s"${baseDirectory.value}/src/scala/deriving/Mirror.scala"), file(s"${baseDirectory.value}/src/scala/compiletime/package.scala"), file(s"${baseDirectory.value}/src/scala/quoted/Type.scala"), From ec8e000f7df1113e10cf23049aa5f5275c2cfdd2 Mon Sep 17 00:00:00 2001 From: noti0na1 Date: Wed, 16 Jul 2025 14:18:21 +0200 Subject: [PATCH 3/6] Rename annotation to `StableNull` and update test --- compiler/src/dotty/tools/dotc/core/Definitions.scala | 2 +- compiler/src/dotty/tools/dotc/typer/Nullables.scala | 6 +++--- .../annotation/{nullTrackable.scala => stableNull.scala} | 2 +- project/Build.scala | 4 ++-- tests/explicit-nulls/pos/force-track-var-fields.scala | 6 ++++-- 5 files changed, 11 insertions(+), 9 deletions(-) rename library/src/scala/annotation/{nullTrackable.scala => stableNull.scala} (88%) diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 01c2eed2fd01..d58c103904b0 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -1101,7 +1101,7 @@ class Definitions { @tu lazy val RetainsByNameAnnot: ClassSymbol = requiredClass("scala.annotation.retainsByName") @tu lazy val PublicInBinaryAnnot: ClassSymbol = requiredClass("scala.annotation.publicInBinary") @tu lazy val WitnessNamesAnnot: ClassSymbol = requiredClass("scala.annotation.internal.WitnessNames") - @tu lazy val NullTrackableAnnot: ClassSymbol = requiredClass("scala.annotation.nullTrackable") + @tu lazy val StableNullAnnot: ClassSymbol = requiredClass("scala.annotation.stableNull") @tu lazy val JavaRepeatableAnnot: ClassSymbol = requiredClass("java.lang.annotation.Repeatable") diff --git a/compiler/src/dotty/tools/dotc/typer/Nullables.scala b/compiler/src/dotty/tools/dotc/typer/Nullables.scala index 97b6aff57ab3..d581c0cbd0e0 100644 --- a/compiler/src/dotty/tools/dotc/typer/Nullables.scala +++ b/compiler/src/dotty/tools/dotc/typer/Nullables.scala @@ -189,14 +189,14 @@ object Nullables: def isTracked(ref: TermRef)(using Context) = // true val sym = ref.symbol - def isNullTrackableField: Boolean = + def isNullStableField: Boolean = ref.prefix.isStable && sym.isField - && sym.hasAnnotation(defn.NullTrackableAnnot) + && sym.hasAnnotation(defn.StableNullAnnot) // println(s"isTracked: $ref, usedOutOfOrder = ${ref.usedOutOfOrder}, isStable = ${ref.isStable}, span = ${ref.symbol.span}, assignmentSpans = ${ctx.compilationUnit.assignmentSpans.get(ref.symbol.span.start)}") ref.isStable - || isNullTrackableField + || isNullStableField || { val unit = ctx.compilationUnit !ref.usedOutOfOrder && sym.span.exists diff --git a/library/src/scala/annotation/nullTrackable.scala b/library/src/scala/annotation/stableNull.scala similarity index 88% rename from library/src/scala/annotation/nullTrackable.scala rename to library/src/scala/annotation/stableNull.scala index d10024de80ee..65cca794e932 100644 --- a/library/src/scala/annotation/nullTrackable.scala +++ b/library/src/scala/annotation/stableNull.scala @@ -7,4 +7,4 @@ package scala.annotation * prefix is a stable path. * See `tests/explicit-nulls/pos/force-track-var-fields.scala` for an example. */ -final class nullTrackable extends StaticAnnotation +private[scala] final class stableNull extends StaticAnnotation diff --git a/project/Build.scala b/project/Build.scala index e8a5bee8fa71..8eb6fd0a8aa3 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -1141,7 +1141,7 @@ object Build { file(s"${baseDirectory.value}/src/scala/annotation/init.scala"), file(s"${baseDirectory.value}/src/scala/annotation/unroll.scala"), file(s"${baseDirectory.value}/src/scala/annotation/targetName.scala"), - file(s"${baseDirectory.value}/src/scala/annotation/nullTrackable.scala"), + file(s"${baseDirectory.value}/src/scala/annotation/stableNull.scala"), file(s"${baseDirectory.value}/src/scala/deriving/Mirror.scala"), file(s"${baseDirectory.value}/src/scala/compiletime/package.scala"), file(s"${baseDirectory.value}/src/scala/quoted/Type.scala"), @@ -1279,7 +1279,7 @@ object Build { file(s"${baseDirectory.value}/src/scala/annotation/init.scala"), file(s"${baseDirectory.value}/src/scala/annotation/unroll.scala"), file(s"${baseDirectory.value}/src/scala/annotation/targetName.scala"), - file(s"${baseDirectory.value}/src/scala/annotation/nullTrackable.scala"), + file(s"${baseDirectory.value}/src/scala/annotation/stableNull.scala"), file(s"${baseDirectory.value}/src/scala/deriving/Mirror.scala"), file(s"${baseDirectory.value}/src/scala/compiletime/package.scala"), file(s"${baseDirectory.value}/src/scala/quoted/Type.scala"), diff --git a/tests/explicit-nulls/pos/force-track-var-fields.scala b/tests/explicit-nulls/pos/force-track-var-fields.scala index 5c4c2d8e93c2..27f0448b8023 100644 --- a/tests/explicit-nulls/pos/force-track-var-fields.scala +++ b/tests/explicit-nulls/pos/force-track-var-fields.scala @@ -1,7 +1,9 @@ -import scala.annotation.nullTrackable +package scala + +import scala.annotation.stableNull class A: - @nullTrackable var s: String | Null = null + @stableNull var s: String | Null = null def getS: String = if s == null then s = "" s From 62f376344eb3ed51aec88240d6eaf8d884450b55 Mon Sep 17 00:00:00 2001 From: noti0na1 Date: Wed, 16 Jul 2025 14:20:41 +0200 Subject: [PATCH 4/6] Remove commented debug print --- compiler/src/dotty/tools/dotc/typer/Nullables.scala | 1 - 1 file changed, 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/typer/Nullables.scala b/compiler/src/dotty/tools/dotc/typer/Nullables.scala index d581c0cbd0e0..609dad894b6c 100644 --- a/compiler/src/dotty/tools/dotc/typer/Nullables.scala +++ b/compiler/src/dotty/tools/dotc/typer/Nullables.scala @@ -194,7 +194,6 @@ object Nullables: && sym.isField && sym.hasAnnotation(defn.StableNullAnnot) - // println(s"isTracked: $ref, usedOutOfOrder = ${ref.usedOutOfOrder}, isStable = ${ref.isStable}, span = ${ref.symbol.span}, assignmentSpans = ${ctx.compilationUnit.assignmentSpans.get(ref.symbol.span.start)}") ref.isStable || isNullStableField || { val unit = ctx.compilationUnit From 41b9b5af1c4e7eedc2f70e54dd327225025e5ee8 Mon Sep 17 00:00:00 2001 From: noti0na1 Date: Sun, 20 Jul 2025 20:28:50 +0800 Subject: [PATCH 5/6] Fix documentation typo and update MiMaFilters --- library/src/scala/annotation/stableNull.scala | 2 +- project/MiMaFilters.scala | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/library/src/scala/annotation/stableNull.scala b/library/src/scala/annotation/stableNull.scala index 65cca794e932..e2ebac72fce5 100644 --- a/library/src/scala/annotation/stableNull.scala +++ b/library/src/scala/annotation/stableNull.scala @@ -1,7 +1,7 @@ package scala.annotation /** An annotation that can be used to mark a mutable field as trackable for nullability. - * With explicit nulls, a normal mutable field can be tracked for nullability by flow typing, + * With explicit nulls, a normal mutable field cannot be tracked for nullability by flow typing, * since it can be updated to a null value at the same time. * This annotation will force the compiler to track the field for nullability, as long as the * prefix is a stable path. diff --git a/project/MiMaFilters.scala b/project/MiMaFilters.scala index 864f6f6f272f..a38090e53f90 100644 --- a/project/MiMaFilters.scala +++ b/project/MiMaFilters.scala @@ -45,6 +45,7 @@ object MiMaFilters { ProblemFilters.exclude[MissingClassProblem]("scala.annotation.internal.reachCapability"), ProblemFilters.exclude[MissingClassProblem]("scala.annotation.internal.preview"), ProblemFilters.exclude[MissingClassProblem]("scala.annotation.unchecked.uncheckedCaptures"), + ProblemFilters.exclude[MissingClassProblem]("scala.annotation.stableNull"), ProblemFilters.exclude[MissingClassProblem]("scala.quoted.Quotes$reflectModule$ValOrDefDefMethods"), ProblemFilters.exclude[MissingClassProblem]("scala.runtime.stdLibPatches.language$3$u002E4$"), ProblemFilters.exclude[MissingClassProblem]("scala.runtime.stdLibPatches.language$3$u002E4$minusmigration$"), From 269ef3994a64e9dc2a1a0ef6dac2ed510104cadd Mon Sep 17 00:00:00 2001 From: noti0na1 Date: Wed, 23 Jul 2025 20:00:22 +0800 Subject: [PATCH 6/6] Fix MiMa --- project/MiMaFilters.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/project/MiMaFilters.scala b/project/MiMaFilters.scala index a38090e53f90..5a4be70987a5 100644 --- a/project/MiMaFilters.scala +++ b/project/MiMaFilters.scala @@ -22,6 +22,8 @@ object MiMaFilters { ProblemFilters.exclude[DirectMissingMethodProblem]("scala.Conversion.underlying"), ProblemFilters.exclude[MissingClassProblem]("scala.Conversion$"), + + ProblemFilters.exclude[MissingClassProblem]("scala.annotation.stableNull"), ), // Additions since last LTS @@ -45,7 +47,6 @@ object MiMaFilters { ProblemFilters.exclude[MissingClassProblem]("scala.annotation.internal.reachCapability"), ProblemFilters.exclude[MissingClassProblem]("scala.annotation.internal.preview"), ProblemFilters.exclude[MissingClassProblem]("scala.annotation.unchecked.uncheckedCaptures"), - ProblemFilters.exclude[MissingClassProblem]("scala.annotation.stableNull"), ProblemFilters.exclude[MissingClassProblem]("scala.quoted.Quotes$reflectModule$ValOrDefDefMethods"), ProblemFilters.exclude[MissingClassProblem]("scala.runtime.stdLibPatches.language$3$u002E4$"), ProblemFilters.exclude[MissingClassProblem]("scala.runtime.stdLibPatches.language$3$u002E4$minusmigration$"),