Version 2.15.0-103.0.dev

Merge commit '0dd3c97147ac077af9c303499dd2784cec48a684' into 'dev'
diff --git a/pkg/_js_interop_checks/lib/src/transformations/js_util_optimizer.dart b/pkg/_js_interop_checks/lib/src/transformations/js_util_optimizer.dart
index 4159999..c5933d5 100644
--- a/pkg/_js_interop_checks/lib/src/transformations/js_util_optimizer.dart
+++ b/pkg/_js_interop_checks/lib/src/transformations/js_util_optimizer.dart
@@ -14,6 +14,8 @@
   final Procedure _jsTarget;
   final Procedure _callMethodTarget;
   final List<Procedure> _callMethodUncheckedTargets;
+  final Procedure _callConstructorTarget;
+  final List<Procedure> _callConstructorUncheckedTargets;
   final Procedure _getPropertyTarget;
   final Procedure _setPropertyTarget;
   final Procedure _setPropertyUncheckedTarget;
@@ -43,6 +45,12 @@
             5,
             (i) => _coreTypes.index.getTopLevelProcedure(
                 'dart:js_util', '_callMethodUnchecked$i')),
+        _callConstructorTarget = _coreTypes.index
+            .getTopLevelProcedure('dart:js_util', 'callConstructor'),
+        _callConstructorUncheckedTargets = List<Procedure>.generate(
+            5,
+            (i) => _coreTypes.index.getTopLevelProcedure(
+                'dart:js_util', '_callConstructorUnchecked$i')),
         _getPropertyTarget = _coreTypes.index
             .getTopLevelProcedure('dart:js_util', 'getProperty'),
         _setPropertyTarget = _coreTypes.index
@@ -90,6 +98,8 @@
       node = _lowerSetProperty(node);
     } else if (node.target == _callMethodTarget) {
       node = _lowerCallMethod(node);
+    } else if (node.target == _callConstructorTarget) {
+      node = _lowerCallConstructor(node);
     }
     node.transformChildren(this);
     return node;
@@ -147,70 +157,102 @@
     assert(arguments.positional.length == 3);
     assert(arguments.named.isEmpty);
 
