Change-Id: Ia6187c3eac630a2bd4351868aadfaa8771648830 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/125821 Commit-Queue: Johnni Winther <johnniwinther@google.com> Reviewed-by: Jens Johansen <jensj@google.com>
35 KiB
Nullability in CFE
Author: The Dart CFE team.
Status: Living document.
CHANGELOG
2019.11.06:
- Described required nullability parameter.
2019.10.18:
- Added information about
NeverType
.
2019.10.17:
- Renamed
Nullability.neither
toNullability.undetermined
.
2019.10.15:
- Added information about implementing the subtype relation.
- Added description of
DartType.withNullability
.
2019.09.26:
- Initial version uploaded.
Summary
The sections below describe the encoding of the nullability property on types in the Kernel ASTs produced by the CFE. Details are discussed, such as the use of Nullability.undetermined
, and computation of the nullability property for the results of type substitution and type intersection is described. The encoding for late fields and variables and required named parameters is described. Finally, the list of the updates to the public interface of the CFE is given along with some recommendations on updating the client code.
This is a living document describing the changes in the output and in the public interface of the CFE related to the NNBD Dart language feature. It is being updated as more changes are made.
Objective
The document describes the updates in the output and in the API of the CFE related to the NNBD Dart language feature. The document attempts to suggest the related updates in the client code for some simple cases.
TODO: Describe non-goals.
Background
The NNBD Dart language feature allows programmers to define types as nullable using the ?
suffix. By default, the types are treated as non-nullable. In addition to that, a weak mode is defined that allows programmers to have some of their libraries to be opted out of the NNBD feature. The types in the opted-out libraries are called legacy types and should be accounted for when they meet with nullable or non-nullable types on the library borders, for example in subtype checks. Finally, there are types in the opted-in libraries that can't be classified as either nullable or non-nullable at compile time.
The presence of the nullability property on types affects some of the existing algorithms, such as type checks or type inference. It also introduces additional notions and algorithms that are convenient and sometimes necessary to work with nullable types.
NNBD adds a number of features to the language that are not purely syntactic and should be communicated from the CFE to the back ends. This document describes the related changes in the CFE output as well as the related changes in the API of the CFE libraries.
Overview
The nullability property on types is represented by an enum
with four possible values: for nullable types, non-nullable types, legacy types, and the types the nullability of which can't be fully determined at compile type.
It is required to compute the nullability of types and their parts when performing type substitution and creating an intersection of two types (also known as type-parameter types with promoted bounds). There are tables that describe those two functions for combining nullabilities.
In addition to the nullability property on types there are new flags on the relevant nodes that mark the use of late
and required
keywords. There's also a new Kernel AST node for the null check operator.
There's a number of changes in the API of the CFE. The most notable changes are the removal of the Class.rawType
getter and the related getters on TypeEnvironment
. There's a number of nullability-aware getters and methods that are added on CoreTypes
that should be used instead of the removed getters.
To help avoid the explicit use of legacy types, there are getters on the Library
AST node that help with choosing the appropriate nullability for the library depending on its opt-in status for the NNBD feature. Some recommendations are given for using those getters.
Detailed Design
Terminology
In the document the term type parameter is used to describe a formal type parameter of a class, a type alias, or a generic function. For example,X
and Y
are type parameters in the declaration class A<X, Y> {}
. The term type argument is used to describe an actual type parameter in an instance of a generic class or a type alias or an invocation of a generic function or a constructor of a generic class. For example, int
and List<String>
are type arguments in the expression new Map<int, List<String>>()
.
Encoding for Nullability
The CFE combines various bits of information available at compile time, such as the nullability markers ?
used by the programmer and the NNBD opted-in status of the libraries, to mark every DartType
in its output as belonging to one of the following four mutually disjoint sets:
- Nullable types. Mainly, these are the types that are marked with
?
by the programmer. They can also be inferred from other nullable types or synthesized according to the language specification. For example, if the programmer omits the type bound on a type variable in an opted-in library, the CFE should synthesizeObject?
for the bound. - Non-nullable types. This is the default category of types. If a type is declared without any modifiers in an opted-in library, in most cases it would be non-nullable.
- Types in opted-in libraries that are potentially nullable or potentially non-nullable, but neither can be said about them at compile time are of undetermined nullability. An example of such type is a type-parameter type with the nullable bound. At run time both nullable and non-nullable types can be passed for that type parameter, so it's impossible to say anything about the nullability of such type at compile time.
- Legacy types. These are the types originating from the opted-out libraries.
Every DartType
subclass implements the nullability
getter. The return value of the getter is described by the following enum
:
enum Nullability {
nullable,
nonNullable,
undetermined,
legacy
}
In the textual representation of Kernel that is commonly used in .expect
files Nullability.nullable
is printed out as ?
, Nullability.nonNullable
is printed as an empty sequence of characters, Nullability.undetermined
is printed as %
, and Nullability.legacy
is printed as *
.
TODO: Add an example of a .expect file.
The 'undetermined' Nullability
Nullability.undetermined
marks all types that can't be put into any other three categories at compile time and that should be categorized at run time. The primary use case for Nullability.undetermined
are type-parameter types with nullable bounds. Consider the following Dart program:
// The enclosing library is opted in.
class A<T extends Object?> {
foo(T x) {
x = null; // Compile-time error.
Object y = x; // Compile-time error.
bar(T z) {}
dynamic f = bar;
f?.call(null); // Runtime error?
}
}
At compile time nothing can be said about the nullability of T
because it is possible that at run time either a nullable or non-nullable type will be passed for it. We have to be restrictive and prohibit both assignments of null
to variables of type T
and assignments of expression values of type T
to variables of non-nullable types. However, there are cases when the error can be caught only at run time when the nullability of the type passed for T
is known. An example of that is an invocation of a function stored in a variable of type dynamic
.
A type-parameter type also has Nullability.undetermined
if its bound have Nullability.undetermined
. In the following example types T
, S
, and V
are of undetermined
nullability:
class B<T extends S, S extends V, V extends Object?> {
foo(T t, S s, V v) {
// ...
}
}
Additionally, if the nullability of a type depends on the nullability of an InvalidType
, the former is set to Nullability.undetermined
for recovery purposes. An example of such dependency is the nullability of a type-parameter type with an InvalidType
as the bound of its type parameter. Note that the .nullability
getter of InvalidType
throws when accessed in order to make sure all such dependencies are treated explicitly.
Type Substitution and Nullability
Substitution of type arguments for occurrences of type parameters, when both the former and the latter have nullability attributes, requires computing the nullability of the substitution result.
In the following table the rows correspond to the possible nullability attributes of the type argument, the columns correspond to the possible nullability attributes of the type parameters, and the table elements contain the nullability for the result of the substitution of a type argument for a type parameter with the corresponding nullabilities.
arg \ var | ! | ? | * | % |
---|---|---|---|---|
! | ! | ? | * | ! |
? | N/A | ? | ? | ? |
* | * | ? | * | * |
% | N/A | ? | * | % |
In the table !
denotes Nullability.nonNullable
, ?
denotes Nullability.nullable
, *
denotes Nullability.legacy
, and %
denotes Nullability.undetermined
.
Type-parameter types are assigned their nullabilities as follows:
- If a type parameter has a non-nullable bound, and a type-parameter type referring to that type parameter isn't marked with
?
, then that type-parameter type is non-nullable. In the following example bothX
andY
are non-nullable when used as types:
class A<X extends Object> { X foo() => null; }
typedef F<Y extends int> = Y Function();
- If a type-parameter type is marked with
?
, the resulting type is nullable regardless of the nullability of the bound. In the following example, types of all positional parameters offoo
are nullable:
class A<X extends Object, Y extends Object?, Z extends Y> {
foo(X? x, Y? y, Z? z) {}
}
- If a type-parameter type is used in an opted-out library, it's nullability is
Nullability.legacy
regardless of the nullability of the bound. Note that in the following example bothX
andY
are legacy types despiteY
having the nullable typedynamic
as the bound.
// Assume the library is opted out.
class A<X extends int, Y extends dynamic> { foo(X x, Y y) {} }
- If a type parameter has a nullable bound or a bound that has
Nullability.undetermined
, and a type-parameter type referring that type parameter isn't marked with?
, then that type-parameter type hasNullability.undetermined
. In the following exampleX
,Y
, andZ
haveNullability.undetermined
when used as types:
// Note that 'Object?' is provided for the omitted bounds in opted-in libraries.
class A<X extends Y, Y extends Z, Z> { foo(X x, Y y, Z z) {} }
All four cases above describe not promoted type-parameter types. Promoted type-parameter types are discussed in section Nullability of Intersection Types.
Type substitution happens both at compile time and at run time. Unaliasing a TypedefType
, such as the return type of foo
in typedef F<X, Y> = X Function(Y?); F<int?, String> foo() => null;
, is an example of type substitution happening at compile time. The corresponding CFE output for the example looks as follows:
library;
import self as self;
import "dart:core" as core;
typedef F<X extends core::Object? = dynamic, contravariant Y extends core::Object? = dynamic> = (Y?) → X%;
static method foo() → (core::String?) → core::int?
return null;
Note that X
has Nullability.undetermined
when used as a type in the right-hand side of the typedef
declaration. The return type of foo
is int? Function(String?)
, which is the result of substituting X
with int?
and Y
with String
in X% Function(Y?)
.
Obtaining the type for a type check, such as the is-check in the body of foo
in the program fragment below, is an example where type substitution is needed at run time.
class A<X> {
foo(dynamic bar) => bar is List<X>;
}
The elements of the table that are marked with N/A correspond to cases that should be rejected by a type check. The case for type argument having Nullability.nullable
and the type parameter having Nullability.nonNullable
should result in a type error because a nullable type can't be a subtype of a non-nullable type. Similar is true for the argument that have Nullability.undetermined
and the type parameter that have Nullability.nonNullable
because at run time Nullability.undetermined
can be replaced with Nullability.nullable
.
The substitution routines in pkg/kernel/lib/type_algebra.dart
follow the nullability computation rules described above.
Nullability of Intersection Types
In Kernel TypeParameterType
represents types of two kinds. The primary purpose of the node is to denote an occurrence of a type parameter in another type, which can be the entire type in the simplest case. The secondary purpose of TypeParameterType
is to represent a limited intersection type that arises from type promotion of variables declared with a TypeParameterType
. In the latter case the overall nullability of the intersection type is computed from the nullabilities of the left-hand side of the intersection (referred to as LHS below) and the right-hand side of the intersection (referred to as RHS below).
In the following table the rows correspond to the possible nullability attributes of the LHS, the columns correspond to the possible nullability attributes of the RHS, and the table elements contain the nullability of the intersection type of two types with the corresponding nullabilities.
LHS \ RHS | ! | ? | * | % |
---|---|---|---|---|
! | ! | N/A | N/A | ! |
? | N/A | N/A | N/A | N/A |
* | N/A | N/A | * | N/A |
% | ! | % | N/A | % |
In the table !
denotes Nullability.nonNullable
, ?
denotes Nullability.nullable
, *
denotes Nullability.legacy
, and %
denotes Nullability.undetermined
.
The table elements marked with N/A represent combinations that can't be a part of a well-formed CFE output. Some of them are due to the restriction on the intersection type that the RHS of the intersection should be a subtype of the bound of the type variable, and some others are due to the restriction that a nullable type is never promoted to an intersection type.
The intersection is induced by some of the type promotions, such as the one in the example below.
class A<T extends num?> {
foo(T t) {
if (t is int?) {
var bar = t;
}
}
}
The type of the variable t
in the example is promoted from T
to T & int?
because the RHS of the intersection, namely int?
, is a subtype of the bound of T
, which is num?
. In Kernel T
has Nullability.undetermined
because the bound of T
is nullable, and the intersection type with the nullabilities of all its parts looks like T% & int?
. The following is the textual representation of the program from the example above compiled by the CFE.
library;
import self as self;
import "dart:core" as core;
class A<T extends core::num? = core::num?> extends core::Object {
synthetic constructor •() → self::A<self::A::T*>*
: super core::Object::•()
;
method foo(generic-covariant-impl self::A::T% t) → dynamic {
if(t is core::int?) {
self::A::T% & core::int? /* '%' & '?' = '%' */ bar = t{self::A::T% & core::int? /* '%' & '?' = '%' */};
}
}
}
Note that the type of t
, which is written in the curly braces right after the getter, inside of the promoting if-statement is an intersection type with self::A::T%
as the LHS and core::int?
as the RHS. The comments generated by the textual serializer show the computation of the overall nullability of the intersection type from the nullabilities of its components.
Instead of having the overall nullability on the TypeParameterType
object, only the nullability of the LHS is serialized as the field TypeParameterType.typeParameterTypeNullability
, and the overall nullability is computed in the TypeParameterType.nullability
getter. The back ends that perform their own deserialization may want to implement the computation of the overall nullability following that in the TypeParameterType.nullability
getter.
The 'required' Flag
Named parameters can be declared using the required
keyword. To reflect the use of the keyword the CFE sets the isRequired
flag on VariableDeclaration
and NamedType
nodes.
The isRequired
flag on VariableDeclaration
is set if the node encodes a named formal parameter and the required
keyword was used.
The use of the required
keyword affects the override rules and the subtyping relationship for function types. To reflect the use of the keyword in a function type, the isRequired
flag is set on the corresponding NamedType
representing the type of the named parameter.
The 'late' Flag on Variables and Fields
Fields and variables can be declared using the late
keyword. To reflect the use of the keyword the CFE sets the isLate
flag on Field
and VariableDeclaration
nodes.
Late field encoding
An optional desugaring of late
fields and variables is provided to aid the initial implementation of the feature. The desugaring is enabled if Target.supportsLateFields
returns false
.
There are 8 variants of the encoding based on whether type of the field is potentially nullable, whether the field is final and whether the field has an initializer.
If a late field is non-nullable, its value is stored in a private nullable field and a field value of null
signals that the field is uninitialized. Otherwise if a late field is potentially nullable, an additional boolean _#isSet#
field is generated to tracking whether the field has been initialized.
1) Potentially nullable late field without initializer
A potentially nullable late non-final field:
late T? x;
is encoded as
bool _#x#isSet = false;
T? _#x;
T? get x => _#x#isSet ? _#x : throw new StateError("Field 'x' has not been initialized.");
void set x(T? value) {
_#x#isSet = true;
_#x = value;
}
2) Potentially nullable late final field without initializer
A potentially nullable late final field without an initializer
late final T? x;
is encoded as
bool _#x#isSet = false;
T? _#x;
T? get x => _#x#isSet ? _#x : throw new StateError("Field 'x' has not been initialized.");
void set x(T? value) {
if (_#x#isSet) {
throw new StateError('Field x has already been initialized.');
} else {
_#x#isSet = true;
_#x = value;
}
}
3) Potentially nullable late field with initializer
A potentially nullable late field with initializer <exp>
late T? x = <exp>;
is encoded as
bool _#x#isSet = false;
T? _#x;
T? get x {
if (!_#x#isSet) {
_#x#isSet = true;
_#x = <exp>;
}
return _#x
}
void set x(T? value) {
_#x#isSet = true
_#x = value;
}
4) Potentially nullable late final field with initializer
A potentially nullable late final field with initializer <exp>
late final T? x = <exp>;
is encoded as
bool _#x#isSet = false;
T? _#x;
T? get x {
if (!_#x#isSet) {
_#x#isSet = true;
_#x = <exp>;
}
return _#x;
}
5) Non-nullable late field without initializer
A non-nullable late non-final field:
late T x;
is encoded as
T? _#x;
T get x => let T? # = _#x in # == null ? throw new StateError("Field 'x' has not been initialized.") : #;
void set x(T value) {
_#x = value;
}
The reason for using a let
expression here, is that while the private field is nullable, the temporary variable in the let
expression will be promoted to a non-nullable type when checking against null
, thus ensuring that the returned value is soundly non-nullable, also when analyzing the kernel ast itself.
6) Non-nullable late final field without initializer
A non-nullable late final field without an initializer
late final T x;
is encoded as
T? _#x;
T get x => let T? # = _#x in # == null ? throw new StateError("Field 'x' has not been initialized.") : #;
void set x(T value) {
if (_#x == null) {
_#x = value;
} else {
throw new StateError("Field 'x' has already been initialized.");
}
}
7) Non-nullable late field with initializer
A non-nullable late field with initializer <exp>
late T x = <exp>;
is encoded as
T? _#x;
T get x => let T? # = _#x in # == null ? _#x = <exp> : #;
void set x(T value) {
_#x = value;
}
8) Non-nullable late final field with initializer
A non-nullable late final field with initializer <exp>
late final T x = <exp>;
is encoded as
T? _#x;
T get x => let T? # = _#x in # == null ? _#x = <exp> : _#x;
Local variables
A late local variable is encoded similarly to a late field. Local functions are created which correspond to the getters and setters for late fields.
For instance, a nullable late local without initializer
method() {
late T? x;
<lhs> = x;
variable = <rhs>;
}
is encoded as
method() {
bool #x#isSet = false;
T? #x;
T? #x#get() => #x#isSet ? #x : throw new StateError("Local 'x' has not been initialized.")
T? #x#set(T? value) {
_#x#isSet = true;
return _#x = value;
}
<lhs> = #x#get.call();
#x#set.call(<rhs>);
}
Instance field initialization
A field initialization of late nullable instance field
class Class {
late T? x;
Class.a();
Class.b(this.x);
Class.c() : x = <exp>;
}
is encoded as
class Class {
bool _#x#isSet = false;
T? _#x;
Class.a();
Class.b(T x) : _#x#isSet true, _#x = x;
Class.c() : _#x#isSet = true, _#x = <exp>;
}
A field initialization of late non-nullable instance field
class Class {
late T x;
Class.a();
Class.b(this.x);
Class.c() : x = <exp>;
}
is encoded as
class Class {
T? _#x;
Class.a();
Class.b(T x) : _#x = x;
Class.b() : _#x = <exp>;
}
The Null Check Operator
TODO: Update the section.
Updates to the API
TODO: Update the section as API changes.
Nullability attribute on types
DartType.nullability
is added toDartType
and the implementations are added to the subclasses (fields for InterfaceType, FunctionType, TypedefType, and TypeParameterType, concrete getter forTypeParameterType
).- The required nullability parameter is added to constructors of InterfaceType, FunctionType, TypedefType, and TypeParameterType. All of the call sites were modified so that
Nullability.legacy
is passed for the parameter. TypeParameterType.typeParameterTypeNullability
is added. For details see section Nullability of Intersection Types of this document.TypeParameterType.computeNullabilityFromBound
is added.DartType.withNullability
method is added toDartType
and is implemented in its subclasses. The method takes a single parameter, the desired nullability, and returns the type that is the receiver with the given nullability. If the receiver already has the nullability that is passed in as the parameter, the receiver object itself is returned, and a copy isn't created. If the types that are represented by a particularDartType
subclass always have a certain nullability, likedynamic
orvoid
, invocations ofwithNullability
on them always return the receiver.
Subtype queries
SubtypeCheckMode
enum is added. It has two values corresponding to the two modes for type checks:SubtypeCheckMode.withNullabilities
corresponds to the mode where the nullability modifiers on the types are taken into the account according to the specification for the subtype relation, andSubtypeCheckMode.ignoringNullabilities
corresponds to the mode where the nullability markers are ignored as if the check was made on pre-NNBD types.SubtypeTester.isSubtypeOf
is updated to receive additional parameter, aSubtypeCheckMode
.IsSubtypeOf
class is added. It represents a result of a nullability-aware type check. Objects ofIsSubtypeOf
can further be queried for whether the checked types are in the subtype relation when the nullability modifiers are taken into account (usingIsSubtypeOf.isSubtypeWhenUsingNullabilities
) or when the modifiers are ignored (usingIsSubtypeOf.isSubtypeWhenIgnoringNullabilities
).SubtypeTester.performNullabilityAwareSubtypeCheck
method is added. It takes two types as input and produces a result of typeIsSubtypeOf
. UsingSubtypeTester.performNullabilityAwareSubtypeCheck
is recommended for performance considerations if a call site needs to differentiate between NNBD and pre-NNBD cases.
Type Never
- A new subclass of
DartType
calledNeverType
is added to represent typeNever
. It is a subtype of all Dart types. It's different fromBottomType
already present in Kernel; the latter is reserved for other purposes with the addition to the subtyping rules thatBottomType
is a subtype ofNeverType
. DartTypeVisitor
andDartTypeVisitor1
visitors are updated to includevisitNeverType
methods. All implementations of the interfaces ofDartTypeVisitor
andDartTypeVisitor1
are updated to include implementations ofvisitNeverType
.
isRequired and isLate flags
VariableDeclaration.isRequired
setter and getter are added.NamedType.isRequired
final field is added.VariableDeclaration.isLate
setter and getter are added.Field.isLate
setter and getter are added.
Null Check
NullCheck
AST node is added.
Changes in caching of raw types
Class.rawType
is removed.TypeEnvironment.*Type
where*
expands toobject
,bool
,int
,num
,double
,string
,symbol
,type
,rawFunction
are removed.CoreTypes.*NonNullableRawType
,CoreTypes.*NullableRawType
,CoreTypes.*LegacyRawType
, andCoreTypes.*RawType(Nullability)
where*
expands intoobject
,bool
,int
,num
,double
,string
,list
,set
,map
,iterable
,iterator
,symbol
,type
,function
,invocation
,invocationMirror
,future
,stackTrace
,stream
,asyncAwaitCompleter
,futureOr
, andpragma
are added.CoreTypes.nullType
is added.CoreTypes.nonNullableRawType(Class)
,CoreTypes.nullableRawType(Class)
,CoreTypes.legacyRawType(Class)
, andCoreTypes.rawType(Class, Nullability)
are added.
The reason for the changes is to avoid having type objects of undefined nullability and have nullability-aware getters and methods for retrieving the types instead. The changes that landed the update (commit 515a5977, commit e034104f) also change the client code so that a legacy type is retrieved from CoreTypes
whenever there was an invocation of Class.rawType
. All of those places should be updated to retrieve a type with the desired nullability (see section Updating the Client Code).
Library status and library-specific nullability treatment
Library.isNonNullableByDefault
is added.Library.nullable
,Library.nonNullable
, andLibrary.nullableIfTrue(bool)
are added.
The Library.isNonNullableByDefault
getter is added to distinguish between opted-in and opted-out libraries. Right now it returns true
if the non-nullable
experiment is enabled, and false
otherwise.
The Library.nullable
and Library.nonNullable
getters are added to simplify the reasoning about types in the CFE and in the client code. They return Nullability.nullable
and Nullability.nonNullable
respectively if the library is opted in, and both return Nullability.legacy
otherwise. Thus, one may only consider what nullability would be desirable in an opted-in library and have the case of opted-out library covered by the getters.
Similarly, Library.nullableIfTrue(bool)
converts a boolean into a Nullability
value: true
is converted into Nullability.nullable
and false
is converted into Nullability.nonNullable
if the library is opted in. If the library is opted out, Library.nullableIfTrue(bool)
returns Nullability.legacy
.
This set of members makes it possible to avoid the explicit use of Nullability.legacy
in the CFE and the client code.
Updating the Client Code
Avoiding explicit legacy
As described in section Nullability attribute on types, all previously existing invocations of the constructors of InterfaceType
, TypedefType
, TypeParameterType
, and FunctionType
were given additional argument that specifies the nullability of the created type; Nullability.legacy
was used in the cases that weren't migrated to the NNBD semantics yet. Additionally, as described in section Changes in caching of raw types, all previously existing invocations of Class.rawType
and of the related getters of TypeEnvironment
were replaced with invocations of nullability-aware members of CoreTypes
. To keep the observable behavior of the client code, legacy types were used wherever before a raw type was used. For example, intClass.rawType
was replaced with coreTypes.intLegacyRawType
and cls.rawType
was replaced with coreTypes.legacyRawType(cls)
. All of those call sites should be updated as a part of the NNBD feature implementation because they are the source of legacy types regardless of the opted-in status of the library they are generated for. Section Library status and library-specific nullability treatment describes the changes in the CFE public interface that are supposed to help with the process.
The easiest way to avoid using explicitly legacy types is to do the following:
- Decide which nullability would be desirable for the type in an opted-in library.
- Specify the desired nullability for the type with
.nullable
or.nonNullable
getter on the library node of the library that the type is used in. In some cases, when a boolean condition tells if a type should be nullable,.nullableIfTrue(bool)
method on the library node may be useful.
Quick recommendations for updating the described code are listed below. The examples use Nullability.nonNullable
as the desired nullability.
- Replace
coreTypes.intLegacyRawType
withcoreTypes.intRawType(library.nonNullable)
. Similarly for other built-in types. - Replace
coreTypes.legacyRawType(cls)
withcoreTypes.rawType(cls, library.nonNullable)
. - Replace
coreTypes.rawType(cls, Nullability.legacy)
withcoreTypes.rawType(cls, library.nonNullable)
. - Replace
new InterfaceType(cls, Nullability.legacy, typeArgs)
withnew InterfaceType(cls, library.nonNullable, typeArgs)
. - Replace
new InterfaceType(cls, Nullability.legacy)
withnew InterfaceType(cls, library.nonNullable)
. - Replace
new InterfaceType.byReference(clsRef, Nullability.legacy, typeArgs)
withnew InterfaceType.byReference(clsRef, library.nonNullable, typeArgs)
. - Replace
new FunctionType(positional, retType, Nullability.legacy, <NAMED>)
withnew FunctionType(positional, retType, library.nonNullable, <NAMED>)
where<NAMED>
are the named arguments passed in. - Replace
new TypedefType(tdef, Nullability.legacy, typeArgs)
withnew TypedefType(tdef, library.nonNullable, typeArgs)
. - Replace
new TypedefType(tdef, Nullability.legacy)
withnew TypedefType(tdef, library.nonNullable)
. - Replace
new TypedefType.byReference(tdefRef, Nullability.legacy, typeArgs)
withnew TypedefType.byReference(tdefRef, library.nonNullable, typeArgs)
.
The code updated this way will generate nullable and non-nullable types as desired for the opted-in libraries and will generate legacy types for the opted-out libraries. It should also be easy to deprecate the weak-NNBD mode for such code: Library.nonNullable
and Library.nullable
will start returning the corresponding nullability constants.
Avoiding SubtypeCheckMode.ignoringNullabilities
As described in section Subtype queries, SubtypeTester.isSubtypeOf
now accepts additional parameter, a SubtypeCheckMode
. The parameter is required to clearly indicate the mode of checking, and is initially set to SubtypeCheckMode.ignoringNullabilities
at all call sites within CFE and in the client code.
As a part of implementing the NNBD feature, all such call sites need to take the nullability attributes into account. In the weak null checking mode all errors that originate from the NNBD-related changes should be treated as warnings. For some of the call sites it means that they should differentiate between two situations: whether one type is a subtype of the other type with nullabilities taken into account or only if the nullability modifiers are ignored.
One way to make the differentiation is to invoke isSubtypeOf
twice. The first time it is invoked with SubtypeCheckMode.withNullabilities
as the mode. If it returns true
, the subtype check succeeds when the nullability modifiers are taken into account and it would have succeeded also if the modifiers were ignored. It means that neither warning nor error should be reported if it was dependent on the result of the type check.
If isSubtypeOf
returns false
when the mode is SubtypeCheckMode.withNullabilities
, it is still possible that the check fails only because of the nullability modifiers. To check if it's true, another invocation of isSubtypeOf
can be made with SubtypeCheckMode.ignoringNullabilities
as the third parameter. If the result is true
, the subtype check fails only because of the nullability modifiers, and a warning should be reported. If the result is false
, the check fails in both modes, and an error should be reported.
To save some computations, a single invocation of performNullabilityAwareSubtypeCheck
can be made instead of the two invocations of isSubtypeOf
. The method takes two types as the input and returns an object of IsSubtypeOf
as the result. The return value of the method can further be queried to determine if an error or a warning should be reported. If invocation of IsSubtypeOf.isSubtypeWhenUsingNullabilities
returns true
, the two types are in the subtype relation in both modes, and neither error nor warning should be reported. If it returns false
, the failure may be due to the nullability modifiers. To differentiate between the two possibilities, an invocation of IsSubtypeOf.isSubtypeWhenIgnoringNullabilities
can be made. If the result is true
, a warning should be issued; otherwise, an error should be reported.
Computing TypeParameterType.nullability
As described in section Nullability of Intersection Types, objects of TypeParameterType
implement .nullability
as a getter, not a field, and the overall nullability value for the type is not serialized in the binary format. The back ends that implement their own deserialization may need to compute the nullability for TypeParameterType
s.
Implementing visitNeverType
Following the introduction of NeverType
, all implementors of DartTypeVisitor
and DartTypeVisitor1
should implement their new methods, visitNeverType
. Reasonable implementations are provided in the CL adding NeverType
, and they might need to be revisited.
Type equality
TODO: Update the section.