mini_types: introduce new classes for special built-in types.

New classes are introduced into the "mini_types" representation to
represent `dynamic`, `FutureOr<T>`, `Never`, `Null`, `void`, and the
"invalid" type. Previously, these were all represented simply using
`PrimaryType`.

Introducing new classes for these types makes the "mini_types"
representation more consistent with the representations used by the
analyzer and the front end. Since the "mini_types" representation is
used solely for unit testing the shared logic in the
`_fe_analyzer_shared` package, it's hard to justify unnecessary
differences between it and the analyzer and front end representations;
these differences just make it harder to effectively share code.

Change-Id: Ie633feef914c5ce4540b9da681a84b7f53fd8f38
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/365340
Reviewed-by: Konstantin Shcheglov <scheglov@google.com>
Reviewed-by: Chloe Stefantsova <cstefantsova@google.com>
Commit-Queue: Paul Berry <paulberry@google.com>
diff --git a/pkg/_fe_analyzer_shared/test/mini_ast.dart b/pkg/_fe_analyzer_shared/test/mini_ast.dart
index bce642a..2452b8e 100644
--- a/pkg/_fe_analyzer_shared/test/mini_ast.dart
+++ b/pkg/_fe_analyzer_shared/test/mini_ast.dart
@@ -1004,7 +1004,7 @@
   ExpressionTypeAnalysisResult<Type> visit(Harness h, TypeSchema schema) {
     var promotedType = promotable._getPromotedType(h);
     expect(promotedType?.type, expectedTypeStr, reason: 'at $location');
-    return SimpleTypeAnalysisResult(type: Type('Null'));
+    return SimpleTypeAnalysisResult(type: NullType.instance);
   }
 }
 
@@ -1023,7 +1023,7 @@
   ExpressionTypeAnalysisResult<Type> visit(Harness h, TypeSchema schema) {
     expect(h.flow.isReachable, expectedReachable, reason: 'at $location');
     h.irBuilder.atom('null', Kind.expression, location: location);
-    return new SimpleTypeAnalysisResult(type: Type('Null'));
+    return new SimpleTypeAnalysisResult(type: NullType.instance);
   }
 }
 
@@ -1790,7 +1790,7 @@
         return _members['Object.$memberName']!;
       default:
         // It's legal to look up any member on the type `dynamic`.
-        if (type.type == 'dynamic') {
+        if (type is DynamicType) {
           return null;
         }
         // But an attempt to look up an unknown member on any other type
@@ -2703,17 +2703,8 @@
   late final Type intType = Type('int');
 
   @override
-  late final Type neverType = Type('Never');
-
-  @override
-  late final Type nullType = Type('Null');
-
-  @override
   late final Type doubleType = Type('double');
 
-  @override
-  late final Type dynamicType = Type('dynamic');
-
   bool? _legacy;
 
   final Map<String, bool> _exhaustiveness = Map.of(_coreExhaustiveness);
@@ -2737,7 +2728,10 @@
   final Type boolType = Type('bool');
 
   @override
-  Type get errorType => Type('error');
+  Type get dynamicType => DynamicType.instance;
+
+  @override
+  Type get errorType => InvalidType.instance;
 
   bool get legacy => _legacy ?? false;
 
@@ -2745,6 +2739,12 @@
     _legacy = value;
   }
 
