Skip to content

Commit 2ec7799

Browse files
committed
Handle while expressions correctly for nullability
1 parent 7f07500 commit 2ec7799

File tree

3 files changed

+78
-27
lines changed

3 files changed

+78
-27
lines changed

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

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,14 +45,15 @@ class CompilationUnit protected (val source: SourceFile) {
4545
ctx.run.suspendedUnits += this
4646
throw CompilationUnit.SuspendException()
4747

48-
private var myTrackedVarSpans: Set[Int] = null
48+
private var myAssignmentSpans: Map[Int, List[Span]] = null
4949

50-
/** The (name-) offsets of all local variables in this compilation unit
51-
* that can be tracked for being not null.
50+
/** A map from (name-) offsets of all local variables in this compilation unit
51+
* that can be tracked for being not null to the list of spans of assignments
52+
* to these variables.
5253
*/
53-
def trackedVarSpans(given Context): Set[Int] =
54-
if myTrackedVarSpans == null then myTrackedVarSpans = Nullables.trackedVarSpans
55-
myTrackedVarSpans
54+
def assignmentSpans(given Context): Map[Int, List[Span]] =
55+
if myAssignmentSpans == null then myAssignmentSpans = Nullables.assignmentSpans
56+
myAssignmentSpans
5657
}
5758

5859
object CompilationUnit {

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

Lines changed: 70 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import util.Property
1010
import Names.Name
1111
import util.Spans.Span
1212
import Flags.Mutable
13+
import collection.mutable
1314

1415
/** Operations for implementing a flow analysis for nullability */
1516
object Nullables with
@@ -110,7 +111,7 @@ object Nullables with
110111
&& sym.owner.enclosingMethod == curCtx.owner.enclosingMethod
111112
&& sym.span.exists
112113
&& curCtx.compilationUnit != null // could be null under -Ytest-pickler
113-
&& curCtx.compilationUnit.trackedVarSpans.contains(sym.span.start)
114+
&& curCtx.compilationUnit.assignmentSpans.contains(sym.span.start)
114115
}
115116

116117
def afterPatternContext(sel: Tree, pat: Tree)(given ctx: Context) = (sel, pat) match
@@ -242,42 +243,54 @@ object Nullables with
242243

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

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.
246+
/** A map from (name-) offsets of all local variables in this compilation unit
247+
* that can be tracked for being not null to the list of spans of assignments
248+
* to these variables. A variable can be tracked if it has only reachable assignments.
249+
* An assignment is reachable if the path of tree nodes between the block enclosing
250+
* the variable declaration to the assignment consists only of if-expressions,
251+
* while-expressions, block-expressions and type-ascriptions.
252+
* Only reachable assignments are handled correctly in the nullability analysis.
253+
* Therefore, variables with unreachable assignments can be assumed to be not-null
254+
* only if their type asserts it.
252255
*/
253-
def trackedVarSpans(given Context): Set[Int] =
256+
def assignmentSpans(given Context): Map[Int, List[Span]] =
254257
import ast.untpd._
258+
255259
object populate extends UntypedTreeTraverser with
256260

257261
/** 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 tracked: Map[Int, List[Span]] = Map.empty
263+
264+
/** Map the names of potentially trackable candidate variables in scope to the spans
265+
* of their reachable assignments
266+
*/
267+
val candidates = mutable.Map[Name, List[Span]]()
268+
269+
/** An assignment to a variable that's not in reachable makes the variable
270+
* ineligible for tracking
271+
*/
262272
var reachable: Set[Name] = Set.empty
263273

264274
def traverse(tree: Tree)(implicit ctx: Context) =
265275
val savedReachable = reachable
266276
tree match
267277
case Block(stats, expr) =>
268-
var shadowed: Set[Name] = Set.empty
278+
var shadowed: Set[(Name, List[Span])] = Set.empty
269279
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
280+
for prevSpans <- candidates.put(stat.name, Nil) do
281+
shadowed += (stat.name -> prevSpans)
272282
reachable += stat.name
273283
traverseChildren(tree)
274284
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
285+
for spans <- candidates.remove(stat.name) do
286+
tracked += (stat.nameSpan.start -> spans) // candidates that survive until here are tracked
278287
candidates ++= shadowed
279288
case Assign(Ident(name), rhs) =>
280-
if !reachable.contains(name) then candidates -= name // variable cannot be tracked
289+
candidates.get(name) match
290+
case Some(spans) =>
291+
if reachable.contains(name) then candidates(name) = tree.span :: spans
292+
else candidates -= name
293+
case None =>
281294
traverseChildren(tree)
282295
case _: (If | WhileDo | Typed) =>
283296
traverseChildren(tree) // assignments to candidate variables are OK here ...
@@ -288,5 +301,41 @@ object Nullables with
288301

289302
populate.traverse(curCtx.compilationUnit.untpdTree)
290303
populate.tracked
291-
end trackedVarSpans
304+
end assignmentSpans
305+
306+
/** The initial context to be used for a while expression with given span.
307+
* In this context, all variables that are assigned within the while expression
308+
* have their nullability status retracted, i.e. are not known to be null.
309+
* While necessary for soundness, this scheme loses precision: Even if
310+
* the initial state of the variable is not null and all assignments to the variable
311+
* in the while expression are also known to be not null, the variable is still
312+
* assumed to be potentially null. The loss of precision is unavoidable during
313+
* normal typing, since we can only do a linear traversal which does not allow
314+
* a fixpoint computation. But it could be mitigated as follows:
315+
*
316+
* - initially, use `whileContext` as computed here
317+
* - when typechecking the while, delay all errors due to a variable being potentially null
318+
* - afterwards, if there are such delayed errors, run the analysis again with
319+
* as a fixpoint computation, reporting all previously delayed errors that remain.
320+
*
321+
* The following code would produce an error in the current analysis, but not in the
322+
* refined analysis:
323+
*
324+
* class Links(val elem: T, val next: Links | Null)
325+
*
326+
* var ys: Links | Null = Links(1, null)
327+
* var xs: Links | Null = xs
328+
* while xs != null
329+
* ys = Links(xs.elem, ys.next) // error in unrefined: ys is potentially null here
330+
* xs = xs.next
331+
*/
332+
def whileContext(whileSpan: Span)(given Context): Context =
333+
def isRetracted(ref: TermRef): Boolean =
334+
val sym = ref.symbol
335+
sym.span.exists
336+
&& assignmentSpans.getOrElse(sym.span.start, Nil).exists(whileSpan.contains(_))
337+
&& curCtx.notNullInfos.containsRef(ref)
338+
val retractedVars = curCtx.notNullInfos.flatMap(_.asserted.filter(isRetracted)).toSet
339+
curCtx.addNotNullInfo(NotNullInfo(Set(), retractedVars))
340+
292341
end Nullables

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1260,6 +1260,7 @@ class Typer extends Namer
12601260
}
12611261

12621262
def typedWhileDo(tree: untpd.WhileDo)(implicit ctx: Context): Tree = {
1263+
given whileCtx: Context = Nullables.whileContext(tree.span)(given ctx)
12631264
val cond1 =
12641265
if (tree.cond eq EmptyTree) EmptyTree
12651266
else typed(tree.cond, defn.BooleanType)

0 commit comments

Comments
 (0)