Skip to content

Commit 49d550e

Browse files
committed
Add flow typing to doc
1 parent 465823d commit 49d550e

File tree

2 files changed

+195
-44
lines changed

2 files changed

+195
-44
lines changed

docs/docs/internals/explicit-nulls.md

Lines changed: 45 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,6 @@ We change the type hierarchy so that `Null` is only a subtype of `Any` by:
2828

2929
## Java Interop
3030

31-
TODO(abeln): add support for recognizing nullability annotations a la
32-
https://kotlinlang.org/docs/reference/java-interop.html#nullability-annotations
33-
3431
The problem we're trying to solve here is: if we see a Java method `String foo(String)`,
3532
what should that method look like to Scala?
3633
- since we should be able to pass `null` into Java methods, the argument type should be `String|JavaNull`
@@ -43,25 +40,19 @@ At a high-level:
4340
- we do this in two places: `Namer` (for Java sources) and `ClassFileParser` (for bytecode)
4441
- whenever we load a Java member, we "nullify" its argument and return types
4542

46-
The nullification logic lives in `JavaNullInterop.scala`, a new file.
47-
48-
The entry point is the function `def nullifyMember(sym: Symbol, tp: Type)(implicit ctx: Context): Type`
49-
which, given a symbol and its "regular" type, produces what the type of the symbol should be in the
50-
explicit nulls world.
51-
52-
In order to nullify a member, we first pass it through a "whitelist" of symbols that need
53-
special handling (e.g. `constructors`, which never return `null`). If none of the "policies" in the
54-
whitelist apply, we then process the symbol with a `TypeMap` that implements the following nullification
55-
function `n`:
56-
1. n(T) = T|JavaNull if T is a reference type
57-
2. n(T) = T if T is a value type
58-
3. n(T) = T|JavaNull if T is a type parameter
59-
4. n(C[T]) = C[T]|JavaNull if C is Java-defined
60-
5. n(C[T]) = C[n(T)]|JavaNull if C is Scala-defined
61-
6. n(A|B) = n(A)|n(B)|JavaNull
62-
7. n(A&B) = (n(A)&n(B))|JavaNull
63-
8. n((A1, ..., Am)R) = (n(A1), ..., n(Am))n(R) for a method with arguments (A1, ..., Am) and return type R
64-
9. n(T) = T otherwise
43+
The nullification logic lives in `compiler/src/dotty/tools/dotc/core/JavaNullInterop.scala`.
44+
45+
The entry point is the function
46+
`def nullifyMember(sym: Symbol, tp: Type, isEnumValueDef: Boolean)(implicit ctx: Context): Type`
47+
which, given a symbol, its "regular" type, and a boolean whether it is a Enum value definition,
48+
produces what the type of the symbol should be in the explicit nulls world.
49+
50+
1. If the symbol is a Enum value definition or a `TYPE_` field, we don't nullify the type
51+
2. If it is `toString()` method or the constructor, or it has a `@NotNull` annotation,
52+
we nullify the type, without a `JavaNull` at the outmost level.
53+
3. Otherwise, we nullify the type in regular way.
54+
55+
See `JavaNullMap` in `JavaNullInterop.scala` for more details about how we nullify different types.
6556

6657
## JavaNull
6758

