Skip to content

Add stableNull annotation to force tracking mutable fields #23528

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions compiler/src/dotty/tools/dotc/core/Definitions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1095,6 +1095,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 StableNullAnnot: ClassSymbol = requiredClass("scala.annotation.stableNull")

@tu lazy val JavaRepeatableAnnot: ClassSymbol = requiredClass("java.lang.annotation.Repeatable")

Expand Down
16 changes: 11 additions & 5 deletions compiler/src/dotty/tools/dotc/typer/Nullables.scala
Original file line number Diff line number Diff line change
Expand Up @@ -186,15 +186,21 @@ 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 isNullStableField: Boolean =
ref.prefix.isStable
&& sym.isField
&& sym.hasAnnotation(defn.StableNullAnnot)

ref.isStable
|| { val sym = ref.symbol
val unit = ctx.compilationUnit
|| isNullStableField
|| { 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.
Expand Down
10 changes: 10 additions & 0 deletions library/src/scala/annotation/stableNull.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
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,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* 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.
* See `tests/explicit-nulls/pos/force-track-var-fields.scala` for an example.
*/
private[scala] final class stableNull extends StaticAnnotation
2 changes: 2 additions & 0 deletions project/Build.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1191,6 +1191,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/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"),
Expand Down Expand Up @@ -1326,6 +1327,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/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"),
Expand Down
16 changes: 16 additions & 0 deletions tests/explicit-nulls/pos/force-track-var-fields.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package scala

import scala.annotation.stableNull

class A:
@stableNull 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
Loading