-    // Lower List.empty factory call.
-    var argumentsList = arguments.positional.last;
+    return _lowerToCallUnchecked(
+        node, _callMethodUncheckedTargets, arguments.positional.sublist(0, 2));
+  }
+
+  /// Lowers the given js_util `callConstructor` call to `_callConstructorUncheckedN`
+  /// when the additional validation checks on the arguments can be elided.
+  ///
+  /// Calls will be lowered when using a List literal or constant list with 0-4
+  /// elements for the `callConstructor` arguments, or the `List.empty()` factory.
+  /// Removing the checks allows further inlining by the compilers.
+  StaticInvocation _lowerCallConstructor(StaticInvocation node) {
+    Arguments arguments = node.arguments;
+    assert(arguments.types.isEmpty);
+    assert(arguments.positional.length == 2);
+    assert(arguments.named.isEmpty);
+
+    return _lowerToCallUnchecked(
+        node, _callConstructorUncheckedTargets, [arguments.positional.first]);
+  }
+
+  /// Helper to lower the given [node] to the relevant unchecked target in the
+  /// [callUncheckedTargets] based on whether the validation checks on the
+  /// [originalArguments] can be elided.
+  ///
+  /// Calls will be lowered when using a List literal or constant list with 0-4
+  /// arguments, or the `List.empty()` factory. Removing the checks allows further
+  /// inlining by the compilers.
+  StaticInvocation _lowerToCallUnchecked(
+      StaticInvocation node,
+      List<Procedure> callUncheckedTargets,
+      List<Expression> originalArguments) {
+    var argumentsList = node.arguments.positional.last;
+    // Lower arguments in a List.empty factory call.
     if (argumentsList is StaticInvocation &&
         argumentsList.target == _listEmptyFactory) {
-      return _createNewCallMethodNode([], arguments, node.fileOffset);
+      return _createCallUncheckedNode(callUncheckedTargets, [],
+          originalArguments, node.fileOffset, node.arguments.fileOffset);
     }
 
-    // Lower other kinds of Lists.
-    var callMethodArguments;
+    // Lower arguments in other kinds of Lists.
+    var callUncheckedArguments;
     var entryType;
     if (argumentsList is ListLiteral) {
-      if (argumentsList.expressions.length >=
-          _callMethodUncheckedTargets.length) {
+      if (argumentsList.expressions.length >= callUncheckedTargets.length) {
         return node;
       }
-      callMethodArguments = argumentsList.expressions;
+      callUncheckedArguments = argumentsList.expressions;
       entryType = argumentsList.typeArgument;
     } else if (argumentsList is ConstantExpression &&
         argumentsList.constant is ListConstant) {
       var argumentsListConstant = argumentsList.constant as ListConstant;
-      if (argumentsListConstant.entries.length >=
-          _callMethodUncheckedTargets.length) {
+      if (argumentsListConstant.entries.length >= callUncheckedTargets.length) {
         return node;
       }
-      callMethodArguments = argumentsListConstant.entries
+      callUncheckedArguments = argumentsListConstant.entries
           .map((constant) => ConstantExpression(
               constant, constant.getType(_staticTypeContext)))
           .toList();
       entryType = argumentsListConstant.typeArgument;
     } else {
-      // Skip lowering any other type of List.
+      // Skip lowering arguments in any other type of List.
       return node;
     }
 
-    // Check the overall List entry type, then verify each argument if needed.
+    // Check the arguments List type, then verify each argument if needed.
     if (!_allowedInteropType(entryType)) {
-      for (var argument in callMethodArguments) {
+      for (var argument in callUncheckedArguments) {
         if (!_allowedInterop(argument)) {
           return node;
         }
       }
     }
 
-    return _createNewCallMethodNode(
-        callMethodArguments, arguments, node.fileOffset);
+    return _createCallUncheckedNode(
+        callUncheckedTargets,
+        callUncheckedArguments,
+        originalArguments,
+        node.fileOffset,
+        node.arguments.fileOffset);
   }
 
-  /// Creates a new StaticInvocation node for `_callMethodUncheckedN` with the
-  /// given 0-4 arguments.
-  StaticInvocation _createNewCallMethodNode(
-      List<Expression> callMethodArguments,
-      Arguments arguments,
-      int nodeFileOffset) {
-    assert(callMethodArguments.length <= 4);
+  /// Creates a new StaticInvocation node for the relevant unchecked target
+  /// with the given 0-4 arguments.
+  StaticInvocation _createCallUncheckedNode(
+      List<Procedure> callUncheckedTargets,
+      List<Expression> callUncheckedArguments,
+      List<Expression> originalArguments,
+      int nodeFileOffset,
+      int argumentsFileOffset) {
+    assert(callUncheckedArguments.length <= 4);
     return StaticInvocation(
-        _callMethodUncheckedTargets[callMethodArguments.length],
+        callUncheckedTargets[callUncheckedArguments.length],
         Arguments(
-          [
-            arguments.positional[0],
-            arguments.positional[1],
-            ...callMethodArguments
-          ],
+          [...originalArguments, ...callUncheckedArguments],
           types: [],
-        )..fileOffset = arguments.fileOffset)
+        )..fileOffset = argumentsFileOffset)
       ..fileOffset = nodeFileOffset;
   }
 
diff --git a/pkg/analysis_server/lib/src/lsp/server_capabilities_computer.dart b/pkg/analysis_server/lib/src/lsp/server_capabilities_computer.dart
index 89f26d4..36bff9b 100644
--- a/pkg/analysis_server/lib/src/lsp/server_capabilities_computer.dart
+++ b/pkg/analysis_server/lib/src/lsp/server_capabilities_computer.dart
@@ -293,6 +293,8 @@
         // interestingFiles. Prefix a `**/` so that the glob matches nested
         // folders as well.
         .map((glob) => DocumentFilter(scheme: 'file', pattern: '**/$glob'));
+    final pluginTypesExcludingDart =
+        pluginTypes.where((filter) => filter.pattern != '**/*.dart');
 
     final fullySupportedTypes = {dartFiles, ...pluginTypes}.toList();
 
@@ -314,7 +316,7 @@
     final completionSupportedTypesExcludingDart = {
       // Dart is excluded here at it's registered separately with trigger/commit
       // characters.
-      ...pluginTypes,
+      ...pluginTypesExcludingDart,
       pubspecFile,
       analysisOptionsFile,
       fixDataFile,
diff --git a/pkg/analysis_server/test/lsp/initialization_test.dart b/pkg/analysis_server/test/lsp/initialization_test.dart
index 7d1bc71..0a72f10 100644
--- a/pkg/analysis_server/test/lsp/initialization_test.dart
+++ b/pkg/analysis_server/test/lsp/initialization_test.dart
@@ -91,6 +91,51 @@
     expect(nonDartOptions.triggerCharacters, isNull);
   }
 
+  Future<void> test_completionRegistrations_withDartPlugin() async {
+    // This tests for a bug that occurred with an analysis server plugin
+    // that works on Dart files. When computing completion registrations we
+    // usually have seperate registrations for Dart + non-Dart to account for
+    // different trigger characters. However, the plugin types were all being
+    // included in the non-Dart registration even if they included Dart.
+    //
+    // The result was two registrations including Dart, which caused duplicate
+    // requests for Dart completions, which resulted in duplicate items
+    // appearing in the editor.
+
+    // Track all current registrations.
+    final registrations = <Registration>[];
+
+    // Perform normal registration (without plugins) to get the initial set.
+    await monitorDynamicRegistrations(
+      registrations,
+      () => initialize(
+        textDocumentCapabilities:
+            withAllSupportedTextDocumentDynamicRegistrations(
+                emptyTextDocumentClientCapabilities),
+      ),
+    );
+
+    // Expect only a single registration that includes Dart files.
+    expect(
+      registrationsForDart(registrations, Method.textDocument_completion),
+      hasLength(1),
+    );
+
+    // Monitor the unregistration/new registrations during the plugin activation.
+    await monitorDynamicReregistration(registrations, () async {
+      final plugin = configureTestPlugin();
+      plugin.currentSession = PluginSession(plugin)
+        ..interestingFiles = ['*.dart'];
+      pluginManager.pluginsChangedController.add(null);
+    });
+
+    // Expect that there is still only a single registration for Dart.
+    expect(
+      registrationsForDart(registrations, Method.textDocument_completion),
+      hasLength(1),
+    );
+  }
+
   Future<void> test_dynamicRegistration_containsAppropriateSettings() async {
     // Basic check that the server responds with the capabilities we'd expect,
     // for ex including analysis_options.yaml in text synchronization but not
diff --git a/pkg/analysis_server/test/lsp/server_abstract.dart b/pkg/analysis_server/test/lsp/server_abstract.dart
index 45b8b3f..b4d14d7 100644
--- a/pkg/analysis_server/test/lsp/server_abstract.dart
+++ b/pkg/analysis_server/test/lsp/server_abstract.dart
@@ -128,19 +128,35 @@
     return registrations.singleWhereOrNull((r) => r.method == method.toJson());
   }
 
-  /// Finds the registration for a given LSP method with Dart in its
+  /// Finds a single registration for a given LSP method with Dart in its
   /// documentSelector.
+  ///
+  /// Throws if there is not exactly one match.
   Registration registrationForDart(
     List<Registration> registrations,
     Method method,
+  ) =>
+      registrationsForDart(registrations, method).single;
+
+  /// Finds the registrations for a given LSP method with Dart in their
+  /// documentSelector.
+  List<Registration> registrationsForDart(
+    List<Registration> registrations,
+    Method method,
   ) {
-    return registrations.singleWhere((r) =>
-        r.method == method.toJson() &&
-        (TextDocumentRegistrationOptions.fromJson(
-                    r.registerOptions as Map<String, Object?>)
-                .documentSelector
-                ?.any((selector) => selector.language == dartLanguageId) ??
-            false));
+    bool includesDart(Registration r) {
+      final options = TextDocumentRegistrationOptions.fromJson(
+          r.registerOptions as Map<String, Object?>);
+
+      return options.documentSelector?.any((selector) =>
+              selector.language == dartLanguageId ||
+              (selector.pattern?.contains('.dart') ?? false)) ??
+          false;
+    }
+
+    return registrations
+        .where((r) => r.method == method.toJson() && includesDart(r))
+        .toList();
   }
 
   void resetContextBuildCounter() {
@@ -1390,11 +1406,11 @@
 
   /// Watches for `client/registerCapability` requests and updates
   /// `registrations`.
-  Future<ResponseMessage> monitorDynamicRegistrations(
+  Future<T> monitorDynamicRegistrations<T>(
     List<Registration> registrations,
-    Future<ResponseMessage> Function() f,
+    Future<T> Function() f,
   ) {
-    return handleExpectedRequest<ResponseMessage, RegistrationParams, void>(
+    return handleExpectedRequest<T, RegistrationParams, void>(
       Method.client_registerCapability,
       RegistrationParams.fromJson,
       f,
@@ -1405,9 +1421,9 @@
   }
 
   /// Expects both unregistration and reregistration.
-  Future<ResponseMessage> monitorDynamicReregistration(
+  Future<T> monitorDynamicReregistration<T>(
     List<Registration> registrations,
-    Future<ResponseMessage> Function() f,
+    Future<T> Function() f,
   ) =>
       monitorDynamicUnregistrations(
         registrations,
@@ -1416,11 +1432,11 @@
 
   /// Watches for `client/unregisterCapability` requests and updates
   /// `registrations`.
-  Future<ResponseMessage> monitorDynamicUnregistrations(
+  Future<T> monitorDynamicUnregistrations<T>(
     List<Registration> registrations,
-    Future<ResponseMessage> Function() f,
+    Future<T> Function() f,
   ) {
-    return handleExpectedRequest<ResponseMessage, UnregistrationParams, void>(
+    return handleExpectedRequest<T, UnregistrationParams, void>(
       Method.client_unregisterCapability,
       UnregistrationParams.fromJson,
       f,
diff --git a/pkg/compiler/lib/src/commandline_options.dart b/pkg/compiler/lib/src/commandline_options.dart
index 92170e9..7a72775 100644
--- a/pkg/compiler/lib/src/commandline_options.dart
+++ b/pkg/compiler/lib/src/commandline_options.dart
@@ -54,11 +54,6 @@
 
   static const String experimentNewRti = '--experiment-new-rti';
 
-  /// Use the dart2js lowering of late instance variables rather than the CFE
-  /// lowering.
-  static const String experimentLateInstanceVariables =
-      '--experiment-late-instance-variables';
-
   static const String enableLanguageExperiments = '--enable-experiment';
 
   static const String fastStartup = '--fast-startup';
diff --git a/pkg/compiler/lib/src/common_elements.dart b/pkg/compiler/lib/src/common_elements.dart
index 6f2a3bd..d2e5709 100644
--- a/pkg/compiler/lib/src/common_elements.dart
+++ b/pkg/compiler/lib/src/common_elements.dart
@@ -531,6 +531,22 @@
   /// Most foreign helpers are located in the `dart:_foreign_helper` library.
   bool isForeignHelper(MemberEntity member);
 
+  /// Returns `true` if [member] is the `createJsSentinel` function defined in
+  /// dart:_foreign_helper.
+  bool isCreateJsSentinel(MemberEntity member);
+
+  /// Returns `true` if [member] is the `isJsSentinel` function defined in
+  /// dart:_foreign_helper.
+  bool isIsJsSentinel(MemberEntity member);
+
+  /// Returns `true` if [member] is the `_lateReadCheck` function defined in
+  /// dart:_internal.
+  bool isLateReadCheck(MemberEntity member);
+
+  /// Returns `true` if [member] is the `createSentinel` function defined in
+  /// dart:_internal.
+  bool isCreateSentinel(MemberEntity member);
+
   ClassEntity getDefaultSuperclass(
       ClassEntity cls, NativeBasicData nativeBasicData);
 
@@ -644,10 +660,6 @@
 
   bool isForeign(MemberEntity element);
 
-  /// Returns `true` if [member] is the `createSentinel` function defined in
-  /// dart:_internal.
-  bool isCreateSentinel(MemberEntity element);
-
   /// Returns `true` if the implementation of the 'operator ==' [function] is
   /// known to handle `null` as argument.
   bool operatorEqHandlesNullArgument(FunctionEntity function);
@@ -2117,13 +2129,28 @@
         isCreateInvocationMirrorHelper(member);
   }
 
+  bool _isTopLevelFunctionNamed(String name, MemberEntity member) =>
+      member.name == name && member.isFunction && member.isTopLevel;
+
   @override
-  bool isCreateSentinel(MemberEntity member) {
-    return member.isTopLevel &&
-        member.isFunction &&
-        member.library == internalLibrary &&
-        member.name == 'createSentinel';
-  }
+  bool isCreateJsSentinel(MemberEntity member) =>
+      member.library == foreignLibrary &&
+      _isTopLevelFunctionNamed('createJsSentinel', member);
+
+  @override
+  bool isIsJsSentinel(MemberEntity member) =>
+      member.library == foreignLibrary &&
+      _isTopLevelFunctionNamed('isJsSentinel', member);
+
+  @override
+  bool isLateReadCheck(MemberEntity member) =>
+      member.library == lateHelperLibrary &&
+      _isTopLevelFunctionNamed('_lateReadCheck', member);
+
+  @override
+  bool isCreateSentinel(MemberEntity member) =>
+      member.library == internalLibrary &&
+      _isTopLevelFunctionNamed('createSentinel', member);
 
   @override
   bool operatorEqHandlesNullArgument(FunctionEntity function) {
diff --git a/pkg/compiler/lib/src/constants/values.dart b/pkg/compiler/lib/src/constants/values.dart
index 449c2b2..062f0f9 100644
--- a/pkg/compiler/lib/src/constants/values.dart
+++ b/pkg/compiler/lib/src/constants/values.dart
@@ -912,7 +912,7 @@
   }
 
   @override
-  DartType getType(CommonElements types) => types.dynamicType;
+  DartType getType(CommonElements types) => types.dartTypes.neverType();
 
   @override
   ConstantValueKind get kind => ConstantValueKind.LATE_SENTINEL;
diff --git a/pkg/compiler/lib/src/dart2js.dart b/pkg/compiler/lib/src/dart2js.dart
index 4693cbc..8435ff0 100644
--- a/pkg/compiler/lib/src/dart2js.dart
+++ b/pkg/compiler/lib/src/dart2js.dart
@@ -646,7 +646,6 @@
     new OptionHandler(Flags.experimentUnreachableMethodsThrow, passThrough),
     new OptionHandler(Flags.experimentCallInstrumentation, passThrough),
     new OptionHandler(Flags.experimentNewRti, ignoreOption),
-    new OptionHandler(Flags.experimentLateInstanceVariables, passThrough),
     new OptionHandler('${Flags.mergeFragmentsThreshold}=.+', passThrough),
 
     // Wire up feature flags.
diff --git a/pkg/compiler/lib/src/inferrer/abstract_value_domain.dart b/pkg/compiler/lib/src/inferrer/abstract_value_domain.dart
index 93a30d0..d2b10ed 100644
--- a/pkg/compiler/lib/src/inferrer/abstract_value_domain.dart
+++ b/pkg/compiler/lib/src/inferrer/abstract_value_domain.dart
@@ -104,7 +104,11 @@
 
 /// A system that implements an abstraction over runtime values.
 abstract class AbstractValueDomain {
-  /// The [AbstractValue] that represents an unknown runtime value.
+  /// The [AbstractValue] that represents an unknown runtime value. This
+  /// includes values internal to the implementation, such as late sentinels.
+  AbstractValue get internalTopType;
+
+  /// The [AbstractValue] that represents an unknown runtime Dart value.
   AbstractValue get dynamicType;
 
   /// The [AbstractValue] that represents a non-null subtype of `Type` at
@@ -153,6 +157,9 @@
   /// The [AbstractValue] that represents the `null` at runtime.
   AbstractValue get nullType;
 
+  /// The [AbstractValue] that represents a late sentinel value at runtime.
+  AbstractValue get lateSentinelType;
+
   /// The [AbstractValue] that represents a non-null growable JavaScript array
   /// at runtime.
   AbstractValue get growableListType;
@@ -273,6 +280,14 @@
   /// Returns the version of the abstract [value] that includes `null`.
   AbstractValue includeNull(covariant AbstractValue value);
 
+  /// Returns the version of the abstract [value] that excludes the late
+  /// sentinel.
+  AbstractValue excludeLateSentinel(covariant AbstractValue value);
+
+  /// Returns the version of the abstract [value] that includes the late
+  /// sentinel.
+  AbstractValue includeLateSentinel(covariant AbstractValue value);
+
   /// Returns an [AbstractBool] that describes whether [value] contains
   /// instances of [cls] at runtime.
   AbstractBool containsType(covariant AbstractValue value, ClassEntity cls);
@@ -299,10 +314,6 @@
   /// exact class at runtime.
   AbstractBool isExact(covariant AbstractValue value);
 
-  /// Returns an [AbstractBool] that describes whether [value] is an exact class
-  /// or `null` at runtime.
-  AbstractBool isExactOrNull(covariant AbstractValue value);
-
   /// Returns the [ClassEntity] if this [value] is a non-null instance of an
   /// exact class at runtime, and `null` otherwise.
   ClassEntity getExactClass(covariant AbstractValue value);
@@ -311,6 +322,10 @@
   /// runtime.
   AbstractBool isNull(covariant AbstractValue value);
 
+  /// Returns an [AbstractBool] that describes whether [value] is a sentinel for
+  /// an uninitialized late variable at runtime.
+  AbstractBool isLateSentinel(covariant AbstractValue value);
+
   /// Returns an [AbstractBool] that describes whether [value] is a JavaScript
   /// bool, number, string, array or `null` at runtime.
   AbstractBool isPrimitive(covariant AbstractValue value);
@@ -324,10 +339,6 @@
   AbstractBool isPrimitiveBoolean(covariant AbstractValue value);
 
   /// Returns an [AbstractBool] that describes whether [value] is a JavaScript
-  /// array at runtime.
-  AbstractBool isPrimitiveArray(covariant AbstractValue value);
-
-  /// Returns an [AbstractBool] that describes whether [value] is a JavaScript
   /// string, array, native HTML list or `null` at runtime.
   AbstractBool isIndexablePrimitive(covariant AbstractValue value);
 
diff --git a/pkg/compiler/lib/src/inferrer/builder_kernel.dart b/pkg/compiler/lib/src/inferrer/builder_kernel.dart
index fe3d41c..7429e8d 100644
--- a/pkg/compiler/lib/src/inferrer/builder_kernel.dart
+++ b/pkg/compiler/lib/src/inferrer/builder_kernel.dart
@@ -1509,6 +1509,10 @@
       return _inferrer.typeOfNativeBehavior(nativeBehavior);
     } else if (name == Identifiers.JS_STRING_CONCAT) {
       return _types.stringType;
+    } else if (_closedWorld.commonElements.isCreateJsSentinel(function)) {
+      return _types.lateSentinelType;
+    } else if (_closedWorld.commonElements.isIsJsSentinel(function)) {
+      return _types.boolType;
     } else {
       _sideEffectsBuilder.setAllSideEffects();
       return _types.dynamicType;
@@ -1522,29 +1526,18 @@
     Selector selector = _elementMap.getSelector(node);
     if (_closedWorld.commonElements.isForeign(member)) {
       return handleForeignInvoke(node, member, arguments, selector);
-    } else if (!_options.useLegacySubtyping &&
-        _closedWorld.commonElements.isCreateSentinel(member)) {
-      // TODO(fishythefish): Support this for --no-sound-null-safety too.
-      // `T createSentinel<T>()` ostensibly returns a `T` based on its static
-      // type. However, we need to handle this specially for a couple of
-      // reasons:
-      // 1. We do not currently handle type arguments during type inference and
-      //    in the abstract value domain. Without additional tracing, this means
-      //    that we lose all call-site sensitivity and `createSentinel` is seen
-      //    as returning `Object?`, which widens the inferred types of late
-      //    fields, resulting in poor codegen.
-      // 2. The sentinel isn't a real Dart value and doesn't really inhabit any
-      //    Dart type. Nevertheless, we must view it as inhabiting every Dart
-      //    type for the signature of `createSentinel` to make sense, making it
-      //    a bottom value (similar to an expression of type `Never`).  This
-      //    matches the expectation that reading an uninitialized late field
-      //    (that is, one initialized with the sentinel value) throws.
-      // Note that this currently breaks if `--experiment-unreachable-throw` is
-      // used. We'll be able to do something more precise here when more of the
-      // lowering is deferred to SSA and the abstract value domain can better
-      // track sentinel values.
+    } else if (_closedWorld.commonElements.isLateReadCheck(member)) {
+      // `_lateReadCheck` is essentially a narrowing to exclude the sentinel
+      // value. In order to avoid poor inference resulting from a large
+      // fan-in/fan-out, we perform the narrowing directly instead of creating a
+      // [TypeInformation] for this member.
       handleStaticInvoke(node, selector, member, arguments);
-      return _types.nonNullEmptyType;
+      return _types.narrowType(arguments.positional[0],
+          _elementMap.getDartType(node.arguments.types.single),
+          excludeLateSentinel: true);
+    } else if (_closedWorld.commonElements.isCreateSentinel(member)) {
+      handleStaticInvoke(node, selector, member, arguments);
+      return _types.lateSentinelType;
     } else if (member.isConstructor) {
       return handleConstructorInvoke(
           node, node.arguments, selector, member, arguments);
@@ -2384,8 +2377,8 @@
       {isCast: true,
       excludeNull: false}) {
     assert(type != null);
-    type = inferrer.types
-        .narrowType(type, staticType, isCast: isCast, excludeNull: excludeNull);
+    type = inferrer.types.narrowType(type, staticType,
+        isCast: isCast, excludeNull: excludeNull, excludeLateSentinel: true);
 
     FieldEntity field = capturedAndBoxed[local];
     if (field != null) {
diff --git a/pkg/compiler/lib/src/inferrer/powersets/powerset_bits.dart b/pkg/compiler/lib/src/inferrer/powersets/powerset_bits.dart
index 22a900b..9557395 100644
--- a/pkg/compiler/lib/src/inferrer/powersets/powerset_bits.dart
+++ b/pkg/compiler/lib/src/inferrer/powersets/powerset_bits.dart
@@ -203,6 +203,8 @@
 
     // TODO(coam): We could be more precise if we implement a visitor to
     // ConstantValue
+    // TODO(fishythefish): Naively calling `getType` on
+    // [LateSentinelConstantValue] will produce Never.
     return createFromStaticType(value.getType(commonElements), nullable: false);
   }
 
@@ -281,8 +283,6 @@
 
   AbstractBool isIndexablePrimitive(int value) => isOther(value);
 
-  AbstractBool isPrimitiveArray(int value) => isOther(value);
-
   AbstractBool isPrimitiveBoolean(int value) {
     if (isDefinitelyTrue(value) || isDefinitelyFalse(value)) {
       return AbstractBool.True;
@@ -300,7 +300,8 @@
       ? AbstractBool.True
       : (isPotentiallyNull(value) ? AbstractBool.Maybe : AbstractBool.False);
 
-  AbstractBool isExactOrNull(int value) => AbstractBool.Maybe;
+  // TODO(fishythefish): Support tracking late sentinels in the powerset domain.
+  AbstractBool isLateSentinel(int value) => AbstractBool.Maybe;
 
   AbstractBool isExact(int value) => AbstractBool.Maybe;
 
@@ -333,6 +334,12 @@
     return value & ~nullValue;
   }
 
+  // TODO(fishythefish): Support tracking late sentinels in the powerset domain.
+  int includeLateSentinel(int value) => value;
+
+  // TODO(fishythefish): Support tracking late sentinels in the powerset domain.
+  int excludeLateSentinel(int value) => value;
+
   AbstractBool couldBeTypedArray(int value) => isOther(value);
 
   AbstractBool isTypedArray(int value) => AbstractBool.Maybe;
@@ -555,6 +562,8 @@
     return finish(dynamicType, false);
   }
 
+  int get internalTopType => powersetTop;
+
   int get dynamicType => powersetTop;
 
   int get asyncStarStreamType => powersetTop;
@@ -591,6 +600,9 @@
 
   int get nonNullType => powersetTop & ~nullValue;
 
+  // TODO(fishythefish): Support tracking late sentinels in the powerset domain.
+  int get lateSentinelType => powersetBottom;
+
   int _mapType;
   int get mapType =>
       _mapType ??= createNonNullSubtype(commonElements.mapLiteralClass);
diff --git a/pkg/compiler/lib/src/inferrer/powersets/powersets.dart b/pkg/compiler/lib/src/inferrer/powersets/powersets.dart
index fedb435..b313818 100644
--- a/pkg/compiler/lib/src/inferrer/powersets/powersets.dart
+++ b/pkg/compiler/lib/src/inferrer/powersets/powersets.dart
@@ -63,10 +63,13 @@
   PowersetBitsDomain get powersetBitsDomain => _powersetBitsDomain;
 
   @override
-  AbstractValue get dynamicType {
-    AbstractValue abstractValue = _abstractValueDomain.dynamicType;
-    return PowersetValue(abstractValue, _powersetBitsDomain.powersetTop);
-  }
+  AbstractValue get internalTopType => PowersetValue(
+      _abstractValueDomain.internalTopType,
+      _powersetBitsDomain.internalTopType);
+
+  @override
+  AbstractValue get dynamicType => PowersetValue(
+      _abstractValueDomain.dynamicType, _powersetBitsDomain.dynamicType);
 
   //TODO(coam)
   @override
@@ -528,12 +531,6 @@
           _abstractValueDomain.isIndexablePrimitive(value._abstractValue));
 
   @override
-  AbstractBool isPrimitiveArray(covariant PowersetValue value) =>
-      AbstractBool.strengthen(
-          _powersetBitsDomain.isPrimitiveArray(value._powersetBits),
-          _abstractValueDomain.isPrimitiveArray(value._abstractValue));
-
-  @override
   AbstractBool isPrimitiveBoolean(covariant PowersetValue value) =>
       AbstractBool.strengthen(
           _powersetBitsDomain.isPrimitiveBoolean(value._powersetBits),
@@ -557,14 +554,14 @@
       _abstractValueDomain.isNull(value._abstractValue));
 
   @override
-  ClassEntity getExactClass(covariant PowersetValue value) =>
-      _abstractValueDomain.getExactClass(value._abstractValue);
+  AbstractBool isLateSentinel(covariant PowersetValue value) =>
+      AbstractBool.strengthen(
+          _powersetBitsDomain.isLateSentinel(value._powersetBits),
+          _abstractValueDomain.isLateSentinel(value._abstractValue));
 
   @override
-  AbstractBool isExactOrNull(covariant PowersetValue value) =>
-      AbstractBool.strengthen(
-          _powersetBitsDomain.isExactOrNull(value._powersetBits),
-          _abstractValueDomain.isExactOrNull(value._abstractValue));
+  ClassEntity getExactClass(covariant PowersetValue value) =>
+      _abstractValueDomain.getExactClass(value._abstractValue);
 
   @override
   AbstractBool isExact(covariant PowersetValue value) =>
@@ -619,6 +616,24 @@
   }
 
   @override
+  AbstractValue includeLateSentinel(covariant PowersetValue value) {
+    int powersetBits =
+        _powersetBitsDomain.includeLateSentinel(value._powersetBits);
+    AbstractValue abstractValue =
+        _abstractValueDomain.includeLateSentinel(value._abstractValue);
+    return PowersetValue(abstractValue, powersetBits);
+  }
+
+  @override
+  AbstractValue excludeLateSentinel(covariant PowersetValue value) {
+    int powersetBits =
+        _powersetBitsDomain.excludeLateSentinel(value._powersetBits);
+    AbstractValue abstractValue =
+        _abstractValueDomain.excludeLateSentinel(value._abstractValue);
+    return PowersetValue(abstractValue, powersetBits);
+  }
+
+  @override
   AbstractBool couldBeTypedArray(covariant PowersetValue value) =>
       AbstractBool.strengthen(
           _powersetBitsDomain.couldBeTypedArray(value._powersetBits),
@@ -747,6 +762,11 @@
       _abstractValueDomain.nonNullType, _powersetBitsDomain.nonNullType);
 
   @override
+  AbstractValue get lateSentinelType => PowersetValue(
+      _abstractValueDomain.lateSentinelType,
+      _powersetBitsDomain.lateSentinelType);
+
+  @override
   AbstractValue get mapType =>
       PowersetValue(_abstractValueDomain.mapType, _powersetBitsDomain.mapType);
 
diff --git a/pkg/compiler/lib/src/inferrer/powersets/wrapped.dart b/pkg/compiler/lib/src/inferrer/powersets/wrapped.dart
index d60a060..aa3679c 100644
--- a/pkg/compiler/lib/src/inferrer/powersets/wrapped.dart
+++ b/pkg/compiler/lib/src/inferrer/powersets/wrapped.dart
@@ -49,6 +49,10 @@
   const WrappedAbstractValueDomain(this._abstractValueDomain);
 
   @override
+  AbstractValue get internalTopType =>
+      WrappedAbstractValue(_abstractValueDomain.internalTopType);
+
+  @override
   AbstractValue get dynamicType =>
       WrappedAbstractValue(_abstractValueDomain.dynamicType);
 
@@ -383,10 +387,6 @@
       _abstractValueDomain.isIndexablePrimitive(value._abstractValue);
 
   @override
-  AbstractBool isPrimitiveArray(covariant WrappedAbstractValue value) =>
-      _abstractValueDomain.isPrimitiveArray(value._abstractValue);
-
-  @override
   AbstractBool isPrimitiveBoolean(covariant WrappedAbstractValue value) =>
       _abstractValueDomain.isPrimitiveBoolean(value._abstractValue);
 
@@ -403,12 +403,12 @@
       _abstractValueDomain.isNull(value._abstractValue);
 
   @override
-  ClassEntity getExactClass(covariant WrappedAbstractValue value) =>
-      _abstractValueDomain.getExactClass(value._abstractValue);
+  AbstractBool isLateSentinel(covariant WrappedAbstractValue value) =>
+      _abstractValueDomain.isLateSentinel(value._abstractValue);
 
   @override
-  AbstractBool isExactOrNull(covariant WrappedAbstractValue value) =>
-      _abstractValueDomain.isExactOrNull(value._abstractValue);
+  ClassEntity getExactClass(covariant WrappedAbstractValue value) =>
+      _abstractValueDomain.getExactClass(value._abstractValue);
 
   @override
   AbstractBool isExact(covariant WrappedAbstractValue value) =>
@@ -449,6 +449,16 @@
           _abstractValueDomain.excludeNull(value._abstractValue));
 
   @override
+  AbstractValue includeLateSentinel(covariant WrappedAbstractValue value) =>
+      WrappedAbstractValue(
+          _abstractValueDomain.includeLateSentinel(value._abstractValue));
+
+  @override
+  AbstractValue excludeLateSentinel(covariant WrappedAbstractValue value) =>
+      WrappedAbstractValue(
+          _abstractValueDomain.excludeLateSentinel(value._abstractValue));
+
+  @override
   AbstractBool couldBeTypedArray(covariant WrappedAbstractValue value) =>
       _abstractValueDomain.couldBeTypedArray(value._abstractValue);
 
@@ -546,6 +556,10 @@
       WrappedAbstractValue(_abstractValueDomain.nonNullType);
 
   @override
+  AbstractValue get lateSentinelType =>
+      WrappedAbstractValue(_abstractValueDomain.lateSentinelType);
+
+  @override
   AbstractValue get mapType =>
       WrappedAbstractValue(_abstractValueDomain.mapType);
 
diff --git a/pkg/compiler/lib/src/inferrer/trivial.dart b/pkg/compiler/lib/src/inferrer/trivial.dart
index 27e4f12b..3f6fe3a 100644
--- a/pkg/compiler/lib/src/inferrer/trivial.dart
+++ b/pkg/compiler/lib/src/inferrer/trivial.dart
@@ -25,6 +25,9 @@
   const TrivialAbstractValueDomain();
 
   @override
+  AbstractValue get internalTopType => const TrivialAbstractValue();
+
+  @override
   AbstractValue get dynamicType => const TrivialAbstractValue();
 
   @override
@@ -275,9 +278,6 @@
   AbstractBool isIndexablePrimitive(AbstractValue value) => AbstractBool.Maybe;
 
   @override
-  AbstractBool isPrimitiveArray(AbstractValue value) => AbstractBool.Maybe;
-
-  @override
   AbstractBool isPrimitiveBoolean(AbstractValue value) => AbstractBool.Maybe;
 
   @override
@@ -290,10 +290,10 @@
   AbstractBool isNull(AbstractValue value) => AbstractBool.Maybe;
 
   @override
-  ClassEntity getExactClass(AbstractValue value) => null;
+  AbstractBool isLateSentinel(AbstractValue value) => AbstractBool.Maybe;
 
   @override
-  AbstractBool isExactOrNull(AbstractValue value) => AbstractBool.Maybe;
+  ClassEntity getExactClass(AbstractValue value) => null;
 
   @override
   AbstractBool isExact(AbstractValue value) => AbstractBool.Maybe;
@@ -326,6 +326,14 @@
       const TrivialAbstractValue();
 
   @override
+  AbstractValue includeLateSentinel(AbstractValue value) =>
+      const TrivialAbstractValue();
+
+  @override
+  AbstractValue excludeLateSentinel(AbstractValue value) =>
+      const TrivialAbstractValue();
+
+  @override
   AbstractBool couldBeTypedArray(AbstractValue value) => AbstractBool.Maybe;
 
   @override
@@ -405,6 +413,9 @@
   AbstractValue get nonNullType => const TrivialAbstractValue();
 
   @override
+  AbstractValue get lateSentinelType => const TrivialAbstractValue();
+
+  @override
   AbstractValue get mapType => const TrivialAbstractValue();
 
   @override
diff --git a/pkg/compiler/lib/src/inferrer/type_graph_nodes.dart b/pkg/compiler/lib/src/inferrer/type_graph_nodes.dart
index e4f2604..b388116 100644
--- a/pkg/compiler/lib/src/inferrer/type_graph_nodes.dart
+++ b/pkg/compiler/lib/src/inferrer/type_graph_nodes.dart
@@ -466,6 +466,18 @@
               inferrer.closedWorld.nativeData.getNativeMethodBehavior(function))
           .type;
     }
+
+    if (inferrer.commonElements.isIsJsSentinel(function)) {
+      giveUp(inferrer);
+      return inferrer.abstractValueDomain.boolType;
+    }
+
+    if (inferrer.commonElements.isCreateSentinel(function) ||
+        inferrer.commonElements.isCreateJsSentinel(function)) {
+      giveUp(inferrer);
+      return inferrer.abstractValueDomain.lateSentinelType;
+    }
+
     return null;
   }
 
@@ -2276,9 +2288,11 @@
     if (isNullable) {
       otherType = abstractValueDomain.includeNull(otherType);
     }
-    return type == null
-        ? otherType
-        : abstractValueDomain.intersection(type, otherType);
+    if (type == null) return otherType;
+    AbstractValue newType = abstractValueDomain.intersection(type, otherType);
+    return abstractValueDomain.isLateSentinel(type).isPotentiallyTrue
+        ? abstractValueDomain.includeLateSentinel(newType)
+        : newType;
   }
 
   // TODO(joshualitt): FutureOrType, TypeVariableType, and FunctionTypeVariable
diff --git a/pkg/compiler/lib/src/inferrer/type_system.dart b/pkg/compiler/lib/src/inferrer/type_system.dart
index 85f2de5..33bf709 100644
--- a/pkg/compiler/lib/src/inferrer/type_system.dart
+++ b/pkg/compiler/lib/src/inferrer/type_system.dart
@@ -278,6 +278,10 @@
         getConcreteTypeFor(_abstractValueDomain.asyncStarStreamType);
   }
 
+  TypeInformation _lateSentinelType;
+  TypeInformation get lateSentinelType => _lateSentinelType ??=
+      getConcreteTypeFor(_abstractValueDomain.lateSentinelType);
+
   TypeInformation nonNullEmptyType;
 
   TypeInformation stringLiteralType(String value) {
@@ -337,8 +341,13 @@
   ///
   /// If [excludeNull] is true, the intersection excludes `null` even if the
   /// Dart type implies `null`.
+  ///
+  /// [narrowType] will not exclude the late sentinel value by default, only if
+  /// [excludeLateSentinel] is `true`.
   TypeInformation narrowType(TypeInformation type, DartType annotation,
-      {bool isCast: true, bool excludeNull: false}) {
+      {bool isCast: true,
+      bool excludeNull: false,
+      bool excludeLateSentinel: false}) {
     // Avoid refining an input with an exact type. It we are almost always
     // adding a narrowing to a subtype of the same class or a superclass.
     if (_abstractValueDomain.isExact(type.type).isDefinitelyTrue) return type;
@@ -351,6 +360,9 @@
     if (excludeNull) {
       abstractValue = _abstractValueDomain.excludeNull(abstractValue);
     }
+    if (!excludeLateSentinel) {
+      abstractValue = _abstractValueDomain.includeLateSentinel(abstractValue);
+    }
 
     if (_abstractValueDomain.containsAll(abstractValue).isPotentiallyTrue) {
       // Top, or non-nullable Top.
@@ -643,25 +655,29 @@
   }
 
   AbstractValue joinTypeMasks(Iterable<AbstractValue> masks) {
-    var dynamicType = _abstractValueDomain.dynamicType;
+    var topType = _abstractValueDomain.internalTopType;
     // Optimization: we are iterating over masks twice, but because `masks` is a
     // mapped iterable, we save the intermediate results to avoid computing them
     // again.
     var list = [];
-    bool isDynamicIngoringNull = false;
+    bool isTopIgnoringFlags = false;
     bool mayBeNull = false;
+    bool mayBeLateSentinel = false;
     for (AbstractValue mask in masks) {
       // Don't do any work on computing unions if we know that after all that
       // work the result will be `dynamic`.
-      // TODO(sigmund): change to `mask == dynamicType` so we can continue to
-      // track the non-nullable bit.
+      // TODO(sigmund): change to `mask == internalTopType` so we can continue
+      // to track the non-nullable and late sentinel bits.
       if (_abstractValueDomain.containsAll(mask).isPotentiallyTrue) {
-        isDynamicIngoringNull = true;
+        isTopIgnoringFlags = true;
       }
       if (_abstractValueDomain.isNull(mask).isPotentiallyTrue) {
         mayBeNull = true;
       }
-      if (isDynamicIngoringNull && mayBeNull) return dynamicType;
+      if (_abstractValueDomain.isLateSentinel(mask).isPotentiallyTrue) {
+        mayBeLateSentinel = true;
+      }
+      if (isTopIgnoringFlags && mayBeNull && mayBeLateSentinel) return topType;
       list.add(mask);
     }
 
@@ -671,12 +687,15 @@
           newType == null ? mask : _abstractValueDomain.union(newType, mask);
       // Likewise - stop early if we already reach dynamic.
       if (_abstractValueDomain.containsAll(newType).isPotentiallyTrue) {
-        isDynamicIngoringNull = true;
+        isTopIgnoringFlags = true;
       }
       if (_abstractValueDomain.isNull(newType).isPotentiallyTrue) {
         mayBeNull = true;
       }
-      if (isDynamicIngoringNull && mayBeNull) return dynamicType;
+      if (_abstractValueDomain.isLateSentinel(newType).isPotentiallyTrue) {
+        mayBeLateSentinel = true;
+      }
+      if (isTopIgnoringFlags && mayBeNull && mayBeLateSentinel) return topType;
     }
 
     return newType ?? _abstractValueDomain.emptyType;
diff --git a/pkg/compiler/lib/src/inferrer/typemasks/constants.dart b/pkg/compiler/lib/src/inferrer/typemasks/constants.dart
index b562b3c..4c92f07 100644
--- a/pkg/compiler/lib/src/inferrer/typemasks/constants.dart
+++ b/pkg/compiler/lib/src/inferrer/typemasks/constants.dart
@@ -57,7 +57,7 @@
   @override
   TypeMask visitLateSentinel(
           LateSentinelConstantValue constant, JClosedWorld closedWorld) =>
-      _abstractValueDomain.dynamicType;
+      _abstractValueDomain.lateSentinelType;
 
   @override
   TypeMask visitUnreachable(
diff --git a/pkg/compiler/lib/src/inferrer/typemasks/container_type_mask.dart b/pkg/compiler/lib/src/inferrer/typemasks/container_type_mask.dart
index 188a951..ab14e11 100644
--- a/pkg/compiler/lib/src/inferrer/typemasks/container_type_mask.dart
+++ b/pkg/compiler/lib/src/inferrer/typemasks/container_type_mask.dart
@@ -27,20 +27,20 @@
   // The length of the container.
   final int length;
 
-  ContainerTypeMask(this.forwardTo, this.allocationNode, this.allocationElement,
-      this.elementType, this.length);
+  const ContainerTypeMask(this.forwardTo, this.allocationNode,
+      this.allocationElement, this.elementType, this.length);
 
   /// Deserializes a [ContainerTypeMask] object from [source].
   factory ContainerTypeMask.readFromDataSource(
       DataSource source, CommonMasks domain) {
     source.begin(tag);
-    TypeMask forwardTo = new TypeMask.readFromDataSource(source, domain);
+    TypeMask forwardTo = TypeMask.readFromDataSource(source, domain);
     ir.TreeNode allocationNode = source.readTreeNodeOrNull();
     MemberEntity allocationElement = source.readMemberOrNull();
-    TypeMask elementType = new TypeMask.readFromDataSource(source, domain);
+    TypeMask elementType = TypeMask.readFromDataSource(source, domain);
     int length = source.readIntOrNull();
     source.end(tag);
-    return new ContainerTypeMask(
+    return ContainerTypeMask(
         forwardTo, allocationNode, allocationElement, elementType, length);
   }
 
@@ -58,19 +58,20 @@
   }
 
   @override
-  TypeMask nullable() {
-    return isNullable
-        ? this
-        : new ContainerTypeMask(forwardTo.nullable(), allocationNode,
-            allocationElement, elementType, length);
-  }
-
-  @override
-  TypeMask nonNullable() {
-    return isNullable
-        ? new ContainerTypeMask(forwardTo.nonNullable(), allocationNode,
-            allocationElement, elementType, length)
-        : this;
+  ContainerTypeMask withFlags({bool isNullable, bool hasLateSentinel}) {
+    isNullable ??= this.isNullable;
+    hasLateSentinel ??= this.hasLateSentinel;
+    if (isNullable == this.isNullable &&
+        hasLateSentinel == this.hasLateSentinel) {
+      return this;
+    }
+    return ContainerTypeMask(
+        forwardTo.withFlags(
+            isNullable: isNullable, hasLateSentinel: hasLateSentinel),
+        allocationNode,
+        allocationElement,
+        elementType,
+        length);
   }
 
   @override
@@ -79,36 +80,17 @@
   bool get isExact => true;
 
   @override
-  bool equalsDisregardNull(other) {
-    if (other is! ContainerTypeMask) return false;
-    return super.equalsDisregardNull(other) &&
-        allocationNode == other.allocationNode &&
-        elementType == other.elementType &&
-        length == other.length;
-  }
-
-  @override
-  TypeMask intersection(TypeMask other, CommonMasks domain) {
-    TypeMask forwardIntersection = forwardTo.intersection(other, domain);
-    if (forwardIntersection.isEmptyOrNull) return forwardIntersection;
-    return forwardIntersection.isNullable ? nullable() : nonNullable();
-  }
-
-  @override
-  TypeMask union(dynamic other, CommonMasks domain) {
-    if (this == other) {
-      return this;
-    } else if (equalsDisregardNull(other)) {
-      return other.isNullable ? other : this;
-    } else if (other.isEmptyOrNull) {
-      return other.isNullable ? this.nullable() : this;
-    } else if (other.isContainer &&
+  TypeMask _unionSpecialCases(TypeMask other, CommonMasks domain,
+      {bool isNullable, bool hasLateSentinel}) {
+    assert(isNullable != null);
+    assert(hasLateSentinel != null);
+    if (other is ContainerTypeMask &&
         elementType != null &&
         other.elementType != null) {
       TypeMask newElementType = elementType.union(other.elementType, domain);
       int newLength = (length == other.length) ? length : null;
       TypeMask newForwardTo = forwardTo.union(other.forwardTo, domain);
-      return new ContainerTypeMask(
+      return ContainerTypeMask(
           newForwardTo,
           allocationNode == other.allocationNode ? allocationNode : null,
           allocationElement == other.allocationElement
@@ -116,19 +98,22 @@
               : null,
           newElementType,
           newLength);
-    } else {
-      return forwardTo.union(other, domain);
     }
+    return null;
   }
 
   @override
-  bool operator ==(other) => super == other;
+  bool operator ==(other) {
+    if (identical(this, other)) return true;
+    if (other is! ContainerTypeMask) return false;
+    return super == other &&
+        elementType == other.elementType &&
+        length == other.length;
+  }
 
   @override
-  int get hashCode {
-    return computeHashCode(
-        allocationNode, isNullable, elementType, length, forwardTo);
-  }
+  int get hashCode => Hashing.objectHash(
+      length, Hashing.objectHash(elementType, super.hashCode));
 
   @override
   String toString() {
diff --git a/pkg/compiler/lib/src/inferrer/typemasks/dictionary_type_mask.dart b/pkg/compiler/lib/src/inferrer/typemasks/dictionary_type_mask.dart
index cc54437..00eece2 100644
--- a/pkg/compiler/lib/src/inferrer/typemasks/dictionary_type_mask.dart
+++ b/pkg/compiler/lib/src/inferrer/typemasks/dictionary_type_mask.dart
@@ -16,9 +16,9 @@
   static const String tag = 'dictionary-type-mask';
 
   // The underlying key/value map of this dictionary.
-  final Map<String, AbstractValue> _typeMap;
+  final Map<String, TypeMask> _typeMap;
 
-  DictionaryTypeMask(
+  const DictionaryTypeMask(
       TypeMask forwardTo,
       ir.Node allocationNode,
       MemberEntity allocationElement,
@@ -31,15 +31,15 @@
   factory DictionaryTypeMask.readFromDataSource(
       DataSource source, CommonMasks domain) {
     source.begin(tag);
-    TypeMask forwardTo = new TypeMask.readFromDataSource(source, domain);
+    TypeMask forwardTo = TypeMask.readFromDataSource(source, domain);
     ir.TreeNode allocationNode = source.readTreeNodeOrNull();
     MemberEntity allocationElement = source.readMemberOrNull();
-    TypeMask keyType = new TypeMask.readFromDataSource(source, domain);
-    TypeMask valueType = new TypeMask.readFromDataSource(source, domain);
-    Map<String, AbstractValue> typeMap = source
-        .readStringMap(() => new TypeMask.readFromDataSource(source, domain));
+    TypeMask keyType = TypeMask.readFromDataSource(source, domain);
+    TypeMask valueType = TypeMask.readFromDataSource(source, domain);
+    Map<String, TypeMask> typeMap =
+        source.readStringMap(() => TypeMask.readFromDataSource(source, domain));
     source.end(tag);
-    return new DictionaryTypeMask(forwardTo, allocationNode, allocationElement,
+    return DictionaryTypeMask(forwardTo, allocationNode, allocationElement,
         keyType, valueType, typeMap);
   }
 
@@ -53,27 +53,28 @@
     sink.writeMemberOrNull(allocationElement);
     keyType.writeToDataSink(sink);
     valueType.writeToDataSink(sink);
-    sink.writeStringMap(_typeMap, (AbstractValue value) {
-      TypeMask typeMask = value;
+    sink.writeStringMap(_typeMap, (TypeMask typeMask) {
       typeMask.writeToDataSink(sink);
     });
     sink.end(tag);
   }
 
   @override
-  TypeMask nullable() {
-    return isNullable
-        ? this
-        : new DictionaryTypeMask(forwardTo.nullable(), allocationNode,
-            allocationElement, keyType, valueType, _typeMap);
-  }
-
-  @override
-  TypeMask nonNullable() {
-    return isNullable
-        ? new DictionaryTypeMask(forwardTo.nonNullable(), allocationNode,
-            allocationElement, keyType, valueType, _typeMap)
-        : this;
+  DictionaryTypeMask withFlags({bool isNullable, bool hasLateSentinel}) {
+    isNullable ??= this.isNullable;
+    hasLateSentinel ??= this.hasLateSentinel;
+    if (isNullable == this.isNullable &&
+        hasLateSentinel == this.hasLateSentinel) {
+      return this;
+    }
+    return DictionaryTypeMask(
+        forwardTo.withFlags(
+            isNullable: isNullable, hasLateSentinel: hasLateSentinel),
+        allocationNode,
+        allocationElement,
+        keyType,
+        valueType,
+        _typeMap);
   }
 
   @override
@@ -86,37 +87,16 @@
   TypeMask getValueForKey(String key) => _typeMap[key];
 
   @override
-  bool equalsDisregardNull(other) {
-    if (other is! DictionaryTypeMask) return false;
-    return allocationNode == other.allocationNode &&
-        keyType == other.keyType &&
-        valueType == other.valueType &&
-        _typeMap.keys.every((k) => other._typeMap.containsKey(k)) &&
-        other._typeMap.keys.every(
-            (k) => _typeMap.containsKey(k) && _typeMap[k] == other._typeMap[k]);
-  }
-
-  @override
-  TypeMask intersection(TypeMask other, CommonMasks domain) {
-    TypeMask forwardIntersection = forwardTo.intersection(other, domain);
-    if (forwardIntersection.isEmptyOrNull) return forwardIntersection;
-    return forwardIntersection.isNullable ? nullable() : nonNullable();
-  }
-
-  @override
-  TypeMask union(dynamic other, CommonMasks domain) {
-    if (this == other) {
-      return this;
-    } else if (equalsDisregardNull(other)) {
-      return other.isNullable ? other : this;
-    } else if (other.isEmptyOrNull) {
-      return other.isNullable ? this.nullable() : this;
-    } else if (other.isDictionary) {
+  TypeMask _unionSpecialCases(TypeMask other, CommonMasks domain,
+      {bool isNullable, bool hasLateSentinel}) {
+    assert(isNullable != null);
+    assert(hasLateSentinel != null);
+    if (other is DictionaryTypeMask) {
       TypeMask newForwardTo = forwardTo.union(other.forwardTo, domain);
       TypeMask newKeyType = keyType.union(other.keyType, domain);
       TypeMask newValueType = valueType.union(other.valueType, domain);
-      Map<String, TypeMask> mappings = <String, TypeMask>{};
-      _typeMap.forEach((k, dynamic v) {
+      Map<String, TypeMask> mappings = {};
+      _typeMap.forEach((k, v) {
         if (!other._typeMap.containsKey(k)) {
           mappings[k] = v.nullable();
         }
@@ -128,28 +108,32 @@
           mappings[k] = v.nullable();
         }
       });
-      return new DictionaryTypeMask(
+      return DictionaryTypeMask(
           newForwardTo, null, null, newKeyType, newValueType, mappings);
-    } else if (other.isMap &&
+    }
+    if (other is MapTypeMask &&
         (other.keyType != null) &&
         (other.valueType != null)) {
       TypeMask newForwardTo = forwardTo.union(other.forwardTo, domain);
       TypeMask newKeyType = keyType.union(other.keyType, domain);
       TypeMask newValueType = valueType.union(other.valueType, domain);
-      return new MapTypeMask(
-          newForwardTo, null, null, newKeyType, newValueType);
-    } else {
-      return forwardTo.union(other, domain);
+      return MapTypeMask(newForwardTo, null, null, newKeyType, newValueType);
     }
+    return null;
   }
 
   @override
-  bool operator ==(other) => super == other;
+  bool operator ==(other) {
+    if (identical(this, other)) return true;
+    if (other is! DictionaryTypeMask) return false;
+    return super == other &&
+        _typeMap.keys.every((k) => other._typeMap.containsKey(k)) &&
+        other._typeMap.keys.every(
+            (k) => _typeMap.containsKey(k) && _typeMap[k] == other._typeMap[k]);
+  }
 
   @override
-  int get hashCode {
-    return computeHashCode(allocationNode, isNullable, _typeMap, forwardTo);
-  }
+  int get hashCode => Hashing.objectHash(_typeMap, super.hashCode);
 
   @override
   String toString() {
diff --git a/pkg/compiler/lib/src/inferrer/typemasks/flat_type_mask.dart b/pkg/compiler/lib/src/inferrer/typemasks/flat_type_mask.dart
index 451af33..0aa5b30 100644
--- a/pkg/compiler/lib/src/inferrer/typemasks/flat_type_mask.dart
+++ b/pkg/compiler/lib/src/inferrer/typemasks/flat_type_mask.dart
@@ -4,74 +4,121 @@
 
 part of masks;
 
+enum _FlatTypeMaskKind { empty, exact, subclass, subtype }
+
 /// A flat type mask is a type mask that has been flattened to contain a
 /// base type.
-class FlatTypeMask implements TypeMask {
+class FlatTypeMask extends TypeMask {
   /// Tag used for identifying serialized [FlatTypeMask] objects in a
   /// debugging data stream.
   static const String tag = 'flat-type-mask';
 
-  static const int EMPTY = 0;
-  static const int EXACT = 1;
-  static const int SUBCLASS = 2;
-  static const int SUBTYPE = 3;
+  static const int _NULL_INDEX = 0;
+  static const int _LATE_SENTINEL_INDEX = 1;
+  static const int _USED_INDICES = 2;
+
+  static const int _NONE_MASK = 0;
+  static const int _NULL_MASK = 1 << _NULL_INDEX;
+  static const int _LATE_SENTINEL_MASK = 1 << _LATE_SENTINEL_INDEX;
+  static const int _ALL_MASK = (1 << _USED_INDICES) - 1;
 
   final ClassEntity base;
   final int flags;
 
-  factory FlatTypeMask.exact(ClassEntity base, JClosedWorld world) =>
-      FlatTypeMask._canonicalize(base, EXACT, true, world);
-  factory FlatTypeMask.subclass(ClassEntity base, JClosedWorld world) =>
-      FlatTypeMask._canonicalize(base, SUBCLASS, true, world);
-  factory FlatTypeMask.subtype(ClassEntity base, JClosedWorld world) =>
-      FlatTypeMask._canonicalize(base, SUBTYPE, true, world);
-
-  const FlatTypeMask.nonNullEmpty()
-      : base = null,
-        flags = 0;
-  const FlatTypeMask.empty()
-      : base = null,
-        flags = 1;
-
-  factory FlatTypeMask.nonNullExact(ClassEntity base, JClosedWorld world) =>
-      FlatTypeMask._canonicalize(base, EXACT, false, world);
-  factory FlatTypeMask.nonNullSubclass(ClassEntity base, JClosedWorld world) =>
-      FlatTypeMask._canonicalize(base, SUBCLASS, false, world);
-  factory FlatTypeMask.nonNullSubtype(ClassEntity base, JClosedWorld world) =>
-      FlatTypeMask._canonicalize(base, SUBTYPE, false, world);
-
-  factory FlatTypeMask._canonicalize(
-      ClassEntity base, int kind, bool isNullable, JClosedWorld world) {
-    if (base == world.commonElements.nullClass) {
-      return FlatTypeMask.empty();
-    }
-    return FlatTypeMask._(base, (kind << 1) | (isNullable ? 1 : 0));
+  static int _computeFlags(_FlatTypeMaskKind kind,
+      {bool isNullable: false, bool hasLateSentinel: false}) {
+    int mask = _NONE_MASK;
+    if (isNullable) mask |= _NULL_MASK;
+    if (hasLateSentinel) mask |= _LATE_SENTINEL_MASK;
+    return _computeFlagsRaw(kind.index, mask);
   }
 
-  FlatTypeMask._(this.base, this.flags);
+  static int _computeFlagsRaw(int kind, int mask) =>
+      kind << _USED_INDICES | mask;
+
+  static _FlatTypeMaskKind _lookupKind(int flags) =>
+      _FlatTypeMaskKind.values[flags >> _USED_INDICES];
+
+  static bool _hasNullableFlag(int flags) => flags & _NULL_MASK != _NONE_MASK;
+
+  static bool _hasLateSentinelFlag(int flags) =>
+      flags & _LATE_SENTINEL_MASK != _NONE_MASK;
+
+  factory FlatTypeMask.exact(ClassEntity base, JClosedWorld world,
+          {bool hasLateSentinel: false}) =>
+      FlatTypeMask._canonicalize(base, _FlatTypeMaskKind.exact, world,
+          isNullable: true, hasLateSentinel: hasLateSentinel);
+  factory FlatTypeMask.subclass(ClassEntity base, JClosedWorld world,
+          {bool hasLateSentinel: false}) =>
+      FlatTypeMask._canonicalize(base, _FlatTypeMaskKind.subclass, world,
+          isNullable: true, hasLateSentinel: hasLateSentinel);
+  factory FlatTypeMask.subtype(ClassEntity base, JClosedWorld world,
+          {bool hasLateSentinel: false}) =>
+      FlatTypeMask._canonicalize(base, _FlatTypeMaskKind.subtype, world,
+          isNullable: true, hasLateSentinel: hasLateSentinel);
+
+  factory FlatTypeMask.nonNullEmpty({bool hasLateSentinel: false}) =>
+      hasLateSentinel
+          ? const FlatTypeMask._(null, _LATE_SENTINEL_MASK)
+          : const FlatTypeMask._(null, _NONE_MASK);
+
+  factory FlatTypeMask.empty({bool hasLateSentinel: false}) => hasLateSentinel
+      ? const FlatTypeMask._(null, _NULL_MASK | _LATE_SENTINEL_MASK)
+      : const FlatTypeMask._(null, _NULL_MASK);
+
+  factory FlatTypeMask.nonNullExact(ClassEntity base, JClosedWorld world,
+          {bool hasLateSentinel: false}) =>
+      FlatTypeMask._canonicalize(base, _FlatTypeMaskKind.exact, world,
+          hasLateSentinel: hasLateSentinel);
+  factory FlatTypeMask.nonNullSubclass(ClassEntity base, JClosedWorld world,
+          {bool hasLateSentinel: false}) =>
+      FlatTypeMask._canonicalize(base, _FlatTypeMaskKind.subclass, world,
+          hasLateSentinel: hasLateSentinel);
+  factory FlatTypeMask.nonNullSubtype(ClassEntity base, JClosedWorld world,
+          {bool hasLateSentinel: false}) =>
+      FlatTypeMask._canonicalize(base, _FlatTypeMaskKind.subtype, world,
+          hasLateSentinel: hasLateSentinel);
+
+  factory FlatTypeMask._canonicalize(
+      ClassEntity base, _FlatTypeMaskKind kind, JClosedWorld world,
+      {bool isNullable: false, bool hasLateSentinel: false}) {
+    if (base == world.commonElements.nullClass) {
+      return FlatTypeMask.empty(hasLateSentinel: hasLateSentinel);
+    }
+    return FlatTypeMask._(
+        base,
+        _computeFlags(kind,
+            isNullable: isNullable, hasLateSentinel: hasLateSentinel));
+  }
+
+  const FlatTypeMask._(this.base, this.flags);
 
   /// Ensures that the generated mask is normalized, i.e., a call to
   /// [TypeMask.assertIsNormalized] with the factory's result returns `true`.
   factory FlatTypeMask.normalized(
       ClassEntity base, int flags, CommonMasks domain) {
+    bool isNullable = _hasNullableFlag(flags);
+    bool hasLateSentinel = _hasLateSentinelFlag(flags);
     if (base == domain.commonElements.nullClass) {
-      return FlatTypeMask.empty();
+      return FlatTypeMask.empty(hasLateSentinel: hasLateSentinel);
     }
-    if ((flags >> 1) == EMPTY || ((flags >> 1) == EXACT)) {
-      return new FlatTypeMask._(base, flags);
+    _FlatTypeMaskKind kind = _lookupKind(flags);
+    if (kind == _FlatTypeMaskKind.empty || kind == _FlatTypeMaskKind.exact) {
+      return FlatTypeMask._(base, flags);
     }
-    if ((flags >> 1) == SUBTYPE) {
+    if (kind == _FlatTypeMaskKind.subtype) {
       if (!domain._closedWorld.classHierarchy.hasAnyStrictSubtype(base) ||
           domain._closedWorld.classHierarchy.hasOnlySubclasses(base)) {
-        flags = (flags & 0x1) | (SUBCLASS << 1);
+        flags = _computeFlags(_FlatTypeMaskKind.subclass,
+            isNullable: isNullable, hasLateSentinel: hasLateSentinel);
       }
     }
-    if (((flags >> 1) == SUBCLASS) &&
+    if (kind == _FlatTypeMaskKind.subclass &&
         !domain._closedWorld.classHierarchy.hasAnyStrictSubclass(base)) {
-      flags = (flags & 0x1) | (EXACT << 1);
+      flags = _computeFlags(_FlatTypeMaskKind.exact,
+          isNullable: isNullable, hasLateSentinel: hasLateSentinel);
     }
-    return domain.getCachedMask(
-        base, flags, () => new FlatTypeMask._(base, flags));
+    return domain.getCachedMask(base, flags, () => FlatTypeMask._(base, flags));
   }
 
   /// Deserializes a [FlatTypeMask] object from [source].
@@ -81,8 +128,7 @@
     ClassEntity base = source.readClassOrNull();
     int flags = source.readInt();
     source.end(tag);
-    return domain.getCachedMask(
-        base, flags, () => new FlatTypeMask._(base, flags));
+    return domain.getCachedMask(base, flags, () => FlatTypeMask._(base, flags));
   }
 
   /// Serializes this [FlatTypeMask] to [sink].
@@ -95,20 +141,34 @@
     sink.end(tag);
   }
 
+  _FlatTypeMaskKind get _kind => _lookupKind(flags);
+
+  int get _mask => flags & _ALL_MASK;
+
   ClassQuery get _classQuery => isExact
       ? ClassQuery.EXACT
       : (isSubclass ? ClassQuery.SUBCLASS : ClassQuery.SUBTYPE);
 
   @override
-  bool get isEmpty => isEmptyOrNull && !isNullable;
+  bool get isEmpty => isEmptyOrFlagged && _mask == _NONE_MASK;
   @override
-  bool get isNull => isEmptyOrNull && isNullable;
+  bool get isNull => isEmptyOrFlagged && _mask == _NULL_MASK;
   @override
-  bool get isEmptyOrNull => (flags >> 1) == EMPTY;
+  bool get isEmptyOrFlagged => _kind == _FlatTypeMaskKind.empty;
   @override
-  bool get isExact => (flags >> 1) == EXACT;
+  bool get isExact => _kind == _FlatTypeMaskKind.exact;
   @override
-  bool get isNullable => (flags & 1) != 0;
+  bool get isNullable => _hasNullableFlag(flags);
+  @override
+  bool get hasLateSentinel => _hasLateSentinelFlag(flags);
+  @override
+  AbstractBool get isLateSentinel {
+    if (!hasLateSentinel) return AbstractBool.False;
+    if (isEmptyOrFlagged && _mask == _LATE_SENTINEL_MASK) {
+      return AbstractBool.True;
+    }
+    return AbstractBool.Maybe;
+  }
 
   @override
   bool get isUnion => false;
@@ -128,22 +188,21 @@
   // TODO(kasperl): Get rid of these. They should not be a visible
   // part of the implementation because they make it hard to add
   // proper union types if we ever want to.
-  bool get isSubclass => (flags >> 1) == SUBCLASS;
-  bool get isSubtype => (flags >> 1) == SUBTYPE;
+  bool get isSubclass => _kind == _FlatTypeMaskKind.subclass;
+  bool get isSubtype => _kind == _FlatTypeMaskKind.subtype;
 
   @override
-  TypeMask nullable() {
-    return isNullable ? this : new FlatTypeMask._(base, flags | 1);
-  }
-
-  @override
-  TypeMask nonNullable() {
-    return isNullable ? new FlatTypeMask._(base, flags & ~1) : this;
+  FlatTypeMask withFlags({bool isNullable, bool hasLateSentinel}) {
+    int newFlags = _computeFlags(_kind,
+        isNullable: isNullable ?? this.isNullable,
+        hasLateSentinel: hasLateSentinel ?? this.hasLateSentinel);
+    if (newFlags == flags) return this;
+    return FlatTypeMask._(base, newFlags);
   }
 
   @override
   bool contains(ClassEntity other, JClosedWorld closedWorld) {
-    if (isEmptyOrNull) {
+    if (isEmptyOrFlagged) {
       return false;
     } else if (identical(base, other)) {
       return true;
@@ -184,11 +243,14 @@
 
   @override
   bool isInMask(TypeMask other, JClosedWorld closedWorld) {
-    if (isEmptyOrNull) return isNullable ? other.isNullable : true;
-    // The empty type contains no classes.
-    if (other.isEmptyOrNull) return false;
     // Quick check whether to handle null.
     if (isNullable && !other.isNullable) return false;
+    if (hasLateSentinel && !other.hasLateSentinel) {
+      return false;
+    }
+    // The empty type contains no classes.
+    if (isEmptyOrFlagged) return true;
+    if (other.isEmptyOrFlagged) return false;
     other = TypeMask.nonForwardingMask(other);
     // If other is union, delegate to UnionTypeMask.containsMask.
     if (other is! FlatTypeMask) return other.containsMask(this, closedWorld);
@@ -259,15 +321,16 @@
 
   @override
   bool satisfies(ClassEntity cls, JClosedWorld closedWorld) {
-    if (isEmptyOrNull) return false;
+    if (isEmptyOrFlagged) return false;
     if (closedWorld.classHierarchy.isSubtypeOf(base, cls)) return true;
     return false;
   }
 
   @override
   ClassEntity singleClass(JClosedWorld closedWorld) {
-    if (isEmptyOrNull) return null;
+    if (isEmptyOrFlagged) return null;
     if (isNullable) return null; // It is Null and some other class.
+    if (hasLateSentinel) return null;
     if (isExact) {
       return base;
     } else if (isSubclass) {
@@ -282,7 +345,7 @@
 
   @override
   bool containsAll(JClosedWorld closedWorld) {
-    if (isEmptyOrNull || isExact) return false;
+    if (isEmptyOrFlagged || isExact) return false;
     return identical(base, closedWorld.commonElements.objectClass);
   }
 
@@ -294,10 +357,14 @@
     assert(TypeMask.assertIsNormalized(other, closedWorld));
     if (other is! FlatTypeMask) return other.union(this, domain);
     FlatTypeMask flatOther = other;
-    if (isEmptyOrNull) {
-      return isNullable ? flatOther.nullable() : flatOther;
-    } else if (flatOther.isEmptyOrNull) {
-      return flatOther.isNullable ? nullable() : this;
+    bool isNullable = this.isNullable || flatOther.isNullable;
+    bool hasLateSentinel = this.hasLateSentinel || flatOther.hasLateSentinel;
+    if (isEmptyOrFlagged) {
+      return flatOther.withFlags(
+          isNullable: isNullable, hasLateSentinel: hasLateSentinel);
+    } else if (flatOther.isEmptyOrFlagged) {
+      return withFlags(
+          isNullable: isNullable, hasLateSentinel: hasLateSentinel);
     } else if (base == flatOther.base) {
       return unionSame(flatOther, domain);
     } else if (closedWorld.classHierarchy.isSubclassOf(flatOther.base, base)) {
@@ -309,9 +376,9 @@
     } else if (closedWorld.classHierarchy.isSubtypeOf(base, flatOther.base)) {
       return flatOther.unionStrictSubtype(this, domain);
     } else {
-      return new UnionTypeMask._internal(
-          <FlatTypeMask>[this.nonNullable(), flatOther.nonNullable()],
-          isNullable || other.isNullable);
+      return UnionTypeMask._internal(
+          <FlatTypeMask>[withoutFlags(), flatOther.withoutFlags()],
+          isNullable: isNullable, hasLateSentinel: hasLateSentinel);
     }
   }
 
@@ -323,15 +390,14 @@
     // constraining kind (the highest) of the two. If either one of
     // the masks are nullable the result should be nullable too.
     // As both masks are normalized, the result will be, too.
-    int combined = (flags > other.flags)
-        ? flags | (other.flags & 1)
-        : other.flags | (flags & 1);
+    int combined =
+        (flags > other.flags) ? flags | other._mask : other.flags | _mask;
     if (flags == combined) {
       return this;
     } else if (other.flags == combined) {
       return other;
     } else {
-      return new FlatTypeMask.normalized(base, combined, domain);
+      return FlatTypeMask.normalized(base, combined, domain);
     }
   }
 
@@ -346,19 +412,19 @@
       // Since the other mask is a subclass of this mask, we need the
       // resulting union to be a subclass too. If either one of the
       // masks are nullable the result should be nullable too.
-      combined = (SUBCLASS << 1) | ((flags | other.flags) & 1);
+      combined = _computeFlagsRaw(
+          _FlatTypeMaskKind.subclass.index, _mask | other._mask);
     } else {
       // Both masks are at least subclass masks, so we pick the least
       // constraining kind (the highest) of the two. If either one of
       // the masks are nullable the result should be nullable too.
-      combined = (flags > other.flags)
-          ? flags | (other.flags & 1)
-          : other.flags | (flags & 1);
+      combined =
+          (flags > other.flags) ? flags | other._mask : other.flags | _mask;
     }
     // If we weaken the constraint on this type, we have to make sure that
     // the result is normalized.
-    return (flags != combined)
-        ? new FlatTypeMask.normalized(base, combined, domain)
+    return flags != combined
+        ? FlatTypeMask.normalized(base, combined, domain)
         : this;
   }
 
@@ -371,11 +437,12 @@
     // Since the other mask is a subtype of this mask, we need the
     // resulting union to be a subtype too. If either one of the masks
     // are nullable the result should be nullable too.
-    int combined = (SUBTYPE << 1) | ((flags | other.flags) & 1);
+    int combined =
+        _computeFlagsRaw(_FlatTypeMaskKind.subtype.index, _mask | other._mask);
     // We know there is at least one subtype, [other.base], so no need
     // to normalize.
-    return (flags != combined)
-        ? new FlatTypeMask.normalized(base, combined, domain)
+    return flags != combined
+        ? FlatTypeMask.normalized(base, combined, domain)
         : this;
   }
 
@@ -390,11 +457,14 @@
     ClassEntity otherBase = flatOther.base;
 
     bool includeNull = isNullable && flatOther.isNullable;
+    bool includeLateSentinel = hasLateSentinel && flatOther.hasLateSentinel;
 
-    if (isEmptyOrNull) {
-      return includeNull ? this : nonNullable();
-    } else if (flatOther.isEmptyOrNull) {
-      return includeNull ? other : other.nonNullable();
+    if (isEmptyOrFlagged) {
+      return withFlags(
+          isNullable: includeNull, hasLateSentinel: includeLateSentinel);
+    } else if (flatOther.isEmptyOrFlagged) {
+      return other.withFlags(
+          isNullable: includeNull, hasLateSentinel: includeLateSentinel);
     }
 
     SubclassResult result = domain._closedWorld.classHierarchy
@@ -402,43 +472,58 @@
 
     switch (result.kind) {
       case SubclassResultKind.EMPTY:
-        return includeNull ? domain.nullType : domain.emptyType;
+        return includeNull
+            ? TypeMask.empty(hasLateSentinel: includeLateSentinel)
+            : TypeMask.nonNullEmpty(hasLateSentinel: includeLateSentinel);
       case SubclassResultKind.EXACT1:
         assert(isExact);
-        return includeNull ? this : nonNullable();
+        return withFlags(
+            isNullable: includeNull, hasLateSentinel: includeLateSentinel);
       case SubclassResultKind.EXACT2:
         assert(other.isExact);
-        return includeNull ? other : other.nonNullable();
+        return other.withFlags(
+            isNullable: includeNull, hasLateSentinel: includeLateSentinel);
       case SubclassResultKind.SUBCLASS1:
         assert(isSubclass);
-        return includeNull ? this : nonNullable();
+        return withFlags(
+            isNullable: includeNull, hasLateSentinel: includeLateSentinel);
       case SubclassResultKind.SUBCLASS2:
         assert(flatOther.isSubclass);
-        return includeNull ? other : other.nonNullable();
+        return other.withFlags(
+            isNullable: includeNull, hasLateSentinel: includeLateSentinel);
       case SubclassResultKind.SUBTYPE1:
         assert(isSubtype);
-        return includeNull ? this : nonNullable();
+        return withFlags(
+            isNullable: includeNull, hasLateSentinel: includeLateSentinel);
       case SubclassResultKind.SUBTYPE2:
         assert(flatOther.isSubtype);
-        return includeNull ? other : other.nonNullable();
+        return other.withFlags(
+            isNullable: includeNull, hasLateSentinel: includeLateSentinel);
       case SubclassResultKind.SET:
       default:
         if (result.classes.isEmpty) {
-          return includeNull ? domain.nullType : domain.emptyType;
+          return includeNull
+              ? TypeMask.empty(hasLateSentinel: includeLateSentinel)
+              : TypeMask.nonNullEmpty(hasLateSentinel: includeLateSentinel);
         } else if (result.classes.length == 1) {
           ClassEntity cls = result.classes.first;
           return includeNull
-              ? new TypeMask.subclass(cls, domain._closedWorld)
-              : new TypeMask.nonNullSubclass(cls, domain._closedWorld);
+              ? TypeMask.subclass(cls, domain._closedWorld,
+                  hasLateSentinel: includeLateSentinel)
+              : TypeMask.nonNullSubclass(cls, domain._closedWorld,
+                  hasLateSentinel: includeLateSentinel);
         }
 
         List<FlatTypeMask> masks = List.from(result.classes.map(
             (ClassEntity cls) =>
                 TypeMask.nonNullSubclass(cls, domain._closedWorld)));
         if (masks.length > UnionTypeMask.MAX_UNION_LENGTH) {
-          return UnionTypeMask.flatten(masks, includeNull, domain);
+          return UnionTypeMask.flatten(masks, domain,
+              includeNull: includeNull,
+              includeLateSentinel: includeLateSentinel);
         }
-        return new UnionTypeMask._internal(masks, includeNull);
+        return UnionTypeMask._internal(masks,
+            isNullable: includeNull, hasLateSentinel: includeLateSentinel);
     }
   }
 
@@ -448,7 +533,8 @@
     FlatTypeMask flatOther = other;
 
     if (isNullable && flatOther.isNullable) return false;
-    if (isEmptyOrNull || flatOther.isEmptyOrNull) return true;
+    if (hasLateSentinel && flatOther.hasLateSentinel) return false;
+    if (isEmptyOrFlagged || flatOther.isEmptyOrFlagged) return true;
     if (base == flatOther.base) return false;
     if (isExact && flatOther.isExact) return true;
 
@@ -495,14 +581,14 @@
     // are nullable, will the result be nullable too.
     // The result will be normalized, as the two inputs are normalized, too.
     int combined = (flags < other.flags)
-        ? flags & ((other.flags & 1) | ~1)
-        : other.flags & ((flags & 1) | ~1);
+        ? flags & (other._mask | ~_ALL_MASK)
+        : other.flags & (_mask | ~_ALL_MASK);
     if (flags == combined) {
       return this;
     } else if (other.flags == combined) {
       return other;
     } else {
-      return new FlatTypeMask.normalized(base, combined, domain);
+      return FlatTypeMask.normalized(base, combined, domain);
     }
   }
 
@@ -517,28 +603,29 @@
     // masks are nullable, will the result be nullable too.
     // The result is guaranteed to be normalized, as the other type
     // was normalized.
-    int combined = other.flags & ((flags & 1) | ~1);
+    int combined = other.flags & (_mask | ~_ALL_MASK);
     if (other.flags == combined) {
       return other;
     } else {
-      return new FlatTypeMask.normalized(other.base, combined, domain);
+      return FlatTypeMask.normalized(other.base, combined, domain);
     }
   }
 
   TypeMask intersectionEmpty(FlatTypeMask other) {
-    return isNullable && other.isNullable
-        ? new TypeMask.empty()
-        : new TypeMask.nonNullEmpty();
+    bool isNullable = this.isNullable && other.isNullable;
+    bool hasLateSentinel = this.hasLateSentinel && other.hasLateSentinel;
+    return isNullable
+        ? TypeMask.empty(hasLateSentinel: hasLateSentinel)
+        : TypeMask.nonNullEmpty(hasLateSentinel: hasLateSentinel);
   }
 
   @override
   bool canHit(MemberEntity element, Name name, JClosedWorld closedWorld) {
     CommonElements commonElements = closedWorld.commonElements;
     assert(element.name == name.text);
-    if (isEmpty) return false;
-    if (isNull) {
-      return closedWorld.hasElementIn(
-          commonElements.jsNullClass, name, element);
+    if (isEmptyOrFlagged) {
+      return isNullable &&
+          closedWorld.hasElementIn(commonElements.jsNullClass, name, element);
     }
 
     ClassEntity other = element.enclosingClass;
@@ -572,7 +659,7 @@
       Selector selector, covariant JClosedWorld closedWorld) {
     // A call on an empty type mask is either dead code, or a call on
     // `null`.
-    if (isEmptyOrNull) return false;
+    if (isEmptyOrFlagged) return false;
     // A call on an exact mask for an abstract class is dead code.
     // TODO(johnniwinther): A type mask cannot be abstract. Remove the need
     // for this noise (currently used for super-calls in inference and mirror
@@ -584,7 +671,7 @@
 
   @override
   MemberEntity locateSingleMember(Selector selector, CommonMasks domain) {
-    if (isEmptyOrNull) return null;
+    if (isEmptyOrFlagged) return null;
     JClosedWorld closedWorld = domain._closedWorld;
     if (closedWorld.includesClosureCallInDomain(selector, this, domain))
       return null;
@@ -628,13 +715,16 @@
 
   @override
   String toString() {
-    if (isEmptyOrNull) return isNullable ? '[null]' : '[empty]';
-    StringBuffer buffer = new StringBuffer();
-    if (isNullable) buffer.write('null|');
-    if (isExact) buffer.write('exact=');
-    if (isSubclass) buffer.write('subclass=');
-    if (isSubtype) buffer.write('subtype=');
-    buffer.write(base.name);
-    return "[$buffer]";
+    StringBuffer buffer = StringBuffer('[');
+    buffer.writeAll([
+      if (isEmpty) 'empty',
+      if (isNullable) 'null',
+      if (hasLateSentinel) 'sentinel',
+      if (isExact) 'exact=${base.name}',
+      if (isSubclass) 'subclass=${base.name}',
+      if (isSubtype) 'subtype=${base.name}',
+    ], '|');
+    buffer.write(']');
+    return buffer.toString();
   }
 }
diff --git a/pkg/compiler/lib/src/inferrer/typemasks/forwarding_type_mask.dart b/pkg/compiler/lib/src/inferrer/typemasks/forwarding_type_mask.dart
index ec29cbe..213c62a 100644
--- a/pkg/compiler/lib/src/inferrer/typemasks/forwarding_type_mask.dart
+++ b/pkg/compiler/lib/src/inferrer/typemasks/forwarding_type_mask.dart
@@ -4,15 +4,15 @@
 
 part of masks;
 
-/// A type mask that wraps an other one, and delegate all its
+/// A type mask that wraps another one, and delegates all its
 /// implementation methods to it.
-abstract class ForwardingTypeMask implements TypeMask {
+abstract class ForwardingTypeMask extends TypeMask {
   TypeMask get forwardTo;
 
-  ForwardingTypeMask();
+  const ForwardingTypeMask();
 
   @override
-  bool get isEmptyOrNull => forwardTo.isEmptyOrNull;
+  bool get isEmptyOrFlagged => forwardTo.isEmptyOrFlagged;
   @override
   bool get isEmpty => forwardTo.isEmpty;
   @override
@@ -20,6 +20,10 @@
   @override
   bool get isNull => forwardTo.isNull;
   @override
+  bool get hasLateSentinel => forwardTo.hasLateSentinel;
+  @override
+  AbstractBool get isLateSentinel => forwardTo.isLateSentinel;
+  @override
   bool get isExact => forwardTo.isExact;
 
   @override
@@ -93,17 +97,29 @@
   }
 
   @override
-  TypeMask union(other, CommonMasks domain) {
+  TypeMask union(TypeMask other, CommonMasks domain) {
     if (this == other) {
       return this;
-    } else if (equalsDisregardNull(other)) {
-      return other.isNullable ? other : this;
-    } else if (other.isEmptyOrNull) {
-      return other.isNullable ? this.nullable() : this;
     }
-    return forwardTo.union(other, domain);
+    bool isNullable = this.isNullable || other.isNullable;
+    bool hasLateSentinel = this.hasLateSentinel || other.hasLateSentinel;
+    if (isEmptyOrFlagged) {
+      return other.withFlags(
+          isNullable: isNullable, hasLateSentinel: hasLateSentinel);
+    }
+    if (other.isEmptyOrFlagged) {
+      return withFlags(
+          isNullable: isNullable, hasLateSentinel: hasLateSentinel);
+    }
+    return _unionSpecialCases(other, domain,
+            isNullable: isNullable, hasLateSentinel: hasLateSentinel) ??
+        forwardTo.union(other, domain);
   }
 
+  TypeMask _unionSpecialCases(TypeMask other, CommonMasks domain,
+          {bool isNullable, bool hasLateSentinel}) =>
+      null;
+
   @override
   bool isDisjoint(TypeMask other, JClosedWorld closedWorld) {
     return forwardTo.isDisjoint(other, closedWorld);
@@ -111,7 +127,11 @@
 
   @override
   TypeMask intersection(TypeMask other, CommonMasks domain) {
-    return forwardTo.intersection(other, domain);
+    TypeMask forwardIntersection = forwardTo.intersection(other, domain);
+    if (forwardIntersection.isEmptyOrFlagged) return forwardIntersection;
+    return withFlags(
+        isNullable: forwardIntersection.isNullable,
+        hasLateSentinel: forwardIntersection.hasLateSentinel);
   }
 
   @override
@@ -130,28 +150,33 @@
     return forwardTo.locateSingleMember(selector, domain);
   }
 
-  bool equalsDisregardNull(other) {
-    if (other is! ForwardingTypeMask) return false;
-    if (forwardTo.isNullable) {
-      return forwardTo == other.forwardTo.nullable();
-    } else {
-      return forwardTo == other.forwardTo.nonNullable();
-    }
-  }
-
   @override
   bool operator ==(other) {
-    return equalsDisregardNull(other) && isNullable == other.isNullable;
+    if (identical(this, other)) return true;
+    if (other is! ForwardingTypeMask) return false;
+    return forwardTo == other.forwardTo;
   }
 
   @override
-  int get hashCode => throw "Subclass should implement hashCode getter";
+  int get hashCode => forwardTo.hashCode;
 }
 
 abstract class AllocationTypeMask extends ForwardingTypeMask {
+  const AllocationTypeMask();
+
   // The [ir.Node] where this type mask was created.
   ir.Node get allocationNode;
 
   // The [Entity] where this type mask was created.
   MemberEntity get allocationElement;
+
+  @override
+  bool operator ==(other) {
+    if (identical(this, other)) return true;
+    if (other is! AllocationTypeMask) return false;
+    return super == other && allocationNode == other.allocationNode;
+  }
+
+  @override
+  int get hashCode => Hashing.objectHash(allocationNode, super.hashCode);
 }
diff --git a/pkg/compiler/lib/src/inferrer/typemasks/map_type_mask.dart b/pkg/compiler/lib/src/inferrer/typemasks/map_type_mask.dart
index 556be49..035fa9d 100644
--- a/pkg/compiler/lib/src/inferrer/typemasks/map_type_mask.dart
+++ b/pkg/compiler/lib/src/inferrer/typemasks/map_type_mask.dart
@@ -27,20 +27,20 @@
   // The key type of this map.
   final TypeMask keyType;
 
-  MapTypeMask(this.forwardTo, this.allocationNode, this.allocationElement,
+  const MapTypeMask(this.forwardTo, this.allocationNode, this.allocationElement,
       this.keyType, this.valueType);
 
   /// Deserializes a [MapTypeMask] object from [source].
   factory MapTypeMask.readFromDataSource(
       DataSource source, CommonMasks domain) {
     source.begin(tag);
-    TypeMask forwardTo = new TypeMask.readFromDataSource(source, domain);
+    TypeMask forwardTo = TypeMask.readFromDataSource(source, domain);
     ir.TreeNode allocationNode = source.readTreeNodeOrNull();
     MemberEntity allocationElement = source.readMemberOrNull();
-    TypeMask keyType = new TypeMask.readFromDataSource(source, domain);
-    TypeMask valueType = new TypeMask.readFromDataSource(source, domain);
+    TypeMask keyType = TypeMask.readFromDataSource(source, domain);
+    TypeMask valueType = TypeMask.readFromDataSource(source, domain);
     source.end(tag);
-    return new MapTypeMask(
+    return MapTypeMask(
         forwardTo, allocationNode, allocationElement, keyType, valueType);
   }
 
@@ -58,19 +58,20 @@
   }
 
   @override
-  TypeMask nullable() {
-    return isNullable
-        ? this
-        : new MapTypeMask(forwardTo.nullable(), allocationNode,
-            allocationElement, keyType, valueType);
-  }
-
-  @override
-  TypeMask nonNullable() {
-    return isNullable
-        ? new MapTypeMask(forwardTo.nonNullable(), allocationNode,
-            allocationElement, keyType, valueType)
-        : this;
+  MapTypeMask withFlags({bool isNullable, bool hasLateSentinel}) {
+    isNullable ??= this.isNullable;
+    hasLateSentinel ??= this.hasLateSentinel;
+    if (isNullable == this.isNullable &&
+        hasLateSentinel == this.hasLateSentinel) {
+      return this;
+    }
+    return MapTypeMask(
+        forwardTo.withFlags(
+            isNullable: isNullable, hasLateSentinel: hasLateSentinel),
+        allocationNode,
+        allocationElement,
+        keyType,
+        valueType);
   }
 
   @override
@@ -81,30 +82,11 @@
   bool get isExact => true;
 
   @override
-  bool equalsDisregardNull(other) {
-    if (other is! MapTypeMask) return false;
-    return super.equalsDisregardNull(other) &&
-        allocationNode == other.allocationNode &&
-        keyType == other.keyType &&
-        valueType == other.valueType;
-  }
-
-  @override
-  TypeMask intersection(TypeMask other, CommonMasks domain) {
-    TypeMask forwardIntersection = forwardTo.intersection(other, domain);
-    if (forwardIntersection.isEmptyOrNull) return forwardIntersection;
-    return forwardIntersection.isNullable ? nullable() : nonNullable();
-  }
-
-  @override
-  TypeMask union(dynamic other, CommonMasks domain) {
-    if (this == other) {
-      return this;
-    } else if (equalsDisregardNull(other)) {
-      return other.isNullable ? other : this;
-    } else if (other.isEmptyOrNull) {
-      return other.isNullable ? this.nullable() : this;
-    } else if (other.isMap &&
+  TypeMask _unionSpecialCases(TypeMask other, CommonMasks domain,
+      {bool isNullable, bool hasLateSentinel}) {
+    assert(isNullable != null);
+    assert(hasLateSentinel != null);
+    if (other is MapTypeMask &&
         keyType != null &&
         other.keyType != null &&
         valueType != null &&
@@ -112,19 +94,19 @@
       TypeMask newKeyType = keyType.union(other.keyType, domain);
       TypeMask newValueType = valueType.union(other.valueType, domain);
       TypeMask newForwardTo = forwardTo.union(other.forwardTo, domain);
-      return new MapTypeMask(
-          newForwardTo, null, null, newKeyType, newValueType);
-    } else if (other.isDictionary) {
+      return MapTypeMask(newForwardTo, null, null, newKeyType, newValueType);
+    }
+    if (other is DictionaryTypeMask) {
       // TODO(johnniwinther): Find another way to check this invariant that
       // doesn't need the compiler.
       assert(other.keyType ==
-          new TypeMask.nonNullExact(
+          TypeMask.nonNullExact(
               domain.commonElements.jsStringClass, domain._closedWorld));
       TypeMask newKeyType = keyType.union(other.keyType, domain);
       TypeMask newValueType =
-          other.typeMap.values.fold(keyType, (p, n) => p.union(n, domain));
+          other._typeMap.values.fold(keyType, (p, n) => p.union(n, domain));
       TypeMask newForwardTo = forwardTo.union(other.forwardTo, domain);
-      MapTypeMask newMapTypeMask = new MapTypeMask(
+      MapTypeMask newMapTypeMask = MapTypeMask(
           newForwardTo,
           allocationNode == other.allocationNode ? allocationNode : null,
           allocationElement == other.allocationElement
@@ -133,19 +115,22 @@
           newKeyType,
           newValueType);
       return newMapTypeMask;
-    } else {
-      return forwardTo.union(other, domain);
     }
+    return null;
   }
 
   @override
-  bool operator ==(other) => super == other;
+  bool operator ==(other) {
+    if (identical(this, other)) return true;
+    if (other is! MapTypeMask) return false;
+    return super == other &&
+        keyType == other.keyType &&
+        valueType == other.valueType;
+  }
 
   @override
-  int get hashCode {
-    return computeHashCode(
-        allocationNode, isNullable, keyType, valueType, forwardTo);
-  }
+  int get hashCode => Hashing.objectHash(
+      valueType, Hashing.objectHash(keyType, super.hashCode));
 
   @override
   String toString() {
diff --git a/pkg/compiler/lib/src/inferrer/typemasks/masks.dart b/pkg/compiler/lib/src/inferrer/typemasks/masks.dart
index 2ac07ca..999d317 100644
--- a/pkg/compiler/lib/src/inferrer/typemasks/masks.dart
+++ b/pkg/compiler/lib/src/inferrer/typemasks/masks.dart
@@ -45,6 +45,7 @@
   CommonElements get commonElements => _closedWorld.commonElements;
   DartTypes get dartTypes => _closedWorld.dartTypes;
 
+  TypeMask _internalTopType;
   TypeMask _dynamicType;
   TypeMask _nonNullType;
   TypeMask _nullType;
@@ -75,10 +76,10 @@
   TypeMask _unmodifiableArrayType;
   TypeMask _interceptorType;
 
-  /// Cache of [FlatTypeMask]s grouped by the 8 possible values of the
+  /// Cache of [FlatTypeMask]s grouped by the possible values of the
   /// `FlatTypeMask.flags` property.
-  final List<Map<ClassEntity, TypeMask>> _canonicalizedTypeMasks =
-      new List<Map<ClassEntity, TypeMask>>.filled(8, null);
+  final List<Map<ClassEntity, TypeMask>> _canonicalizedTypeMasks = List.filled(
+      _FlatTypeMaskKind.values.length << FlatTypeMask._USED_INDICES, null);
 
   /// Return the cached mask for [base] with the given flags, or
   /// calls [createMask] to create the mask and cache it.
@@ -89,126 +90,129 @@
   }
 
   @override
-  TypeMask get dynamicType => _dynamicType ??= new TypeMask.subclass(
-      _closedWorld.commonElements.objectClass, _closedWorld);
+  TypeMask get internalTopType => _internalTopType ??= TypeMask.subclass(
+      _closedWorld.commonElements.objectClass, _closedWorld,
+      hasLateSentinel: true);
 
   @override
-  TypeMask get nonNullType => _nonNullType ??= new TypeMask.nonNullSubclass(
+  TypeMask get dynamicType => _dynamicType ??=
+      TypeMask.subclass(_closedWorld.commonElements.objectClass, _closedWorld);
+
+  @override
+  TypeMask get nonNullType => _nonNullType ??= TypeMask.nonNullSubclass(
       _closedWorld.commonElements.objectClass, _closedWorld);
 
   @override
   TypeMask get intType => _intType ??=
-      new TypeMask.nonNullSubclass(commonElements.jsIntClass, _closedWorld);
+      TypeMask.nonNullSubclass(commonElements.jsIntClass, _closedWorld);
 
   @override
   TypeMask get uint32Type => _uint32Type ??=
-      new TypeMask.nonNullSubclass(commonElements.jsUInt32Class, _closedWorld);
+      TypeMask.nonNullSubclass(commonElements.jsUInt32Class, _closedWorld);
 
   @override
   TypeMask get uint31Type => _uint31Type ??=
-      new TypeMask.nonNullExact(commonElements.jsUInt31Class, _closedWorld);
+      TypeMask.nonNullExact(commonElements.jsUInt31Class, _closedWorld);
 
   @override
-  TypeMask get positiveIntType =>
-      _positiveIntType ??= new TypeMask.nonNullSubclass(
-          commonElements.jsPositiveIntClass, _closedWorld);
+  TypeMask get positiveIntType => _positiveIntType ??=
+      TypeMask.nonNullSubclass(commonElements.jsPositiveIntClass, _closedWorld);
 
   @override
   TypeMask get numNotIntType => _numNotIntType ??=
-      new TypeMask.nonNullExact(commonElements.jsNumNotIntClass, _closedWorld);
+      TypeMask.nonNullExact(commonElements.jsNumNotIntClass, _closedWorld);
 
   @override
   TypeMask get numType => _numType ??=
-      new TypeMask.nonNullSubclass(commonElements.jsNumberClass, _closedWorld);
+      TypeMask.nonNullSubclass(commonElements.jsNumberClass, _closedWorld);
 
   @override
   TypeMask get boolType => _boolType ??=
-      new TypeMask.nonNullExact(commonElements.jsBoolClass, _closedWorld);
+      TypeMask.nonNullExact(commonElements.jsBoolClass, _closedWorld);
 
   @override
   TypeMask get functionType => _functionType ??=
-      new TypeMask.nonNullSubtype(commonElements.functionClass, _closedWorld);
+      TypeMask.nonNullSubtype(commonElements.functionClass, _closedWorld);
 
   @override
   TypeMask get listType => _listType ??=
-      new TypeMask.nonNullSubtype(commonElements.jsArrayClass, _closedWorld);
+      TypeMask.nonNullSubtype(commonElements.jsArrayClass, _closedWorld);
 
   @override
-  TypeMask get constListType => _constListType ??= new TypeMask.nonNullExact(
+  TypeMask get constListType => _constListType ??= TypeMask.nonNullExact(
       commonElements.jsUnmodifiableArrayClass, _closedWorld);
 
   @override
   TypeMask get fixedListType => _fixedListType ??=
-      new TypeMask.nonNullExact(commonElements.jsFixedArrayClass, _closedWorld);
+      TypeMask.nonNullExact(commonElements.jsFixedArrayClass, _closedWorld);
 
   @override
-  TypeMask get growableListType =>
-      _growableListType ??= new TypeMask.nonNullExact(
-          commonElements.jsExtendableArrayClass, _closedWorld);
+  TypeMask get growableListType => _growableListType ??= TypeMask.nonNullExact(
+      commonElements.jsExtendableArrayClass, _closedWorld);
 
   @override
   TypeMask get setType => _setType ??=
-      new TypeMask.nonNullSubtype(commonElements.setLiteralClass, _closedWorld);
+      TypeMask.nonNullSubtype(commonElements.setLiteralClass, _closedWorld);
 
   @override
-  TypeMask get constSetType => _constSetType ??= new TypeMask.nonNullSubtype(
+  TypeMask get constSetType => _constSetType ??= TypeMask.nonNullSubtype(
       commonElements.constSetLiteralClass, _closedWorld);
 
   @override
   TypeMask get mapType => _mapType ??=
-      new TypeMask.nonNullSubtype(commonElements.mapLiteralClass, _closedWorld);
+      TypeMask.nonNullSubtype(commonElements.mapLiteralClass, _closedWorld);
 
   @override
-  TypeMask get constMapType => _constMapType ??= new TypeMask.nonNullSubtype(
+  TypeMask get constMapType => _constMapType ??= TypeMask.nonNullSubtype(
       commonElements.constMapLiteralClass, _closedWorld);
 
   @override
   TypeMask get stringType => _stringType ??=
-      new TypeMask.nonNullExact(commonElements.jsStringClass, _closedWorld);
+      TypeMask.nonNullExact(commonElements.jsStringClass, _closedWorld);
 
   @override
   TypeMask get typeType => _typeType ??=
-      new TypeMask.nonNullExact(commonElements.typeLiteralClass, _closedWorld);
+      TypeMask.nonNullExact(commonElements.typeLiteralClass, _closedWorld);
 
   @override
   TypeMask get syncStarIterableType => _syncStarIterableType ??=
-      new TypeMask.nonNullExact(commonElements.syncStarIterable, _closedWorld);
+      TypeMask.nonNullExact(commonElements.syncStarIterable, _closedWorld);
 
   @override
-  TypeMask get asyncFutureType =>
-      _asyncFutureType ??= new TypeMask.nonNullExact(
-          commonElements.futureImplementation, _closedWorld);
+  TypeMask get asyncFutureType => _asyncFutureType ??=
+      TypeMask.nonNullExact(commonElements.futureImplementation, _closedWorld);
 
   @override
   TypeMask get asyncStarStreamType => _asyncStarStreamType ??=
-      new TypeMask.nonNullExact(commonElements.controllerStream, _closedWorld);
+      TypeMask.nonNullExact(commonElements.controllerStream, _closedWorld);
 
   // TODO(johnniwinther): Assert that the null type has been resolved.
   @override
-  TypeMask get nullType => _nullType ??= const TypeMask.empty();
+  TypeMask get nullType => _nullType ??= TypeMask.empty();
 
   @override
-  TypeMask get emptyType => const TypeMask.nonNullEmpty();
+  TypeMask get lateSentinelType => TypeMask.nonNullEmpty(hasLateSentinel: true);
 
-  TypeMask get indexablePrimitiveType =>
-      _indexablePrimitiveType ??= new TypeMask.nonNullSubtype(
-          commonElements.jsIndexableClass, _closedWorld);
+  @override
+  TypeMask get emptyType => TypeMask.nonNullEmpty();
+
+  TypeMask get indexablePrimitiveType => _indexablePrimitiveType ??=
+      TypeMask.nonNullSubtype(commonElements.jsIndexableClass, _closedWorld);
 
   TypeMask get readableArrayType => _readableArrayType ??=
-      new TypeMask.nonNullSubclass(commonElements.jsArrayClass, _closedWorld);
+      TypeMask.nonNullSubclass(commonElements.jsArrayClass, _closedWorld);
 
   @override
   TypeMask get mutableArrayType =>
-      _mutableArrayType ??= new TypeMask.nonNullSubclass(
+      _mutableArrayType ??= TypeMask.nonNullSubclass(
           commonElements.jsMutableArrayClass, _closedWorld);
 
   TypeMask get unmodifiableArrayType =>
-      _unmodifiableArrayType ??= new TypeMask.nonNullExact(
+      _unmodifiableArrayType ??= TypeMask.nonNullExact(
           commonElements.jsUnmodifiableArrayClass, _closedWorld);
 
-  TypeMask get interceptorType =>
-      _interceptorType ??= new TypeMask.nonNullSubclass(
-          commonElements.jsInterceptorClass, _closedWorld);
+  TypeMask get interceptorType => _interceptorType ??=
+      TypeMask.nonNullSubclass(commonElements.jsInterceptorClass, _closedWorld);
 
   @override
   AbstractBool isTypedArray(TypeMask mask) {
@@ -232,37 +236,37 @@
     ClassEntity typedDataClass = _closedWorld.commonElements.typedDataClass;
     return AbstractBool.maybeOrFalse(typedDataClass != null &&
         _closedWorld.classHierarchy.isInstantiated(typedDataClass) &&
-        intersects(mask, new TypeMask.subtype(typedDataClass, _closedWorld)) &&
+        intersects(mask, TypeMask.subtype(typedDataClass, _closedWorld)) &&
         intersects(
             mask,
-            new TypeMask.subtype(
+            TypeMask.subtype(
                 _closedWorld.commonElements.jsIndexingBehaviorInterface,
                 _closedWorld)));
   }
 
   @override
   TypeMask createNonNullExact(ClassEntity cls) {
-    return new TypeMask.nonNullExact(cls, _closedWorld);
+    return TypeMask.nonNullExact(cls, _closedWorld);
   }
 
   @override
   TypeMask createNullableExact(ClassEntity cls) {
-    return new TypeMask.exact(cls, _closedWorld);
+    return TypeMask.exact(cls, _closedWorld);
   }
 
   @override
   TypeMask createNonNullSubclass(ClassEntity cls) {
-    return new TypeMask.nonNullSubclass(cls, _closedWorld);
+    return TypeMask.nonNullSubclass(cls, _closedWorld);
   }
 
   @override
   TypeMask createNonNullSubtype(ClassEntity cls) {
-    return new TypeMask.nonNullSubtype(cls, _closedWorld);
+    return TypeMask.nonNullSubtype(cls, _closedWorld);
   }
 
   @override
   TypeMask createNullableSubtype(ClassEntity cls) {
-    return new TypeMask.subtype(cls, _closedWorld);
+    return TypeMask.subtype(cls, _closedWorld);
   }
 
   @override
@@ -395,6 +399,14 @@
   TypeMask includeNull(TypeMask mask) => mask.nullable();
 
   @override
+  TypeMask excludeLateSentinel(TypeMask mask) =>
+      mask.withFlags(hasLateSentinel: false);
+
+  @override
+  TypeMask includeLateSentinel(TypeMask mask) =>
+      mask.withFlags(hasLateSentinel: true);
+
+  @override
   AbstractBool containsType(TypeMask typeMask, ClassEntity cls) {
     return AbstractBool.trueOrFalse(_containsType(typeMask, cls));
   }
@@ -443,12 +455,8 @@
       AbstractBool.trueOrMaybe(value.isEmpty);
 
   @override
-  AbstractBool isExact(TypeMask value) =>
-      AbstractBool.trueOrMaybe(value.isExact && !value.isNullable);
-
-  @override
-  AbstractBool isExactOrNull(TypeMask value) =>
-      AbstractBool.trueOrMaybe(value.isExact || _isNull(value));
+  AbstractBool isExact(TypeMask value) => AbstractBool.trueOrMaybe(
+      value.isExact && !value.isNullable && !value.hasLateSentinel);
 
   @override
   ClassEntity getExactClass(TypeMask mask) {
@@ -470,7 +478,7 @@
   @override
   AbstractValue createPrimitiveValue(
       covariant TypeMask originalValue, PrimitiveConstantValue value) {
-    return new ValueTypeMask(originalValue, value);
+    return ValueTypeMask(originalValue, value);
   }
 
   @override
@@ -484,7 +492,8 @@
     }
   }
 
-  bool _isNull(TypeMask value) => value.isNull;
+  @override
+  AbstractBool isLateSentinel(TypeMask value) => value.isLateSentinel;
 
   @override
   AbstractBool isPrimitive(TypeMask value) {
@@ -492,7 +501,7 @@
         _canBePrimitiveArray(value) ||
         _canBePrimitiveBoolean(value) ||
         _canBePrimitiveString(value) ||
-        _isNull(value));
+        value.isNull);
   }
 
   @override
@@ -518,10 +527,6 @@
     return _containsType(value, commonElements.jsBoolClass);
   }
 
-  @override
-  AbstractBool isPrimitiveArray(TypeMask value) =>
-      AbstractBool.maybeOrFalse(_canBePrimitiveArray(value));
-
   bool _canBePrimitiveArray(TypeMask value) {
     return _containsType(value, commonElements.jsArrayClass) ||
         _containsType(value, commonElements.jsFixedArrayClass) ||
@@ -580,25 +585,29 @@
 
   @override
   AbstractBool isInteger(TypeMask value) {
-    return AbstractBool.trueOrMaybe(
-        value.containsOnlyInt(_closedWorld) && !value.isNullable);
+    return AbstractBool.trueOrMaybe(value.containsOnlyInt(_closedWorld) &&
+        !value.isNullable &&
+        !value.hasLateSentinel);
   }
 
   @override
   AbstractBool isUInt32(TypeMask value) {
     return AbstractBool.trueOrMaybe(!value.isNullable &&
+        !value.hasLateSentinel &&
         _isInstanceOfOrNull(value, commonElements.jsUInt32Class));
   }
 
   @override
   AbstractBool isUInt31(TypeMask value) {
     return AbstractBool.trueOrMaybe(!value.isNullable &&
+        !value.hasLateSentinel &&
         _isInstanceOfOrNull(value, commonElements.jsUInt31Class));
   }
 
   @override
   AbstractBool isPositiveInteger(TypeMask value) {
     return AbstractBool.trueOrMaybe(!value.isNullable &&
+        !value.hasLateSentinel &&
         _isInstanceOfOrNull(value, commonElements.jsPositiveIntClass));
   }
 
@@ -615,8 +624,9 @@
 
   @override
   AbstractBool isNumber(TypeMask value) {
-    return AbstractBool.trueOrMaybe(
-        value.containsOnlyNum(_closedWorld) && !value.isNullable);
+    return AbstractBool.trueOrMaybe(value.containsOnlyNum(_closedWorld) &&
+        !value.isNullable &&
+        !value.hasLateSentinel);
   }
 
   @override
@@ -629,8 +639,9 @@
 
   @override
   AbstractBool isBoolean(TypeMask value) {
-    return AbstractBool.trueOrMaybe(
-        value.containsOnlyBool(_closedWorld) && !value.isNullable);
+    return AbstractBool.trueOrMaybe(value.containsOnlyBool(_closedWorld) &&
+        !value.isNullable &&
+        !value.hasLateSentinel);
   }
 
   @override
@@ -643,7 +654,7 @@
 
   @override
   AbstractBool isTruthy(TypeMask value) {
-    if (value is ValueTypeMask && !value.isNullable) {
+    if (value is ValueTypeMask && !value.isNullable && !value.hasLateSentinel) {
       PrimitiveConstantValue constant = value.value;
       if (constant is BoolConstantValue) {
         return constant.boolValue ? AbstractBool.True : AbstractBool.False;
@@ -655,8 +666,9 @@
 
   @override
   AbstractBool isString(TypeMask value) {
-    return AbstractBool.trueOrMaybe(
-        value.containsOnlyString(_closedWorld) && !value.isNullable);
+    return AbstractBool.trueOrMaybe(value.containsOnlyString(_closedWorld) &&
+        !value.isNullable &&
+        !value.hasLateSentinel);
   }
 
   @override
@@ -672,7 +684,7 @@
     return _isIndexablePrimitive(value) ||
         _isNumberOrNull(value) ||
         _isBooleanOrNull(value) ||
-        _isNull(value);
+        value.isNull;
   }
 
   @override
@@ -731,13 +743,13 @@
       MemberEntity allocationElement,
       AbstractValue elementType,
       int length) {
-    return new ContainerTypeMask(
+    return ContainerTypeMask(
         forwardTo, allocationNode, allocationElement, elementType, length);
   }
 
   @override
   AbstractValue unionOfMany(Iterable<AbstractValue> values) {
-    TypeMask result = const TypeMask.nonNullEmpty();
+    TypeMask result = TypeMask.nonNullEmpty();
     for (TypeMask value in values) {
       result = result.union(value, this);
     }
@@ -748,18 +760,18 @@
   AbstractValue computeReceiver(Iterable<MemberEntity> members) {
     assert(_closedWorld.classHierarchy
         .hasAnyStrictSubclass(_closedWorld.commonElements.objectClass));
-    return new TypeMask.unionOf(
+    return TypeMask.unionOf(
         members.expand((MemberEntity element) {
           ClassEntity cls = element.enclosingClass;
           return [cls]..addAll(_closedWorld.mixinUsesOf(cls));
         }).map((cls) {
           if (_closedWorld.commonElements.jsNullClass == cls) {
-            return const TypeMask.empty();
+            return TypeMask.empty();
           } else if (_closedWorld.classHierarchy.isInstantiated(cls)) {
-            return new TypeMask.nonNullSubclass(cls, _closedWorld);
+            return TypeMask.nonNullSubclass(cls, _closedWorld);
           } else {
             // TODO(johnniwinther): Avoid the need for this case.
-            return const TypeMask.empty();
+            return TypeMask.empty();
           }
         }),
         this);
@@ -880,7 +892,7 @@
   @override
   AbstractValue createMapValue(AbstractValue forwardTo, Object allocationNode,
       MemberEntity allocationElement, AbstractValue key, AbstractValue value) {
-    return new MapTypeMask(
+    return MapTypeMask(
         forwardTo, allocationNode, allocationElement, key, value);
   }
 
@@ -892,14 +904,14 @@
       AbstractValue key,
       AbstractValue value,
       Map<String, AbstractValue> mappings) {
-    return new DictionaryTypeMask(
-        forwardTo, allocationNode, allocationElement, key, value, mappings);
+    return DictionaryTypeMask(forwardTo, allocationNode, allocationElement, key,
+        value, Map.from(mappings));
   }
 
   @override
   AbstractValue createSetValue(AbstractValue forwardTo, Object allocationNode,
       MemberEntity allocationElement, AbstractValue elementType) {
-    return new SetTypeMask(
+    return SetTypeMask(
         forwardTo, allocationNode, allocationElement, elementType);
   }
 
@@ -963,8 +975,8 @@
 
   @override
   TypeMask readAbstractValueFromDataSource(DataSource source) {
-    return source.readCached<TypeMask>(
-        () => new TypeMask.readFromDataSource(source, this));
+    return source
+        .readCached<TypeMask>(() => TypeMask.readFromDataSource(source, this));
   }
 
   @override
@@ -985,14 +997,21 @@
     //     can be really long and mess up the layout.
     // Capitalize Null to emphasize that it's the null type mask and not
     // a null value we accidentally printed out.
-    if (type.isEmptyOrNull) return type.isNullable ? 'Null' : 'Empty';
+    if (type.isEmpty) return 'Empty';
+    if (type.isEmptyOrFlagged) {
+      return [
+        if (type.isNullable) 'Null',
+        if (type.hasLateSentinel) '\$',
+      ].join('');
+    }
     String nullFlag = type.isNullable ? '?' : '';
     String subFlag = type.isExact
         ? ''
         : type.isSubclass
             ? '+'
             : '*';
-    return '${type.base.name}$nullFlag$subFlag';
+    String sentinelFlag = type.hasLateSentinel ? '\$' : '';
+    return '${type.base.name}$nullFlag$subFlag$sentinelFlag';
   }
   if (type is UnionTypeMask) {
     return type.disjointMasks.map((m) => formatType(dartTypes, m)).join(' | ');
diff --git a/pkg/compiler/lib/src/inferrer/typemasks/set_type_mask.dart b/pkg/compiler/lib/src/inferrer/typemasks/set_type_mask.dart
index cfe96da..bfa9098 100644
--- a/pkg/compiler/lib/src/inferrer/typemasks/set_type_mask.dart
+++ b/pkg/compiler/lib/src/inferrer/typemasks/set_type_mask.dart
@@ -24,19 +24,19 @@
   // The element type of this set.
   final TypeMask elementType;
 
-  SetTypeMask(this.forwardTo, this.allocationNode, this.allocationElement,
+  const SetTypeMask(this.forwardTo, this.allocationNode, this.allocationElement,
       this.elementType);
 
   /// Deserializes a [SetTypeMask] object from [source].
   factory SetTypeMask.readFromDataSource(
       DataSource source, CommonMasks domain) {
     source.begin(tag);
-    TypeMask forwardTo = new TypeMask.readFromDataSource(source, domain);
+    TypeMask forwardTo = TypeMask.readFromDataSource(source, domain);
     ir.TreeNode allocationNode = source.readTreeNodeOrNull();
     MemberEntity allocationElement = source.readMemberOrNull();
-    TypeMask elementType = new TypeMask.readFromDataSource(source, domain);
+    TypeMask elementType = TypeMask.readFromDataSource(source, domain);
     source.end(tag);
-    return new SetTypeMask(
+    return SetTypeMask(
         forwardTo, allocationNode, allocationElement, elementType);
   }
 
@@ -53,16 +53,20 @@
   }
 
   @override
-  TypeMask nullable() => isNullable
-      ? this
-      : new SetTypeMask(
-          forwardTo.nullable(), allocationNode, allocationElement, elementType);
-
-  @override
-  TypeMask nonNullable() => isNullable
-      ? new SetTypeMask(forwardTo.nonNullable(), allocationNode,
-          allocationElement, elementType)
-      : this;
+  SetTypeMask withFlags({bool isNullable, bool hasLateSentinel}) {
+    isNullable ??= this.isNullable;
+    hasLateSentinel ??= this.hasLateSentinel;
+    if (isNullable == this.isNullable &&
+        hasLateSentinel == this.hasLateSentinel) {
+      return this;
+    }
+    return SetTypeMask(
+        forwardTo.withFlags(
+            isNullable: isNullable, hasLateSentinel: hasLateSentinel),
+        allocationNode,
+        allocationElement,
+        elementType);
+  }
 
   @override
   bool get isSet => true;
@@ -71,45 +75,29 @@
   bool get isExact => true;
 
   @override
-  bool equalsDisregardNull(other) {
-    if (other is! SetTypeMask) return false;
-    return super.equalsDisregardNull(other) &&
-        allocationNode == other.allocationNode &&
-        elementType == other.elementType;
-  }
-
-  @override
-  TypeMask intersection(TypeMask other, CommonMasks domain) {
-    TypeMask forwardIntersection = forwardTo.intersection(other, domain);
-    if (forwardIntersection.isEmptyOrNull) return forwardIntersection;
-    return forwardIntersection.isNullable ? nullable() : nonNullable();
-  }
-
-  @override
-  TypeMask union(dynamic other, CommonMasks domain) {
-    if (this == other) {
-      return this;
-    } else if (equalsDisregardNull(other)) {
-      return other.isNullable ? other : this;
-    } else if (other.isEmptyOrNull) {
-      return other.isNullable ? this.nullable() : this;
-    } else if (other.isSet &&
+  TypeMask _unionSpecialCases(TypeMask other, CommonMasks domain,
+      {bool isNullable, bool hasLateSentinel}) {
+    assert(isNullable != null);
+    assert(hasLateSentinel != null);
+    if (other is SetTypeMask &&
         elementType != null &&
         other.elementType != null) {
       TypeMask newElementType = elementType.union(other.elementType, domain);
       TypeMask newForwardTo = forwardTo.union(other.forwardTo, domain);
-      return new SetTypeMask(newForwardTo, null, null, newElementType);
-    } else {
-      return forwardTo.union(other, domain);
+      return SetTypeMask(newForwardTo, null, null, newElementType);
     }
+    return null;
   }
 
   @override
-  bool operator ==(other) => super == other;
+  bool operator ==(other) {
+    if (identical(this, other)) return true;
+    if (other is! SetTypeMask) return false;
+    return super == other && elementType == other.elementType;
+  }
 
   @override
-  int get hashCode =>
-      computeHashCode(allocationNode, isNullable, elementType, forwardTo);
+  int get hashCode => Hashing.objectHash(elementType, super.hashCode);
 
   @override
   String toString() => 'Set($forwardTo, element: $elementType)';
diff --git a/pkg/compiler/lib/src/inferrer/typemasks/type_mask.dart b/pkg/compiler/lib/src/inferrer/typemasks/type_mask.dart
index 7fd62a8..7b62f9c 100644
--- a/pkg/compiler/lib/src/inferrer/typemasks/type_mask.dart
+++ b/pkg/compiler/lib/src/inferrer/typemasks/type_mask.dart
@@ -25,7 +25,7 @@
   bool needsNoSuchMethodHandling(Selector selector, JClosedWorld world) {
     if (isAll) {
       TypeMask mask =
-          new TypeMask.subclass(world.commonElements.objectClass, world);
+          TypeMask.subclass(world.commonElements.objectClass, world);
       return mask.needsNoSuchMethodHandling(selector, world);
     }
     for (TypeMask mask in _masks) {
@@ -44,7 +44,7 @@
       _masks = null;
       return true;
     }
-    _masks ??= new Set<TypeMask>();
+    _masks ??= {};
     return _masks.add(mask);
   }
 
@@ -65,12 +65,12 @@
 
   @override
   AbstractValueDomain createDomain(JClosedWorld closedWorld) {
-    return new CommonMasks(closedWorld);
+    return CommonMasks(closedWorld);
   }
 
   @override
   SelectorConstraintsStrategy createSelectorStrategy() {
-    return new TypeMaskSelectorStrategy();
+    return TypeMaskSelectorStrategy();
   }
 }
 
@@ -80,8 +80,7 @@
   @override
   UniverseSelectorConstraints createSelectorConstraints(
       Selector selector, Object initialConstraint) {
-    return new IncreasingTypeMaskSet()
-      ..addReceiverConstraint(initialConstraint);
+    return IncreasingTypeMaskSet()..addReceiverConstraint(initialConstraint);
   }
 
   @override
@@ -109,31 +108,34 @@
 /// operations on it are not guaranteed to be precise and they may
 /// yield conservative answers that contain too many classes.
 abstract class TypeMask implements AbstractValue {
-  factory TypeMask(
-      ClassEntity base, int kind, bool isNullable, CommonMasks domain) {
-    return new FlatTypeMask.normalized(
-        base, (kind << 1) | (isNullable ? 1 : 0), domain);
-  }
+  const TypeMask();
 
-  const factory TypeMask.empty() = FlatTypeMask.empty;
+  factory TypeMask.empty({bool hasLateSentinel: false}) =>
+      FlatTypeMask.empty(hasLateSentinel: hasLateSentinel);
 
-  factory TypeMask.exact(ClassEntity base, JClosedWorld closedWorld) {
+  factory TypeMask.exact(ClassEntity base, JClosedWorld closedWorld,
+      {bool hasLateSentinel: false}) {
     assert(
         closedWorld.classHierarchy.isInstantiated(base),
         failedAt(
             base ?? CURRENT_ELEMENT_SPANNABLE,
             "Cannot create exact type mask for uninstantiated "
             "class $base.\n${closedWorld.classHierarchy.dump(base)}"));
-    return new FlatTypeMask.exact(base, closedWorld);
+    return FlatTypeMask.exact(base, closedWorld,
+        hasLateSentinel: hasLateSentinel);
   }
 
-  factory TypeMask.exactOrEmpty(ClassEntity base, JClosedWorld closedWorld) {
-    if (closedWorld.classHierarchy.isInstantiated(base))
-      return new FlatTypeMask.exact(base, closedWorld);
-    return const TypeMask.empty();
+  factory TypeMask.exactOrEmpty(ClassEntity base, JClosedWorld closedWorld,
+      {bool hasLateSentinel: false}) {
+    if (closedWorld.classHierarchy.isInstantiated(base)) {
+      return FlatTypeMask.exact(base, closedWorld,
+          hasLateSentinel: hasLateSentinel);
+    }
+    return TypeMask.empty(hasLateSentinel: hasLateSentinel);
   }
 
-  factory TypeMask.subclass(ClassEntity base, JClosedWorld closedWorld) {
+  factory TypeMask.subclass(ClassEntity base, JClosedWorld closedWorld,
+      {bool hasLateSentinel: false}) {
     assert(
         closedWorld.classHierarchy.isInstantiated(base),
         failedAt(
@@ -142,50 +144,62 @@
             "class $base.\n${closedWorld.classHierarchy.dump(base)}"));
     ClassEntity topmost = closedWorld.getLubOfInstantiatedSubclasses(base);
     if (topmost == null) {
-      return new TypeMask.empty();
+      return TypeMask.empty(hasLateSentinel: hasLateSentinel);
     } else if (closedWorld.classHierarchy.hasAnyStrictSubclass(topmost)) {
-      return new FlatTypeMask.subclass(topmost, closedWorld);
+      return FlatTypeMask.subclass(topmost, closedWorld,
+          hasLateSentinel: hasLateSentinel);
     } else {
-      return new TypeMask.exact(topmost, closedWorld);
+      return TypeMask.exact(topmost, closedWorld,
+          hasLateSentinel: hasLateSentinel);
     }
   }
 
-  factory TypeMask.subtype(ClassEntity base, JClosedWorld closedWorld) {
+  factory TypeMask.subtype(ClassEntity base, JClosedWorld closedWorld,
+      {bool hasLateSentinel: false}) {
     ClassEntity topmost = closedWorld.getLubOfInstantiatedSubtypes(base);
     if (topmost == null) {
-      return new TypeMask.empty();
+      return TypeMask.empty(hasLateSentinel: hasLateSentinel);
     }
     if (closedWorld.classHierarchy.hasOnlySubclasses(topmost)) {
-      return new TypeMask.subclass(topmost, closedWorld);
+      return TypeMask.subclass(topmost, closedWorld,
+          hasLateSentinel: hasLateSentinel);
     }
     if (closedWorld.classHierarchy.hasAnyStrictSubtype(topmost)) {
-      return new FlatTypeMask.subtype(topmost, closedWorld);
+      return FlatTypeMask.subtype(topmost, closedWorld,
+          hasLateSentinel: hasLateSentinel);
     } else {
-      return new TypeMask.exact(topmost, closedWorld);
+      return TypeMask.exact(topmost, closedWorld,
+          hasLateSentinel: hasLateSentinel);
     }
   }
 
-  const factory TypeMask.nonNullEmpty() = FlatTypeMask.nonNullEmpty;
+  factory TypeMask.nonNullEmpty({bool hasLateSentinel: false}) =>
+      FlatTypeMask.nonNullEmpty(hasLateSentinel: hasLateSentinel);
 
-  factory TypeMask.nonNullExact(ClassEntity base, JClosedWorld closedWorld) {
+  factory TypeMask.nonNullExact(ClassEntity base, JClosedWorld closedWorld,
+      {bool hasLateSentinel: false}) {
     assert(
         closedWorld.classHierarchy.isInstantiated(base),
         failedAt(
             base ?? CURRENT_ELEMENT_SPANNABLE,
             "Cannot create exact type mask for uninstantiated "
             "class $base.\n${closedWorld.classHierarchy.dump(base)}"));
-    return new FlatTypeMask.nonNullExact(base, closedWorld);
+    return FlatTypeMask.nonNullExact(base, closedWorld,
+        hasLateSentinel: hasLateSentinel);
   }
 
   factory TypeMask.nonNullExactOrEmpty(
-      ClassEntity base, JClosedWorld closedWorld) {
+      ClassEntity base, JClosedWorld closedWorld,
+      {bool hasLateSentinel: false}) {
     if (closedWorld.classHierarchy.isInstantiated(base)) {
-      return new FlatTypeMask.nonNullExact(base, closedWorld);
+      return FlatTypeMask.nonNullExact(base, closedWorld,
+          hasLateSentinel: hasLateSentinel);
     }
-    return const TypeMask.nonNullEmpty();
+    return TypeMask.nonNullEmpty(hasLateSentinel: hasLateSentinel);
   }
 
-  factory TypeMask.nonNullSubclass(ClassEntity base, JClosedWorld closedWorld) {
+  factory TypeMask.nonNullSubclass(ClassEntity base, JClosedWorld closedWorld,
+      {bool hasLateSentinel: false}) {
     assert(
         closedWorld.classHierarchy.isInstantiated(base),
         failedAt(
@@ -194,26 +208,32 @@
             "class $base.\n${closedWorld.classHierarchy.dump(base)}"));
     ClassEntity topmost = closedWorld.getLubOfInstantiatedSubclasses(base);
     if (topmost == null) {
-      return new TypeMask.nonNullEmpty();
+      return TypeMask.nonNullEmpty(hasLateSentinel: hasLateSentinel);
     } else if (closedWorld.classHierarchy.hasAnyStrictSubclass(topmost)) {
-      return new FlatTypeMask.nonNullSubclass(topmost, closedWorld);
+      return FlatTypeMask.nonNullSubclass(topmost, closedWorld,
+          hasLateSentinel: hasLateSentinel);
     } else {
-      return new TypeMask.nonNullExact(topmost, closedWorld);
+      return TypeMask.nonNullExact(topmost, closedWorld,
+          hasLateSentinel: hasLateSentinel);
     }
   }
 
-  factory TypeMask.nonNullSubtype(ClassEntity base, JClosedWorld closedWorld) {
+  factory TypeMask.nonNullSubtype(ClassEntity base, JClosedWorld closedWorld,
+      {bool hasLateSentinel: false}) {
     ClassEntity topmost = closedWorld.getLubOfInstantiatedSubtypes(base);
     if (topmost == null) {
-      return new TypeMask.nonNullEmpty();
+      return TypeMask.nonNullEmpty(hasLateSentinel: hasLateSentinel);
     }
     if (closedWorld.classHierarchy.hasOnlySubclasses(topmost)) {
-      return new TypeMask.nonNullSubclass(topmost, closedWorld);
+      return TypeMask.nonNullSubclass(topmost, closedWorld,
+          hasLateSentinel: hasLateSentinel);
     }
     if (closedWorld.classHierarchy.hasAnyStrictSubtype(topmost)) {
-      return new FlatTypeMask.nonNullSubtype(topmost, closedWorld);
+      return FlatTypeMask.nonNullSubtype(topmost, closedWorld,
+          hasLateSentinel: hasLateSentinel);
     } else {
-      return new TypeMask.nonNullExact(topmost, closedWorld);
+      return TypeMask.nonNullExact(topmost, closedWorld,
+          hasLateSentinel: hasLateSentinel);
     }
   }
 
@@ -226,21 +246,21 @@
     TypeMaskKind kind = source.readEnum(TypeMaskKind.values);
     switch (kind) {
       case TypeMaskKind.flat:
-        return new FlatTypeMask.readFromDataSource(source, domain);
+        return FlatTypeMask.readFromDataSource(source, domain);
       case TypeMaskKind.union:
-        return new UnionTypeMask.readFromDataSource(source, domain);
+        return UnionTypeMask.readFromDataSource(source, domain);
       case TypeMaskKind.container:
-        return new ContainerTypeMask.readFromDataSource(source, domain);
+        return ContainerTypeMask.readFromDataSource(source, domain);
       case TypeMaskKind.set:
-        return new SetTypeMask.readFromDataSource(source, domain);
+        return SetTypeMask.readFromDataSource(source, domain);
       case TypeMaskKind.map:
-        return new MapTypeMask.readFromDataSource(source, domain);
+        return MapTypeMask.readFromDataSource(source, domain);
       case TypeMaskKind.dictionary:
-        return new DictionaryTypeMask.readFromDataSource(source, domain);
+        return DictionaryTypeMask.readFromDataSource(source, domain);
       case TypeMaskKind.value:
-        return new ValueTypeMask.readFromDataSource(source, domain);
+        return ValueTypeMask.readFromDataSource(source, domain);
     }
-    throw new UnsupportedError("Unexpected TypeMaskKind $kind.");
+    throw UnsupportedError("Unexpected TypeMaskKind $kind.");
   }
 
   /// Serializes this [TypeMask] to [sink].
@@ -271,7 +291,7 @@
       TypeMask mask, JClosedWorld closedWorld) {
     mask = nonForwardingMask(mask);
     if (mask is FlatTypeMask) {
-      if (mask.isEmptyOrNull) return null;
+      if (mask.isEmptyOrFlagged) return null;
       if (mask.base == closedWorld.commonElements.nullClass) {
         return 'The class ${mask.base} is not canonicalized.';
       }
@@ -308,10 +328,17 @@
   }
 
   /// Returns a nullable variant of [this] type mask.
-  TypeMask nullable();
+  TypeMask nullable() => withFlags(isNullable: true);
 
   /// Returns a non-nullable variant of [this] type mask.
-  TypeMask nonNullable();
+  TypeMask nonNullable() => withFlags(isNullable: false);
+
+  /// Returns a variant of [this] type mask whose value is neither `null` nor
+  /// the late sentinel.
+  TypeMask withoutFlags() =>
+      withFlags(isNullable: false, hasLateSentinel: false);
+
+  TypeMask withFlags({bool isNullable, bool hasLateSentinel});
 
   /// Whether nothing matches this mask, not even null.
   bool get isEmpty;
@@ -322,8 +349,15 @@
   /// Whether the only possible value in this mask is Null.
   bool get isNull;
 
-  /// Whether [isEmpty] or [isNull] is true.
-  bool get isEmptyOrNull;
+  /// Whether [this] is a sentinel for an uninitialized late variable.
+  AbstractBool get isLateSentinel;
+
+  /// Whether a late sentinel is a valid value of this mask.
+  bool get hasLateSentinel => isLateSentinel.isPotentiallyTrue;
+
+  /// Whether [this] mask is empty or only represents values tracked by flags
+  /// (i.e. `null` and the late sentinel).
+  bool get isEmptyOrFlagged;
 
   /// Whether this mask only includes instances of an exact class, and none of
   /// it's subclasses or subtypes.
diff --git a/pkg/compiler/lib/src/inferrer/typemasks/union_type_mask.dart b/pkg/compiler/lib/src/inferrer/typemasks/union_type_mask.dart
index dddd0ed..3a647ea 100644
--- a/pkg/compiler/lib/src/inferrer/typemasks/union_type_mask.dart
+++ b/pkg/compiler/lib/src/inferrer/typemasks/union_type_mask.dart
@@ -4,7 +4,7 @@
 
 part of masks;
 
-class UnionTypeMask implements TypeMask {
+class UnionTypeMask extends TypeMask {
   /// Tag used for identifying serialized [UnionTypeMask] objects in a
   /// debugging data stream.
   static const String tag = 'union-type-mask';
@@ -22,21 +22,32 @@
   @override
   final bool isNullable;
 
-  UnionTypeMask._internal(this.disjointMasks, this.isNullable) {
-    assert(disjointMasks.length > 1);
-    assert(disjointMasks.every((TypeMask mask) => !mask.isUnion));
-    assert(disjointMasks.every((TypeMask mask) => !mask.isNullable));
-  }
+  @override
+  final bool hasLateSentinel;
+
+  @override
+  AbstractBool get isLateSentinel => AbstractBool.maybeOrFalse(hasLateSentinel);
+
+  UnionTypeMask._internal(this.disjointMasks,
+      {this.isNullable, this.hasLateSentinel})
+      : assert(isNullable != null),
+        assert(hasLateSentinel != null),
+        assert(disjointMasks.length > 1),
+        assert(disjointMasks.every((TypeMask mask) => !mask.isUnion)),
+        assert(disjointMasks.every((TypeMask mask) => !mask.isNullable)),
+        assert(disjointMasks.every((TypeMask mask) => !mask.hasLateSentinel));
 
   /// Deserializes a [UnionTypeMask] object from [source].
   factory UnionTypeMask.readFromDataSource(
       DataSource source, CommonMasks domain) {
     source.begin(tag);
     List<FlatTypeMask> disjointMasks =
-        source.readList(() => new TypeMask.readFromDataSource(source, domain));
+        source.readList(() => TypeMask.readFromDataSource(source, domain));
     bool isNullable = source.readBool();
+    bool hasLateSentinel = source.readBool();
     source.end(tag);
-    return new UnionTypeMask._internal(disjointMasks, isNullable);
+    return UnionTypeMask._internal(disjointMasks,
+        isNullable: isNullable, hasLateSentinel: hasLateSentinel);
   }
 
   /// Serializes this [UnionTypeMask] to [sink].
@@ -47,6 +58,7 @@
     sink.writeList(
         disjointMasks, (FlatTypeMask mask) => mask.writeToDataSink(sink));
     sink.writeBool(isNullable);
+    sink.writeBool(hasLateSentinel);
     sink.end(tag);
   }
 
@@ -55,15 +67,21 @@
         (mask) => TypeMask.assertIsNormalized(mask, domain._closedWorld)));
     List<FlatTypeMask> disjoint = <FlatTypeMask>[];
     bool isNullable = masks.any((TypeMask mask) => mask.isNullable);
+    bool hasLateSentinel = masks.any((TypeMask mask) => mask.hasLateSentinel);
     unionOfHelper(masks, disjoint, domain);
     if (disjoint.isEmpty)
-      return isNullable ? TypeMask.empty() : TypeMask.nonNullEmpty();
+      return isNullable
+          ? TypeMask.empty(hasLateSentinel: hasLateSentinel)
+          : TypeMask.nonNullEmpty(hasLateSentinel: hasLateSentinel);
     if (disjoint.length > MAX_UNION_LENGTH) {
-      return flatten(disjoint, isNullable, domain);
+      return flatten(disjoint, domain,
+          includeNull: isNullable, includeLateSentinel: hasLateSentinel);
     }
     if (disjoint.length == 1)
-      return isNullable ? disjoint[0].nullable() : disjoint[0];
-    UnionTypeMask union = new UnionTypeMask._internal(disjoint, isNullable);
+      return disjoint.single
+          .withFlags(isNullable: isNullable, hasLateSentinel: hasLateSentinel);
+    UnionTypeMask union = UnionTypeMask._internal(disjoint,
+        isNullable: isNullable, hasLateSentinel: hasLateSentinel);
     assert(TypeMask.assertIsNormalized(union, domain._closedWorld));
     return union;
   }
@@ -73,7 +91,7 @@
     // TODO(johnniwinther): Impose an order on the mask to ensure subclass masks
     // are preferred to subtype masks.
     for (TypeMask mask in masks) {
-      mask = TypeMask.nonForwardingMask(mask).nonNullable();
+      mask = TypeMask.nonForwardingMask(mask).withoutFlags();
       if (mask.isUnion) {
         UnionTypeMask union = mask;
         unionOfHelper(union.disjointMasks, disjoint, domain);
@@ -121,8 +139,11 @@
     }
   }
 
-  static TypeMask flatten(
-      List<FlatTypeMask> masks, bool includeNull, CommonMasks domain) {
+  static TypeMask flatten(List<FlatTypeMask> masks, CommonMasks domain,
+      {bool includeNull, bool includeLateSentinel}) {
+    assert(includeNull != null);
+    assert(includeLateSentinel != null);
+
     // TODO(johnniwinther): Move this computation to [ClosedWorld] and use the
     // class set structures.
     if (masks.isEmpty) throw ArgumentError.value(masks, 'masks');
@@ -136,7 +157,7 @@
 
     // Compute the best candidate and its kind.
     ClassEntity bestElement;
-    int bestKind;
+    _FlatTypeMaskKind bestKind;
     int bestSize;
     for (ClassEntity candidate in candidates) {
       bool isInstantiatedStrictSubclass(cls) =>
@@ -145,13 +166,13 @@
           domain._closedWorld.classHierarchy.isSubclassOf(cls, candidate);
 
       int size;
-      int kind;
+      _FlatTypeMaskKind kind;
       if (useSubclass && masksBases.every(isInstantiatedStrictSubclass)) {
         // If both [this] and [other] are subclasses of the supertype,
         // then we prefer to construct a subclass type mask because it
         // will always be at least as small as the corresponding
         // subtype type mask.
-        kind = FlatTypeMask.SUBCLASS;
+        kind = _FlatTypeMaskKind.subclass;
         // TODO(sigmund, johnniwinther): computing length here (and below) is
         // expensive. If we can't prevent `flatten` from being called a lot, it
         // might be worth caching results.
@@ -160,7 +181,7 @@
         assert(size <=
             domain._closedWorld.classHierarchy.strictSubtypeCount(candidate));
       } else {
-        kind = FlatTypeMask.SUBTYPE;
+        kind = _FlatTypeMaskKind.subtype;
         size = domain._closedWorld.classHierarchy.strictSubtypeCount(candidate);
       }
       // Update the best candidate if the new one is better.
@@ -170,56 +191,67 @@
         bestKind = kind;
       }
     }
-    return new TypeMask(bestElement, bestKind, includeNull, domain);
+    int flags = FlatTypeMask._computeFlags(bestKind,
+        isNullable: includeNull, hasLateSentinel: includeLateSentinel);
+    return FlatTypeMask.normalized(bestElement, flags, domain);
   }
 
   @override
-  TypeMask union(dynamic other, CommonMasks domain) {
+  TypeMask union(TypeMask other, CommonMasks domain) {
     other = TypeMask.nonForwardingMask(other);
+    bool isNullable = this.isNullable || other.isNullable;
+    bool hasLateSentinel = this.hasLateSentinel || other.hasLateSentinel;
     if (other is UnionTypeMask) {
-      if (_containsNonNullableUnion(other)) {
-        return other.isNullable ? nullable() : this;
+      if (_containsDisjointMasks(other)) {
+        return withFlags(
+            isNullable: isNullable, hasLateSentinel: hasLateSentinel);
       }
-      if (other._containsNonNullableUnion(this)) {
-        return isNullable ? other.nullable() : other;
+      if (other._containsDisjointMasks(this)) {
+        return other.withFlags(
+            isNullable: isNullable, hasLateSentinel: hasLateSentinel);
       }
     } else {
-      if (disjointMasks.contains(other.nonNullable())) {
-        return other.isNullable ? nullable() : this;
+      if (disjointMasks.contains(other.withoutFlags())) {
+        return withFlags(
+            isNullable: isNullable, hasLateSentinel: hasLateSentinel);
       }
     }
 
-    List<FlatTypeMask> newList = new List<FlatTypeMask>.of(disjointMasks);
-    if (!other.isUnion) {
-      newList.add(other);
-    } else {
-      assert(other is UnionTypeMask);
+    List<FlatTypeMask> newList = List<FlatTypeMask>.of(disjointMasks);
+    if (other is UnionTypeMask) {
       newList.addAll(other.disjointMasks);
+    } else {
+      newList.add(other);
     }
-    TypeMask newMask = new TypeMask.unionOf(newList, domain);
-    return isNullable || other.isNullable ? newMask.nullable() : newMask;
+    TypeMask newMask = TypeMask.unionOf(newList, domain);
+    return newMask.withFlags(
+        isNullable: isNullable, hasLateSentinel: hasLateSentinel);
   }
 
   @override
-  TypeMask intersection(dynamic other, CommonMasks domain) {
+  TypeMask intersection(TypeMask other, CommonMasks domain) {
     other = TypeMask.nonForwardingMask(other);
+    bool isNullable = this.isNullable && other.isNullable;
+    bool hasLateSentinel = this.hasLateSentinel && other.hasLateSentinel;
     if (other is UnionTypeMask) {
-      if (_containsNonNullableUnion(other)) {
-        return isNullable ? other : other.nonNullable();
+      if (_containsDisjointMasks(other)) {
+        return other.withFlags(
+            isNullable: isNullable, hasLateSentinel: hasLateSentinel);
       }
-      if (other._containsNonNullableUnion(this)) {
-        return other.isNullable ? this : nonNullable();
+      if (other._containsDisjointMasks(this)) {
+        return withFlags(
+            isNullable: isNullable, hasLateSentinel: hasLateSentinel);
       }
     } else {
-      TypeMask otherNonNullable = other.nonNullable();
-      if (disjointMasks.contains(otherNonNullable)) {
-        return isNullable ? other : otherNonNullable;
+      if (disjointMasks.contains(other.withoutFlags())) {
+        return other.withFlags(
+            isNullable: isNullable, hasLateSentinel: hasLateSentinel);
       }
     }
 
     List<TypeMask> intersections = <TypeMask>[];
     for (TypeMask current in disjointMasks) {
-      if (other.isUnion) {
+      if (other is UnionTypeMask) {
         if (other.disjointMasks.contains(current)) {
           intersections.add(current);
         } else {
@@ -232,12 +264,14 @@
       }
     }
     TypeMask newMask = TypeMask.unionOf(intersections, domain);
-    return isNullable && other.isNullable ? newMask.nullable() : newMask;
+    return newMask.withFlags(
+        isNullable: isNullable, hasLateSentinel: hasLateSentinel);
   }
 
   @override
   bool isDisjoint(TypeMask other, JClosedWorld closedWorld) {
     if (isNullable && other.isNullable) return false;
+    if (hasLateSentinel && other.hasLateSentinel) return false;
     for (var current in disjointMasks) {
       if (!current.isDisjoint(other, closedWorld)) return false;
     }
@@ -245,21 +279,20 @@
   }
 
   @override
-  TypeMask nullable() {
-    if (isNullable) return this;
-    List<FlatTypeMask> newList = new List<FlatTypeMask>.of(disjointMasks);
-    return new UnionTypeMask._internal(newList, true);
+  UnionTypeMask withFlags({bool isNullable, bool hasLateSentinel}) {
+    isNullable ??= this.isNullable;
+    hasLateSentinel ??= this.hasLateSentinel;
+    if (isNullable == this.isNullable &&
+        hasLateSentinel == this.hasLateSentinel) {
+      return this;
+    }
+    List<FlatTypeMask> newList = List<FlatTypeMask>.of(disjointMasks);
+    return UnionTypeMask._internal(newList,
+        isNullable: isNullable, hasLateSentinel: hasLateSentinel);
   }
 
   @override
-  TypeMask nonNullable() {
-    if (!isNullable) return this;
-    List<FlatTypeMask> newList = new List<FlatTypeMask>.of(disjointMasks);
-    return new UnionTypeMask._internal(newList, false);
-  }
-
-  @override
-  bool get isEmptyOrNull => false;
+  bool get isEmptyOrFlagged => false;
   @override
   bool get isEmpty => false;
   @override
@@ -292,7 +325,8 @@
     assert(!other.isUnion);
     // Likewise, nullness should be covered.
     assert(isNullable || !other.isNullable);
-    other = other.nonNullable();
+    assert(hasLateSentinel || !other.hasLateSentinel);
+    other = other.withoutFlags();
     // Ensure the cheap test fails.
     assert(!disjointMasks.any((mask) => mask.containsMask(other, closedWorld)));
     // If we cover object, we should never get here.
@@ -321,6 +355,7 @@
   bool isInMask(TypeMask other, JClosedWorld closedWorld) {
     other = TypeMask.nonForwardingMask(other);
     if (isNullable && !other.isNullable) return false;
+    if (hasLateSentinel && !other.hasLateSentinel) return false;
     if (other.isUnion) {
       UnionTypeMask union = other;
       return disjointMasks.every((FlatTypeMask disjointMask) {
@@ -341,8 +376,9 @@
   bool containsMask(TypeMask other, JClosedWorld closedWorld) {
     other = TypeMask.nonForwardingMask(other);
     if (other.isNullable && !isNullable) return false;
+    if (other.hasLateSentinel && !hasLateSentinel) return false;
     if (other.isUnion) return other.isInMask(this, closedWorld);
-    other = other.nonNullable();
+    other = other.withoutFlags();
     bool contained =
         disjointMasks.any((mask) => mask.containsMask(other, closedWorld));
     if (PERFORM_EXTRA_CONTAINS_CHECK &&
@@ -419,7 +455,8 @@
   MemberEntity locateSingleMember(Selector selector, CommonMasks domain) {
     MemberEntity candidate;
     for (FlatTypeMask mask in disjointMasks) {
-      if (isNullable) mask = mask.nullable();
+      mask = mask.withFlags(
+          isNullable: isNullable, hasLateSentinel: hasLateSentinel);
       MemberEntity current = mask.locateSingleMember(selector, domain);
       if (current == null) {
         return null;
@@ -436,6 +473,7 @@
   String toString() {
     String masksString = [
       if (isNullable) 'null',
+      if (hasLateSentinel) 'sentinel',
       ...disjointMasks.map((TypeMask mask) => mask.toString()).toList()..sort(),
     ].join(", ");
     return 'Union($masksString)';
@@ -447,21 +485,19 @@
 
     return other is UnionTypeMask &&
         other.isNullable == isNullable &&
+        other.hasLateSentinel == hasLateSentinel &&
         other.disjointMasks.length == disjointMasks.length &&
-        _containsNonNullableUnion(other);
+        _containsDisjointMasks(other);
   }
 
   @override
   int get hashCode {
-    int hashCode = isNullable ? 86 : 43;
     // The order of the masks in [disjointMasks] must not affect the
     // hashCode.
-    for (var mask in disjointMasks) {
-      hashCode = (hashCode ^ mask.hashCode) & 0x3fffffff;
-    }
-    return hashCode;
+    return Hashing.setHash(
+        disjointMasks, Hashing.objectsHash(isNullable, hasLateSentinel));
   }
 
-  bool _containsNonNullableUnion(UnionTypeMask other) =>
+  bool _containsDisjointMasks(UnionTypeMask other) =>
       other.disjointMasks.every((e) => disjointMasks.contains(e));
 }
diff --git a/pkg/compiler/lib/src/inferrer/typemasks/value_type_mask.dart b/pkg/compiler/lib/src/inferrer/typemasks/value_type_mask.dart
index 38486ab..62b6747 100644
--- a/pkg/compiler/lib/src/inferrer/typemasks/value_type_mask.dart
+++ b/pkg/compiler/lib/src/inferrer/typemasks/value_type_mask.dart
@@ -13,16 +13,16 @@
   final TypeMask forwardTo;
   final PrimitiveConstantValue value;
 
-  ValueTypeMask(this.forwardTo, this.value);
+  const ValueTypeMask(this.forwardTo, this.value);
 
   /// Deserializes a [ValueTypeMask] object from [source].
   factory ValueTypeMask.readFromDataSource(
       DataSource source, CommonMasks domain) {
     source.begin(tag);
-    TypeMask forwardTo = new TypeMask.readFromDataSource(source, domain);
+    TypeMask forwardTo = TypeMask.readFromDataSource(source, domain);
     ConstantValue constant = source.readConstant();
     source.end(tag);
-    return new ValueTypeMask(forwardTo, constant);
+    return ValueTypeMask(forwardTo, constant);
   }
 
   /// Serializes this [ValueTypeMask] to [sink].
@@ -36,40 +36,45 @@
   }
 
   @override
-  TypeMask nullable() {
-    return isNullable ? this : new ValueTypeMask(forwardTo.nullable(), value);
-  }
-
-  @override
-  TypeMask nonNullable() {
-    return isNullable
-        ? new ValueTypeMask(forwardTo.nonNullable(), value)
-        : this;
+  ValueTypeMask withFlags({bool isNullable, bool hasLateSentinel}) {
+    isNullable ??= this.isNullable;
+    hasLateSentinel ??= this.hasLateSentinel;
+    if (isNullable == this.isNullable &&
+        hasLateSentinel == this.hasLateSentinel) {
+      return this;
+    }
+    return ValueTypeMask(
+        forwardTo.withFlags(
+            isNullable: isNullable, hasLateSentinel: hasLateSentinel),
+        value);
   }
 
   @override
   bool get isValue => true;
 
   @override
-  bool equalsDisregardNull(other) {
+  TypeMask _unionSpecialCases(TypeMask other, CommonMasks domain,
+      {bool isNullable, bool hasLateSentinel}) {
+    assert(isNullable != null);
+    assert(hasLateSentinel != null);
+    if (other is ValueTypeMask &&
+        forwardTo.withoutFlags() == other.forwardTo.withoutFlags() &&
+        value == other.value) {
+      return withFlags(
+          isNullable: isNullable, hasLateSentinel: hasLateSentinel);
+    }
+    return null;
+  }
+
+  @override
+  bool operator ==(other) {
+    if (identical(this, other)) return true;
     if (other is! ValueTypeMask) return false;
-    return super.equalsDisregardNull(other) && value == other.value;
+    return super == other && value == other.value;
   }
 
   @override
-  TypeMask intersection(TypeMask other, CommonMasks domain) {
-    TypeMask forwardIntersection = forwardTo.intersection(other, domain);
-    if (forwardIntersection.isEmptyOrNull) return forwardIntersection;
-    return forwardIntersection.isNullable ? nullable() : nonNullable();
-  }
-
-  @override
-  bool operator ==(other) => super == other;
-
-  @override
-  int get hashCode {
-    return computeHashCode(value, isNullable, forwardTo);
-  }
+  int get hashCode => Hashing.objectHash(value, super.hashCode);
 
   @override
   String toString() {
diff --git a/pkg/compiler/lib/src/kernel/dart2js_target.dart b/pkg/compiler/lib/src/kernel/dart2js_target.dart
index 312fad7..326253f 100644
--- a/pkg/compiler/lib/src/kernel/dart2js_target.dart
+++ b/pkg/compiler/lib/src/kernel/dart2js_target.dart
@@ -62,19 +62,6 @@
   return allowedNativeTest(uri) || allowedDartLibrary();
 }
 
-int _foldLateLowerings(List<int> lowerings) =>
-    lowerings.fold(LateLowering.none, (a, b) => a | b);
-
-/// Late lowerings which the frontend performs for dart2js.
-const List<int> _allEnabledLateLowerings = [
-  LateLowering.uninitializedNonFinalInstanceField,
-  LateLowering.uninitializedFinalInstanceField,
-  LateLowering.initializedNonFinalInstanceField,
-  LateLowering.initializedFinalInstanceField,
-];
-
-final int _enabledLateLowerings = _foldLateLowerings(_allEnabledLateLowerings);
-
 /// A kernel [Target] to configure the Dart Front End for dart2js.
 class Dart2jsTarget extends Target {
   @override
@@ -90,10 +77,7 @@
   bool get enableNoSuchMethodForwarders => true;
 
   @override
-  int get enabledLateLowerings =>
-      (options != null && options!.experimentLateInstanceVariables)
-          ? LateLowering.none
-          : _enabledLateLowerings;
+  int get enabledLateLowerings => LateLowering.none;
 
   @override
   bool get supportsLateLoweringSentinel => true;
diff --git a/pkg/compiler/lib/src/kernel/transformations/late_lowering.dart b/pkg/compiler/lib/src/kernel/transformations/late_lowering.dart
index 212aac6..d2b3a00 100644
--- a/pkg/compiler/lib/src/kernel/transformations/late_lowering.dart
+++ b/pkg/compiler/lib/src/kernel/transformations/late_lowering.dart
@@ -24,7 +24,6 @@
   final CoreTypes _coreTypes;
 
   final bool _omitLateNames;
-  final bool _lowerInstanceVariables;
 
   final _Reader _readLocal;
   final _Reader _readField;
@@ -51,8 +50,6 @@
 
   LateLowering(this._coreTypes, CompilerOptions? _options)
       : _omitLateNames = _options?.omitLateNames ?? false,
-        _lowerInstanceVariables =
-            _options?.experimentLateInstanceVariables ?? false,
         _readLocal = _Reader(_coreTypes.cellReadLocal),
         _readField = _Reader(_coreTypes.cellReadField),
         _readInitialized = _Reader(_coreTypes.initializedCellRead),
@@ -72,7 +69,7 @@
       field.isLate && field.isStatic && field.initializer == null;
 
   bool _shouldLowerInstanceField(Field field) =>
-      field.isLate && !field.isStatic && _lowerInstanceVariables;
+      field.isLate && !field.isStatic;
 
   String _mangleFieldName(Field field) {
     assert(_shouldLowerInstanceField(field));
diff --git a/pkg/compiler/lib/src/options.dart b/pkg/compiler/lib/src/options.dart
index a4097df..9000dab 100644
--- a/pkg/compiler/lib/src/options.dart
+++ b/pkg/compiler/lib/src/options.dart
@@ -491,10 +491,6 @@
   /// called.
   bool experimentCallInstrumentation = false;
 
-  /// Use the dart2js lowering of late instance variables rather than the CFE
-  /// lowering.
-  bool experimentLateInstanceVariables = false;
-
   /// When null-safety is enabled, whether the compiler should emit code with
   /// unsound or sound semantics.
   ///
@@ -619,8 +615,6 @@
           _hasOption(options, Flags.experimentUnreachableMethodsThrow)
       ..experimentCallInstrumentation =
           _hasOption(options, Flags.experimentCallInstrumentation)
-      ..experimentLateInstanceVariables =
-          _hasOption(options, Flags.experimentLateInstanceVariables)
       ..generateSourceMap = !_hasOption(options, Flags.noSourceMaps)
       ..outputUri = _extractUriOption(options, '--out=')
       ..platformBinaries = platformBinaries
diff --git a/pkg/compiler/lib/src/ssa/nodes.dart b/pkg/compiler/lib/src/ssa/nodes.dart
index 47298de..ef0a3fa 100644
--- a/pkg/compiler/lib/src/ssa/nodes.dart
+++ b/pkg/compiler/lib/src/ssa/nodes.dart
@@ -1147,6 +1147,9 @@
   AbstractBool isNull(AbstractValueDomain domain) =>
       domain.isNull(instructionType);
 
+  AbstractBool isLateSentinel(AbstractValueDomain domain) =>
+      domain.isLateSentinel(instructionType);
+
   AbstractBool isConflicting(AbstractValueDomain domain) =>
       domain.isEmpty(instructionType);
 
@@ -1159,9 +1162,6 @@
   AbstractBool isPrimitiveBoolean(AbstractValueDomain domain) =>
       domain.isPrimitiveBoolean(instructionType);
 
-  AbstractBool isPrimitiveArray(AbstractValueDomain domain) =>
-      domain.isPrimitiveArray(instructionType);
-
   AbstractBool isIndexablePrimitive(AbstractValueDomain domain) =>
       domain.isIndexablePrimitive(instructionType);
 
diff --git a/pkg/compiler/lib/src/ssa/optimize.dart b/pkg/compiler/lib/src/ssa/optimize.dart
index 9faff8ee..bf83ffc 100644
--- a/pkg/compiler/lib/src/ssa/optimize.dart
+++ b/pkg/compiler/lib/src/ssa/optimize.dart
@@ -447,7 +447,8 @@
 
   ConstantValue getConstantFromType(HInstruction node) {
     if (node.isValue(_abstractValueDomain) &&
-        node.isNull(_abstractValueDomain).isDefinitelyFalse) {
+        node.isNull(_abstractValueDomain).isDefinitelyFalse &&
+        node.isLateSentinel(_abstractValueDomain).isDefinitelyFalse) {
       ConstantValue value =
           _abstractValueDomain.getPrimitiveValue(node.instructionType);
       if (value.isBool) {
@@ -1167,14 +1168,13 @@
   @override
   HInstruction visitIsLateSentinel(HIsLateSentinel node) {
     HInstruction value = node.inputs[0];
-    if (value is HConstant) {
-      return _graph.addConstantBool(
-          value.constant is LateSentinelConstantValue, _closedWorld);
+    AbstractBool isLateSentinel = value.isLateSentinel(_abstractValueDomain);
+    if (isLateSentinel.isDefinitelyTrue) {
+      return _graph.addConstantBool(true, _closedWorld);
+    } else if (isLateSentinel.isDefinitelyFalse) {
+      return _graph.addConstantBool(false, _closedWorld);
     }
 
-    // TODO(fishythefish): Simplify to `false` when the input cannot evalute to
-    // the sentinel. This can be implemented in the powerset domain.
-
     return super.visitIsLateSentinel(node);
   }
 
diff --git a/pkg/compiler/lib/src/ssa/type_builder.dart b/pkg/compiler/lib/src/ssa/type_builder.dart
index 39140be..84a46b6 100644
--- a/pkg/compiler/lib/src/ssa/type_builder.dart
+++ b/pkg/compiler/lib/src/ssa/type_builder.dart
@@ -49,7 +49,7 @@
 
   /// Create a type mask for 'trusting' a DartType. Returns `null` if there is
   /// no approximating type mask (i.e. the type mask would be `dynamic`).
-  AbstractValue trustTypeMask(DartType type) {
+  AbstractValue trustTypeMask(DartType type, {bool hasLateSentinel: false}) {
     if (type == null) return null;
     type = builder.localsHandler.substInContext(type);
     if (_closedWorld.dartTypes.isTopType(type)) return null;
@@ -59,15 +59,20 @@
     if (type is! InterfaceType) return null;
     // The type element is either a class or the void element.
     ClassEntity element = (type as InterfaceType).element;
-    return includeNull
+    AbstractValue mask = includeNull
         ? _abstractValueDomain.createNullableSubtype(element)
         : _abstractValueDomain.createNonNullSubtype(element);
+    if (hasLateSentinel) mask = _abstractValueDomain.includeLateSentinel(mask);
+    return mask;
   }
 
   /// Create an instruction to simply trust the provided type.
   HInstruction _trustType(HInstruction original, DartType type) {
     assert(type != null);
-    AbstractValue mask = trustTypeMask(type);
+    bool hasLateSentinel = _abstractValueDomain
+        .isLateSentinel(original.instructionType)
+        .isPotentiallyTrue;
+    AbstractValue mask = trustTypeMask(type, hasLateSentinel: hasLateSentinel);
     if (mask == null) return original;
     return new HTypeKnown.pinned(mask, original);
   }
diff --git a/pkg/compiler/lib/src/util/util.dart b/pkg/compiler/lib/src/util/util.dart
index 6a7894f..7c5f50b 100644
--- a/pkg/compiler/lib/src/util/util.dart
+++ b/pkg/compiler/lib/src/util/util.dart
@@ -233,15 +233,6 @@
   buffer.write(string);
 }
 
-int computeHashCode(part1, [part2, part3, part4, part5]) {
-  return (part1.hashCode ^
-          part2.hashCode ^
-          part3.hashCode ^
-          part4.hashCode ^
-          part5.hashCode) &
-      0x3fffffff;
-}
-
 class Pair<A, B> {
   final A a;
   final B b;
diff --git a/pkg/compiler/test/analyses/dart2js_allowed.json b/pkg/compiler/test/analyses/dart2js_allowed.json
index 242faa0..d9943f4 100644
--- a/pkg/compiler/test/analyses/dart2js_allowed.json
+++ b/pkg/compiler/test/analyses/dart2js_allowed.json
@@ -47,66 +47,10 @@
     "Dynamic access of 'memberContext'.": 1,
     "Dynamic access of 'name'.": 1
   },
-  "pkg/compiler/lib/src/inferrer/typemasks/container_type_mask.dart": {
-    "Dynamic access of 'isNullable'.": 2,
-    "Dynamic access of 'isEmptyOrNull'.": 1,
-    "Dynamic access of 'isContainer'.": 1,
-    "Dynamic access of 'elementType'.": 2,
-    "Dynamic access of 'length'.": 1,
-    "Dynamic access of 'forwardTo'.": 1,
-    "Dynamic access of 'allocationNode'.": 1,
-    "Dynamic access of 'allocationElement'.": 1
-  },
-  "pkg/compiler/lib/src/inferrer/typemasks/dictionary_type_mask.dart": {
-    "Dynamic access of 'isNullable'.": 2,
-    "Dynamic access of 'isEmptyOrNull'.": 1,
-    "Dynamic access of 'isDictionary'.": 1,
-    "Dynamic access of 'forwardTo'.": 2,
-    "Dynamic access of 'keyType'.": 3,
-    "Dynamic access of 'valueType'.": 3,
-    "Dynamic access of 'masks::_typeMap'.": 2,
-    "Dynamic invocation of 'containsKey'.": 1,
-    "Dynamic invocation of 'nullable'.": 2,
-    "Dynamic invocation of 'union'.": 1,
-    "Dynamic invocation of 'forEach'.": 1,
-    "Dynamic access of 'isMap'.": 1
-  },
-  "pkg/compiler/lib/src/inferrer/typemasks/forwarding_type_mask.dart": {
-    "Dynamic access of 'isNullable'.": 1
-  },
-  "pkg/compiler/lib/src/inferrer/typemasks/map_type_mask.dart": {
-    "Dynamic access of 'isNullable'.": 2,
-    "Dynamic access of 'isEmptyOrNull'.": 1,
-    "Dynamic access of 'isMap'.": 1,
-    "Dynamic access of 'keyType'.": 4,
-    "Dynamic access of 'valueType'.": 2,
-    "Dynamic access of 'forwardTo'.": 2,
-    "Dynamic access of 'isDictionary'.": 1,
-    "Dynamic invocation of 'union'.": 1,
-    "Dynamic access of 'typeMap'.": 1,
-    "Dynamic access of 'values'.": 1,
-    "Dynamic invocation of 'fold'.": 1,
-    "Dynamic access of 'allocationNode'.": 1,
-    "Dynamic access of 'allocationElement'.": 1
-  },
-  "pkg/compiler/lib/src/inferrer/typemasks/set_type_mask.dart": {
-    "Dynamic access of 'isNullable'.": 2,
-    "Dynamic access of 'isEmptyOrNull'.": 1,
-    "Dynamic access of 'isSet'.": 1,
-    "Dynamic access of 'elementType'.": 2,
-    "Dynamic access of 'forwardTo'.": 1
-  },
   "pkg/compiler/lib/src/inferrer/typemasks/type_mask.dart": {
     "Dynamic access of 'isForwarding'.": 1,
     "Dynamic access of 'forwardTo'.": 1
   },
-  "pkg/compiler/lib/src/inferrer/typemasks/union_type_mask.dart": {
-    "Dynamic invocation of 'nonNullable'.": 2,
-    "Dynamic access of 'isNullable'.": 3,
-    "Dynamic access of 'isUnion'.": 2,
-    "Dynamic access of 'disjointMasks'.": 3,
-    "Dynamic invocation of 'contains'.": 1
-  },
   "pkg/compiler/lib/src/helpers/expensive_map.dart": {
     "Dynamic access of 'length'.": 1,
     "Dynamic access of 'isEmpty'.": 1,
diff --git a/pkg/compiler/test/inference/type_combination_test.dart b/pkg/compiler/test/inference/type_combination_test.dart
index 517ac76..0588216 100644
--- a/pkg/compiler/test/inference/type_combination_test.dart
+++ b/pkg/compiler/test/inference/type_combination_test.dart
@@ -69,19 +69,23 @@
 class RuleSet {
   final name;
   final operate;
-  final Set typesSeen = new Set();
-  final Set pairsSeen = new Set();
+  final Set typesSeen = {};
+  final Set pairsSeen = {};
 
   RuleSet(this.name, this.operate);
 
   void rule(type1, type2, result) {
-    typesSeen..add(type1)..add(type2);
-    var pair1 = new Pair(type1, type2);
-    var pair2 = new Pair(type2, type1);
+    typesSeen
+      ..add(type1)
+      ..add(type2);
+    var pair1 = Pair(type1, type2);
+    var pair2 = Pair(type2, type1);
     if (pairsSeen.contains(pair1)) {
       Expect.isFalse(true, 'Redundant rule ($type1, $type2, ...)');
     }
-    pairsSeen..add(pair1)..add(pair2);
+    pairsSeen
+      ..add(pair1)
+      ..add(pair2);
 
     var r1 = operate(type1, type2);
     var r2 = operate(type2, type1);
@@ -90,8 +94,10 @@
   }
 
   void check(type1, type2, predicate) {
-    typesSeen..add(type1)..add(type2);
-    var pair = new Pair(type1, type2);
+    typesSeen
+      ..add(type1)
+      ..add(type2);
+    var pair = Pair(type1, type2);
     pairsSeen..add(pair);
     var result = operate(type1, type2);
     Expect.isTrue(predicate(result));
@@ -100,7 +106,7 @@
   void validateCoverage() {
     for (var type1 in typesSeen) {
       for (var type2 in typesSeen) {
-        var pair = new Pair(type1, type2);
+        var pair = Pair(type1, type2);
         if (!pairsSeen.contains(pair)) {
           Expect.isTrue(false, 'Missing rule: $name($type1, $type2)');
         }
@@ -111,7 +117,7 @@
 
 void testUnion(JClosedWorld closedWorld) {
   AbstractValueDomain commonMasks = closedWorld.abstractValueDomain;
-  RuleSet ruleSet = new RuleSet(
+  RuleSet ruleSet = RuleSet(
       'union', (t1, t2) => simplify(t1.union(t2, commonMasks), commonMasks));
   rule(type1, type2, result) => ruleSet.rule(type1, type2, result);
   check(type1, type2, predicate) => ruleSet.check(type1, type2, predicate);
@@ -422,7 +428,7 @@
 }
 
 void testIntersection(JClosedWorld closedWorld) {
-  RuleSet ruleSet = new RuleSet('intersection',
+  RuleSet ruleSet = RuleSet('intersection',
       (t1, t2) => t1.intersection(t2, closedWorld.abstractValueDomain));
   rule(type1, type2, result) => ruleSet.rule(type1, type2, result);
 
@@ -566,12 +572,12 @@
   rule(
       jsIndexable,
       potentialArray,
-      new TypeMask.nonNullSubtype(
+      TypeMask.nonNullSubtype(
           closedWorld.commonElements.jsArrayClass, closedWorld));
   rule(
       jsIndexable,
       potentialString,
-      new TypeMask.nonNullSubtype(
+      TypeMask.nonNullSubtype(
           closedWorld.commonElements.jsStringClass, closedWorld));
   rule(jsIndexable, jsBooleanOrNull, emptyType);
   rule(jsIndexable, jsNumberOrNull, emptyType);
@@ -738,7 +744,7 @@
 
 void testRegressions(JClosedWorld closedWorld) {
   TypeMask nonNullPotentialString =
-      new TypeMask.nonNullSubtype(patternClass, closedWorld);
+      TypeMask.nonNullSubtype(patternClass, closedWorld);
   Expect.equals(
       potentialString,
       jsStringOrNull.union(
@@ -776,67 +782,67 @@
   LibraryEntity coreLibrary = commonElements.coreLibrary;
   patternClass = elementEnvironment.lookupClass(coreLibrary, 'Pattern');
 
-  nonPrimitive1 = new TypeMask.nonNullSubtype(
-      closedWorld.commonElements.mapClass, closedWorld);
-  nonPrimitive2 = new TypeMask.nonNullSubtype(
+  nonPrimitive1 =
+      TypeMask.nonNullSubtype(closedWorld.commonElements.mapClass, closedWorld);
+  nonPrimitive2 = TypeMask.nonNullSubtype(
       closedWorld.commonElements.functionClass, closedWorld);
   potentialArray =
-      new TypeMask.subtype(closedWorld.commonElements.listClass, closedWorld);
-  potentialString = new TypeMask.subtype(patternClass, closedWorld);
-  jsInterceptor = new TypeMask.nonNullSubclass(
+      TypeMask.subtype(closedWorld.commonElements.listClass, closedWorld);
+  potentialString = TypeMask.subtype(patternClass, closedWorld);
+  jsInterceptor = TypeMask.nonNullSubclass(
       closedWorld.commonElements.jsInterceptorClass, closedWorld);
-  jsArrayOrNull = new TypeMask.subclass(
+  jsArrayOrNull =
+      TypeMask.subclass(closedWorld.commonElements.jsArrayClass, closedWorld);
+  jsReadableArray = TypeMask.nonNullSubclass(
       closedWorld.commonElements.jsArrayClass, closedWorld);
-  jsReadableArray = new TypeMask.nonNullSubclass(
-      closedWorld.commonElements.jsArrayClass, closedWorld);
-  jsMutableArrayOrNull = new TypeMask.subclass(
+  jsMutableArrayOrNull = TypeMask.subclass(
       closedWorld.commonElements.jsMutableArrayClass, closedWorld);
-  jsMutableArray = new TypeMask.nonNullSubclass(
+  jsMutableArray = TypeMask.nonNullSubclass(
       closedWorld.commonElements.jsMutableArrayClass, closedWorld);
-  jsFixedArrayOrNull = new TypeMask.exact(
+  jsFixedArrayOrNull =
+      TypeMask.exact(closedWorld.commonElements.jsFixedArrayClass, closedWorld);
+  jsFixedArray = TypeMask.nonNullExact(
       closedWorld.commonElements.jsFixedArrayClass, closedWorld);
-  jsFixedArray = new TypeMask.nonNullExact(
-      closedWorld.commonElements.jsFixedArrayClass, closedWorld);
-  jsExtendableArrayOrNull = new TypeMask.exact(
+  jsExtendableArrayOrNull = TypeMask.exact(
       closedWorld.commonElements.jsExtendableArrayClass, closedWorld);
-  jsExtendableArray = new TypeMask.nonNullExact(
+  jsExtendableArray = TypeMask.nonNullExact(
       closedWorld.commonElements.jsExtendableArrayClass, closedWorld);
-  jsUnmodifiableArrayOrNull = new TypeMask.exact(
+  jsUnmodifiableArrayOrNull = TypeMask.exact(
       closedWorld.commonElements.jsUnmodifiableArrayClass, closedWorld);
-  jsUnmodifiableArray = new TypeMask.nonNullExact(
+  jsUnmodifiableArray = TypeMask.nonNullExact(
       closedWorld.commonElements.jsUnmodifiableArrayClass, closedWorld);
-  jsIndexableOrNull = new TypeMask.subtype(
+  jsIndexableOrNull = TypeMask.subtype(
       closedWorld.commonElements.jsIndexableClass, closedWorld);
-  jsIndexable = new TypeMask.nonNullSubtype(
+  jsIndexable = TypeMask.nonNullSubtype(
       closedWorld.commonElements.jsIndexableClass, closedWorld);
-  jsInterceptorOrNull = new TypeMask.subclass(
+  jsInterceptorOrNull = TypeMask.subclass(
       closedWorld.commonElements.jsInterceptorClass, closedWorld);
   jsStringOrNull =
-      new TypeMask.exact(closedWorld.commonElements.jsStringClass, closedWorld);
-  jsString = new TypeMask.nonNullExact(
+      TypeMask.exact(closedWorld.commonElements.jsStringClass, closedWorld);
+  jsString = TypeMask.nonNullExact(
       closedWorld.commonElements.jsStringClass, closedWorld);
-  jsBoolean = new TypeMask.nonNullExact(
+  jsBoolean = TypeMask.nonNullExact(
       closedWorld.commonElements.jsBoolClass, closedWorld);
-  jsNumber = new TypeMask.nonNullSubclass(
+  jsNumber = TypeMask.nonNullSubclass(
       closedWorld.commonElements.jsNumberClass, closedWorld);
-  jsInteger = new TypeMask.nonNullExact(
-      closedWorld.commonElements.jsIntClass, closedWorld);
-  jsNumNotInt = new TypeMask.nonNullExact(
+  jsInteger =
+      TypeMask.nonNullExact(closedWorld.commonElements.jsIntClass, closedWorld);
+  jsNumNotInt = TypeMask.nonNullExact(
       closedWorld.commonElements.jsNumNotIntClass, closedWorld);
   jsBooleanOrNull =
-      new TypeMask.exact(closedWorld.commonElements.jsBoolClass, closedWorld);
-  jsNumberOrNull = new TypeMask.subclass(
-      closedWorld.commonElements.jsNumberClass, closedWorld);
+      TypeMask.exact(closedWorld.commonElements.jsBoolClass, closedWorld);
+  jsNumberOrNull =
+      TypeMask.subclass(closedWorld.commonElements.jsNumberClass, closedWorld);
   jsIntegerOrNull =
-      new TypeMask.exact(closedWorld.commonElements.jsIntClass, closedWorld);
-  jsNumNotIntOrNull = new TypeMask.exact(
-      closedWorld.commonElements.jsNumNotIntClass, closedWorld);
-  nullType = const TypeMask.empty();
-  objectType = new TypeMask.nonNullSubclass(
+      TypeMask.exact(closedWorld.commonElements.jsIntClass, closedWorld);
+  jsNumNotIntOrNull =
+      TypeMask.exact(closedWorld.commonElements.jsNumNotIntClass, closedWorld);
+  nullType = TypeMask.empty();
+  objectType = TypeMask.nonNullSubclass(
       closedWorld.commonElements.objectClass, closedWorld);
-  emptyType = const TypeMask.nonNullEmpty();
-  dynamicType = new TypeMask.subclass(
-      closedWorld.commonElements.objectClass, closedWorld);
+  emptyType = TypeMask.nonNullEmpty();
+  dynamicType =
+      TypeMask.subclass(closedWorld.commonElements.objectClass, closedWorld);
 
   jsInterceptorOrComparable =
       interceptorOrComparable(closedWorld, nullable: false);
diff --git a/pkg/compiler/test/inference/type_mask2_test.dart b/pkg/compiler/test/inference/type_mask2_test.dart
index e5056f8..5fcc9b1 100644
--- a/pkg/compiler/test/inference/type_mask2_test.dart
+++ b/pkg/compiler/test/inference/type_mask2_test.dart
@@ -35,18 +35,20 @@
     List<ClassEntity> containedClasses}) {
   AbstractValueDomain commonMasks = closedWorld.abstractValueDomain;
   bool isNullable = masks.any((FlatTypeMask mask) => mask.isNullable);
+  bool hasLateSentinel = masks.any((FlatTypeMask mask) => mask.hasLateSentinel);
   List<FlatTypeMask> disjoint = <FlatTypeMask>[];
   UnionTypeMask.unionOfHelper(masks, disjoint, commonMasks);
   Expect.listEquals(disjointMasks, disjoint,
       'Unexpected disjoint masks: $disjoint, expected $disjointMasks.');
   if (flattened == null) {
     Expect.throws(
-        () => UnionTypeMask.flatten(disjoint, isNullable, commonMasks),
+        () => UnionTypeMask.flatten(disjoint, commonMasks,
+            includeNull: isNullable, includeLateSentinel: hasLateSentinel),
         (e) => e is ArgumentError,
         'Expect argument error on flattening of $disjoint.');
   } else {
-    TypeMask flattenResult =
-        UnionTypeMask.flatten(disjoint, isNullable, commonMasks);
+    TypeMask flattenResult = UnionTypeMask.flatten(disjoint, commonMasks,
+        includeNull: isNullable, includeLateSentinel: hasLateSentinel);
     Expect.equals(
         flattened,
         flattenResult,
@@ -120,16 +122,25 @@
         containedClasses: containedClasses);
   }
 
-  TypeMask empty = const TypeMask.nonNullEmpty();
-  TypeMask subclassObject = new TypeMask.nonNullSubclass(Object_, closedWorld);
-  TypeMask exactA = new TypeMask.nonNullExact(A, closedWorld);
-  TypeMask subclassA = new TypeMask.nonNullSubclass(A, closedWorld);
-  TypeMask subtypeA = new TypeMask.nonNullSubtype(A, closedWorld);
-  TypeMask exactB = new TypeMask.nonNullExact(B, closedWorld);
-  TypeMask subclassB = new TypeMask.nonNullSubclass(B, closedWorld);
-  TypeMask exactC = new TypeMask.nonNullExact(C, closedWorld);
-  TypeMask exactD = new TypeMask.nonNullExact(D, closedWorld);
-  TypeMask exactE = new TypeMask.nonNullExact(E, closedWorld);
+  TypeMask empty = TypeMask.nonNullEmpty();
+  TypeMask sentinel = TypeMask.nonNullEmpty(hasLateSentinel: true);
+  TypeMask subclassObject = TypeMask.nonNullSubclass(Object_, closedWorld);
+  TypeMask subclassObjectOrSentinel =
+      TypeMask.nonNullSubclass(Object_, closedWorld, hasLateSentinel: true);
+  TypeMask exactA = TypeMask.nonNullExact(A, closedWorld);
+  TypeMask exactAOrSentinel =
+      TypeMask.nonNullExact(A, closedWorld, hasLateSentinel: true);
+  TypeMask subclassA = TypeMask.nonNullSubclass(A, closedWorld);
+  TypeMask subtypeA = TypeMask.nonNullSubtype(A, closedWorld);
+  TypeMask subtypeAOrSentinel =
+      TypeMask.nonNullSubtype(A, closedWorld, hasLateSentinel: true);
+  TypeMask exactB = TypeMask.nonNullExact(B, closedWorld);
+  TypeMask exactBOrSentinel =
+      TypeMask.nonNullExact(B, closedWorld, hasLateSentinel: true);
+  TypeMask subclassB = TypeMask.nonNullSubclass(B, closedWorld);
+  TypeMask exactC = TypeMask.nonNullExact(C, closedWorld);
+  TypeMask exactD = TypeMask.nonNullExact(D, closedWorld);
+  TypeMask exactE = TypeMask.nonNullExact(E, closedWorld);
 
   check([],
       result: empty,
@@ -213,6 +224,52 @@
       disjointMasks: [subclassB, exactA],
       flattened: subclassObject,
       containedClasses: [A, B, E]);
+
+  check([sentinel],
+      result: sentinel,
+      disjointMasks: const [],
+      flattened: null,
+      containedClasses: const []);
+
+  check([sentinel, sentinel],
+      result: sentinel,
+      disjointMasks: const [],
+      flattened: null,
+      containedClasses: const []);
+
+  check([empty, sentinel],
+      result: sentinel,
+      disjointMasks: const [],
+      flattened: null,
+      containedClasses: const []);
+
+  check([sentinel, empty],
+      result: sentinel,
+      disjointMasks: const [],
+      flattened: null,
+      containedClasses: const []);
+
+  check([exactAOrSentinel],
+      result: exactAOrSentinel,
+      disjointMasks: [exactA],
+      flattened: subtypeAOrSentinel, // TODO(37602): Imprecise.
+      containedClasses: [A]);
+
+  check([exactA, exactAOrSentinel],
+      result: exactAOrSentinel,
+      disjointMasks: [exactA],
+      flattened: subtypeAOrSentinel, // TODO(37602): Imprecise.
+      containedClasses: [A]);
+
+  check([exactAOrSentinel, exactB],
+      disjointMasks: [exactA, exactB],
+      flattened: subclassObjectOrSentinel,
+      containedClasses: [A, B]);
+
+  check([exactAOrSentinel, exactBOrSentinel],
+      disjointMasks: [exactA, exactB],
+      flattened: subclassObjectOrSentinel,
+      containedClasses: [A, B]);
 }
 
 Future testStringSubtypes() async {
@@ -241,11 +298,10 @@
   Expect.isFalse(closedWorld.classHierarchy.isIndirectlyInstantiated(JSString));
   Expect.isTrue(closedWorld.classHierarchy.isInstantiated(JSString));
 
-  TypeMask subtypeString = new TypeMask.nonNullSubtype(String_, closedWorld);
-  TypeMask exactJSString = new TypeMask.nonNullExact(JSString, closedWorld);
-  TypeMask subtypeJSString = new TypeMask.nonNullSubtype(JSString, closedWorld);
-  TypeMask subclassJSString =
-      new TypeMask.nonNullSubclass(JSString, closedWorld);
+  TypeMask subtypeString = TypeMask.nonNullSubtype(String_, closedWorld);
+  TypeMask exactJSString = TypeMask.nonNullExact(JSString, closedWorld);
+  TypeMask subtypeJSString = TypeMask.nonNullSubtype(JSString, closedWorld);
+  TypeMask subclassJSString = TypeMask.nonNullSubclass(JSString, closedWorld);
 
   Expect.equals(exactJSString, subtypeString);
   Expect.equals(exactJSString, subtypeJSString);
diff --git a/pkg/compiler/test/inference/type_mask_test_helper.dart b/pkg/compiler/test/inference/type_mask_test_helper.dart
index ba29b08..8eb0171 100644
--- a/pkg/compiler/test/inference/type_mask_test_helper.dart
+++ b/pkg/compiler/test/inference/type_mask_test_helper.dart
@@ -16,7 +16,9 @@
   if (value is ForwardingTypeMask) {
     return simplify(value.forwardTo, domain);
   } else if (value is UnionTypeMask) {
-    return UnionTypeMask.flatten(value.disjointMasks, value.isNullable, domain);
+    return UnionTypeMask.flatten(value.disjointMasks, domain,
+        includeNull: value.isNullable,
+        includeLateSentinel: value.hasLateSentinel);
   } else {
     return value;
   }
diff --git a/pkg/front_end/testcases/dart2js/late_fields.dart.strong.expect b/pkg/front_end/testcases/dart2js/late_fields.dart.strong.expect
index 02d73bd..a546427 100644
--- a/pkg/front_end/testcases/dart2js/late_fields.dart.strong.expect
+++ b/pkg/front_end/testcases/dart2js/late_fields.dart.strong.expect
@@ -1,33 +1,15 @@
 library /*isNonNullableByDefault*/;
 import self as self;
 import "dart:core" as core;
-import "dart:_internal" as _in;
 
 class C extends core::Object {
-  field core::int? _#C#a = null;
-  field core::int? _#C#b = null;
-  field core::int? _#C#c = null;
-  field core::int? _#C#d = null;
+  late field core::int a;
+  late final [setter] field core::int b;
+  late field core::int c = 1.{core::int::unary-}(){() → core::int};
+  late final field core::int d = 1.{core::int::unary-}(){() → core::int};
   synthetic constructor •() → self::C
     : super core::Object::•()
     ;
-  get a() → core::int
-    return let final core::int? #t1 = this.{self::C::_#C#a}{core::int?} in #t1 == null ?{core::int} throw new _in::LateError::fieldNI("a") : #t1{core::int};
-  set a(core::int #t2) → void
-    this.{self::C::_#C#a} = #t2;
-  get b() → core::int
-    return let final core::int? #t3 = this.{self::C::_#C#b}{core::int?} in #t3 == null ?{core::int} throw new _in::LateError::fieldNI("b") : #t3{core::int};
-  set b(core::int #t4) → void
-    if(this.{self::C::_#C#b}{core::int?} == null)
-      this.{self::C::_#C#b} = #t4;
-    else
-      throw new _in::LateError::fieldAI("b");
-  get c() → core::int
-    return let final core::int? #t5 = this.{self::C::_#C#c}{core::int?} in #t5 == null ?{core::int} this.{self::C::_#C#c} = 1.{core::int::unary-}(){() → core::int} : #t5{core::int};
-  set c(core::int #t6) → void
-    this.{self::C::_#C#c} = #t6;
-  get d() → core::int
-    return let final core::int? #t7 = this.{self::C::_#C#d}{core::int?} in #t7 == null ?{core::int} let final core::int #t8 = 1.{core::int::unary-}(){() → core::int} in this.{self::C::_#C#d}{core::int?} == null ?{core::int} this.{self::C::_#C#d} = #t8 : throw new _in::LateError::fieldADI("d") : #t7{core::int};
   static method _#new#tearOff() → self::C
     return new self::C::•();
 }
diff --git a/pkg/front_end/testcases/dart2js/late_fields.dart.strong.transformed.expect b/pkg/front_end/testcases/dart2js/late_fields.dart.strong.transformed.expect
index bf9c284..4b80ddd 100644
--- a/pkg/front_end/testcases/dart2js/late_fields.dart.strong.transformed.expect
+++ b/pkg/front_end/testcases/dart2js/late_fields.dart.strong.transformed.expect
@@ -1,35 +1,46 @@
 library /*isNonNullableByDefault*/;
 import self as self;
 import "dart:core" as core;
+import "dart:_late_helper" as _la;
 import "dart:_internal" as _in;
 
 class C extends core::Object {
-  field core::int? _#C#a = null;
-  field core::int? _#C#b = null;
-  field core::int? _#C#c = null;
-  field core::int? _#C#d = null;
+  field core::int _#C#a = _in::createSentinel<core::int>();
+  field core::int _#C#b = _in::createSentinel<core::int>();
+  field core::int _#C#c = _in::createSentinel<core::int>();
+  field core::int _#C#d = _in::createSentinel<core::int>();
   synthetic constructor •() → self::C
     : super core::Object::•()
     ;
-  get a() → core::int
-    return let final core::int? #t1 = this.{self::C::_#C#a}{core::int?} in #t1 == null ?{core::int} throw new _in::LateError::fieldNI("a") : #t1{core::int};
-  set a(core::int #t2) → void
-    this.{self::C::_#C#a} = #t2;
-  get b() → core::int
-    return let final core::int? #t3 = this.{self::C::_#C#b}{core::int?} in #t3 == null ?{core::int} throw new _in::LateError::fieldNI("b") : #t3{core::int};
-  set b(core::int #t4) → void
-    if(this.{self::C::_#C#b}{core::int?} == null)
-      this.{self::C::_#C#b} = #t4;
-    else
-      throw new _in::LateError::fieldAI("b");
-  get c() → core::int
-    return let final core::int? #t5 = this.{self::C::_#C#c}{core::int?} in #t5 == null ?{core::int} this.{self::C::_#C#c} = 1.{core::int::unary-}(){() → core::int} : #t5{core::int};
-  set c(core::int #t6) → void
-    this.{self::C::_#C#c} = #t6;
-  get d() → core::int
-    return let final core::int? #t7 = this.{self::C::_#C#d}{core::int?} in #t7 == null ?{core::int} let final core::int #t8 = 1.{core::int::unary-}(){() → core::int} in this.{self::C::_#C#d}{core::int?} == null ?{core::int} this.{self::C::_#C#d} = #t8 : throw new _in::LateError::fieldADI("d") : #t7{core::int};
   static method _#new#tearOff() → self::C
     return new self::C::•();
+  get a() → core::int
+    return _la::_lateReadCheck<core::int>(this.{self::C::_#C#a}{core::int}, "a");
+  set a(core::int value) → void
+    this.{self::C::_#C#a} = value;
+  get b() → core::int
+    return _la::_lateReadCheck<core::int>(this.{self::C::_#C#b}{core::int}, "b");
+  set b(core::int value) → void {
+    _la::_lateWriteOnceCheck(this.{self::C::_#C#b}{core::int}, "b");
+    this.{self::C::_#C#b} = value;
+  }
+  get c() → core::int {
+    core::int value = this.{self::C::_#C#c}{core::int};
+    if(_in::isSentinel(value))
+      value = this.{self::C::_#C#c} = 1.{core::int::unary-}(){() → core::int};
+    return value;
+  }
+  set c(core::int value) → void
+    this.{self::C::_#C#c} = value;
+  get d() → core::int {
+    core::int value = this.{self::C::_#C#d}{core::int};
+    if(_in::isSentinel(value)) {
+      final core::int result = 1.{core::int::unary-}(){() → core::int};
+      _la::_lateInitializeOnceCheck(this.{self::C::_#C#d}{core::int}, "d");
+      value = this.{self::C::_#C#d} = result;
+    }
+    return value;
+  }
 }
 static field self::C c = new self::C::•();
 static method main() → void {
@@ -61,5 +72,4 @@
 Extra constant evaluation status:
 Evaluated: InstanceInvocation @ org-dartlang-testcase:///late_fields.dart:15:16 -> DoubleConstant(-1.0)
 Evaluated: InstanceInvocation @ org-dartlang-testcase:///late_fields.dart:16:22 -> DoubleConstant(-1.0)
-Evaluated: VariableGet @ org-dartlang-testcase:///late_fields.dart:16:18 -> DoubleConstant(-1.0)
-Extra constant evaluation: evaluated: 93, effectively constant: 3
+Extra constant evaluation: evaluated: 77, effectively constant: 2
diff --git a/pkg/front_end/testcases/dart2js/late_fields.dart.weak.expect b/pkg/front_end/testcases/dart2js/late_fields.dart.weak.expect
index 95a7e38..a546427 100644
--- a/pkg/front_end/testcases/dart2js/late_fields.dart.weak.expect
+++ b/pkg/front_end/testcases/dart2js/late_fields.dart.weak.expect
@@ -1,33 +1,15 @@
 library /*isNonNullableByDefault*/;
 import self as self;
 import "dart:core" as core;
-import "dart:_internal" as _in;
 
 class C extends core::Object {
-  field core::int? _#C#a = _in::createSentinel<core::int>();
-  field core::int? _#C#b = _in::createSentinel<core::int>();
-  field core::int? _#C#c = _in::createSentinel<core::int>();
-  field core::int? _#C#d = _in::createSentinel<core::int>();
+  late field core::int a;
+  late final [setter] field core::int b;
+  late field core::int c = 1.{core::int::unary-}(){() → core::int};
+  late final field core::int d = 1.{core::int::unary-}(){() → core::int};
   synthetic constructor •() → self::C
     : super core::Object::•()
     ;
-  get a() → core::int
-    return let final core::int? #t1 = this.{self::C::_#C#a}{core::int?} in _in::isSentinel(#t1) ?{core::int} throw new _in::LateError::fieldNI("a") : #t1{core::int};
-  set a(core::int #t2) → void
-    this.{self::C::_#C#a} = #t2;
-  get b() → core::int
-    return let final core::int? #t3 = this.{self::C::_#C#b}{core::int?} in _in::isSentinel(#t3) ?{core::int} throw new _in::LateError::fieldNI("b") : #t3{core::int};
-  set b(core::int #t4) → void
-    if(_in::isSentinel(this.{self::C::_#C#b}{core::int?}))
-      this.{self::C::_#C#b} = #t4;
-    else
-      throw new _in::LateError::fieldAI("b");
-  get c() → core::int
-    return let final core::int? #t5 = this.{self::C::_#C#c}{core::int?} in _in::isSentinel(#t5) ?{core::int} this.{self::C::_#C#c} = 1.{core::int::unary-}(){() → core::int} : #t5{core::int};
-  set c(core::int #t6) → void
-    this.{self::C::_#C#c} = #t6;
-  get d() → core::int
-    return let final core::int #t7 = this.{self::C::_#C#d}{core::int?} in _in::isSentinel(#t7) ?{core::int} let final core::int #t8 = 1.{core::int::unary-}(){() → core::int} in _in::isSentinel(this.{self::C::_#C#d}{core::int?}) ?{core::int} this.{self::C::_#C#d} = #t8 : throw new _in::LateError::fieldADI("d") : #t7;
   static method _#new#tearOff() → self::C
     return new self::C::•();
 }
diff --git a/pkg/front_end/testcases/dart2js/late_fields.dart.weak.outline.expect b/pkg/front_end/testcases/dart2js/late_fields.dart.weak.outline.expect
index 57eb7ff..7c10260 100644
--- a/pkg/front_end/testcases/dart2js/late_fields.dart.weak.outline.expect
+++ b/pkg/front_end/testcases/dart2js/late_fields.dart.weak.outline.expect
@@ -3,19 +3,12 @@
 import "dart:core" as core;
 
 class C extends core::Object {
-  field core::int? _#C#a;
-  field core::int? _#C#b;
-  field core::int? _#C#c;
-  field core::int? _#C#d;
+  late field core::int a;
+  late final [setter] field core::int b;
+  late field core::int c;
+  late final field core::int d;
   synthetic constructor •() → self::C
     ;
-  get a() → core::int;
-  set a(core::int #t1) → void;
-  get b() → core::int;
-  set b(core::int #t2) → void;
-  get c() → core::int;
-  set c(core::int #t3) → void;
-  get d() → core::int;
   static method _#new#tearOff() → self::C
     return new self::C::•();
 }
diff --git a/pkg/front_end/testcases/dart2js/late_fields.dart.weak.transformed.expect b/pkg/front_end/testcases/dart2js/late_fields.dart.weak.transformed.expect
index 15df6e5..4b80ddd 100644
--- a/pkg/front_end/testcases/dart2js/late_fields.dart.weak.transformed.expect
+++ b/pkg/front_end/testcases/dart2js/late_fields.dart.weak.transformed.expect
@@ -1,35 +1,46 @@
 library /*isNonNullableByDefault*/;
 import self as self;
 import "dart:core" as core;
+import "dart:_late_helper" as _la;
 import "dart:_internal" as _in;
 
 class C extends core::Object {
-  field core::int? _#C#a = _in::createSentinel<core::int>();
-  field core::int? _#C#b = _in::createSentinel<core::int>();
-  field core::int? _#C#c = _in::createSentinel<core::int>();
-  field core::int? _#C#d = _in::createSentinel<core::int>();
+  field core::int _#C#a = _in::createSentinel<core::int>();
+  field core::int _#C#b = _in::createSentinel<core::int>();
+  field core::int _#C#c = _in::createSentinel<core::int>();
+  field core::int _#C#d = _in::createSentinel<core::int>();
   synthetic constructor •() → self::C
     : super core::Object::•()
     ;
-  get a() → core::int
-    return let final core::int? #t1 = this.{self::C::_#C#a}{core::int?} in _in::isSentinel(#t1) ?{core::int} throw new _in::LateError::fieldNI("a") : #t1{core::int};
-  set a(core::int #t2) → void
-    this.{self::C::_#C#a} = #t2;
-  get b() → core::int
-    return let final core::int? #t3 = this.{self::C::_#C#b}{core::int?} in _in::isSentinel(#t3) ?{core::int} throw new _in::LateError::fieldNI("b") : #t3{core::int};
-  set b(core::int #t4) → void
-    if(_in::isSentinel(this.{self::C::_#C#b}{core::int?}))
-      this.{self::C::_#C#b} = #t4;
-    else
-      throw new _in::LateError::fieldAI("b");
-  get c() → core::int
-    return let final core::int? #t5 = this.{self::C::_#C#c}{core::int?} in _in::isSentinel(#t5) ?{core::int} this.{self::C::_#C#c} = 1.{core::int::unary-}(){() → core::int} : #t5{core::int};
-  set c(core::int #t6) → void
-    this.{self::C::_#C#c} = #t6;
-  get d() → core::int
-    return let final core::int #t7 = this.{self::C::_#C#d}{core::int?} in _in::isSentinel(#t7) ?{core::int} let final core::int #t8 = 1.{core::int::unary-}(){() → core::int} in _in::isSentinel(this.{self::C::_#C#d}{core::int?}) ?{core::int} this.{self::C::_#C#d} = #t8 : throw new _in::LateError::fieldADI("d") : #t7;
   static method _#new#tearOff() → self::C
     return new self::C::•();
+  get a() → core::int
+    return _la::_lateReadCheck<core::int>(this.{self::C::_#C#a}{core::int}, "a");
+  set a(core::int value) → void
+    this.{self::C::_#C#a} = value;
+  get b() → core::int
+    return _la::_lateReadCheck<core::int>(this.{self::C::_#C#b}{core::int}, "b");
+  set b(core::int value) → void {
+    _la::_lateWriteOnceCheck(this.{self::C::_#C#b}{core::int}, "b");
+    this.{self::C::_#C#b} = value;
+  }
+  get c() → core::int {
+    core::int value = this.{self::C::_#C#c}{core::int};
+    if(_in::isSentinel(value))
+      value = this.{self::C::_#C#c} = 1.{core::int::unary-}(){() → core::int};
+    return value;
+  }
+  set c(core::int value) → void
+    this.{self::C::_#C#c} = value;
+  get d() → core::int {
+    core::int value = this.{self::C::_#C#d}{core::int};
+    if(_in::isSentinel(value)) {
+      final core::int result = 1.{core::int::unary-}(){() → core::int};
+      _la::_lateInitializeOnceCheck(this.{self::C::_#C#d}{core::int}, "d");
+      value = this.{self::C::_#C#d} = result;
+    }
+    return value;
+  }
 }
 static field self::C c = new self::C::•();
 static method main() → void {
@@ -61,5 +72,4 @@
 Extra constant evaluation status:
 Evaluated: InstanceInvocation @ org-dartlang-testcase:///late_fields.dart:15:16 -> DoubleConstant(-1.0)
 Evaluated: InstanceInvocation @ org-dartlang-testcase:///late_fields.dart:16:22 -> DoubleConstant(-1.0)
-Evaluated: VariableGet @ org-dartlang-testcase:///late_fields.dart:16:18 -> DoubleConstant(-1.0)
-Extra constant evaluation: evaluated: 97, effectively constant: 3
+Extra constant evaluation: evaluated: 77, effectively constant: 2
diff --git a/runtime/vm/compiler/aot/precompiler.cc b/runtime/vm/compiler/aot/precompiler.cc
index 6cd7640..c0e53d5 100644
--- a/runtime/vm/compiler/aot/precompiler.cc
+++ b/runtime/vm/compiler/aot/precompiler.cc
@@ -782,6 +782,7 @@
 }
 
 void Precompiler::CollectCallbackFields() {
+  PRECOMPILER_TIMER_SCOPE(this, CollectCallbackFields);
   HANDLESCOPE(T);
   Library& lib = Library::Handle(Z);
   Class& cls = Class::Handle(Z);
@@ -883,6 +884,7 @@
 }
 
 void Precompiler::AddCalleesOf(const Function& function, intptr_t gop_offset) {
+  PRECOMPILER_TIMER_SCOPE(this, AddCalleesOf);
   ASSERT(function.HasCode());
 
   const Code& code = Code::Handle(Z, function.CurrentCode());
@@ -987,6 +989,12 @@
 void Precompiler::AddCalleesOfHelper(const Object& entry,
                                      String* temp_selector,
                                      Class* temp_cls) {
+  const intptr_t cid = entry.GetClassId();
+  if ((cid == kOneByteStringCid) || (cid == kNullCid)) {
+    // Skip common leaf constants early in order to
+    // process object pools faster.
+    return;
+  }
   if (entry.IsUnlinkedCall()) {
     const auto& call_site = UnlinkedCall::Cast(entry);
     // A dynamic call.
@@ -1603,6 +1611,7 @@
 }
 
 void Precompiler::CheckForNewDynamicFunctions() {
+  PRECOMPILER_TIMER_SCOPE(this, CheckForNewDynamicFunctions);
   HANDLESCOPE(T);
   Library& lib = Library::Handle(Z);
   Class& cls = Class::Handle(Z);
diff --git a/runtime/vm/compiler/backend/inliner.cc b/runtime/vm/compiler/backend/inliner.cc
index 09359cc..d4748c4 100644
--- a/runtime/vm/compiler/backend/inliner.cc
+++ b/runtime/vm/compiler/backend/inliner.cc
@@ -2310,6 +2310,9 @@
 }
 
 bool FlowGraphInliner::FunctionHasPreferInlinePragma(const Function& function) {
+  if (!function.has_pragma()) {
+    return false;
+  }
   Thread* thread = dart::Thread::Current();
   COMPILER_TIMINGS_TIMER_SCOPE(thread, CheckForPragma);
   Object& options = Object::Handle();
@@ -2319,6 +2322,9 @@
 }
 
 bool FlowGraphInliner::FunctionHasNeverInlinePragma(const Function& function) {
+  if (!function.has_pragma()) {
+    return false;
+  }
   Thread* thread = dart::Thread::Current();
   COMPILER_TIMINGS_TIMER_SCOPE(thread, CheckForPragma);
   Object& options = Object::Handle();
diff --git a/runtime/vm/compiler/compiler_timings.h b/runtime/vm/compiler/compiler_timings.h
index 6f9b31b..2f63f78 100644
--- a/runtime/vm/compiler/compiler_timings.h
+++ b/runtime/vm/compiler/compiler_timings.h
@@ -20,6 +20,9 @@
   V(CompileAll)                                                                \
   V(Iterate)                                                                   \
   V(CompileFunction)                                                           \
+  V(AddCalleesOf)                                                              \
+  V(CheckForNewDynamicFunctions)                                               \
+  V(CollectCallbackFields)                                                     \
   V(PrecompileConstructors)                                                    \
   V(AttachOptimizedTypeTestingStub)                                            \
   V(TraceForRetainedFunctions)                                                 \
diff --git a/sdk/lib/_internal/js_runtime/lib/native_helper.dart b/sdk/lib/_internal/js_runtime/lib/native_helper.dart
index 4c404e0..8634929 100644
--- a/sdk/lib/_internal/js_runtime/lib/native_helper.dart
+++ b/sdk/lib/_internal/js_runtime/lib/native_helper.dart
@@ -260,7 +260,8 @@
 }
 
 String constructorNameFallback(object) {
-  return JS('String', '#(#)', _constructorNameFallback, object);
+  return JS('returns:String;effects:none;depends:none', '#(#)',
+      _constructorNameFallback, object);
 }
 
 var initNativeDispatchFlag; // null or true
diff --git a/sdk/lib/js_util/js_util.dart b/sdk/lib/js_util/js_util.dart
index ee3dd0f..bfe6cf3 100644
--- a/sdk/lib/js_util/js_util.dart
+++ b/sdk/lib/js_util/js_util.dart
@@ -195,6 +195,38 @@
   //     return _wrapToDart(jsObj);
 }
 
+/// Unchecked version for 0 arguments, only used in a CFE transformation.
+@pragma('dart2js:tryInline')
+dynamic _callConstructorUnchecked0(Object constr) {
+  return JS('Object', 'new #()', constr);
+}
+
+/// Unchecked version for 1 argument, only used in a CFE transformation.
+@pragma('dart2js:tryInline')
+dynamic _callConstructorUnchecked1(Object constr, Object? arg1) {
+  return JS('Object', 'new #(#)', constr, arg1);
+}
+
+/// Unchecked version for 2 arguments, only used in a CFE transformation.
+@pragma('dart2js:tryInline')
+dynamic _callConstructorUnchecked2(Object constr, Object? arg1, Object? arg2) {
+  return JS('Object', 'new #(#, #)', constr, arg1, arg2);
+}
+
+/// Unchecked version for 3 arguments, only used in a CFE transformation.
+@pragma('dart2js:tryInline')
+dynamic _callConstructorUnchecked3(
+    Object constr, Object? arg1, Object? arg2, Object? arg3) {
+  return JS('Object', 'new #(#, #, #)', constr, arg1, arg2, arg3);
+}
+
+/// Unchecked version for 4 arguments, only used in a CFE transformation.
+@pragma('dart2js:tryInline')
+dynamic _callConstructorUnchecked4(
+    Object constr, Object? arg1, Object? arg2, Object? arg3, Object? arg4) {
+  return JS('Object', 'new #(#, #, #, #)', constr, arg1, arg2, arg3, arg4);
+}
+
 /// Exception for when the promise is rejected with a `null` or `undefined`
 /// value.
 ///
diff --git a/tests/lib/js/js_util/properties_test.dart b/tests/lib/js/js_util/properties_test.dart
index 326de20..84a09c8 100644
--- a/tests/lib/js/js_util/properties_test.dart
+++ b/tests/lib/js/js_util/properties_test.dart
@@ -85,6 +85,19 @@
   external five(a, b, c, d, e);
 }
 
+@JS()
+external get Zero;
+@JS()
+external get One;
+@JS()
+external get Two;
+@JS()
+external get Three;
+@JS()
+external get Four;
+@JS()
+external get Five;
+
 main() {
   eval(r"""
     function Foo(a) {
@@ -157,6 +170,25 @@
     CallMethodTest.prototype.five = function(a, b, c, d, e) {
       return 'five';
     }
+
+    function Zero() {
+      this.count = 0;
+    }
+    function One(a) {
+      this.count = 1;
+    }
+    function Two(a, b) {
+      this.count = 2;
+    }
+    function Three(a, b, c) {
+      this.count = 3;
+    }
+    function Four(a, b, c, d) {
+      this.count = 4;
+    }
+    function Five(a, b, c, d, e) {
+      this.count = 5;
+    }
     """);
 
   group('newObject', () {
@@ -543,8 +575,131 @@
 
   group('callConstructor', () {
     test('typed object', () {
-      Foo f = js_util.callConstructor(JSFooType, [42]);
+      var f = js_util.callConstructor(JSFooType, [42]);
       expect(f.a, equals(42));
+
+      var f2 =
+          js_util.callConstructor(js_util.getProperty(f, 'constructor'), [5]);
+      expect(f2.a, equals(5));
+    });
+
+    test('typed literal', () {
+      ExampleTypedLiteral literal = js_util.callConstructor(
+          js_util.getProperty(ExampleTypedLiteral(), 'constructor'), []);
+      expect(literal.a, equals(null));
+    });
+
+    test('callConstructor with List edge cases', () {
+      expect(
+          js_util.getProperty(
+              js_util.callConstructor(Zero, List.empty()), 'count'),
+          equals(0));
+      expect(
+          js_util.getProperty(
+              js_util.callConstructor(Zero, List<int>.empty()), 'count'),
+          equals(0));
+
+      expect(
+          js_util.getProperty(
+              js_util.callConstructor(Two, List<int>.filled(2, 0)), 'count'),
+          equals(2));
+      expect(
+          js_util.getProperty(
+              js_util.callConstructor(Three, List<int>.generate(3, (i) => i)),
+              'count'),
+          equals(3));
+
+      Iterable<String> iterableStrings = <String>['foo', 'bar'];
+      expect(
+          js_util.getProperty(
+              js_util.callConstructor(Two, List.of(iterableStrings)), 'count'),
+          equals(2));
+
+      const l1 = [1, 2];
+      const l2 = [3, 4];
+      expect(
+          js_util.getProperty(
+              js_util.callConstructor(Four, List.from(l1)..addAll(l2)),
+              'count'),
+          equals(4));
+      expect(
+          js_util.getProperty(js_util.callConstructor(Four, l1 + l2), 'count'),
+          equals(4));
+      expect(
+          js_util.getProperty(
+              js_util.callConstructor(Four, List.unmodifiable([1, 2, 3, 4])),
+              'count'),
+          equals(4));
+
+      var setElements = {1, 2};
+      expect(
+          js_util.getProperty(
+              js_util.callConstructor(Two, setElements.toList()), 'count'),
+          equals(2));
+
+      var spreadList = [1, 2, 3];
+      expect(
+          js_util.getProperty(
+              js_util.callConstructor(Four, [1, ...spreadList]), 'count'),
+          equals(4));
+    });
+
+    test('edge cases for lowering to _callConstructorUncheckedN', () {
+      expect(js_util.getProperty(js_util.callConstructor(Zero, []), 'count'),
+          equals(0));
+      expect(js_util.getProperty(js_util.callConstructor(One, [1]), 'count'),
+          equals(1));
+      expect(
+          js_util.getProperty(
+              js_util.callConstructor(Four, [1, 2, 3, 4]), 'count'),
+          equals(4));
+      expect(
+          js_util.getProperty(
+              js_util.callConstructor(Five, [1, 2, 3, 4, 5]), 'count'),
+          equals(5));
+
+      // List with a type declaration, short circuits element checking
+      expect(
+          js_util.getProperty(
+              js_util.callConstructor(Two, <int>[1, 2]), 'count'),
+          equals(2));
+
+      // List as a variable instead of a List Literal or constant
+      var list = [1, 2];
+      expect(js_util.getProperty(js_util.callConstructor(Two, list), 'count'),
+          equals(2));
+
+      // Mixed types of elements to check in the given list.
+      var x = 4;
+      var str = 'cat';
+      var b = false;
+      var evens = [2, 4, 6];
+      expect(
+          js_util.getProperty(
+              js_util.callConstructor(Four, [x, str, b, evens]), 'count'),
+          equals(4));
+      var obj = Object();
+      expect(js_util.getProperty(js_util.callConstructor(One, [obj]), 'count'),
+          equals(1));
+      var nullElement = null;
+      expect(
+          js_util.getProperty(
+              js_util.callConstructor(One, [nullElement]), 'count'),
+          equals(1));
+
+      // const lists.
+      expect(
+          js_util.getProperty(js_util.callConstructor(One, const [3]), 'count'),
+          equals(1));
+      const constList = [10, 20, 30];
+      expect(
+          js_util.getProperty(
+              js_util.callConstructor(Three, constList), 'count'),
+          equals(3));
+      expect(
+          js_util.getProperty(
+              js_util.callConstructor(One, DartClass.staticConstList), 'count'),
+          equals(1));
     });
   });
 }
diff --git a/tests/lib_2/js/js_util/properties_test.dart b/tests/lib_2/js/js_util/properties_test.dart
index 8479414..c19fe14 100644
--- a/tests/lib_2/js/js_util/properties_test.dart
+++ b/tests/lib_2/js/js_util/properties_test.dart
@@ -87,6 +87,19 @@
   external five(a, b, c, d, e);
 }
 
+@JS()
+external get Zero;
+@JS()
+external get One;
+@JS()
+external get Two;
+@JS()
+external get Three;
+@JS()
+external get Four;
+@JS()
+external get Five;
+
 main() {
   eval(r"""
     function Foo(a) {
@@ -159,6 +172,25 @@
     CallMethodTest.prototype.five = function(a, b, c, d, e) {
       return 'five';
     }
+
+    function Zero() {
+      this.count = 0;
+    }
+    function One(a) {
+      this.count = 1;
+    }
+    function Two(a, b) {
+      this.count = 2;
+    }
+    function Three(a, b, c) {
+      this.count = 3;
+    }
+    function Four(a, b, c, d) {
+      this.count = 4;
+    }
+    function Five(a, b, c, d, e) {
+      this.count = 5;
+    }
     """);
 
   group('newObject', () {
@@ -547,8 +579,138 @@
 
   group('callConstructor', () {
     test('typed object', () {
-      Foo f = js_util.callConstructor(JSFooType, [42]);
+      var f = js_util.callConstructor(JSFooType, [42]);
       expect(f.a, equals(42));
+
+      var f2 =
+          js_util.callConstructor(js_util.getProperty(f, 'constructor'), [5]);
+      expect(f2.a, equals(5));
+    });
+
+    test('typed literal', () {
+      ExampleTypedLiteral literal = js_util.callConstructor(
+          js_util.getProperty(ExampleTypedLiteral(), 'constructor'), []);
+      expect(literal.a, equals(null));
+    });
+
+    test('callConstructor with List edge cases', () {
+      expect(
+          js_util.getProperty(js_util.callConstructor(Zero, List()), 'count'),
+          equals(0));
+      expect(
+          js_util.getProperty(
+              js_util.callConstructor(Zero, List<int>()), 'count'),
+          equals(0));
+      expect(
+          js_util.getProperty(
+              js_util.callConstructor(Zero, List.empty()), 'count'),
+          equals(0));
+      expect(
+          js_util.getProperty(
+              js_util.callConstructor(Zero, List<int>.empty()), 'count'),
+          equals(0));
+
+      expect(
+          js_util.getProperty(
+              js_util.callConstructor(Two, List<int>.filled(2, 0)), 'count'),
+          equals(2));
+      expect(
+          js_util.getProperty(
+              js_util.callConstructor(Three, List<int>.generate(3, (i) => i)),
+              'count'),
+          equals(3));
+
+      Iterable<String> iterableStrings = <String>['foo', 'bar'];
+      expect(
+          js_util.getProperty(
+              js_util.callConstructor(Two, List.of(iterableStrings)), 'count'),
+          equals(2));
+
+      const l1 = [1, 2];
+      const l2 = [3, 4];
+      expect(
+          js_util.getProperty(
+              js_util.callConstructor(Four, List.from(l1)..addAll(l2)),
+              'count'),
+          equals(4));
+      expect(
+          js_util.getProperty(js_util.callConstructor(Four, l1 + l2), 'count'),
+          equals(4));
+      expect(
+          js_util.getProperty(
+              js_util.callConstructor(Four, List.unmodifiable([1, 2, 3, 4])),
+              'count'),
+          equals(4));
+
+      var setElements = {1, 2};
+      expect(
+          js_util.getProperty(
+              js_util.callConstructor(Two, setElements.toList()), 'count'),
+          equals(2));
+
+      var spreadList = [1, 2, 3];
+      expect(
+          js_util.getProperty(
+              js_util.callConstructor(Four, [1, ...spreadList]), 'count'),
+          equals(4));
+    });
+
+    test('edge cases for lowering to _callConstructorUncheckedN', () {
+      expect(js_util.getProperty(js_util.callConstructor(Zero, []), 'count'),
+          equals(0));
+      expect(js_util.getProperty(js_util.callConstructor(One, [1]), 'count'),
+          equals(1));
+      expect(
+          js_util.getProperty(
+              js_util.callConstructor(Four, [1, 2, 3, 4]), 'count'),
+          equals(4));
+      expect(
+          js_util.getProperty(
+              js_util.callConstructor(Five, [1, 2, 3, 4, 5]), 'count'),
+          equals(5));
+
+      // List with a type declaration, short circuits element checking
+      expect(
+          js_util.getProperty(
+              js_util.callConstructor(Two, <int>[1, 2]), 'count'),
+          equals(2));
+
+      // List as a variable instead of a List Literal or constant
+      var list = [1, 2];
+      expect(js_util.getProperty(js_util.callConstructor(Two, list), 'count'),
+          equals(2));
+
+      // Mixed types of elements to check in the given list.
+      var x = 4;
+      var str = 'cat';
+      var b = false;
+      var evens = [2, 4, 6];
+      expect(
+          js_util.getProperty(
+              js_util.callConstructor(Four, [x, str, b, evens]), 'count'),
+          equals(4));
+      var obj = Object();
+      expect(js_util.getProperty(js_util.callConstructor(One, [obj]), 'count'),
+          equals(1));
+      var nullElement = null;
+      expect(
+          js_util.getProperty(
+              js_util.callConstructor(One, [nullElement]), 'count'),
+          equals(1));
+
+      // const lists.
+      expect(
+          js_util.getProperty(js_util.callConstructor(One, const [3]), 'count'),
+          equals(1));
+      const constList = [10, 20, 30];
+      expect(
+          js_util.getProperty(
+              js_util.callConstructor(Three, constList), 'count'),
+          equals(3));
+      expect(
+          js_util.getProperty(
+              js_util.callConstructor(One, DartClass.staticConstList), 'count'),
+          equals(1));
     });
   });
 }
diff --git a/tests/web/late_narrowing_test.dart b/tests/web/late_narrowing_test.dart
new file mode 100644
index 0000000..d7dcc11
--- /dev/null
+++ b/tests/web/late_narrowing_test.dart
@@ -0,0 +1,23 @@
+// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:expect/expect.dart';
+
+// Tests to ensure that narrowing type information does not discard late
+// sentinel values unintentionally.
+
+class Foo<T> {
+  // Since `List<T>` contains a free type variable, any access of [x] will be
+  // immediately followed by a narrowing to the appropriate instantiation of
+  // `List<T>`. This narrowing should not exclude the late sentinel value from
+  // the abstract value.
+  late final List<T> x;
+}
+
+void main() {
+  Foo<int> foo = Foo();
+  Expect.throws(() => foo.x);
+  foo.x = const [];
+  Expect.isTrue(foo.x.isEmpty);
+}
diff --git a/tests/web/regress/192964907a_test.dart b/tests/web/regress/192964907a_test.dart
new file mode 100644
index 0000000..c92e8f8
--- /dev/null
+++ b/tests/web/regress/192964907a_test.dart
@@ -0,0 +1,22 @@
+// Copyright (c) 2021, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:expect/expect.dart';
+
+class Foo {
+  late final bool x;
+
+  Foo() {
+    build();
+  }
+
+  void build() {
+    x = true;
+  }
+}
+
+void main() {
+  final foo = Foo();
+  Expect.isTrue(foo.x);
+}
diff --git a/tests/web/regress/192964907b_test.dart b/tests/web/regress/192964907b_test.dart
new file mode 100644
index 0000000..7046686
--- /dev/null
+++ b/tests/web/regress/192964907b_test.dart
@@ -0,0 +1,18 @@
+// Copyright (c) 2021, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:expect/expect.dart';
+
+class Foo {
+  late final bool? field;
+
+  void build() {
+    field = true;
+    Expect.isTrue(field!);
+  }
+}
+
+void main() {
+  (Foo().build)();
+}
diff --git a/tools/VERSION b/tools/VERSION
index d226819..783443a 100644
--- a/tools/VERSION
+++ b/tools/VERSION
@@ -27,5 +27,5 @@
 MAJOR 2
 MINOR 15
 PATCH 0
-PRERELEASE 102
+PRERELEASE 103
 PRERELEASE_PATCH 0
\ No newline at end of file