@@ -73,32 +64,46 @@ val s: String|JavaNull = "hello"
7364
s.length // allowed, but might throw NPE
7465
```
7566

76-
`JavaNull` is defined as `JavaNullAlias` in `Definitions`.
67+
`JavaNull` is defined as `JavaNullAlias` in `Definitions.scala`.
7768
The logic to allow member selections is defined in `findMember` in `Types.scala`:
7869
- if we're finding a member in a type union
7970
- and the union contains `JavaNull` on the r.h.s. after normalization (see below)
8071
- then we can continue with `findMember` on the l.h.s of the union (as opposed to failing)
8172

8273
## Working with Nullable Unions
8374

84-
Within `Types.scala`, we defined a few utility methods to work with nullable unions. All of these
75+
Within `Types.scala`, we defined some extractors to work with nullable unions:
76+
`OrNull` and `OrJavaNull`.
77+
78+
```scala
79+
(tp: Type) match {
80+
case OrNull(tp1) => // if tp is a nullable union: tp1 | Null
81+
case _ => // otherwise
82+
}
83+
```
84+
85+
These extractor will call utility methods in `NullOpsDecorator.scala`. All of these
8586
are methods of the `Type` class, so call them with `this` as a receiver:
86-
- `isNullableUnion` determines whether `this` is a nullable union. Here, what constitutes
87-
a nullable union is determined purely syntactically:
88-
1. first we "normalize" `this` (see below)
89-
2. if the result is of the form `T | Null`, then the type is considered a nullable union.
90-
Otherwise, it isn't.
91-
- `isJavaNullableUnion` determines whether `this` is syntactically a union of the form `T|JavaNull`
92-
- `normNullableUnion` normalizes `this` as follows:
93-
1. if `this` is not a nullable union, it's returned unchanged.
94-
2. if `this` is a union, then it's re-arranged so that all the `Null`s are to the right of all
95-
the non-`Null`s.
96-
- `stripNull` syntactically strips nullability from `this`: e.g. `String|Null => String`. Notice this
97-
works only at the "top level": e.g. if we have an `Array[String|Null]|Null` and we call `stripNull`
98-
we'll get `Array[String|Null]` (only the outermost nullable union was removed).
99-
- `stripAllJavaNull` is like `stripNull` but removes _all_ nullable unions in the type (and only works
100-
for `JavaNull`). This is needed when we want to "revert" the Java nullification function.
87+
88+
- `normNullableUnion` normalizes unions so that the `Null` type (or aliases to `Null`)
89+
appears to the right of all other types.
90+
91+
- `isNullableUnion` determines whether `this` is a nullable union.
92+
- `isJavaNullableUnion` determines whether `this` is syntactically a union of the form
93+
`T|JavaNull`
94+
- `stripNull` syntactically strips all `Null` types in the union:
95+
e.g. `String|Null => String`.
96+
- `stripAllJavaNull` is like `stripNull` but only removes `JavaNull` from the union.
97+
This is needed when we want to "revert" the Java nullification function.
10198

10299
## Flow Typing
103100

104-
TODO
101+
`NotNullInfo`s are collected as we typing each statements, see `Nullables.scala` for more
102+
details about how we compute `NotNullInfo`s.
103+
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.

docs/docs/reference/other-new-features/explicit-nulls.md

Lines changed: 150 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,6 @@ Specifically, we patch
192192
final char CHAR = 'a';
193193

194194
final String NAME_GENERATED = getNewName();
195-
final int VALUE = 0 * 2;
196195
}
197196
```
198197
==>
@@ -203,7 +202,6 @@ Specifically, we patch
203202
val CHAR: Char('a') = 'a'
204203

