Nullables

object Nullables

Operations for implementing a flow analysis for nullability

class Object
trait Matchable
class Any

Type members

Classlikes

object CompareNull

An extractor for null comparisons

An extractor for null comparisons

case class NotNullConditional(ifTrue: Set[TermRef], ifFalse: Set[TermRef])

A pair of not-null sets, depending on whether a condition is true or false

A pair of not-null sets, depending on whether a condition is true or false

Companion
object
Companion
class
case class NotNullInfo(asserted: Set[TermRef], retracted: Set[TermRef])

A set of val or var references that are known to be not null, plus a set of variable references that are not known (anymore) to be not null

A set of val or var references that are known to be not null, plus a set of variable references that are not known (anymore) to be not null

Companion
object
object NotNullInfo
Companion
class
object TrackedRef

An extractor for null-trackable references

An extractor for null-trackable references

Value members

Concrete methods

def afterPatternContext(sel: Tree, pat: Tree)(using Context): Context

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.

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.

A map from (name-) offsets of all local variables in this compilation unit that can be tracked for being not null to the list of spans of assignments to these variables. A variable can be tracked if it has only reachable assignments An assignment is reachable if the path of tree nodes between the block enclosing the variable declaration to the assignment consists only of if-expressions, while-expressions, block-expressions and type-ascriptions. Only reachable assignments are handled correctly in the nullability analysis. Therefore, variables with unreachable assignments can be assumed to be not-null only if their type asserts it.

A map from (name-) offsets of all local variables in this compilation unit that can be tracked for being not null to the list of spans of assignments to these variables. A variable can be tracked if it has only reachable assignments An assignment is reachable if the path of tree nodes between the block enclosing the variable declaration to the assignment consists only of if-expressions, while-expressions, block-expressions and type-ascriptions. Only reachable assignments are handled correctly in the nullability analysis. Therefore, variables with unreachable assignments can be assumed to be not-null only if their type asserts it.

Note: we track the local variables through their offset and not through their name because of shadowing.

def caseContext(sel: Tree, pat: Tree)(using Context): Context

The nullability context to be used for the guard and rhs of a case with given pattern pat. If the pattern can only match non-null values, this will assert that the selector sel is not null in these regions.

The nullability context to be used for the guard and rhs of a case with given pattern pat. If the pattern can only match non-null values, this will assert that the selector sel is not null in these regions.

Create a nullable type bound If lo is Null, | Null is added to hi

Create a nullable type bound If lo is Null, | Null is added to hi

Create a nullable type bound tree If lo is Null, | Null is added to hi

Create a nullable type bound tree If lo is Null, | Null is added to hi

def isTracked(ref: TermRef)(using Context): Boolean

Is the given reference tracked for nullability?

Is the given reference tracked for nullability?

This is the case if one of the following holds:

  1. The reference is a path to an immutable val.
  2. The reference is to a mutable variable, in which case all assignments to it must be reachable (in the sense of how it is defined in assignmentSpans) and the variable must not be used "out of order" (in the sense specified by usedOutOfOrder).

Whether to track a local mutable variable during flow typing? We track a local mutable variable iff the variable is not assigned in a closure. For example, in the following code x is assigned to by the closure y, so we do not do flow typing on x.

var x: String|Null = ???
def y = {
  x = null
}
if (x != null) {
  // y can be called here, which break the fact
  val a: String = x // error: x is captured and mutated by the closure, not trackable
}

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.

Post process all arguments to by-name parameters by removing any not-null info that was used when typing them. Concretely: If an argument corresponds to a call-by-name parameter, drop all embedded not-null assertions of the form x.$asInstanceOf[x.type & T] where x is a reference to a mutable variable. If the argument still typechecks with the removed assertions and is still compatible with the formal parameter, keep it. Otherwise issue an error that the call-by-name argument was typed using flow assumptions about mutable variables and suggest that it is enclosed in a byName(...) call instead.

Post process all arguments to by-name parameters by removing any not-null info that was used when typing them. Concretely: If an argument corresponds to a call-by-name parameter, drop all embedded not-null assertions of the form x.$asInstanceOf[x.type & T] where x is a reference to a mutable variable. If the argument still typechecks with the removed assertions and is still compatible with the formal parameter, keep it. Otherwise issue an error that the call-by-name argument was typed using flow assumptions about mutable variables and suggest that it is enclosed in a byName(...) call instead.

