Narrowing improvements in dart2js global inference.

* no longer narrow by selector use, only narrow by type and non-null
* check for narrow nesting
* add non-null in a couple known places

One large apps, I compared the type-masks in dump-info and all differences were improvements with non-null. We weren't doing any narrowing based on the possible targets of a selector.

Change-Id: I270f360f70fbe3171d09ccd71d10517be9140194
Reviewed-on: https://dart-review.googlesource.com/c/90340
Reviewed-by: Stephen Adams <sra@google.com>
Reviewed-by: Johnni Winther <johnniwinther@google.com>
Commit-Queue: Sigmund Cherem <sigmund@google.com>
diff --git a/pkg/compiler/lib/src/inferrer/builder_kernel.dart b/pkg/compiler/lib/src/inferrer/builder_kernel.dart
index 518bf66..52981a0 100644
--- a/pkg/compiler/lib/src/inferrer/builder_kernel.dart
+++ b/pkg/compiler/lib/src/inferrer/builder_kernel.dart
@@ -899,15 +899,10 @@
     if (variable != null) {
       Local local = _localsMap.getLocalVariable(variable);
       if (!_capturedVariables.contains(local)) {
-        TypeInformation refinedType = _types
-            .refineReceiver(selector, mask, receiverType, isConditional: false);
         DartType type = _localsMap.getLocalType(_elementMap, local);
         _state.updateLocal(
-            _inferrer, _capturedAndBoxed, local, refinedType, node, type);
-        List<Refinement> refinements = _localRefinementMap[variable];
-        if (refinements != null) {
-          refinements.add(new Refinement(selector, mask));
-        }
+            _inferrer, _capturedAndBoxed, local, receiverType, node, type,
+            isNullable: _appliesToNullWithoutThrow(selector));
       }
     }
 
@@ -916,6 +911,27 @@
         inLoop: inLoop, isConditional: false);
   }
 
+  /// Whether [selector] could be a valid selector on `Null` without throwing.
+  bool _appliesToNullWithoutThrow(Selector selector) {
+    var name = selector.name;
+    if (selector.isOperator && name == "==") return true;
+    // Known getters and valid tear-offs.
+    if (selector.isGetter &&
+        (name == "hashCode" ||
+            name == "runtimeType" ||
+            name == "toString" ||
+            name == "noSuchMethod")) return true;
+    // Calling toString always succeeds, calls to `noSuchMethod` (even well
+    // formed calls) always throw.
+    if (selector.isCall &&
+        name == "toString" &&
+        selector.positionalArgumentCount == 0 &&
+        selector.namedArgumentCount == 0) {
+      return true;
+    }
+    return false;
+  }
+
   TypeInformation handleDynamicGet(ir.Node node, Selector selector,
       AbstractValue mask, TypeInformation receiverType) {
     return _handleDynamic(
@@ -944,60 +960,10 @@
         callType, node, selector, mask, receiverType, arguments);
   }
 
-  /// Map from synthesized variables created for non-null operations to observed
-  /// refinements. This is used to refine locals in cases like:
-  ///
-  ///     local?.method()
-  ///
-  /// which in kernel is encoded as
-  ///
-  ///     let #t1 = local in #t1 == null ? null : #1.method()
-  ///
-  Map<ir.VariableDeclaration, List<Refinement>> _localRefinementMap =
-      <ir.VariableDeclaration, List<Refinement>>{};
-
   @override
   TypeInformation visitLet(ir.Let node) {
-    ir.VariableDeclaration alias;
-    ir.Expression body = node.body;
-    if (node.variable.name == null &&
-        node.variable.isFinal &&
-        node.variable.initializer is ir.VariableGet &&
-        body is ir.ConditionalExpression &&
-        body.condition is ir.MethodInvocation &&
-        body.then is ir.NullLiteral) {
-      ir.VariableGet get = node.variable.initializer;
-      ir.MethodInvocation invocation = body.condition;
-      ir.Expression receiver = invocation.receiver;
-      if (invocation.name.name == '==' &&
-          receiver is ir.VariableGet &&
-          receiver.variable == node.variable &&
-          invocation.arguments.positional.single is ir.NullLiteral) {
-        // We have
-        //   let #t1 = local in #t1 == null ? null : e
-        alias = get.variable;
-        _localRefinementMap[node.variable] = <Refinement>[];
-      }
-    }
     visit(node.variable);
-    TypeInformation type = visit(body);
-    if (alias != null) {
-      List<Refinement> refinements = _localRefinementMap.remove(node.variable);
-      if (refinements.isNotEmpty) {
-        Local local = _localsMap.getLocalVariable(alias);
-        DartType type = _localsMap.getLocalType(_elementMap, local);
-        TypeInformation localType =
-            _state.readLocal(_inferrer, _capturedAndBoxed, local);
-        for (Refinement refinement in refinements) {
-          localType = _types.refineReceiver(
-              refinement.selector, refinement.mask, localType,
-              isConditional: true);
-          _state.updateLocal(
-              _inferrer, _capturedAndBoxed, local, localType, node, type);
-        }
-      }
-    }
-    return type;
+    return visit(node.body);
   }
 
   @override
