Skip to content

Commit c57fe6e

Browse files
committed
Allow local variables to be tracked for nullability
1 parent 40bb86d commit c57fe6e

File tree

3 files changed

+88
-3
lines changed

3 files changed

+88
-3
lines changed

compiler/src/dotty/tools/dotc/CompilationUnit.scala

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@ import util.SourceFile
55
import ast.{tpd, untpd}
66
import tpd.{Tree, TreeTraverser}
77
import typer.PrepareInlineable.InlineAccessors
8+
import typer.Nullables
89
import dotty.tools.dotc.core.Contexts.Context
910
import dotty.tools.dotc.core.SymDenotations.ClassDenotation
1011
import dotty.tools.dotc.core.Symbols._
1112
import dotty.tools.dotc.transform.SymUtils._
1213
import util.{NoSource, SourceFile}
14+
import util.Spans.Span
1315
import core.Decorators._
1416

1517
class CompilationUnit protected (val source: SourceFile) {
@@ -42,6 +44,15 @@ class CompilationUnit protected (val source: SourceFile) {
4244
suspended = true
4345
ctx.run.suspendedUnits += this
4446
throw CompilationUnit.SuspendException()
47+
48+
private var myTrackedVarSpans: Set[Int] = null
49+
50+
/** The (name-) offsets of all local variables in this compilation unit
51+
* that can be tracked for being not null.
52+
*/
53+
def trackedVarSpans(given Context): Set[Int] =
54+
if myTrackedVarSpans == null then myTrackedVarSpans = Nullables.trackedVarSpans
55+
myTrackedVarSpans
4556
}
4657

4758
object CompilationUnit {

compiler/src/dotty/tools/dotc/typer/Nullables.scala

Lines changed: 67 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ import Types._, Contexts._, Symbols._, Decorators._, Constants._
77
import annotation.tailrec
88
import StdNames.nme
99
import util.Property
10+
import Names.Name
11+
import util.Spans.Span
12+
import Flags.Mutable
1013

1114
/** Operations for implementing a flow analysis for nullability */
1215
object Nullables with
@@ -94,8 +97,21 @@ object Nullables with
9497
case _ => None
9598
end TrackedRef
9699

97-
/** Is given reference tracked for nullability? */
98-
def isTracked(ref: TermRef)(given Context) = ref.isStable
100+
/** Is given reference tracked for nullability?
101+
* This is the case if the reference is a path to an immutable val,
102+
* or if it refers to a local mutable variable where all assignments
103+
* to the variable are reachable.
104+
*/
105+
def isTracked(ref: TermRef)(given Context) =
106+
ref.isStable
107+
|| { val sym = ref.symbol
108+
sym.is(Mutable)
109+
&& sym.owner.isTerm
110+
&& sym.owner.enclosingMethod == curCtx.owner.enclosingMethod
111+
&& sym.span.exists
112+
&& curCtx.compilationUnit.trackedVarSpans.contains(sym.span.start)
113+
// .reporting(i"tracked? $sym ${sym.span} = $result")
114+
}
99115

100116
def afterPatternContext(sel: Tree, pat: Tree)(given ctx: Context) = (sel, pat) match
101117
case (TrackedRef(ref), Literal(Constant(null))) => ctx.addNotNullRefs(Set(ref))
@@ -151,7 +167,7 @@ object Nullables with
151167
* by `tree` yields `true` or `false`. Two empty sets if `tree` is not
152168
* a condition.
153169
*/
154-
private def notNullConditional(given Context): NotNullConditional =
170+
def notNullConditional(given Context): NotNullConditional =
155171
stripBlock(tree).getAttachment(NNConditional) match
156172
case Some(cond) if !curCtx.erasedTypes => cond
157173
case _ => NotNullConditional.empty
@@ -226,4 +242,52 @@ object Nullables with
226242

227243
private val analyzedOps = Set(nme.EQ, nme.NE, nme.eq, nme.ne, nme.ZAND, nme.ZOR, nme.UNARY_!)
228244

245+
/** The name offsets of all local mutable variables in the current compilation unit
246+
* that have only reachable assignments. An assignment is reachable if the
247+
* path of tree nodes between the block enclosing the variable declaration to
248+
* the assignment consists only of if-expressions, while-expressions, block-expressions
249+
* and type-ascriptions. Only reachable assignments are handled correctly in the
250+
* nullability analysis. Therefore, variables with unreachable assignments can
251+
* be assumed to be not-null only if their type asserts it.
252+
*/
253+
def trackedVarSpans(given Context): Set[Int] =
254+
import ast.untpd._
255+
object populate extends UntypedTreeTraverser with
256+
257+
/** The name offsets of variables that are tracked */
258+
var tracked: Set[Int] = Set.empty
259+
/** The names of candidate variables in scope that might be tracked */
260+
var candidates: Set[Name] = Set.empty
261+
/** An assignment to a variable that's not in reachable makes the variable ineligible for tracking */
262+
var reachable: Set[Name] = Set.empty
263+
264+
def traverse(tree: Tree)(implicit ctx: Context) =
265+
val savedReachable = reachable
266+
tree match
267+
case Block(stats, expr) =>
268+
var shadowed: Set[Name] = Set.empty
269+
for case (stat: ValDef) <- stats if stat.mods.is(Mutable) do
270+
if candidates.contains(stat.name) then shadowed += stat.name
271+
else candidates += stat.name
272+
reachable += stat.name
273+
traverseChildren(tree)
274+
for case (stat: ValDef) <- stats if stat.mods.is(Mutable) do
275+
if candidates.contains(stat.name) then
276+
tracked += stat.nameSpan.start // candidates that survive until here are tracked
277+
candidates -= stat.name
278+
candidates ++= shadowed
279+
case Assign(Ident(name), rhs) =>
280+
if !reachable.contains(name) then candidates -= name // variable cannot be tracked
281+
traverseChildren(tree)
282+
case _: (If | WhileDo | Typed) =>
283+
traverseChildren(tree) // assignments to candidate variables are OK here ...
284+
case _ =>
285+
reachable = Set.empty // ... but not here
286+
traverseChildren(tree)
287+
reachable = savedReachable
288+
289+
populate.traverse(curCtx.compilationUnit.untpdTree)
290+
populate.tracked
291+
.reporting(i"tracked vars: ${result.toList}%, %")
292+
end trackedVarSpans
229293
end Nullables

tests/pos/nullable.scala

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,3 +54,13 @@ def test: Unit =
5454

5555
assert(x4 != null)
5656
if x4 == null then impossible(new T{})
57+
58+
class C(val x: Int, val next: C)
59+
var xs: C = C(1, C(2, null))
60+
while xs != null do
61+
if xs == null then println("?")
62+
// looking at this with -Xprint-frontend -Xprint-types shows that the
63+
// type of `xs == null` is indeed `false`. We cannot currently use this in a test
64+
// since `xs == null` is not technically a pure expression since `xs` is not a path.
65+
// We should test variable tracking once this is integrated with explicit not null types.
66+
xs = xs.next

0 commit comments

Comments
 (0)