[fasta][nnbd] Add nullability modifiers to DartType

Change-Id: Ica2e5e250831573453c1d469571d9d001e7702a3
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/106642
Commit-Queue: Johnni Winther <johnniwinther@google.com>
Reviewed-by: Johnni Winther <johnniwinther@google.com>
diff --git a/pkg/front_end/lib/src/fasta/kernel/implicit_field_type.dart b/pkg/front_end/lib/src/fasta/kernel/implicit_field_type.dart
index c82886b..fedc609 100644
--- a/pkg/front_end/lib/src/fasta/kernel/implicit_field_type.dart
+++ b/pkg/front_end/lib/src/fasta/kernel/implicit_field_type.dart
@@ -17,6 +17,9 @@
   final MemberBuilder member;
   Token initializerToken;
 
+  get nullability =>
+      unsupported("nullability", member.charOffset, member.fileUri);
+
   ImplicitFieldType(this.member, this.initializerToken);
 
   accept(DartTypeVisitor<Object> v) {
diff --git a/pkg/front_end/lib/src/fasta/kernel/implicit_type_argument.dart b/pkg/front_end/lib/src/fasta/kernel/implicit_type_argument.dart
index 7195bac..2a35783 100644
--- a/pkg/front_end/lib/src/fasta/kernel/implicit_type_argument.dart
+++ b/pkg/front_end/lib/src/fasta/kernel/implicit_type_argument.dart
@@ -7,7 +7,7 @@
 import 'package:kernel/ast.dart'
     show DartType, DartTypeVisitor, DartTypeVisitor1, Visitor;
 
-import '../problems.dart' show unhandled;
+import '../problems.dart' show unhandled, unsupported;
 
 /// Marker type used as type argument on list, set and map literals whenever
 /// type arguments are omitted in the source.
@@ -18,6 +18,9 @@
   const ImplicitTypeArgument();
 
   @override
+  get nullability => unsupported("nullability", -1, null);
+
+  @override
   accept(DartTypeVisitor<Object> v) {
     unhandled("$runtimeType", "${v.runtimeType}", -1, null);
   }
diff --git a/pkg/front_end/lib/src/fasta/type_inference/type_schema.dart b/pkg/front_end/lib/src/fasta/type_inference/type_schema.dart
index e742fd0..01418ae 100644
--- a/pkg/front_end/lib/src/fasta/type_inference/type_schema.dart
+++ b/pkg/front_end/lib/src/fasta/type_inference/type_schema.dart
@@ -17,6 +17,8 @@
 import 'package:kernel/text/ast_to_text.dart'
     show Annotator, NameSystem, Printer, globalDebuggingNames;
 
+import '../problems.dart' show unsupported;
+
 /// Determines whether a type schema contains `?` somewhere inside it.
 bool isKnown(DartType schema) => schema.accept(new _IsKnownVisitor());
 
@@ -62,6 +64,9 @@
 /// The unknown type cannot appear in programs or in final inferred types: it is
 /// purely part of the local inference process.
 class UnknownType extends DartType {
+  @override
+  get nullability => unsupported("nullability", -1, null);
+
   const UnknownType();
 
   bool operator ==(Object other) {
diff --git a/pkg/kernel/lib/ast.dart b/pkg/kernel/lib/ast.dart
index 0ff11cf..45fa1d8 100644
--- a/pkg/kernel/lib/ast.dart
+++ b/pkg/kernel/lib/ast.dart
@@ -4799,6 +4799,41 @@
 //                             TYPES
 // ------------------------------------------------------------------------
 
+/// Represents nullability of a type.
+enum Nullability {
+  /// Types in opt-out libraries are 'legacy' types.
+  ///
+  /// They are both subtypes and supertypes of the nullable and non-nullable
+  /// versions of the type.
+  legacy,
+
+  /// Nullable types are marked with the '?' modifier.
+  ///
+  /// Null, dynamic, and void are nullable by default.
+  nullable,
+
+  /// Non-nullable types are types that aren't marked with the '?' modifier.
+  ///
+  /// Note that Null, dynamic, and void that are nullable by default.  Note also
+  /// that some types denoted by a type parameter without the '?' modifier can
+  /// be something else rather than non-nullable.
+  nonNullable,
+
+  /// Non-legacy types that are neither nullable, nor non-nullable.
+  ///
+  /// An example of such type is type T in the example below.  Note that both
+  /// int and int? can be passed in for T, so an attempt to assign null to x is
+  /// a compile-time error as well as assigning x to y.
+  ///
+  ///   class A<T extends Object?> {
+  ///     foo(T x) {
+  ///       x = null;      // Compile-time error.
+  ///       Object y = x;  // Compile-time error.
+  ///     }
+  ///   }
+  neither
+}
+
 /// A syntax-independent notion of a type.
 ///
 /// [DartType]s are not AST nodes and may be shared between different parents.
@@ -4818,6 +4853,8 @@
 
   bool operator ==(Object other);
 
+  Nullability get nullability;
+
   /// If this is a typedef type, repeatedly unfolds its type definition until
   /// the root term is not a typedef type, otherwise returns the type itself.
   ///
@@ -4843,6 +4880,8 @@
   visitChildren(Visitor v) {}
 
   bool operator ==(Object other) => other is InvalidType;
+
+  Nullability get nullability => throw "InvalidType doesn't have nullabiliity";
 }
 
 class DynamicType extends DartType {
@@ -4855,6 +4894,8 @@
   visitChildren(Visitor v) {}
 
   bool operator ==(Object other) => other is DynamicType;
+
+  Nullability get nullability => Nullability.nullable;
 }
 
 class VoidType extends DartType {
@@ -4867,6 +4908,8 @@
   visitChildren(Visitor v) {}
 
   bool operator ==(Object other) => other is VoidType;
+
+  Nullability get nullability => Nullability.nullable;
 }
 
 class BottomType extends DartType {
@@ -4879,21 +4922,29 @@
   visitChildren(Visitor v) {}
 
   bool operator ==(Object other) => other is BottomType;
+
+  Nullability get nullability => Nullability.nonNullable;
 }
 
 @coq
 class InterfaceType extends DartType {
   Reference className;
+
+  final Nullability nullability;
+
   @nocoq
   final List<DartType> typeArguments;
 
   /// The [typeArguments] list must not be modified after this call. If the
   /// list is omitted, 'dynamic' type arguments are filled in.
-  InterfaceType(Class classNode, [List<DartType> typeArguments])
+  InterfaceType(Class classNode,
+      [List<DartType> typeArguments,
+      Nullability nullability = Nullability.legacy])
       : this.byReference(getClassReference(classNode),
-            typeArguments ?? _defaultTypeArguments(classNode));
+            typeArguments ?? _defaultTypeArguments(classNode), nullability);
 
-  InterfaceType.byReference(this.className, this.typeArguments);
+  InterfaceType.byReference(this.className, this.typeArguments,
+      [this.nullability = Nullability.legacy]);
 
   Class get classNode => className.asClass;
 
@@ -4946,6 +4997,7 @@
   @coqsingle
   final List<DartType> positionalParameters;
   final List<NamedType> namedParameters; // Must be sorted.
+  final Nullability nullability;
 
   /// The [Typedef] this function type is created for.
   final TypedefType typedefType;
@@ -4956,6 +5008,7 @@
   FunctionType(List<DartType> positionalParameters, this.returnType,
       {this.namedParameters: const <NamedType>[],
       this.typeParameters: const <TypeParameter>[],
+      this.nullability: Nullability.legacy,
       int requiredParameterCount,
       this.typedefType})
       : this.positionalParameters = positionalParameters,
@@ -5073,14 +5126,18 @@
 ///
 /// The underlying type can be extracted using [unalias].
 class TypedefType extends DartType {
+  final Nullability nullability;
   final Reference typedefReference;
   final List<DartType> typeArguments;
 
-  TypedefType(Typedef typedefNode, [List<DartType> typeArguments])
-      : this.byReference(
-            typedefNode.reference, typeArguments ?? const <DartType>[]);
+  TypedefType(Typedef typedefNode,
+      [List<DartType> typeArguments,
+      Nullability nullability = Nullability.legacy])
+      : this.byReference(typedefNode.reference,
+            typeArguments ?? const <DartType>[], nullability);
 
-  TypedefType.byReference(this.typedefReference, this.typeArguments);
+  TypedefType.byReference(this.typedefReference, this.typeArguments,
+      [this.nullability = Nullability.legacy]);
 
   Typedef get typedefNode => typedefReference.asTypedef;
 
@@ -5163,6 +5220,8 @@
 /// is the same as the [TypeParameter]'s bound.  This allows one to detect
 /// whether the bound has been promoted.
 class TypeParameterType extends DartType {
+  final Nullability nullability;
+
   TypeParameter parameter;
 
   /// An optional promoted bound on the type parameter.
@@ -5171,7 +5230,10 @@
   /// is therefore the same as the bound of [parameter].
   DartType promotedBound;
 
-  TypeParameterType(this.parameter, [this.promotedBound]);
+  TypeParameterType(this.parameter,
+      [this.promotedBound, Nullability nullability])
+      : this.nullability =
+            getNullability(parameter, promotedBound, nullability);
 
   accept(DartTypeVisitor v) => v.visitTypeParameterType(this);
   accept1(DartTypeVisitor1 v, arg) => v.visitTypeParameterType(this, arg);
@@ -5186,6 +5248,115 @@
 
   /// Returns the bound of the type parameter, accounting for promotions.
   DartType get bound => promotedBound ?? parameter.bound;
+
+  /// Get nullability of [TypeParameterType] from arguments to its constructor.
+  ///
+  /// This method is supposed to be used only in the constructor of
+  /// [TypeParameterType] to compute the value of
+  /// [TypeParameterType.nullability] from the arguments passed to the constructor.
+  static Nullability getNullability(TypeParameter parameter,
+      DartType promotedBound, Nullability nullability) {
+    // If promotedBound is null, getNullability returns the nullability of
+    // either T or T? where T is parameter and the presence of '?' is determined
+    // by nullability.
+
+    // If promotedBound isn't null, getNullability returns the nullability of an
+    // instesection of the left-hand side (referred to as LHS below) and the
+    // right-hand side (referred to as RHS below).  LHS is parameter followed by
+    // nullability, and RHS is promotedBound.  That is, getNullability returns
+    // the nullability of either T & P or T? & P where T is parameter, P is
+    // promotedBound, and the presence of '?' is determined by nullability.
+    // Note that RHS is always a subtype of the bound of the type parameter.
+
+    Nullability lhsNullability;
+
+    // If the nullability is explicitly nullable, that is, if the type parameter
+    // type is followed by '?' in the code, the nullability of the type is
+    // 'nullable.'
+    if (nullability == Nullability.nullable) {
+      lhsNullability = Nullability.nullable;
+    } else {
+      // If the bound is nullable, both nullable and non-nullable types can be
+      // passed in for the type parameter, making the corresponding type
+      // parameter types 'neither.'  Otherwise, the nullability matches that of
+      // the bound.
+      DartType bound = parameter.bound ?? const DynamicType();
+      Nullability boundNullability =
+          bound is InvalidType ? Nullability.neither : bound.nullability;
+      lhsNullability = boundNullability == Nullability.nullable
+          ? Nullability.neither
+          : boundNullability;
+    }
+    if (promotedBound == null) {
+      return lhsNullability;
+    }
+
+    // In practice a type parameter of legacy type can only be used in type
+    // annotations within the corresponding class declaration.  If it's legacy,
+    // then the entire library containing the class is opt-out, and any RHS is
+    // deemed to be legacy too.  So, it's necessary to only check LHS for being
+    // legacy.
+    if (lhsNullability == Nullability.legacy) {
+      return Nullability.legacy;
+    }
+
+    // Intersection is non-nullable if and only if RHS is non-nullable.
+    //
+    // The proof is as follows.  Intersection is non-nullable if at least one of
+    // LHS or RHS is non-nullable.  The case of non-nullable RHS is trivial.  In
+    // the case of non-nullable LHS, its bound should be non-nullable.  RHS is
+    // known to always be a subtype of the bound of LHS; therefore, RHS is
+    // non-nullable.
+    //
+    // Note that it also follows from the above that non-nullable RHS implies
+    // non-nullable LHS, so the check below covers the case lhsNullability ==
+    // Nullability.nonNullable.
+    if (promotedBound.nullability == Nullability.nonNullable) {
+      return Nullability.nonNullable;
+    }
+
+    // If the nullability of LHS is 'neither,' the nullability of the
+    // intersection is also 'neither' if RHS is 'neither' or nullable.
+    //
+    // Consider the following example:
+    //
+    //     class A<X extends Object?, Y extends X> {
+    //       foo(X x) {
+    //         if (x is Y) {
+    //           x = null;     // Compile-time error.  Consider X = Y = int.
+    //           Object a = x; // Compile-time error.  Consider X = Y = int?.
+    //         }
+    //         if (x is int?) {
+    //           x = null;     // Compile-time error.  Consider X = int.
+    //           Object b = x; // Compile-time error.  Consider X = int?.
+    //         }
+    //       }
+    //     }
+    //
+    // Note that RHS can't be 'legacy' or non-nullable at this point due to the
+    // checks above.
+    if (lhsNullability == Nullability.neither) {
+      return Nullability.neither;
+    }
+
+    // At this point the only possibility for LHS is to be nullable, and for RHS
+    // is to be either nullable or legacy.  Both combinations for LHS and RHS
+    // should yield the nullability of RHS as the nullability for the
+    // intersection.  Consider the following code for clarification:
+    //
+    //   class A<X extends Object?, Y extends X> {
+    //     foo(X? x) {
+    //       if (x is Y) {
+    //         x = null;     // Compile-time error.  Consider X = Y = int.
+    //         Object a = x; // Compile-time error.  Consider X = Y = int?.
+    //       }
+    //       if (x is int?) {
+    //         x = null;     // Ok.  Both X? and int? are nullable.
+    //       }
+    //     }
+    //   }
+    return promotedBound.nullability;
+  }
 }
 
 /// Declaration of a type variable.