@@ -1553,7 +1519,8 @@
       Local local = _localsMap.getLocalVariable(variable);
       DartType type = _localsMap.getLocalType(_elementMap, local);
       _state.updateLocal(
-          _inferrer, _capturedAndBoxed, local, localFunctionType, node, type);
+          _inferrer, _capturedAndBoxed, local, localFunctionType, node, type,
+          isNullable: false);
     }
 
     // We don't put the closure in the work queue of the
@@ -1669,14 +1636,16 @@
       }
       Local local = _localsMap.getLocalVariable(exception);
       _state.updateLocal(
-          _inferrer, _capturedAndBoxed, local, mask, node, const DynamicType());
+          _inferrer, _capturedAndBoxed, local, mask, node, const DynamicType(),
+          isNullable: false /* `throw null` produces a NullThrownError */);
     }
     ir.VariableDeclaration stackTrace = node.stackTrace;
     if (stackTrace != null) {
       Local local = _localsMap.getLocalVariable(stackTrace);
       // TODO(johnniwinther): Use a mask based on [StackTrace].
       _state.updateLocal(_inferrer, _capturedAndBoxed, local,
-          _types.dynamicType, node, const DynamicType());
+          _types.dynamicType, node, const DynamicType(),
+          isNullable: false /* if requested, the stack is never null */);
     }
     visit(node.body);
     return null;
@@ -1905,9 +1874,10 @@
       Local local,
       TypeInformation type,
       ir.Node node,