def whileContext(whileSpan: Span)(using Context): Context

The initial context to be used for a while expression with given span. In this context, all variables that are assigned within the while expression have their nullability status retracted, i.e. are not known to be not null. While necessary for soundness, this scheme loses precision: Even if the initial state of the variable is not null and all assignments to the variable in the while expression are also known to be not null, the variable is still assumed to be potentially null. The loss of precision is unavoidable during normal typing, since we can only do a linear traversal which does not allow a fixpoint computation. But it could be mitigated as follows:

The initial context to be used for a while expression with given span. In this context, all variables that are assigned within the while expression have their nullability status retracted, i.e. are not known to be not null. While necessary for soundness, this scheme loses precision: Even if the initial state of the variable is not null and all assignments to the variable in the while expression are also known to be not null, the variable is still assumed to be potentially null. The loss of precision is unavoidable during normal typing, since we can only do a linear traversal which does not allow a fixpoint computation. But it could be mitigated as follows:

  • initially, use whileContext as computed here
  • when typechecking the while, delay all errors due to a variable being potentially null
  • afterwards, if there are such delayed errors, run the analysis again with as a fixpoint computation, reporting all previously delayed errors that remain.

The following code would produce an error in the current analysis, but not in the refined analysis:

class Links(val elem: T, val next: Links | Null)

var xs: Links | Null = Links(1, null) var ys: Links | Null = xs while xs != null ys = Links(xs.elem, ys.next) // error in unrefined: ys is potentially null here xs = xs.next

Extensions

Extensions

extension (infos: List[NotNullInfo])

Add info as the most recent entry to the list of null infos. Assertions or retractions in info supersede infos in existing entries of infos.

Add info as the most recent entry to the list of null infos. Assertions or retractions in info supersede infos in existing entries of infos.

Do the current not-null infos imply that ref is not null? Not-null infos are as a history where earlier assertions and retractions replace later ones (i.e. it records the assignment history in reverse, with most recent first)

Do the current not-null infos imply that ref is not null? Not-null infos are as a history where earlier assertions and retractions replace later ones (i.e. it records the assignment history in reverse, with most recent first)

Retract all references to mutable variables

Retract all references to mutable variables

extension (ref: TermRef)

Is the use of a mutable variable out of order

Is the use of a mutable variable out of order

Whether to generate and use flow typing on a specific use of a local mutable variable? We only want to do flow typing on a use that belongs to the same method as the definition of the local variable. For example, in the following code, even x is not assigned to by a closure, but we can only use flow typing in one of the occurrences (because the other occurrence happens within a nested closure).

var x: String|Null = ???
def y = {
  if (x != null) {
    // not safe to use the fact (x != null) here
    // since y can be executed at the same time as the outer block
    val _: String = x
  }
}
if (x != null) {
  val a: String = x // ok to use the fact here
  x = null
}

Another example:

var x: String|Null = ???
if (x != null) {
  def f: String = {
    val y: String = x // error: the use of x is out of order
    y
  }
  x = null
  val y: String = f // danger
}
extension (tree: Tree)
def computeNullable(using Context): tree

The tree augmented with nullability information in an attachment. The following operations lead to nullability info being recorded:

The tree augmented with nullability information in an attachment. The following operations lead to nullability info being recorded:

  1. Null tests using ==, !=, eq, ne, if the compared entity is a path (i.e. a stable TermRef)
  2. Boolean &&, ||, !

Compute nullability information for this tree and all its subtrees

Compute nullability information for this tree and all its subtrees

The paths that are known to be not null if the condition represented by tree yields true or false. Two empty sets if tree is not a condition.

The paths that are known to be not null if the condition represented by tree yields true or false. Two empty sets if tree is not a condition.

The current context augmented with nullability information of tree

The current context augmented with nullability information of tree

The current context augmented with nullability information, assuming the result of the condition represented by tree is the same as the value of c.

The current context augmented with nullability information, assuming the result of the condition represented by tree is the same as the value of c.

The context to use for the arguments of the function represented by tree. This is the current context, augmented with nullability information of the left argument, if the application is a boolean && or ||.

The context to use for the arguments of the function represented by tree. This is the current context, augmented with nullability information of the left argument, if the application is a boolean && or ||.

def withNotNullInfo(info: NotNullInfo): tree
extension (tree: Assign)
def computeAssignNullable(using Context): tree