+  @override
+  Type get neverType => NeverType.instance;
+
+  @override
+  Type get nullType => NullType.instance;
+
   /// Updates the harness with a new result for [downwardInfer].
   void addDownwardInfer({
     required String name,
@@ -2788,7 +2788,7 @@
   TypeClassification classifyType(Type type) {
     if (isSubtypeOf(type, Type('Object'))) {
       return TypeClassification.nonNullable;
-    } else if (isSubtypeOf(type, Type('Null'))) {
+    } else if (isSubtypeOf(type, NullType.instance)) {
       return TypeClassification.nullOrEquivalent;
     } else {
       return TypeClassification.potentiallyNullable;
@@ -2881,8 +2881,8 @@
   @override
   bool isAssignableTo(Type fromType, Type toType) {
     if (legacy && isSubtypeOf(toType, fromType)) return true;
-    if (fromType.type == 'dynamic') return true;
-    if (fromType.type == 'error') return true;
+    if (fromType is DynamicType) return true;
+    if (fromType is InvalidType) return true;
     return isSubtypeOf(fromType, toType);
   }
 
@@ -2892,12 +2892,10 @@
   }
 
   @override
-  bool isDynamic(Type type) =>
-      type is PrimaryType && type.name == 'dynamic' && type.args.isEmpty;
+  bool isDynamic(Type type) => type is DynamicType;
 
   @override
-  bool isError(Type type) =>
-      type is PrimaryType && type.name == 'error' && type.args.isEmpty;
+  bool isError(Type type) => type is InvalidType;
 
   @override
   bool isExtensionType(Type type) {
@@ -2919,16 +2917,16 @@
 
   @override
   bool isNever(Type type) {
-    return type.type == 'Never';
+    return type is NeverType;
   }
 
   @override
   bool isNonNullable(TypeSchema typeSchema) {
     Type type = typeSchema.toType();
-    if (isDynamic(type) ||
+    if (type is DynamicType ||
         typeSchema is SharedUnknownType ||
-        isVoid(type) ||
-        isNull(type)) {
+        type is VoidType ||
+        type is NullType) {
       return false;
     } else if (type is PromotedTypeVariableType) {
       return isNonNullable(typeToSchema(type.promotion));
@@ -2940,13 +2938,11 @@
     // TODO(cstefantsova): Update to a fast-pass implementation when the
     // mini-ast testing framework supports looking up superinterfaces of
     // extension types or looking up bounds of type parameters.
-    return _typeSystem.isSubtype(new Type('Null'), type);
+    return _typeSystem.isSubtype(NullType.instance, type);
   }
 
   @override
-  bool isNull(Type type) {
-    return type.type == 'Null';
-  }
+  bool isNull(Type type) => type is NullType;
 
   @override
   bool isObject(Type type) {
@@ -2981,8 +2977,7 @@
   }
 
   @override
-  bool isVoid(Type type) =>
-      type is PrimaryType && type.name == 'void' && type.args.isEmpty;
+  bool isVoid(Type type) => type is VoidType;
 
   @override
   TypeSchema iterableTypeSchema(TypeSchema elementTypeSchema) {
@@ -3006,15 +3001,15 @@
       return type1;
     } else if (promoteToNonNull(type2) == type1) {
       return type2;
-    } else if (type1.type == 'Null' && promoteToNonNull(type2) != type2) {
+    } else if (type1 is NullType && promoteToNonNull(type2) != type2) {
       // type2 is already nullable
       return type2;
-    } else if (type2.type == 'Null' && promoteToNonNull(type1) != type1) {
+    } else if (type2 is NullType && promoteToNonNull(type1) != type1) {
       // type1 is already nullable
       return type1;
-    } else if (type1.type == 'Never') {
+    } else if (type1 is NeverType) {
       return type2;
-    } else if (type2.type == 'Never') {
+    } else if (type2 is NeverType) {
       return type1;
     } else {
       var typeNames = [type1.type, type2.type];
@@ -3025,11 +3020,11 @@
   }
 
   @override
-  Type makeNullable(Type type) => lub(type, Type('Null'));
+  Type makeNullable(Type type) => lub(type, NullType.instance);
 
   @override
   TypeSchema makeTypeSchemaNullable(TypeSchema typeSchema) =>
-      TypeSchema.fromType(lub(typeSchema.toType(), Type('Null')));
+      TypeSchema.fromType(lub(typeSchema.toType(), NullType.instance));
 
   @override
   Type mapType({
@@ -3050,10 +3045,8 @@
   @override
   Type? matchFutureOr(Type type) {
     Type underlyingType = withNullabilitySuffix(type, NullabilitySuffix.none);
-    if (underlyingType is PrimaryType && underlyingType.args.length == 1) {
-      if (underlyingType.name == 'FutureOr') {
-        return underlyingType.args[0];
-      }
+    if (underlyingType is FutureOrType) {
+      return underlyingType.typeArgument;
     }
     return null;
   }
@@ -3143,8 +3136,8 @@
   Type promoteToNonNull(Type type) {
     if (type is QuestionType) {
       return type.innerType;
-    } else if (type.type == 'Null') {
-      return Type('Never');
+    } else if (type is NullType) {
+      return NeverType.instance;
     } else {
       return type;
     }
@@ -3202,7 +3195,7 @@
   @override
   bool typeSchemaIsDynamic(TypeSchema typeSchema) {
     var type = typeSchema.toType();
-    return type is PrimaryType && type.name == 'dynamic' && type.args.isEmpty;
+    return type is DynamicType;
   }
 
   @override
@@ -3366,7 +3359,7 @@
     var rhsType =
         h.typeAnalyzer.analyzeExpression(rhs, h.operations.unknownType);
     h.flow.nullAwareAccess_end();
-    var type = h.operations.lub(rhsType, Type('Null'));
+    var type = h.operations.lub(rhsType, NullType.instance);
     h.irBuilder.apply(
         _fakeMethodName, [Kind.expression, Kind.expression], Kind.expression,
         location: location);
@@ -5404,8 +5397,6 @@
 
   final _irBuilder = MiniIRBuilder();
 
-  late final Type nullType = Type('Null');
-
   @override
   final TypeAnalyzerOptions options;
 
@@ -5425,6 +5416,8 @@
   FlowAnalysis<Node, Statement, Expression, Var, Type> get flow =>
       _harness.flow;
 
+  Type get nullType => NullType.instance;
+
   @override
   MiniAstOperations get operations => _harness.operations;
 
diff --git a/pkg/_fe_analyzer_shared/test/mini_types.dart b/pkg/_fe_analyzer_shared/test/mini_types.dart
index ab58808..3c6c10a 100644
--- a/pkg/_fe_analyzer_shared/test/mini_types.dart
+++ b/pkg/_fe_analyzer_shared/test/mini_types.dart
@@ -8,6 +8,14 @@
 
 import 'package:_fe_analyzer_shared/src/types/shared_type.dart';
 
+/// Representation of the type `dynamic` suitable for unit testing of code in
+/// the `_fe_analyzer_shared` package.
+class DynamicType extends _SpecialSimpleType {
+  static final instance = DynamicType._();
+
+  DynamicType._() : super._('dynamic');
+}
+
 /// Representation of a function type suitable for unit testing of code in the
 /// `_fe_analyzer_shared` package.
 ///
@@ -57,6 +65,38 @@
   }
 }
 
+/// Representation of the type `FutureOr<T>` suitable for unit testing of code
+/// in the `_fe_analyzer_shared` package.
+class FutureOrType extends PrimaryType {
+  FutureOrType(Type typeArgument)
+      : super._withSpecialName('FutureOr', args: [typeArgument]);
+
+  Type get typeArgument => args.single;
+
+  @override
+  Type? closureWithRespectToUnknown({required bool covariant}) {
+    Type? newArg =
+        typeArgument.closureWithRespectToUnknown(covariant: covariant);
+    if (newArg == null) return null;
+    return FutureOrType(newArg);
+  }
+
+  @override
+  Type? recursivelyDemote({required bool covariant}) {
+    Type? newArg = typeArgument.recursivelyDemote(covariant: covariant);
+    if (newArg == null) return null;
+    return FutureOrType(newArg);
+  }
+}
+
+/// Representation of an invalid type suitable for unit testing of code in the
+/// `_fe_analyzer_shared` package.
+class InvalidType extends _SpecialSimpleType {
+  static final instance = InvalidType._();
+
+  InvalidType._() : super._('error');
+}
+
 class NamedType implements SharedNamedType<Type> {
   @override
   final String name;
@@ -67,6 +107,22 @@
   NamedType({required this.name, required this.type});
 }
 
+/// Representation of the type `Never` suitable for unit testing of code in the
+/// `_fe_analyzer_shared` package.
+class NeverType extends _SpecialSimpleType {
+  static final instance = NeverType._();
+
+  NeverType._() : super._('Never');
+}
+
+/// Representation of the type `Null` suitable for unit testing of code in the
+/// `_fe_analyzer_shared` package.
+class NullType extends _SpecialSimpleType {
+  static final instance = NullType._();
+
+  NullType._() : super._('Null');
+}
+
 /// Exception thrown if a type fails to parse properly.
 class ParseError extends Error {
   final String message;
@@ -85,10 +141,12 @@
 class PrimaryType extends Type {
   /// Names of primary types not originating from a class, a mixin, or an enum.
   static const List<String> namedNonInterfaceTypes = [
+    'dynamic',
+    'error',
     'FutureOr',
     'Never',
     'Null',
-    'dynamic',
+    'void'
   ];
 
   /// The name of the type.
@@ -97,7 +155,19 @@
   /// The type arguments, or `const []` if there are no type arguments.
   final List<Type> args;
 
-  PrimaryType(this.name, {this.args = const []}) : super._();
+  PrimaryType(this.name, {this.args = const []}) : super._() {
+    if (namedNonInterfaceTypes.contains(name)) {
+      throw StateError('Tried to create a PrimaryType with special name $name');
+    }
+  }
+
+  PrimaryType._withSpecialName(this.name, {this.args = const []}) : super._() {
+    if (!namedNonInterfaceTypes.contains(name)) {
+      throw StateError(
+          'Tried to use PrimaryType._withSpecialName with non-special name '
+          '$name');
+    }
+  }
 
   bool get isInterfaceType => !namedNonInterfaceTypes.contains(name);
 
@@ -148,7 +218,7 @@
 
   @override
   Type? recursivelyDemote({required bool covariant}) =>
-      covariant ? innerType : new PrimaryType('Never');
+      covariant ? innerType : NeverType.instance;
 
   @override
   String _toString({required bool allowSuffixes}) {
@@ -413,8 +483,6 @@
     'String': (_) => [Type('Object')],
   };
 
-  static final _nullType = Type('Null');
-
   static final _objectQuestionType = Type('Object?');
 
   static final _objectType = Type('Object');
@@ -435,10 +503,10 @@
 
   Type factor(Type t, Type s) {
     // If T <: S then Never
-    if (isSubtype(t, s)) return Type('Never');
+    if (isSubtype(t, s)) return NeverType.instance;
 
     // Else if T is R? and Null <: S then factor(R, S)
-    if (t is QuestionType && isSubtype(_nullType, s)) {
+    if (t is QuestionType && isSubtype(NullType.instance, s)) {
       return factor(t.innerType, s);
     }
 
@@ -446,20 +514,22 @@
     if (t is QuestionType) return QuestionType(factor(t.innerType, s));
 
     // Else if T is R* and Null <: S then factor(R, S)
-    if (t is StarType && isSubtype(_nullType, s)) return factor(t.innerType, s);
+    if (t is StarType && isSubtype(NullType.instance, s)) {
+      return factor(t.innerType, s);
+    }
 
     // Else if T is R* then factor(R, S)*
     if (t is StarType) return StarType(factor(t.innerType, s));
 
     // Else if T is FutureOr<R> and Future<R> <: S then factor(R, S)
-    if (t is PrimaryType && t.args.length == 1 && t.name == 'FutureOr') {
-      var r = t.args[0];
+    if (t is FutureOrType) {
+      var r = t.typeArgument;
       if (isSubtype(PrimaryType('Future', args: [r]), s)) return factor(r, s);
     }
 
     // Else if T is FutureOr<R> and R <: S then factor(Future<R>, S)
-    if (t is PrimaryType && t.args.length == 1 && t.name == 'FutureOr') {
-      var r = t.args[0];
+    if (t is FutureOrType) {
+      var r = t.typeArgument;
       if (isSubtype(r, s)) return factor(PrimaryType('Future', args: [r]), s);
     }
 
@@ -492,14 +562,12 @@
     if (_isTop(t1)) return true;
 
     // Left Top: if T0 is dynamic or void then T0 <: T1 if Object? <: T1
-    if (t0 is PrimaryType &&
-        t0.args.isEmpty &&
-        (t0.name == 'dynamic' || t0.name == 'error' || t0.name == 'void')) {
+    if (t0 is DynamicType || t0 is InvalidType || t0 is VoidType) {
       return isSubtype(_objectQuestionType, t1);
     }
 
     // Left Bottom: if T0 is Never then T0 <: T1
-    if (t0 is PrimaryType && t0.args.isEmpty && t0.name == 'Never') return true;
+    if (t0 is NeverType) return true;
 
     // Right Object: if T1 is Object then:
     if (t1 is PrimaryType && t1.args.isEmpty && t1.name == 'Object') {
@@ -515,8 +583,8 @@
       }
 
       // - if T0 is FutureOr<S> for some S, then T0 <: T1 iff S <: Object.
-      if (t0 is PrimaryType && t0.args.length == 1 && t0.name == 'FutureOr') {
-        return isSubtype(t0.args[0], _objectType);
+      if (t0 is FutureOrType) {
+        return isSubtype(t0.typeArgument, _objectType);
       }
 
       // - if T0 is S* for any S, then T0 <: T1 iff S <: T1
@@ -525,12 +593,10 @@
       // - if T0 is Null, dynamic, void, or S? for any S, then the subtyping
       //   does not hold (per above, the result of the subtyping query is
       //   false).
-      if (t0 is PrimaryType &&
-              t0.args.isEmpty &&
-              (t0.name == 'Null' ||
-                  t0.name == 'dynamic' ||
-                  t0.name == 'error' ||
-                  t0.name == 'void') ||
+      if (t0 is NullType ||
+          t0 is DynamicType ||
+          t0 is InvalidType ||
+          t0 is VoidType ||
           t0 is QuestionType) {
         return false;
       }
@@ -540,20 +606,18 @@
     }
 
     // Left Null: if T0 is Null then:
-    if (t0 is PrimaryType && t0.args.isEmpty && t0.name == 'Null') {
+    if (t0 is NullType) {
       // - if T1 is a type variable (promoted or not) the query is false
       if (_isTypeVar(t1)) return false;
 
       // - If T1 is FutureOr<S> for some S, then the query is true iff
       //   Null <: S.
-      if (t1 is PrimaryType && t1.args.length == 1 && t1.name == 'FutureOr') {
-        return isSubtype(_nullType, t1.args[0]);
+      if (t1 is FutureOrType) {
+        return isSubtype(NullType.instance, t1.typeArgument);
       }
 
       // - If T1 is Null, S? or S* for some S, then the query is true.
-      if (t1 is PrimaryType && t1.args.isEmpty && t1.name == 'Null' ||
-          t1 is QuestionType ||
-          t1 is StarType) {
+      if (t1 is NullType || t1 is QuestionType || t1 is StarType) {
         return true;
       }
 
@@ -574,8 +638,8 @@
     }
 
     // Left FutureOr: if T0 is FutureOr<S0> then:
-    if (t0 is PrimaryType && t0.args.length == 1 && t0.name == 'FutureOr') {
-      var s0 = t0.args[0];
+    if (t0 is FutureOrType) {
+      var s0 = t0.typeArgument;
 
       // - T0 <: T1 iff Future<S0> <: T1 and S0 <: T1
       return isSubtype(PrimaryType('Future', args: [s0]), t1) &&
@@ -585,7 +649,7 @@
     // Left Nullable: if T0 is S0? then:
     if (t0 is QuestionType) {
       // - T0 <: T1 iff S0 <: T1 and Null <: T1
-      return isSubtype(t0.innerType, t1) && isSubtype(_nullType, t1);
+      return isSubtype(t0.innerType, t1) && isSubtype(NullType.instance, t1);
     }
 
     // Type Variable Reflexivity 1: if T0 is a type variable X0 or a promoted
@@ -614,8 +678,8 @@
     }
 
     // Right FutureOr: if T1 is FutureOr<S1> then:
-    if (t1 is PrimaryType && t1.args.length == 1 && t1.name == 'FutureOr') {
-      var s1 = t1.args[0];
+    if (t1 is FutureOrType) {
+      var s1 = t1.typeArgument;
 
       // - T0 <: T1 iff any of the following hold:
       return
@@ -640,7 +704,7 @@
           //   - either T0 <: S1
           isSubtype(t0, s1) ||
               //   - or T0 <: Null
-              isSubtype(t0, _nullType) ||
+              isSubtype(t0, NullType.instance) ||
               //   - or T0 is X0 and X0 has bound S0 and S0 <: T1
               t0 is PrimaryType &&
                   _isTypeVar(t0) &&
@@ -817,8 +881,7 @@
 
   bool _isTop(Type t) {
     if (t is PrimaryType) {
-      return t.args.isEmpty &&
-          (t.name == 'dynamic' || t.name == 'error' || t.name == 'void');
+      return t is DynamicType || t is InvalidType || t is VoidType;
     } else if (t is QuestionType) {
       var innerType = t.innerType;
       return innerType is PrimaryType &&
@@ -858,7 +921,7 @@
 
   @override
   Type closureWithRespectToUnknown({required bool covariant}) =>
-      covariant ? Type('Object?') : Type('Never');
+      covariant ? Type('Object?') : NeverType.instance;
 
   @override
   Type? recursivelyDemote({required bool covariant}) => null;
@@ -867,6 +930,30 @@
   String _toString({required bool allowSuffixes}) => '?';
 }
 
+/// Representation of the type `void` suitable for unit testing of code in the
+/// `_fe_analyzer_shared` package.
+class VoidType extends _SpecialSimpleType {
+  static final instance = VoidType._();
+
+  VoidType._() : super._('void');
+}
+
+/// Shared implementation of the types `void`, `dynamic`, `null`, `Never`, and
+/// the invalid type.
+///
+/// These types share the property that they are special cases of [PrimaryType]
+/// that don't need special functionality for the [closureWithRespectToUnknown]
+/// and [recursivelyDemote] methods.
+class _SpecialSimpleType extends PrimaryType {
+  _SpecialSimpleType._(super.name) : super._withSpecialName();
+
+  @override
+  Type? closureWithRespectToUnknown({required bool covariant}) => null;
+
+  @override
+  Type? recursivelyDemote({required bool covariant}) => null;
+}
+
 class _TypeParser {
   static final _typeTokenizationRegexp =
       RegExp(_identifierPattern + r'|\(|\)|<|>|,|\?|\*|&|{|}');
@@ -1053,7 +1140,39 @@
     } else {
       typeArgs = const [];
     }
-    return PrimaryType(typeName, args: typeArgs);
+    if (typeName == 'dynamic') {
+      if (typeArgs.isNotEmpty) {
+        throw ParseError('`dynamic` does not accept type arguments');
+      }
+      return DynamicType.instance;
+    } else if (typeName == 'error') {
+      if (typeArgs.isNotEmpty) {
+        throw ParseError('`error` does not accept type arguments');
+      }
+      return InvalidType.instance;
+    } else if (typeName == 'FutureOr') {
+      if (typeArgs.length != 1) {
+        throw ParseError('`FutureOr` requires exactly one type argument');
+      }
+      return FutureOrType(typeArgs.single);
+    } else if (typeName == 'Never') {
+      if (typeArgs.isNotEmpty) {
+        throw ParseError('`Never` does not accept type arguments');
+      }
+      return NeverType.instance;
+    } else if (typeName == 'Null') {
+      if (typeArgs.isNotEmpty) {
+        throw ParseError('`Null` does not accept type arguments');
+      }
+      return NullType.instance;
+    } else if (typeName == 'void') {
+      if (typeArgs.isNotEmpty) {
+        throw ParseError('`void` does not accept type arguments');
+      }
+      return VoidType.instance;
+    } else {
+      return PrimaryType(typeName, args: typeArgs);
+    }
   }
 
   static Type parse(String typeStr) {
diff --git a/pkg/_fe_analyzer_shared/test/mini_types_test.dart b/pkg/_fe_analyzer_shared/test/mini_types_test.dart
index 72891c3..d902d36 100644
--- a/pkg/_fe_analyzer_shared/test/mini_types_test.dart
+++ b/pkg/_fe_analyzer_shared/test/mini_types_test.dart
@@ -35,6 +35,31 @@
       test('invalid type arg separator', () {
         expect(() => Type('Map<int) String>'), throwsParseError);
       });
+
+      test('dynamic', () {
+        expect(Type('dynamic'), same(DynamicType.instance));
+      });
+
+      test('error', () {
+        expect(Type('error'), same(InvalidType.instance));
+      });
+
+      test('FutureOr', () {
+        var t = Type('FutureOr<int>') as FutureOrType;
+        expect(t.typeArgument.type, 'int');
+      });
+
+      test('Never', () {
+        expect(Type('Never'), same(NeverType.instance));
+      });
+
+      test('Null', () {
+        expect(Type('Null'), same(NullType.instance));
+      });
+
+      test('void', () {
+        expect(Type('void'), same(VoidType.instance));
+      });
     });
 
     test('invalid initial token', () {