Skip to content

Commit 58db0c0

Browse files
committed
Refine comments
1 parent bdbde1d commit 58db0c0

File tree

3 files changed

+80
-14
lines changed

3 files changed

+80
-14
lines changed

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

+38-5
Original file line numberDiff line numberDiff line change
@@ -105,17 +105,50 @@ object Nullables with
105105
* This is the case if the reference is a path to an immutable val, or if it refers
106106
* to a local mutable variable where all assignments to the variable are _reachable_
107107
* (in the sense of how it is defined in assignmentSpans).
108+
*
109+
* A mutable vriable is trackable with following restrictions:
110+
* 1. All the assignment must be reachable by the definition.
111+
* 2. We only analyze the comparisons and use the facts in the same closure as
112+
* the definition.
113+
*
114+
* ```scala
115+
* var x: String|Null = ???
116+
* def y = {
117+
* x = null
118+
* }
119+
* if (x != null) {
120+
* // y can be called here
121+
* val a: String = x // error: x is captured and mutated by the closure, not tackable
122+
* }
123+
* ```
124+
*
125+
* ```scala
126+
* var x: String|Null = ???
127+
* def y = {
128+
* if (x != null) {
129+
* // not safe to use the fact (x != null) here
130+
* // since y can be executed at the same time as the outer block
131+
* val _: String = x
132+
* }
133+
* }
134+
* if (x != null) {
135+
* val a: String = x // ok to use the fact here
136+
* x = null
137+
* }
138+
* ```
139+
*
140+
* See more examples in `tests/explicit-nulls/neg/var-ref-in-closure.scala`.
108141
*/
109142
def isTracked(ref: TermRef)(given Context) =
110143
ref.isStable
111144
|| { val sym = ref.symbol
112145
sym.is(Mutable)
113146
&& sym.owner.isTerm
114-
&& (if sym.owner != curCtx.owner then
115-
// TODO: need to check by-name parameters
116-
!curCtx.owner.is(Flags.Lazy) // not at the rhs of lazy ValDef
117-
&& sym.owner.enclosingMethod == curCtx.owner.enclosingMethod // not in different DefDef
118-
else true)
147+
&& ( sym.owner == curCtx.owner
148+
|| !curCtx.owner.is(Flags.Lazy) // not at the rhs of lazy ValDef
149+
&& sym.owner.enclosingMethod == curCtx.owner.enclosingMethod // not in different methods
150+
// TODO: need to check by-name paramter
151+
)
119152
&& sym.span.exists
120153
&& curCtx.compilationUnit != null // could be null under -Ytest-pickler
121154
&& curCtx.compilationUnit.assignmentSpans.contains(sym.span.start)

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

+2
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,8 @@ class Typer extends Namer
348348
findRefRecur(NoType, BindingPrec.NothingBound, NoContext)
349349
}
350350

351+
// If `tree`'s type is a `TermRef` identified by flow typing to be non-null, then
352+
// cast away `tree`s nullability. Otherwise, `tree` remains unchanged.
351353
def toNotNullTermRef(tree: Tree, pt: Type)(implicit ctx: Context): Tree = tree.tpe match
352354
case ref @ OrNull(tpnn) : TermRef
353355
if pt != AssignProto && // Ensure it is not the lhs of Assign

docs/docs/internals/explicit-nulls.md

+40-9
Original file line numberDiff line numberDiff line change
@@ -90,20 +90,51 @@ are methods of the `Type` class, so call them with `this` as a receiver:
9090

9191
- `isNullableUnion` determines whether `this` is a nullable union.
9292
- `isJavaNullableUnion` determines whether `this` is syntactically a union of the form
93-
`T|JavaNull`
93+
`T|JavaNull`.
9494
- `stripNull` syntactically strips all `Null` types in the union:
9595
e.g. `String|Null => String`.
9696
- `stripAllJavaNull` is like `stripNull` but only removes `JavaNull` from the union.
9797
This is needed when we want to "revert" the Java nullification function.
9898

9999
## Flow Typing
100100

101-
`NotNullInfo`s are collected as we typing each statements, see `Nullables.scala` for more
102-
details about how we compute `NotNullInfo`s.
101+
As typing happens, we accumulate a set of `NotNullInfo`s in the `Context` (see
102+
`Contexts.scala`). A `NotNullInfo` contains the set of `TermRef`s that are known to
103+
be non-null at the current program point. See `Nullables.scala` for how `NotNullInfo`s
104+
are computed.
103105

104-
When we type an identity or a select tree (in `typedIdent` and `typedSelect`), we will
105-
call `toNotNullTermRef` on the tree before reture the result. If the tree `x` has nullable
106-
type `T|Null` and it is known to be not null according to the `NotNullInfo` and it is not
107-
on the lhs of assignment, then we cast it to `x.type & T` using `defn.Any_typeCast`. The
108-
reason to have a `TermRef(x)` in the `AndType` is that we can track the new result as well and
109-
use it as a path.
106+
During type-checking, when we type an identity or a select tree (in `typedIdent` and
107+
`typedSelect`), we will call `toNotNullTermRef` on the tree before return the typed tree.
108+
If the tree `x` has nullable type `T|Null` and it is known to be not null according to
109+
the `NotNullInfo` and it is not on the lhs of assignment, then we cast it to `x.type & T`
110+
using `defn.Any_typeCast`.
111+
112+
The reason for casting to `x.type & T`, as opposed to just `T`, is that it allows us to
113+
support flow typing for paths of length greater than one.
114+
115+
```scala
116+
abstract class Node {
117+
val x: String
118+
val next: Node | Null
119+
}
120+
121+
def f = {
122+
val l: Node|Null = ???
123+
if (l != null && l.next != null) {
124+
val third: l.next.next.type = l.next.next
125+
}
126+
}
127+
```
128+
129+
After typing, `f` becomes:
130+
131+
```scala
132+
def f = {
133+
val l: Node|Null = ???
134+
if (l != null && l.$asInstanceOf$[l.type & Node].next != null) {
135+
val third:
136+
l.$asInstanceOf$[l.type & Node].next.$asInstanceOf$[(l.type & Node).next.type & Node].next.type =
137+
l.$asInstanceOf$[l.type & Node].next.$asInstanceOf$[(l.type & Node).next.type & Node].next
138+
}
139+
}
140+
```

0 commit comments

Comments
 (0)