-      DartType staticType) {
+      DartType staticType,
+      {isNullable: true}) {
     assert(type != null);
-    type = inferrer.types.narrowType(type, staticType);
+    type = inferrer.types.narrowType(type, staticType, isNullable: isNullable);
 
     FieldEntity field = capturedAndBoxed[local];
     if (field != null) {
@@ -1923,10 +1893,9 @@
       Local local,
       DartType type,
       ir.Node node) {
-    TypeInformation existing = readLocal(inferrer, capturedAndBoxed, local);
-    TypeInformation newType =
-        inferrer.types.narrowType(existing, type, isNullable: false);
-    updateLocal(inferrer, capturedAndBoxed, local, newType, node, type);
+    TypeInformation currentType = readLocal(inferrer, capturedAndBoxed, local);
+    updateLocal(inferrer, capturedAndBoxed, local, currentType, node, type,
+        isNullable: false);
   }
 
   LocalState mergeFlow(InferrerEngine inferrer, LocalState other) {
diff --git a/pkg/compiler/lib/src/inferrer/type_system.dart b/pkg/compiler/lib/src/inferrer/type_system.dart
index 22d41df..aa827d3 100644
--- a/pkg/compiler/lib/src/inferrer/type_system.dart
+++ b/pkg/compiler/lib/src/inferrer/type_system.dart
@@ -6,7 +6,6 @@
 import '../common.dart';
 import '../elements/entities.dart';
 import '../elements/types.dart';
-import '../universe/selector.dart';
 import '../world.dart';
 import 'abstract_value_domain.dart';
 import 'type_graph_nodes.dart';
@@ -309,52 +308,28 @@
     return info.type != mask;
   }
 
-  /// Returns a new receiver type for this [selector] applied to
-  /// [receiverType].
-  ///
-  /// The option [isConditional] is true when [selector] was seen in a
-  /// conditional send (e.g.  `a?.selector`), in which case the returned type
-  /// may be null.
-  TypeInformation refineReceiver(
-      Selector selector, AbstractValue mask, TypeInformation receiver,
-      {bool isConditional}) {
-    if (_abstractValueDomain.isExact(receiver.type).isDefinitelyTrue) {
-      return receiver;
-    }
-    AbstractValue otherType = _closedWorld.computeReceiverType(selector, mask);
-    // Conditional sends (a?.b) can still narrow the possible types of `a`,
-    // however, we still need to consider that `a` may be null.
-    if (isConditional) {
-      // Note: we don't check that receiver.type.isNullable here because this is
-      // called during the graph construction.
-      otherType = _abstractValueDomain.includeNull(otherType);
-    }
-    // If this is refining to nullable subtype of `Object` just return
-    // the receiver. We know the narrowing is useless.
-    if (_abstractValueDomain.isNull(otherType).isPotentiallyTrue &&
-        _abstractValueDomain.containsAll(otherType).isPotentiallyTrue) {
-      return receiver;
-    }
-    TypeInformation newType =
-        new NarrowTypeInformation(_abstractValueDomain, receiver, otherType);
-    allocatedTypes.add(newType);
-    return newType;
-  }
+  bool _isNonNullNarrow(TypeInformation type) =>
+      type is NarrowTypeInformation &&
+      _abstractValueDomain.isNull(type.typeAnnotation).isDefinitelyFalse;
 
   /// Returns the intersection between [type] and [annotation].
   /// [isNullable] indicates whether the annotation implies a null
   /// type.
   TypeInformation narrowType(TypeInformation type, DartType annotation,
       {bool isNullable: true}) {
-    if (annotation.treatAsDynamic) return type;
-    if (annotation.isVoid) return type;
     AbstractValue otherType;
-    if (annotation.isInterfaceType) {
+    if (annotation.isVoid) return type;
+    if (annotation.treatAsDynamic) {
+      if (isNullable) return type;
+      // If the input is already narrowed to be not-null, there is no value
+      // in adding another narrowing node.
+      if (_isNonNullNarrow(type)) return type;
+      otherType = _abstractValueDomain.excludeNull(dynamicType.type);
+    } else if (annotation.isInterfaceType) {
       InterfaceType interface = annotation;
       if (interface.element == _closedWorld.commonElements.objectClass) {
-        if (isNullable) {
-          return type;
-        }
+        if (isNullable) return type;
+        if (_isNonNullNarrow(type)) return type;
         otherType = _abstractValueDomain.excludeNull(dynamicType.type);
       } else {
         otherType =
@@ -373,23 +348,9 @@
     if (isNullable) {
       otherType = _abstractValueDomain.includeNull(otherType);
     }
-    if (_abstractValueDomain.isExact(type.type).isDefinitelyTrue) {
-      return type;
-    } else {
-      TypeInformation newType =
-          new NarrowTypeInformation(_abstractValueDomain, type, otherType);
-      allocatedTypes.add(newType);
-      return newType;
-    }
-  }
-
-  /// Returns the non-nullable type of [type].
-  TypeInformation narrowNotNull(TypeInformation type) {
-    if (_abstractValueDomain.isExact(type.type).isDefinitelyTrue) {
-      return type;
-    }
-    TypeInformation newType = new NarrowTypeInformation(_abstractValueDomain,
-        type, _abstractValueDomain.excludeNull(dynamicType.type));
+    if (_abstractValueDomain.isExact(type.type).isDefinitelyTrue) return type;
+    TypeInformation newType =
+        new NarrowTypeInformation(_abstractValueDomain, type, otherType);
     allocatedTypes.add(newType);
     return newType;
   }
@@ -674,14 +635,20 @@
     // mapped iterable, we save the intermediate results to avoid computing them
     // again.
     var list = [];
+    bool isDynamicIngoringNull = false;
+    bool mayBeNull = 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.
       if (_abstractValueDomain.containsAll(mask).isPotentiallyTrue) {
-        return dynamicType;
+        isDynamicIngoringNull = true;
       }
+      if (_abstractValueDomain.isNull(mask).isPotentiallyTrue) {
+        mayBeNull = true;
+      }
+      if (isDynamicIngoringNull && mayBeNull) return dynamicType;
       list.add(mask);
     }
 
@@ -691,8 +658,12 @@
           newType == null ? mask : _abstractValueDomain.union(newType, mask);
       // Likewise - stop early if we already reach dynamic.
       if (_abstractValueDomain.containsAll(newType).isPotentiallyTrue) {
-        return dynamicType;
+        isDynamicIngoringNull = true;
       }
+      if (_abstractValueDomain.isNull(newType).isPotentiallyTrue) {
+        mayBeNull = true;
+      }
+      if (isDynamicIngoringNull && mayBeNull) return dynamicType;
     }
 
     return newType ?? _abstractValueDomain.emptyType;
diff --git a/sdk/lib/html/dart2js/html_dart2js.dart b/sdk/lib/html/dart2js/html_dart2js.dart
index 51d104c..110a9ca 100644
--- a/sdk/lib/html/dart2js/html_dart2js.dart
+++ b/sdk/lib/html/dart2js/html_dart2js.dart
@@ -14869,7 +14869,7 @@
       _convertNativeToDart_EventTarget(this._get_currentTarget);
   @JSName('currentTarget')
   @Creates('Null')
-  @Returns('EventTarget|=Object')
+  @Returns('EventTarget|=Object|Null')
   final dynamic _get_currentTarget;
 
   final bool defaultPrevented;
@@ -21153,7 +21153,7 @@
       _convertNativeToDart_EventTarget(this._get_relatedTarget);
   @JSName('relatedTarget')
   @Creates('Node')
-  @Returns('EventTarget|=Object')
+  @Returns('EventTarget|=Object|Null')
   final dynamic _get_relatedTarget;
 
   @JSName('screenX')
diff --git a/tests/compiler/dart2js/inference/data/catch.dart b/tests/compiler/dart2js/inference/data/catch.dart
index d468579..4d84e87 100644
--- a/tests/compiler/dart2js/inference/data/catch.dart
+++ b/tests/compiler/dart2js/inference/data/catch.dart
@@ -13,9 +13,9 @@
 /// Untyped catch clause.
 ////////////////////////////////////////////////////////////////////////////////
 
-/*element: catchUntyped:[null|subclass=Object]*/
+/*element: catchUntyped:[subclass=Object]*/
 catchUntyped() {
-  var local;
+  dynamic local = 0;
   try {} catch (e) {
     local = e;
   }
@@ -39,7 +39,7 @@
 /// Catch clause with stack trace.
 ////////////////////////////////////////////////////////////////////////////////
 
-/*element: catchStackTrace:[null|subclass=Object]*/
+/*element: catchStackTrace:[subclass=Object]*/
 catchStackTrace() {
   dynamic local = 0;
   try {} catch (_, s) {
diff --git a/tests/compiler/dart2js/inference/data/for_in.dart b/tests/compiler/dart2js/inference/data/for_in.dart
index 231c016..c622275 100644
--- a/tests/compiler/dart2js/inference/data/for_in.dart
+++ b/tests/compiler/dart2js/inference/data/for_in.dart
@@ -7,11 +7,7 @@
   forInDirect();
   forInReturn();
   forInReturnMulti();
-  forInReturnRefined();
-  forInReturnRefinedDynamic();
-  testInForIn();
-  operatorInForIn();
-  updateInForIn();
+  forInReturnNonNull();
 }
 
 ////////////////////////////////////////////////////////////////////////////////
@@ -66,12 +62,12 @@
 }
 
 ////////////////////////////////////////////////////////////////////////////////
-// Sequentially refine element and return it from a for-in loop on known list
-// type.
+// Sequentially refine that an element is not null and return it from a for-in
+// loop on known list type.
 ////////////////////////////////////////////////////////////////////////////////
 
-/*element: forInReturnRefined:[null|subclass=JSInt]*/
-forInReturnRefined() {
+/*element: forInReturnNonNull:[subclass=JSInt]*/
+forInReturnNonNull() {
   /*iterator: Container([exact=JSExtendableArray], element: [exact=JSUInt31], length: 3)*/
   /*current: [exact=ArrayIterator]*/
   /*moveNext: [exact=ArrayIterator]*/
@@ -82,113 +78,5 @@
     a. /*[subclass=JSInt]*/ isEven;
     return a;
   }
-}
-
-////////////////////////////////////////////////////////////////////////////////
-// Sequentially refine element and return it from a for-in loop on known list
-// type with a dynamic variable.
-////////////////////////////////////////////////////////////////////////////////
-
-/*element: forInReturnRefinedDynamic:[null|subclass=JSInt]*/
-forInReturnRefinedDynamic() {
-  /*iterator: Container([exact=JSExtendableArray], element: [exact=JSUInt31], length: 3)*/
-  /*current: [exact=ArrayIterator]*/
-  /*moveNext: [exact=ArrayIterator]*/
-  for (dynamic a in [1, 2, 3]) {
-    // TODO(johnniwinther): We should know the type of [a] here.
-    a.isEven;
-    a. /*[subclass=JSInt]*/ isEven;
-    return a;
-  }
-}
-
-////////////////////////////////////////////////////////////////////////////////
-// Refine element through test and return it from a for-in loop on known list
-// type.
-////////////////////////////////////////////////////////////////////////////////
-
-/*element: Class1.:[exact=Class1]*/
-class Class1 {
-  /*element: Class1.field1:[exact=JSUInt31]*/
-  var field1 = 42;
-}
-
-/*element: _testInForIn:[null|exact=Class1]*/
-_testInForIn(
-    /*Container([exact=JSExtendableArray], element: [exact=Class1], length: 2)*/ list) {
-  /*iterator: Container([exact=JSExtendableArray], element: [exact=Class1], length: 2)*/
-  /*current: [exact=ArrayIterator]*/
-  /*moveNext: [exact=ArrayIterator]*/
-  for (var t in list) {
-    if (t.field1) {
-      return t;
-    }
-  }
-}
-
-/*element: testInForIn:[null]*/
-testInForIn() {
-  _testInForIn([new Class1(), new Class1()]);
-}
-
-////////////////////////////////////////////////////////////////////////////////
-// Refine element through operator and return it from a for-in loop on known
-// list type.
-////////////////////////////////////////////////////////////////////////////////
-
-/*element: Class2.:[exact=Class2]*/
-class Class2 {
-  /*element: Class2.field2a:[exact=JSUInt31]*/
-  var field2a = 42;
-  /*element: Class2.field2b:[exact=JSUInt31]*/
-  var field2b = 42;
-}
-
-/*element: _operatorInForIn:[null|exact=Class2]*/
-_operatorInForIn(
-    /*Container([exact=JSExtendableArray], element: [exact=Class2], length: 2)*/ list) {
-  /*iterator: Container([exact=JSExtendableArray], element: [exact=Class2], length: 2)*/
-  /*current: [exact=ArrayIterator]*/
-  /*moveNext: [exact=ArrayIterator]*/
-  for (var t in list) {
-    if (t.field2a /*invoke: [exact=JSUInt31]*/ <
-        t. /*[exact=Class2]*/ field2b) {
-      return t;
-    }
-  }
-}
-
-/*element: operatorInForIn:[null]*/
-operatorInForIn() {
-  _operatorInForIn([new Class2(), new Class2()]);
-}
-
-////////////////////////////////////////////////////////////////////////////////
-// Refine element through operator and return it from a for-in loop on known
-// list type.
-////////////////////////////////////////////////////////////////////////////////
-
-/*element: Class3.:[exact=Class3]*/
-class Class3 {
-  /*element: Class3.field3a:[exact=JSUInt31]*/
-  var field3a = 42;
-  /*element: Class3.field3b:[exact=JSUInt31]*/
-  var field3b = 42;
-}
-
-/*element: _updateInForIn:[null]*/
-_updateInForIn(
-    /*Container([exact=JSExtendableArray], element: [exact=Class3], length: 2)*/ list) {
-  /*iterator: Container([exact=JSExtendableArray], element: [exact=Class3], length: 2)*/
-  /*current: [exact=ArrayIterator]*/
-  /*moveNext: [exact=ArrayIterator]*/
-  for (var t in list) {
-    t.field3b = t.field3a;
-    t. /*update: [exact=Class3]*/ field3a = 87;
-  }
-}
-
-/*element: updateInForIn:[null]*/
-updateInForIn() {
-  _updateInForIn([new Class3(), new Class3()]);
+  return 0;
 }
diff --git a/tests/compiler/dart2js/inference/data/general.dart b/tests/compiler/dart2js/inference/data/general.dart
index f4e11b1..a00c12b 100644
--- a/tests/compiler/dart2js/inference/data/general.dart
+++ b/tests/compiler/dart2js/inference/data/general.dart
@@ -384,7 +384,7 @@
   var c;
   L1:
   if (a /*invoke: Value([exact=JSBool], value: true)*/ > 1) {
-    if (a /*invoke: [empty]*/ == 2) {
+    if (a /*invoke: Value([exact=JSBool], value: true)*/ == 2) {
       break L1;
     }
     c = 42;
@@ -581,7 +581,7 @@
   return a;
 }
 
-/*element: testSpecialization1:[subclass=JSNumber]*/
+/*element: testSpecialization1:[subclass=Object]*/
 testSpecialization1() {
   var a = topLevelGetter();
   a - 42;
@@ -621,7 +621,7 @@
   return a;
 }
 
-/*element: testReturnNull3:[null|subclass=Object]*/
+/*element: testReturnNull3:[subclass=Object]*/
 testReturnNull3(/*[null|subclass=Object]*/ a) {
   if (a == null) return 42;
   return a;
@@ -641,7 +641,7 @@
   return a;
 }
 
-/*element: testReturnNull6:[null|subclass=Object]*/
+/*element: testReturnNull6:[subclass=Object]*/
 testReturnNull6() {
   var a = topLevelGetter();
   if (a == null) return 42;
diff --git a/tests/compiler/dart2js/inference/data/postfix_prefix.dart b/tests/compiler/dart2js/inference/data/postfix_prefix.dart
index 9ede9a2..92a47c2 100644
--- a/tests/compiler/dart2js/inference/data/postfix_prefix.dart
+++ b/tests/compiler/dart2js/inference/data/postfix_prefix.dart
@@ -18,7 +18,7 @@
   /*element: A.[]=:[null]*/
   operator []=(/*[empty]*/ index, /*[subclass=JSNumber]*/ value) {}
 
-  /*element: A.returnDynamic1:[exact=JSUInt31]*/
+  /*element: A.returnDynamic1:Union([exact=JSString], [exact=JSUInt31])*/
   returnDynamic1() => /*[subclass=A]*/ /*update: [subclass=A]*/ foo
       /*invoke: Union([exact=JSString], [exact=JSUInt31])*/ --;
 
@@ -30,7 +30,7 @@
   returnNum2() => /*[subclass=A]*/ /*update: [subclass=A]*/ foo
       /*invoke: Union([exact=JSString], [exact=JSUInt31])*/ -= 42;
 
-  /*element: A.returnDynamic2:[exact=JSUInt31]*/
+  /*element: A.returnDynamic2:Union([exact=JSString], [exact=JSUInt31])*/
   returnDynamic2() => this
           /*[subclass=A]*/ /*update: [subclass=A]*/ [index]
       /*invoke: Union([exact=JSString], [exact=JSUInt31])*/ --;
@@ -44,12 +44,10 @@
           /*[subclass=A]*/ /*update: [subclass=A]*/ [index]
       /*invoke: Union([exact=JSString], [exact=JSUInt31])*/ -= 42;
 
-  // TODO(johnniwinther): Investigate why implementations differ on update.
   /*element: A.returnEmpty3:[empty]*/
   returnEmpty3() {
     dynamic a = this;
-    return a. /*[subclass=A]*/
-            /*update: [empty]*/
+    return a. /*[subclass=A]*/ /*update: [subclass=A]*/
             bar
         /*invoke: [empty]*/ --;
   }
@@ -76,7 +74,7 @@
   /*element: B.[]:[exact=JSUInt31]*/
   operator [](/*[empty]*/ index) => 42;
 
-  /*element: B.returnString1:[empty]*/
+  /*element: B.returnString1:Value([exact=JSString], value: "string")*/
   returnString1() =>
       super.foo /*invoke: Value([exact=JSString], value: "string")*/ --;
 
@@ -89,7 +87,7 @@
   returnDynamic2() =>
       super.foo /*invoke: Value([exact=JSString], value: "string")*/ -= 42;
 
-  /*element: B.returnString2:[empty]*/
+  /*element: B.returnString2:Value([exact=JSString], value: "string")*/
   returnString2() => super[index]
       /*invoke: Value([exact=JSString], value: "string")*/ --;
 
diff --git a/tests/compiler/dart2js/inference/data/refine_captured_locals.dart b/tests/compiler/dart2js/inference/data/refine_captured_locals.dart
index f1695af..f11fc71 100644
--- a/tests/compiler/dart2js/inference/data/refine_captured_locals.dart
+++ b/tests/compiler/dart2js/inference/data/refine_captured_locals.dart
@@ -20,12 +20,9 @@
   method1() {}
 }
 
-/*element: Class2.:[exact=Class2]*/
-class Class2 {}
-
 /*element: _refineBeforeCapture:[exact=Class1]*/
-_refineBeforeCapture(/*Union([exact=Class1], [exact=Class2])*/ o) {
-  o. /*invoke: Union([exact=Class1], [exact=Class2])*/ method1();
+_refineBeforeCapture(/*[null|exact=Class1]*/ o) {
+  o. /*invoke: [null|exact=Class1]*/ method1();
   o. /*invoke: [exact=Class1]*/ method1();
 
   /*[exact=Class1]*/ localFunction() => o;
@@ -35,7 +32,7 @@
 /*element: refineBeforeCapture:[null]*/
 refineBeforeCapture() {
   _refineBeforeCapture(new Class1());
-  _refineBeforeCapture(new Class2());
+  _refineBeforeCapture(null);
 }
 
 ////////////////////////////////////////////////////////////////////////////////
diff --git a/tests/compiler/dart2js/inference/data/refine_locals.dart b/tests/compiler/dart2js/inference/data/refine_locals.dart
index 8711587..557770d 100644
--- a/tests/compiler/dart2js/inference/data/refine_locals.dart
+++ b/tests/compiler/dart2js/inference/data/refine_locals.dart
@@ -2,6 +2,8 @@
 // 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';
+
 /*element: main:[null]*/
 main() {
   refineToClass();
@@ -9,7 +11,7 @@
 }
 
 ////////////////////////////////////////////////////////////////////////////////
-// Refine the type of a non-captured local variable through a sequence of
+// Refine nullability of a non-captured local variable through a sequence of
 // accesses and updates.
 ////////////////////////////////////////////////////////////////////////////////
 
@@ -31,141 +33,87 @@
   method0() {}
   /*element: Class2.method2:[null]*/
   method2() {}
-  /*element: Class2.field0:[null|exact=JSUInt31]*/
+  /*element: Class2.field0:[null]*/
   var field0;
-  /*element: Class2.field2:[null|exact=JSUInt31]*/
+  /*element: Class2.field2:[null]*/
   var field2;
 }
 
-/*element: _refineToClass1Invoke:[empty]*/
-_refineToClass1Invoke(/*Union([exact=Class1], [exact=Class2])*/ o) {
+/*element: _refineUnion:Union([exact=Class1], [exact=Class2])*/
+_refineUnion(/*Union([null|exact=Class1], [null|exact=Class2])*/ o) {
+  o. /*invoke: Union([null|exact=Class1], [null|exact=Class2])*/ method0();
   o. /*invoke: Union([exact=Class1], [exact=Class2])*/ method1();
-  o. /*invoke: [exact=Class1]*/ method0();
-  o. /*invoke: [exact=Class1]*/ method2();
-  return o;
-}
-
-/*element: _refineToClass2Invoke:[empty]*/
-_refineToClass2Invoke(/*Union([exact=Class1], [exact=Class2])*/ o) {
   o. /*invoke: Union([exact=Class1], [exact=Class2])*/ method2();
-  o. /*invoke: [exact=Class2]*/ method0();
-  o. /*invoke: [exact=Class2]*/ method1();
   return o;
 }
 
-/*element: _refineToEmptyInvoke:[empty]*/
-_refineToEmptyInvoke(/*Union([exact=Class1], [exact=Class2])*/ o) {
-  o. /*invoke: Union([exact=Class1], [exact=Class2])*/ method1();
-  o. /*invoke: [exact=Class1]*/ method2();
-  o. /*invoke: [empty]*/ method0();
+/*element: _refineFromMethod:[exact=Class1]*/
+_refineFromMethod(/*[null|exact=Class1]*/ o) {
+  o. /*invoke: [null|exact=Class1]*/ method0();
+  o. /*invoke: [exact=Class1]*/ method1();
   return o;
 }
 
-/*element: _refineToClass1Get:[empty]*/
-_refineToClass1Get(/*Union([exact=Class1], [exact=Class2])*/ o) {
-  o. /*Union([exact=Class1], [exact=Class2])*/ field0;
-  o. /*Union([exact=Class1], [exact=Class2])*/ field1;
-  o. /*[exact=Class1]*/ field2;
+/*element: _refineFromGetter:[exact=Class2]*/
+_refineFromGetter(/*[null|exact=Class2]*/ o) {
+  o. /*[null|exact=Class2]*/ field0;
+  o. /*[exact=Class2]*/ field2;
   return o;
 }
 
-/*element: _refineToClass2Get:[empty]*/
-_refineToClass2Get(/*Union([exact=Class1], [exact=Class2])*/ o) {
-  o. /*Union([exact=Class1], [exact=Class2])*/ field0;
-  o. /*Union([exact=Class1], [exact=Class2])*/ field2;
-  o. /*[exact=Class2]*/ field1;
+/*element: _refineFromSetter:[exact=Class1]*/
+_refineFromSetter(/*[null|exact=Class1]*/ o) {
+  o. /*update: [null|exact=Class1]*/ field0 = 0;
+  o. /*update: [exact=Class1]*/ field1 = 0;
   return o;
 }
 
-/*element: _refineToEmptyGet:[empty]*/
-_refineToEmptyGet(/*Union([exact=Class1], [exact=Class2])*/ o) {
-  o. /*Union([exact=Class1], [exact=Class2])*/ field1;
-  o. /*[exact=Class1]*/ field2;
-  o. /*[empty]*/ field0;
-  return o;
-}
-
-/*element: _refineToClass1Set:[empty]*/
-_refineToClass1Set(/*Union([exact=Class1], [exact=Class2])*/ o) {
-  o. /*update: Union([exact=Class1], [exact=Class2])*/ field0 = 0;
-  o. /*update: Union([exact=Class1], [exact=Class2])*/ field1 = 0;
-  o. /*update: [exact=Class1]*/ field2 = 0;
-  return o;
-}
-
-/*element: _refineToClass2Set:[empty]*/
-_refineToClass2Set(/*Union([exact=Class1], [exact=Class2])*/ o) {
-  o. /*update: Union([exact=Class1], [exact=Class2])*/ field0 = 0;
-  o. /*update: Union([exact=Class1], [exact=Class2])*/ field2 = 0;
-  o. /*update: [exact=Class2]*/ field1 = 0;
-  return o;
-}
-
-/*element: _refineToEmptySet:[empty]*/
-_refineToEmptySet(/*Union([exact=Class1], [exact=Class2])*/ o) {
-  o. /*update: Union([exact=Class1], [exact=Class2])*/ field1 = 0;
-  o. /*update: [exact=Class1]*/ field2 = 0;
-  o. /*update: [empty]*/ field0 = 0;
-  return o;
-}
-
-/*element: _refineToClass1InvokeIfNotNull:[null]*/
-_refineToClass1InvokeIfNotNull(
-    /*Union([exact=Class2], [null|exact=Class1])*/ o) {
+/*element: _noRefinementNullAware:[null|exact=Class1]*/
+_noRefinementNullAware(/*[null|exact=Class1]*/ o) {
   o
       ?.
-      /*invoke: Union([exact=Class1], [exact=Class2])*/
+      /*invoke: [exact=Class1]*/
       method1();
-  o
-      ?.
-      /*invoke: [exact=Class1]*/
-      method0();
-  o
-      ?.
-      /*invoke: [exact=Class1]*/
-      method2();
   return o;
 }
 
-/*element: _noRefinementToClass1InvokeSet:Union([exact=Class2], [null|exact=Class1])*/
-_noRefinementToClass1InvokeSet(
-    /*Union([exact=Class2], [null|exact=Class1])*/ o) {
-  (o = o). /*invoke: Union([exact=Class2], [null|exact=Class1])*/ method1();
-  (o = o). /*invoke: Union([exact=Class2], [null|exact=Class1])*/ method0();
-  (o = o). /*invoke: Union([exact=Class2], [null|exact=Class1])*/ method2();
+/*element: _noRefinementNullSelectors:[exact=Class2]*/
+_noRefinementNullSelectors(/*[null|exact=Class2]*/ o) {
+  o /*invoke: [null|exact=Class2]*/ == 2;
+  o. /*[null|exact=Class2]*/ hashCode;
+  o. /*[null|exact=Class2]*/ runtimeType;
+  o. /*[null|exact=Class2]*/ toString;
+  o. /*[null|exact=Class2]*/ noSuchMethod;
+  o. /*invoke: [null|exact=Class2]*/ toString();
+  o. /*invoke: [null|exact=Class2]*/ noSuchMethod(null); // assumed to throw.
+  o. /*[exact=Class2]*/ toString;
   return o;
 }
 
+/*element: _noRefinementUpdatedVariable:[null|exact=Class1]*/
+_noRefinementUpdatedVariable(/*[null|exact=Class1]*/ o) {
+  (o = o). /*invoke: [null|exact=Class1]*/ method1();
+  (o = o). /*invoke: [null|exact=Class1]*/ method0();
+  return o;
+}
+
+/*element: _condition:Value([exact=JSBool], value: false)*/
+@AssumeDynamic()
+get _condition => false;
+
 /*element: refineToClass:[null]*/
 refineToClass() {
-  _refineToClass1Invoke(new Class1());
-  _refineToClass1Invoke(new Class2());
-  _refineToClass2Invoke(new Class1());
-  _refineToClass2Invoke(new Class2());
-  _refineToEmptyInvoke(new Class1());
-  _refineToEmptyInvoke(new Class2());
+  var nullOrClass1 = _condition ? null : new Class1();
+  var nullOrClass2 = _condition ? null : new Class2();
+  _refineUnion(nullOrClass1);
+  _refineUnion(nullOrClass2);
 
-  _refineToClass1Get(new Class1());
-  _refineToClass1Get(new Class2());
-  _refineToClass2Get(new Class1());
-  _refineToClass2Get(new Class2());
-  _refineToEmptyGet(new Class1());
-  _refineToEmptyGet(new Class2());
-
-  _refineToClass1Set(new Class1());
-  _refineToClass1Set(new Class2());
-  _refineToClass2Set(new Class1());
-  _refineToClass2Set(new Class2());
-  _refineToEmptySet(new Class1());
-  _refineToEmptySet(new Class2());
-
-  _refineToClass1InvokeIfNotNull(null);
-  _refineToClass1InvokeIfNotNull(new Class1());
-  _refineToClass1InvokeIfNotNull(new Class2());
-
-  _noRefinementToClass1InvokeSet(null);
-  _noRefinementToClass1InvokeSet(new Class1());
-  _noRefinementToClass1InvokeSet(new Class2());
+  _refineFromMethod(nullOrClass1);
+  _refineFromGetter(nullOrClass2);
+  _refineFromSetter(nullOrClass1);
+  _noRefinementNullAware(nullOrClass1);
+  _noRefinementNullSelectors(nullOrClass2);
+  _noRefinementUpdatedVariable(nullOrClass1);
 }
 
 ////////////////////////////////////////////////////////////////////////////////
diff --git a/tests/compiler/dart2js/inference/data/refine_order.dart b/tests/compiler/dart2js/inference/data/refine_order.dart
index c511c73..b2c6d9e 100644
--- a/tests/compiler/dart2js/inference/data/refine_order.dart
+++ b/tests/compiler/dart2js/inference/data/refine_order.dart
@@ -37,7 +37,7 @@
 @AssumeDynamic()
 statementOrderFieldAccess(/*[null|subclass=Object]*/ o) {
   o.field;
-  o. /*[exact=Class]*/ field;
+  o. /*[subclass=Object]*/ field;
 }
 
 ////////////////////////////////////////////////////////////////////////////////
@@ -48,7 +48,7 @@
 @AssumeDynamic()
 statementOrderFieldUpdate(/*[null|subclass=Object]*/ o) {
   o.field = 42;
-  o. /*update: [exact=Class]*/ field = 42;
+  o. /*update: [subclass=Object]*/ field = 42;
 }
 
 ////////////////////////////////////////////////////////////////////////////////
@@ -59,7 +59,7 @@
 @AssumeDynamic()
 statementOrderInvocation(/*[null|subclass=Object]*/ o) {
   o.method(null);
-  o. /*invoke: [exact=Class]*/ method(null);
+  o. /*invoke: [subclass=Object]*/ method(null);
 }
 
 ////////////////////////////////////////////////////////////////////////////////
@@ -71,7 +71,7 @@
 receiverVsArgument(/*[null|subclass=Object]*/ o) {
   // TODO(johnniwinther): The arguments should refine the receiver.
   o.method(o.field);
-  o. /*[exact=Class]*/ field;
+  o. /*[subclass=Object]*/ field;
 }
 
 ////////////////////////////////////////////////////////////////////////////////
@@ -82,8 +82,8 @@
 @AssumeDynamic()
 argumentsOrder(/*[null|subclass=Object]*/ o) {
   // TODO(johnniwinther): The arguments should refine the receiver.
-  o.method(o.field, o. /*[exact=Class]*/ field);
-  o. /*[exact=Class]*/ field;
+  o.method(o.field, o. /*[subclass=Object]*/ field);
+  o. /*[subclass=Object]*/ field;
 }
 
 ////////////////////////////////////////////////////////////////////////////////
@@ -93,8 +93,8 @@
 /*element: operatorOrder:[null]*/
 @AssumeDynamic()
 operatorOrder(/*[null|subclass=Object]*/ o) {
-  o.field /*invoke: [exact=JSUInt31]*/ < o. /*[exact=Class]*/ field;
-  o. /*[exact=Class]*/ field;
+  o.field /*invoke: [exact=JSUInt31]*/ < o. /*[subclass=Object]*/ field;
+  o. /*[subclass=Object]*/ field;
 }
 
 ////////////////////////////////////////////////////////////////////////////////
@@ -107,7 +107,7 @@
   // TODO(johnniwinther): The right-hand side should refine the left-hand side
   // receiver.
   o.field = o.field;
-  o. /*[exact=Class]*/ field;
+  o. /*[subclass=Object]*/ field;
 }
 
 ////////////////////////////////////////////////////////////////////////////////
@@ -117,8 +117,8 @@
 /*element: logicalOr:[null]*/
 @AssumeDynamic()
 logicalOr(/*[null|subclass=Object]*/ o) {
-  o.field || o. /*[exact=Class]*/ field;
-  o. /*[exact=Class]*/ field;
+  o.field || o. /*[subclass=Object]*/ field;
+  o. /*[subclass=Object]*/ field;
 }
 
 ////////////////////////////////////////////////////////////////////////////////
@@ -128,8 +128,8 @@
 /*element: conditionalCondition:[null]*/
 @AssumeDynamic()
 conditionalCondition(/*[null|subclass=Object]*/ o) {
-  o.field ? o. /*[exact=Class]*/ field : o. /*[exact=Class]*/ field;
-  o. /*[exact=Class]*/ field;
+  o.field ? o. /*[subclass=Object]*/ field : o. /*[subclass=Object]*/ field;
+  o. /*[subclass=Object]*/ field;
 }
 
 ////////////////////////////////////////////////////////////////////////////////
@@ -141,7 +141,7 @@
 conditionalBothBranches(/*[null|subclass=Object]*/ o) {
   // ignore: DEAD_CODE
   true ? o.field : o.field;
-  o. /*[exact=Class]*/ field;
+  o. /*[subclass=Object]*/ field;
 }
 
 ////////////////////////////////////////////////////////////////////////////////
@@ -154,5 +154,5 @@
   // ignore: DEAD_CODE
   true ? o.field : null;
   o.field;
-  o. /*[exact=Class]*/ field;
+  o. /*[subclass=Object]*/ field;
 }
diff --git a/tools/dom/scripts/dartmetadata.py b/tools/dom/scripts/dartmetadata.py
index cb81c49..a5a92f1 100644
--- a/tools/dom/scripts/dartmetadata.py
+++ b/tools/dom/scripts/dartmetadata.py
@@ -157,7 +157,7 @@
     # addEventListener on the target, so we avoid
     'Event.currentTarget': [
       "@Creates('Null')",
-      "@Returns('EventTarget|=Object')",
+      "@Returns('EventTarget|=Object|Null')",
     ],
 
     # Only nodes in the DOM bubble and have target !== currentTarget.
@@ -324,7 +324,7 @@
 
     'MouseEvent.relatedTarget': [
       "@Creates('Node')",
-      "@Returns('EventTarget|=Object')",
+      "@Returns('EventTarget|=Object|Null')",
     ],
 
     'Notification.data': [