205204
val NAME_GENERATED: String | Null = ???
206-
val VALUE: Int = ???
207205
}
208206
```
209207

@@ -289,12 +287,160 @@ val s2 = if (ret != null) {
289287

290288
## Flow Typing
291289

292-
TODO
290+
We added a simple form of flow-sensitive type inference. The idea is that if `p` is a
291+
stable path or a trackable variable, then we can know that `p` is non-null if it's compared
292+
with the `null`. This information can then be propagated to the `then` and `else` branches
293+
of an if-statement (among other places).
294+
295+
Example:
296+
297+
```scala
298+
val s: String|Null = ???
299+
if (s != null) {
300+
// s: String
301+
}
302+
// s: String|Null
303+
304+
assert(x != null)
305+
// s: String
306+
```
307+
308+
A similar inference can be made for the `else` case if the test is `p == null`
309+
310+
```scala
311+
if (s == null) {
312+
// s: String|Null
313+
} else {
314+
// s: String
315+
}
316+
```
317+
318+
`==` and `!=` is considered a comparison for the purposes of the flow inference.
319+
320+
### Logical Operators
321+
322+
We also support logical operators (`&&`, `||`, and `!`):
323+
324+
```scala
325+
val s: String|Null = ???
326+
val s2: String|Null = ???
327+
if (s != null && s2 != null) {
328+
// s: String
329+
// s2: String
330+
}
331+
332+
if (s == null || s2 == null) {
333+
// s: String|Null
334+
// s2: String|Null
335+
} else {
336+
// s: String
337+
// s2: String
338+
}
339+
```
340+
341+
### Inside Conditions
342+
343+
We also support type specialization _within_ the condition, taking into account that `&&` and `||` are short-circuiting:
344+
345+
```scala
346+
val s: String|Null = ???
347+
348+
if (s != null && s.length > 0) { // s: String in `s.length > 0`
349+
// s: String
350+
}
351+
352+
if (s == null || s.length > 0) // s: String in `s.length > 0` {
353+
// s: String|Null
354+
} else {
355+
// s: String|Null
356+
}
357+
```
358+
359+
### Match Case
360+
361+
The non-null cases can be detected in match statements.
362+
363+
```scala
364+
val s: String|Null = ???
365+
366+
s match {
367+
case _: String => // s: String
368+
case _ =>
369+
}
370+
```
371+
372+
### Mutable Variable
373+
374+
A mutable vriable is trackable with following restrictions:
375+
376+
1. All the assignment must in the same closure as the definition (more strictly,
377+
reachable by the definition).
378+
2. We only analyze the comparisons and use the facts in the same closure as
379+
the definition.
380+
381+
```scala
382+
class C(val x: Int, val next: C|Null)
383+
384+
var xs: C|Null = C(1, C(2, null))
385+
// xs is trackable, since all assignments are in the same mathod
386+
while (xs != null) {
387+
// xs: C
388+
val xsx: Int = xs.x
389+
val xscpy: C = xs
390+
xs = xscpy // since xscpy is non-null, xs still has type C after this line
391+
// xs: C
392+
xs = xs.next // after this assignment, xs can be null again
393+
// xs: C | Null
394+
}
395+
```
396+
397+
```scala
398+
var x: String|Null = ???
399+
def y = {
400+
x = null
401+
}
402+
if (x != null) {
403+
// y can be called here
404+
val a: String = x // error: x is captured and mutated by the closure, not tackable
405+
}
406+
```
407+
408+
```scala
409+
var x: String|Null = ???
410+
def y = {
411+
if (x != null) {
412+
// not safe to use the fact (x != null) here
413+
// since y can be executed at the same time as the outer block
414+
val _: String = x
415+
}
416+
}
417+
if (x != null) {
418+
val a: String = x // ok to use the fact here
419+
x = null
420+
}
421+
```
422+
423+
Currently, we are unable to track `x.a` if `x` is mutable.
424+
425+
### Unsupported Idioms
426+
427+
We don't support:
428+
429+
- flow facts not related to nullability (`if (x == 0) { // x: 0.type not inferred }`)
430+
- tracking aliasing between non-nullable paths
431+
```scala
432+
val s: String|Null = ???
433+
val s2: String|Null = ???
434+
if (s != null && s == s2) {
435+
// s: String inferred
436+
// s2: String not inferred
437+
}
438+
```
293439

294440
## Binary Compatibility
295441

296442
Our strategy for binary compatibility with Scala binaries that predate explicit nulls
297443
and new libraries compiled without `-Yexplicit-nulls` is to leave the types unchanged
298444
and be compatible but unsound.
299445

300-
[More details](../../internals/intersection-types-spec.md)
446+
[More details](../../internals/explicit-nulls.md)

0 commit comments

Comments
 (0)