@@ -10,6 +10,7 @@ import util.Property
10
10
import Names .Name
11
11
import util .Spans .Span
12
12
import Flags .Mutable
13
+ import collection .mutable
13
14
14
15
/** Operations for implementing a flow analysis for nullability */
15
16
object Nullables with
@@ -110,7 +111,7 @@ object Nullables with
110
111
&& sym.owner.enclosingMethod == curCtx.owner.enclosingMethod
111
112
&& sym.span.exists
112
113
&& 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)
114
115
}
115
116
116
117
def afterPatternContext (sel : Tree , pat : Tree )(given ctx : Context ) = (sel, pat) match
@@ -242,42 +243,54 @@ object Nullables with
242
243
243
244
private val analyzedOps = Set (nme.EQ , nme.NE , nme.eq, nme.ne, nme.ZAND , nme.ZOR , nme.UNARY_! )
244
245
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.
252
255
*/
253
- def trackedVarSpans (given Context ): Set [Int ] =
256
+ def assignmentSpans (given Context ): Map [Int , List [ Span ] ] =
254
257
import ast .untpd ._
258
+
255
259
object populate extends UntypedTreeTraverser with
256
260
257
261
/** 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
+ */
262
272
var reachable : Set [Name ] = Set .empty
263
273
264
274
def traverse (tree : Tree )(implicit ctx : Context ) =
265
275
val savedReachable = reachable
266
276
tree match
267
277
case Block (stats, expr) =>
268
- var shadowed : Set [Name ] = Set .empty
278
+ var shadowed : Set [( Name , List [ Span ]) ] = Set .empty
269
279
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)
272
282
reachable += stat.name
273
283
traverseChildren(tree)
274
284
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
278
287
candidates ++= shadowed
279
288
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 =>
281
294
traverseChildren(tree)
282
295
case _ : (If | WhileDo | Typed ) =>
283
296
traverseChildren(tree) // assignments to candidate variables are OK here ...
@@ -288,5 +301,41 @@ object Nullables with
288
301
289
302
populate.traverse(curCtx.compilationUnit.untpdTree)
290
303
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
+
292
341
end Nullables
0 commit comments