Remove effectively empty type holders

+ correctly join with path with less information.

Change-Id: I44921f5b1d1613934d5321d87840db689b3b4b0b
Reviewed-on: https://dart-review.googlesource.com/c/88573
Commit-Queue: Johnni Winther <johnniwinther@google.com>
Reviewed-by: Sigmund Cherem <sigmund@google.com>
diff --git a/pkg/compiler/lib/src/ir/static_type.dart b/pkg/compiler/lib/src/ir/static_type.dart
index 4003b5b..47129b6 100644
--- a/pkg/compiler/lib/src/ir/static_type.dart
+++ b/pkg/compiler/lib/src/ir/static_type.dart
@@ -34,6 +34,7 @@
 /// the readily compute static types of subexpressions.
 abstract class StaticTypeVisitor extends StaticTypeBase {
   Map<ir.Expression, ir.DartType> _cache = {};
+  Map<ir.Expression, TypeMap> typeMapsForTesting;
 
   StaticTypeVisitor(ir.TypeEnvironment typeEnvironment)
       : super(typeEnvironment);
@@ -51,6 +52,7 @@
   VariableScopeModel get variableScopeModel;
 
   bool completes(ir.DartType type) => type != const ir.BottomType();
+
   Set<ir.VariableDeclaration> _currentVariables;
   Set<ir.VariableDeclaration> _invalidatedVariables =
       new Set<ir.VariableDeclaration>();
@@ -511,6 +513,9 @@
 
   @override
   ir.DartType visitVariableGet(ir.VariableGet node) {
+    if (typeMapsForTesting != null) {
+      typeMapsForTesting[node] = typeMap;
+    }
     ir.DartType promotedType = typeMap.typeOf(node, typeEnvironment);
     assert(
         node.promotedType == null ||
@@ -1246,6 +1251,23 @@
         equalSets(falseTypes, other.falseTypes);
   }
 
+  void _getText(
+      StringBuffer sb, String Function(Iterable<ir.DartType>) typesToText) {
+    sb.write('{');
+    String comma = '';
+    if (trueTypes != null) {
+      sb.write('true:');
+      sb.write(typesToText(trueTypes));
+      comma = ',';
+    }
+    if (falseTypes != null) {
+      sb.write(comma);
+      sb.write('false:');
+      sb.write(typesToText(falseTypes));
+    }
+    sb.write('}');
+  }
+
   String toString() {
     StringBuffer sb = new StringBuffer();
     sb.write('TypeHolder(');
@@ -1318,51 +1340,6 @@
       newTypeHolders.add(TypeHolder(declaredType, trueTypes, falseTypes));
       return changed;
     }
-    /*bool addTypeHolder(TypeHolder typeHolder) {
-      bool changed = true;
-      TypeHolder newTypeHolder;
-      if (typeHolder != null) {
-        if (isTrue) {
-          if (typeHolder.trueTypes == null) {
-            newTypeHolder = new TypeHolder(declaredType,
-                new Set<ir.DartType>()..add(type), typeHolder.falseTypes);
-          } else if (typeHolder.trueTypes.contains(type)) {
-            changed = false;
-            newTypeHolder = typeHolder;
-          } else {
-            newTypeHolder = new TypeHolder(
-                declaredType,
-                new Set<ir.DartType>.from(typeHolder.trueTypes)..add(type),
-                typeHolder.falseTypes);
-          }
-        } else {
-          if (typeHolder.falseTypes == null) {
-            newTypeHolder = new TypeHolder(declaredType, typeHolder.trueTypes,
-                new Set<ir.DartType>()..add(type));
-          } else if (typeHolder.falseTypes.contains(type)) {
-            changed = false;
-            newTypeHolder = typeHolder;
-          } else {
-            newTypeHolder = new TypeHolder(declaredType, typeHolder.trueTypes,
-                new Set<ir.DartType>.from(typeHolder.falseTypes)..add(type));
-          }
-        }
-      } else {
-        if (isTrue) {
-          newTypeHolder = new TypeHolder(
-              declaredType, new Set<ir.DartType>()..add(type), null);
-        } else {
-          newTypeHolder = new TypeHolder(
-              declaredType, null, new Set<ir.DartType>()..add(type));
-        }
-      }
-      // TODO(johnniwinther): Check validity; if the true types are
-      // contradicting, for instance if the local is known to be and instance
-      // of types `int` and `String` simultaneously, then we could flag code
-      // as dead code.
-      newTypeHolders.add(newTypeHolder);
-      return changed;
-    }*/
 
     bool changed = false;
     if (typeHolders.isEmpty) {
@@ -1387,16 +1364,101 @@
 
   /// Returns the [TargetInfo] that describes that the local is either of [this]
   /// or the [other] type.
+  ///
+  /// Returns `null` if the join is empty.
   TargetInfo join(TargetInfo other) {
+    if (other == null) return null;
     if (identical(this, other)) return this;
 
-    Set<TypeHolder> newTypeHolders = new Set<TypeHolder>.from(typeHolders);
-    newTypeHolders.addAll(other.typeHolders);
-    Set<ir.DartType> newDeclarationsOfInterest =
-        new Set<ir.DartType>.from(typesOfInterest);
-    newDeclarationsOfInterest.addAll(other.typesOfInterest);
-    return new TargetInfo(
-        declaredType, newTypeHolders, newDeclarationsOfInterest);
+    Set<TypeHolder> newTypeHolders = new Set<TypeHolder>();
+    Set<ir.DartType> newTypesOfInterest = new Set<ir.DartType>();
+
+    /// Adds the [typeHolders] to [newTypeHolders] for types in
+    /// [otherTypesOfInterest] while removing the information
+    /// invalidated by [otherTrueTypes] and [otherFalseTypes].
+    void addTypeHolders(
+        Iterable<TypeHolder> typeHolders,
+        Set<ir.DartType> otherTrueTypes,
+        Set<ir.DartType> otherFalseTypes,
+        Iterable<ir.DartType> otherTypesOfInterest) {
+      for (TypeHolder typeHolder in typeHolders) {
+        Set<ir.DartType> newTrueTypes;
+        if (typeHolder.trueTypes != null) {
+          newTrueTypes = new Set<ir.DartType>.from(typeHolder.trueTypes);
+
+          /// Only types in [otherTypesOfInterest] has information from all
+          /// paths.
+          newTrueTypes.retainAll(otherTypesOfInterest);
+
+          /// Remove types that are known to be false on other paths; these
+          /// would amount to knowing that a variable is or is not of some
+          /// type.
+          newTrueTypes.removeAll(otherFalseTypes);
+          if (newTrueTypes.isEmpty) {
+            newTrueTypes = null;
+          } else {
+            newTypesOfInterest.addAll(newTrueTypes);
+          }
+        }
+        Set<ir.DartType> newFalseTypes;
+        if (typeHolder.falseTypes != null) {
+          newFalseTypes = new Set<ir.DartType>.from(typeHolder.falseTypes);
+
+          /// Only types in [otherTypesOfInterest] has information from all
+          /// paths.
+          newFalseTypes.retainAll(otherTypesOfInterest);
+
+          /// Remove types that are known to be true on other paths; these
+          /// would amount to knowing that a variable is or is not of some
+          /// type.
+          newFalseTypes.removeAll(otherTrueTypes);
+          if (newFalseTypes.isEmpty) {
+            newFalseTypes = null;
+          } else {
+            newTypesOfInterest.addAll(newFalseTypes);
+          }
+        }
+        if (newTrueTypes != null || newFalseTypes != null) {
+          // Only include type holders with information.
+          newTypeHolders
+              .add(new TypeHolder(declaredType, newTrueTypes, newFalseTypes));
+        }
+      }
+    }
+
+    Set<ir.DartType> thisTrueTypes = new Set<ir.DartType>();
+    Set<ir.DartType> thisFalseTypes = new Set<ir.DartType>();
+    for (TypeHolder typeHolder in typeHolders) {
+      if (typeHolder.trueTypes != null) {
+        thisTrueTypes.addAll(typeHolder.trueTypes);
+      }
+      if (typeHolder.falseTypes != null) {
+        thisFalseTypes.addAll(typeHolder.falseTypes);
+      }
+    }
+
+    Set<ir.DartType> otherTrueTypes = new Set<ir.DartType>();
+    Set<ir.DartType> otherFalseTypes = new Set<ir.DartType>();
+    for (TypeHolder typeHolder in other.typeHolders) {
+      if (typeHolder.trueTypes != null) {
+        otherTrueTypes.addAll(typeHolder.trueTypes);
+      }
+      if (typeHolder.falseTypes != null) {
+        otherFalseTypes.addAll(typeHolder.falseTypes);
+      }
+    }
+
+    addTypeHolders(this.typeHolders, otherTrueTypes, otherFalseTypes,
+        other.typesOfInterest);
+    addTypeHolders(
+        other.typeHolders, thisTrueTypes, thisFalseTypes, this.typesOfInterest);
+
+    if (newTypeHolders.isEmpty) {
+      assert(newTypesOfInterest.isEmpty);
+      return null;
+    }
+
+    return new TargetInfo(declaredType, newTypeHolders, newTypesOfInterest);
   }
 
   /// Computes a single type that soundly represents the promoted type of the
@@ -1429,6 +1491,20 @@
     return candidate;
   }
 
+  void _getText(
+      StringBuffer sb, String Function(Iterable<ir.DartType>) typesToText) {
+    sb.write('[');
+    String comma = '';
+    for (TypeHolder typeHolder in typeHolders) {
+      sb.write(comma);
+      typeHolder._getText(sb, typesToText);
+      comma = ',';
+    }
+    sb.write('|');
+    sb.write(typesToText(typesOfInterest));
+    sb.write(']');
+  }
+
   String toString() {
     StringBuffer sb = new StringBuffer();
     sb.write('TargetInfo(');
@@ -1489,22 +1565,11 @@
     Map<ir.VariableDeclaration, TargetInfo> newInfoMap = {};
     bool changed = false;
     _targetInfoMap.forEach((ir.VariableDeclaration variable, TargetInfo info) {
-      TargetInfo otherInfo = other._targetInfoMap[variable];
-      if (otherInfo != null) {
-        TargetInfo result = info.join(otherInfo);
-        changed |= !identical(info, result);
+      TargetInfo result = info.join(other._targetInfoMap[variable]);
+      changed |= !identical(info, result);
+      if (result != null) {
+        // Add only non-empty information.
         newInfoMap[variable] = result;
-      } else {
-        changed = true;
-        newInfoMap[variable] = info;
-      }
-    });
-    other._targetInfoMap
-        .forEach((ir.VariableDeclaration variable, TargetInfo otherInfo) {
-      TargetInfo info = _targetInfoMap[variable];
-      if (info == null) {
-        changed = true;
-        newInfoMap[variable] = otherInfo;
       }
     });
     return changed ? new TypeMap(newInfoMap) : this;
@@ -1571,6 +1636,19 @@
     return type ?? node.promotedType ?? node.variable.type;
   }
 
+  String getText(String Function(Iterable<ir.DartType>) typesToText) {
+    StringBuffer sb = new StringBuffer();
+    sb.write('{');
+    String comma = '';
+    _targetInfoMap.forEach((ir.VariableDeclaration variable, TargetInfo info) {
+      sb.write('${comma}${variable.name}:');
+      info._getText(sb, typesToText);
+      comma = ',';
+    });
+    sb.write('}');
+    return sb.toString();
+  }
+
   String toString() {
     StringBuffer sb = new StringBuffer();
     sb.write('TypeMap(');
diff --git a/pkg/compiler/lib/src/kernel/element_map_impl.dart b/pkg/compiler/lib/src/kernel/element_map_impl.dart
index 695412d..6c0a91c 100644
--- a/pkg/compiler/lib/src/kernel/element_map_impl.dart
+++ b/pkg/compiler/lib/src/kernel/element_map_impl.dart
@@ -30,6 +30,7 @@
 import '../frontend_strategy.dart';
 import '../ir/debug.dart';
 import '../ir/element_map.dart';
+import '../ir/static_type.dart';
 import '../ir/scope.dart';
 import '../ir/types.dart';
 import '../ir/visitors.dart';
@@ -109,6 +110,8 @@
   BehaviorBuilder _nativeBehaviorBuilder;
   FrontendStrategy _frontendStrategy;
 
+  Map<KMember, Map<ir.Expression, TypeMap>> typeMapsForTesting;
+
   KernelToElementMapImpl(this.reporter, Environment environment,
       this._frontendStrategy, this.options) {
     _elementEnvironment = new KernelElementEnvironment(this);
@@ -1344,6 +1347,10 @@
     ir.Member node = memberData.node;
     KernelImpactBuilder builder = new KernelImpactBuilder(
         this, member, reporter, options, variableScopeModel, annotations);
+    if (retainDataForTesting) {
+      typeMapsForTesting ??= {};
+      typeMapsForTesting[member] = builder.typeMapsForTesting = {};
+    }
     node.accept(builder);
     memberData.staticTypes = builder.cachedStaticTypes;
     return builder.impactBuilder;
@@ -1361,6 +1368,10 @@
     return staticTypes;
   }
 
+  Map<ir.Expression, TypeMap> getTypeMapsForTesting(KMember member) {
+    return typeMapsForTesting[member];
+  }
+
   /// Returns the kernel [ir.Procedure] node for the [method].
   ir.Procedure _lookupProcedure(KFunction method) {
     return members.getData(method).node;
diff --git a/tests/compiler/dart2js/helpers/ir_types.dart b/tests/compiler/dart2js/helpers/ir_types.dart
new file mode 100644
index 0000000..280ae12
--- /dev/null
+++ b/tests/compiler/dart2js/helpers/ir_types.dart
@@ -0,0 +1,137 @@
+// Copyright (c) 2019, 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:kernel/ast.dart' as ir;
+import 'package:kernel/class_hierarchy.dart' as ir;
+import 'package:kernel/core_types.dart' as ir;
+import 'package:kernel/type_algebra.dart' as ir;
+import 'package:kernel/type_environment.dart' as ir;
+
+class TypeTextVisitor implements ir.DartTypeVisitor1<void, StringBuffer> {
+  const TypeTextVisitor();
+
+  @override
+  void defaultDartType(ir.DartType node, StringBuffer sb) {
+    throw new UnsupportedError("Unhandled type $node (${node.runtimeType}).");
+  }
+
+  void writeType(ir.DartType type, StringBuffer sb) {
+    type.accept1(this, sb);
+  }
+
+  void _writeTypes(List<ir.DartType> types, StringBuffer sb) {
+    String comma = '';
+    for (ir.DartType type in types) {
+      sb.write(comma);
+      writeType(type, sb);
+      comma = ',';
+    }
+  }
+
+  void _writeTypeArguments(List<ir.DartType> typeArguments, StringBuffer sb) {
+    if (typeArguments.isNotEmpty) {
+      sb.write('<');
+      _writeTypes(typeArguments, sb);
+      sb.write('>');
+    }
+  }
+
+  @override
+  void visitTypedefType(ir.TypedefType node, StringBuffer sb) {
+    sb.write(node.typedefNode.name);
+    _writeTypeArguments(node.typeArguments, sb);
+  }
+
+  @override
+  void visitTypeParameterType(ir.TypeParameterType node, StringBuffer sb) {
+    sb.write(node.parameter.name);
+  }
+
+  @override
+  void visitFunctionType(ir.FunctionType node, StringBuffer sb) {
+    writeType(node.returnType, sb);
+    sb.write(' Function');
+    if (node.typeParameters.isNotEmpty) {
+      sb.write('<');
+      String comma = '';
+      for (ir.TypeParameter typeParameter in node.typeParameters) {
+        sb.write(comma);
+        sb.write(typeParameter.name);
+        if (typeParameter is! ir.DynamicType) {
+          sb.write(' extends ');
+          writeType(typeParameter.bound, sb);
+        }
+        comma = ',';
+      }
+      sb.write('>');
+    }
+    sb.write('(');
+    _writeTypes(
+        node.positionalParameters.take(node.requiredParameterCount), sb);
+    if (node.requiredParameterCount < node.positionalParameters.length) {
+      if (node.requiredParameterCount > 0) {
+        sb.write(',');
+      }
+      _writeTypes(
+          node.positionalParameters.skip(node.requiredParameterCount), sb);
+    }
+    if (node.namedParameters.isNotEmpty) {
+      if (node.positionalParameters.isNotEmpty) {
+        sb.write(',');
+      }
+      String comma = '';
+      for (ir.NamedType namedType in node.namedParameters) {
+        sb.write(comma);
+        sb.write(namedType.name);
+        sb.write(': ');
+        writeType(namedType.type, sb);
+        comma = ',';
+      }
+    }
+    sb.write(')');
+  }
+
+  @override
+  void visitInterfaceType(ir.InterfaceType node, StringBuffer sb) {
+    sb.write(node.classNode.name);
+    _writeTypeArguments(node.typeArguments, sb);
+  }
+
+  @override
+  void visitBottomType(ir.BottomType node, StringBuffer sb) {
+    sb.write('<bottom>');
+  }
+
+  @override
+  void visitVoidType(ir.VoidType node, StringBuffer sb) {
+    sb.write('void');
+  }
+
+  @override
+  void visitDynamicType(ir.DynamicType node, StringBuffer sb) {
+    sb.write('dynamic');
+  }
+
+  @override
+  void visitInvalidType(ir.InvalidType node, StringBuffer sb) {
+    sb.write('<invalid>');
+  }
+}
+
+String typeToText(ir.DartType type) {
+  StringBuffer sb = new StringBuffer();
+  const TypeTextVisitor().writeType(type, sb);
+  return sb.toString();
+}
+
+String typesToText(Iterable<ir.DartType> types) {
+  StringBuffer sb = new StringBuffer();
+  String comma = '';
+  for (ir.DartType type in types) {
+    sb.write(comma);
+    const TypeTextVisitor().writeType(type, sb);
+    comma = ',';
+  }
+  return sb.toString();
+}
diff --git a/tests/compiler/dart2js/static_type/static_type_test.dart b/tests/compiler/dart2js/static_type/static_type_test.dart
index 0216e66..070d0f6 100644
--- a/tests/compiler/dart2js/static_type/static_type_test.dart
+++ b/tests/compiler/dart2js/static_type/static_type_test.dart
@@ -17,6 +17,7 @@
 import 'package:kernel/type_environment.dart' as ir;
 import '../equivalence/id_equivalence.dart';
 import '../equivalence/id_equivalence_helper.dart';
+import '../helpers/ir_types.dart';
 
 main(List<String> args) {
   asyncTest(() async {
@@ -26,14 +27,6 @@
   });
 }
 
-class Tags {
-  static const String typeUse = 'type';
-  static const String staticUse = 'static';
-  static const String dynamicUse = 'dynamic';
-  static const String constantUse = 'constant';
-  static const String runtimeTypeUse = 'runtimeType';
-}
-
 class StaticTypeDataComputer extends DataComputer<String> {
   ir.TypeEnvironment _typeEnvironment;
 
@@ -70,117 +63,6 @@
   DataInterpreter<String> get dataValidator => const StringDataInterpreter();
 }
 
-class TypeTextVisitor implements ir.DartTypeVisitor1<void, StringBuffer> {
-  const TypeTextVisitor();
-
-  @override
-  void defaultDartType(ir.DartType node, StringBuffer sb) {
-    throw new UnsupportedError("Unhandled type $node (${node.runtimeType}).");
-  }
-
-  void writeType(ir.DartType type, StringBuffer sb) {
-    type.accept1(this, sb);
-  }
-
-  void _writeTypes(List<ir.DartType> types, StringBuffer sb) {
-    String comma = '';
-    for (ir.DartType type in types) {
-      sb.write(comma);
-      writeType(type, sb);
-      comma = ',';
-    }
-  }
-
-  void _writeTypeArguments(List<ir.DartType> typeArguments, StringBuffer sb) {
-    if (typeArguments.isNotEmpty) {
-      sb.write('<');
-      _writeTypes(typeArguments, sb);
-      sb.write('>');
-    }
-  }
-
-  @override
-  void visitTypedefType(ir.TypedefType node, StringBuffer sb) {
-    sb.write(node.typedefNode.name);
-    _writeTypeArguments(node.typeArguments, sb);
-  }
-
-  @override
-  void visitTypeParameterType(ir.TypeParameterType node, StringBuffer sb) {
-    sb.write(node.parameter.name);
-  }
-
-  @override
-  void visitFunctionType(ir.FunctionType node, StringBuffer sb) {
-    writeType(node.returnType, sb);
-    sb.write(' Function');
-    if (node.typeParameters.isNotEmpty) {
-      sb.write('<');
-      String comma = '';
-      for (ir.TypeParameter typeParameter in node.typeParameters) {
-        sb.write(comma);
-        sb.write(typeParameter.name);
-        if (typeParameter is! ir.DynamicType) {
-          sb.write(' extends ');
-          writeType(typeParameter.bound, sb);
-        }
-        comma = ',';
-      }
-      sb.write('>');
-    }
-    sb.write('(');
-    _writeTypes(
-        node.positionalParameters.take(node.requiredParameterCount), sb);
-    if (node.requiredParameterCount < node.positionalParameters.length) {
-      if (node.requiredParameterCount > 0) {
-        sb.write(',');
-      }
-      _writeTypes(
-          node.positionalParameters.skip(node.requiredParameterCount), sb);
-    }
-    if (node.namedParameters.isNotEmpty) {
-      if (node.positionalParameters.isNotEmpty) {
-        sb.write(',');
-      }
-      String comma = '';
-      for (ir.NamedType namedType in node.namedParameters) {
-        sb.write(comma);
-        sb.write(namedType.name);
-        sb.write(': ');
-        writeType(namedType.type, sb);
-        comma = ',';
-      }
-    }
-    sb.write(')');
-  }
-
-  @override
-  void visitInterfaceType(ir.InterfaceType node, StringBuffer sb) {
-    sb.write(node.classNode.name);
-    _writeTypeArguments(node.typeArguments, sb);
-  }
-
-  @override
-  void visitBottomType(ir.BottomType node, StringBuffer sb) {
-    sb.write('<bottom>');
-  }
-
-  @override
-  void visitVoidType(ir.VoidType node, StringBuffer sb) {
-    sb.write('void');
-  }
-
-  @override
-  void visitDynamicType(ir.DynamicType node, StringBuffer sb) {
-    sb.write('dynamic');
-  }
-
-  @override
-  void visitInvalidType(ir.InvalidType node, StringBuffer sb) {
-    sb.write('<invalid>');
-  }
-}
-
 /// IR visitor for computing inference data for a member.
 class StaticTypeIrComputer extends IrDataExtractor<String> {
   final CachedStaticType staticTypeCache;
@@ -189,12 +71,6 @@
       Map<Id, ActualData<String>> actualMap, this.staticTypeCache)
       : super(reporter, actualMap);
 
-  String getStaticTypeValue(ir.DartType type) {
-    StringBuffer sb = new StringBuffer();
-    const TypeTextVisitor().writeType(type, sb);
-    return sb.toString();
-  }
-
   @override
   String computeMemberValue(Id id, ir.Member node) {
     return null;
@@ -203,7 +79,7 @@
   @override
   String computeNodeValue(Id id, ir.TreeNode node) {
     if (node is ir.VariableGet || node is ir.MethodInvocation) {
-      return getStaticTypeValue(node.accept(staticTypeCache));
+      return typeToText(node.accept(staticTypeCache));
     }
     return null;
   }
diff --git a/tests/compiler/dart2js/static_type/type_promotion_data/equals.dart b/tests/compiler/dart2js/static_type/type_promotion_data/equals.dart
new file mode 100644
index 0000000..17df94a
--- /dev/null
+++ b/tests/compiler/dart2js/static_type/type_promotion_data/equals.dart
@@ -0,0 +1,76 @@
+// Copyright (c) 2019, 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.
+
+bool equals(e1, e2, bool unordered) {
+  if (/*{}*/ e1 is Set) {
+    return /*{e1:[{true:Set<dynamic>}|Set<dynamic>]}*/
+        e2 is Set &&
+            /*{
+             e1:[{true:Set<dynamic>}|Set<dynamic>],
+             e2:[{true:Set<dynamic>}|Set<dynamic>]}
+            */
+            e1 == null;
+  }
+  if (/*{e1:[{false:Set<dynamic>}|Set<dynamic>]}*/ e1 is Map) {
+    return
+        /*{e1:[{true:Map<dynamic,dynamic>,false:Set<dynamic>}|Set<dynamic>,Map<dynamic,dynamic>]}*/
+        e2 is Map &&
+            /*{
+             e1:[{true:Map<dynamic,dynamic>,false:Set<dynamic>}|Set<dynamic>,Map<dynamic,dynamic>],
+             e2:[{true:Map<dynamic,dynamic>}|Map<dynamic,dynamic>]}
+            */
+            e1 == null;
+  }
+  if (! /*{e1:[{false:Set<dynamic>,Map<dynamic,dynamic>}|Set<dynamic>,Map<dynamic,dynamic>]}*/
+      unordered) {
+    if (/*{e1:[{false:Set<dynamic>,Map<dynamic,dynamic>}|Set<dynamic>,Map<dynamic,dynamic>]}*/
+        e1 is List) {
+      return
+          /*{e1:[{true:List<dynamic>,false:Set<dynamic>,Map<dynamic,dynamic>}|Set<dynamic>,Map<dynamic,dynamic>,List<dynamic>]}*/
+          e2 is List &&
+              /*{
+               e1:[{true:List<dynamic>,false:Set<dynamic>,Map<dynamic,dynamic>}|Set<dynamic>,Map<dynamic,dynamic>,List<dynamic>],
+               e2:[{true:List<dynamic>}|List<dynamic>]}
+              */
+              e1 == null;
+    }
+    if (/*{e1:[{false:Set<dynamic>,Map<dynamic,dynamic>,List<dynamic>}|Set<dynamic>,Map<dynamic,dynamic>,List<dynamic>]}*/
+        e1 is Iterable) {
+      return
+          /*{e1:[{true:Iterable<dynamic>,false:Set<dynamic>,Map<dynamic,dynamic>,List<dynamic>}|Set<dynamic>,Map<dynamic,dynamic>,List<dynamic>,Iterable<dynamic>]}*/
+          e2 is Iterable &&
+              /*{
+               e1:[{true:Iterable<dynamic>,false:Set<dynamic>,Map<dynamic,dynamic>,List<dynamic>}|Set<dynamic>,Map<dynamic,dynamic>,List<dynamic>,Iterable<dynamic>],
+               e2:[{true:Iterable<dynamic>}|Iterable<dynamic>]}
+              */
+              e1 == null;
+    }
+  } else if (/*{e1:[{false:Set<dynamic>,Map<dynamic,dynamic>}|Set<dynamic>,Map<dynamic,dynamic>]}*/
+      e1 is Iterable) {
+    if (
+        /*{e1:[{true:Iterable<dynamic>,false:Set<dynamic>,Map<dynamic,dynamic>}|Set<dynamic>,Map<dynamic,dynamic>,Iterable<dynamic>]}*/
+        e1 is List !=
+            /*{e1:[{true:Iterable<dynamic>,false:Set<dynamic>,Map<dynamic,dynamic>}|Set<dynamic>,Map<dynamic,dynamic>,Iterable<dynamic>]}*/
+            e2 is List) {
+      return
+          /*{e1:[{true:Iterable<dynamic>,false:Set<dynamic>,Map<dynamic,dynamic>}|Iterable<dynamic>,Set<dynamic>,Map<dynamic,dynamic>]}*/
+          e1 == null;
+    }
+    return /*{e1:[{true:Iterable<dynamic>,false:Set<dynamic>,Map<dynamic,dynamic>}|Iterable<dynamic>,Set<dynamic>,Map<dynamic,dynamic>]}*/
+        e2 is Iterable &&
+
+            /*{
+             e1:[{true:Iterable<dynamic>,false:Set<dynamic>,Map<dynamic,dynamic>}|Iterable<dynamic>,Set<dynamic>,Map<dynamic,dynamic>],
+             e2:[{true:Iterable<dynamic>}|Iterable<dynamic>]}
+            */
+            e1 == null;
+  }
+  return
+      /*{e1:[{false:Set<dynamic>,Map<dynamic,dynamic>,Iterable<dynamic>}|Set<dynamic>,Map<dynamic,dynamic>,Iterable<dynamic>]}*/
+      e1 == null;
+}
+
+main() {
+  equals(null, null, true);
+}
diff --git a/tests/compiler/dart2js/static_type/type_promotion_data/if.dart b/tests/compiler/dart2js/static_type/type_promotion_data/if.dart
new file mode 100644
index 0000000..5c54faa
--- /dev/null
+++ b/tests/compiler/dart2js/static_type/type_promotion_data/if.dart
@@ -0,0 +1,102 @@
+// Copyright (c) 2019, 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.
+
+class A {}
+
+class B {}
+
+main() {
+  ifThen(null);
+  ifThenSequence(null);
+  ifThenElse(null);
+  ifThenElseSequence(null);
+  ifNotReturn(null);
+  nestedIf(null);
+  nestedIf2(null);
+  nestedIfNotReturn(null);
+}
+
+ifThen(o) {
+  /*{}*/ o;
+  if (/*{}*/ o is A) {
+    /*{o:[{true:A}|A]}*/ o;
+  }
+  /*{}*/ o;
+}
+
+ifThenSequence(o) {
+  /*{}*/ o;
+  if (/*{}*/ o is A) {
+    /*{o:[{true:A}|A]}*/ o;
+  }
+  /*{}*/ o;
+  if (/*{}*/ o is B) {
+    /*{o:[{true:B}|B]}*/ o;
+  }
+  /*{}*/ o;
+}
+
+ifThenElse(o) {
+  /*{}*/ o;
+  if (/*{}*/ o is A) {
+    /*{o:[{true:A}|A]}*/ o;
+  } else {
+    /*{o:[{false:A}|A]}*/ o;
+  }
+  /*{}*/ o;
+}
+
+ifThenElseSequence(o) {
+  /*{}*/ o;
+  if (/*{}*/ o is A) {
+    /*{o:[{true:A}|A]}*/ o;
+  } else {
+    /*{o:[{false:A}|A]}*/ o;
+  }
+  /*{}*/ o;
+  if (/*{}*/ o is B) {
+    /*{o:[{true:B}|B]}*/ o;
+  } else {
+    /*{o:[{false:B}|B]}*/ o;
+  }
+  /*{}*/ o;
+}
+
+ifNotReturn(o) {
+  /*{}*/ o;
+  if (/*{}*/ o is! A) {
+    return /*{o:[{false:A}|A]}*/ o;
+  }
+  /*{o:[{true:A}|A]}*/ o;
+}
+
+nestedIf(o) {
+  if (/*{}*/ o is A) {
+    if (/*{o:[{true:A}|A]}*/ o is B) {
+      return /*{o:[{true:A,B}|A,B]}*/ o;
+    }
+  }
+  /*{}*/ o;
+}
+
+nestedIf2(o) {
+  if (/*{}*/ o is A) {
+    if (/*{o:[{true:A}|A]}*/ o is B) {
+      return /*{o:[{true:A,B}|A,B]}*/ o;
+    }
+  } else if (/*{o:[{false:A}|A]}*/ o is B) {
+    /*{o:[{true:B,false:A}|A,B]}*/ o;
+  }
+  /*{}*/ o;
+}
+
+nestedIfNotReturn(o) {
+  if (/*{}*/ o is A) {
+    if (/*{o:[{true:A}|A]}*/ o is! B) {
+      return /*{o:[{true:A,false:B}|A,B]}*/ o;
+    }
+    /*{o:[{true:A,B}|A,B]}*/ o;
+  }
+  /*{}*/ o;
+}
diff --git a/tests/compiler/dart2js/static_type/type_promotion_data/null.dart b/tests/compiler/dart2js/static_type/type_promotion_data/null.dart
new file mode 100644
index 0000000..02a3697
--- /dev/null
+++ b/tests/compiler/dart2js/static_type/type_promotion_data/null.dart
@@ -0,0 +1,46 @@
+// Copyright (c) 2019, 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.
+
+main() {
+  ifNull(null);
+  ifNullElse(null);
+  ifNotNull(null);
+  ifNotNullElse(null);
+}
+
+ifNull(o) {
+  /*{}*/ o;
+  if (/*{}*/ o == null) {
+    /*{o:[{false:dynamic}|dynamic]}*/ o;
+  }
+  /*{}*/ o;
+}
+
+ifNullElse(o) {
+  /*{}*/ o;
+  if (/*{}*/ o == null) {
+    /*{o:[{false:dynamic}|dynamic]}*/ o;
+  } else {
+    /*{o:[{true:dynamic}|dynamic]}*/ o;
+  }
+  /*{}*/ o;
+}
+
+ifNotNull(o) {
+  /*{}*/ o;
+  if (/*{}*/ o != null) {
+    /*{o:[{true:dynamic}|dynamic]}*/ o;
+  }
+  /*{}*/ o;
+}
+
+ifNotNullElse(o) {
+  /*{}*/ o;
+  if (/*{}*/ o != null) {
+    /*{o:[{true:dynamic}|dynamic]}*/ o;
+  } else {
+    /*{o:[{false:dynamic}|dynamic]}*/ o;
+  }
+  /*{}*/ o;
+}
diff --git a/tests/compiler/dart2js/static_type/type_promotion_test.dart b/tests/compiler/dart2js/static_type/type_promotion_test.dart
new file mode 100644
index 0000000..76f448f
--- /dev/null
+++ b/tests/compiler/dart2js/static_type/type_promotion_test.dart
@@ -0,0 +1,84 @@
+// Copyright (c) 2018, 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 'dart:io';
+import 'package:async_helper/async_helper.dart';
+import 'package:compiler/src/compiler.dart';
+import 'package:compiler/src/diagnostics/diagnostic_listener.dart';
+import 'package:compiler/src/elements/entities.dart';
+import 'package:compiler/src/ir/static_type.dart';
+import 'package:compiler/src/kernel/element_map_impl.dart';
+import 'package:compiler/src/kernel/kernel_strategy.dart';
+import 'package:kernel/ast.dart' as ir;
+import 'package:kernel/class_hierarchy.dart' as ir;
+import 'package:kernel/core_types.dart' as ir;
+import 'package:kernel/type_algebra.dart' as ir;
+import 'package:kernel/type_environment.dart' as ir;
+import '../equivalence/id_equivalence.dart';
+import '../equivalence/id_equivalence_helper.dart';
+import '../helpers/ir_types.dart';
+
+main(List<String> args) {
+  asyncTest(() async {
+    Directory dataDir =
+        new Directory.fromUri(Platform.script.resolve('type_promotion_data'));
+    await checkTests(dataDir, new TypePromotionDataComputer(),
+        args: args, testFrontend: true);
+  });
+}
+
+class TypePromotionDataComputer extends DataComputer<String> {
+  ir.TypeEnvironment _typeEnvironment;
+
+  ir.TypeEnvironment getTypeEnvironment(KernelToElementMapImpl elementMap) {
+    if (_typeEnvironment == null) {
+      ir.Component component = elementMap.env.mainComponent;
+      _typeEnvironment = new ir.TypeEnvironment(
+          new ir.CoreTypes(component), new ir.ClassHierarchy(component));
+    }
+    return _typeEnvironment;
+  }
+
+  /// Compute type inference data for [member] from kernel based inference.
+  ///
+  /// Fills [actualMap] with the data.
+  @override
+  void computeMemberData(Compiler compiler, MemberEntity member,
+      Map<Id, ActualData<String>> actualMap,
+      {bool verbose: false}) {
+    KernelFrontEndStrategy frontendStrategy = compiler.frontendStrategy;
+    KernelToElementMapImpl elementMap = frontendStrategy.elementMap;
+    Map<ir.Expression, TypeMap> typeMaps =
+        elementMap.getTypeMapsForTesting(member);
+    ir.Member node = elementMap.getMemberNode(member);
+    new TypePromotionIrComputer(compiler.reporter, actualMap, typeMaps)
+        .run(node);
+  }
+
+  @override
+  DataInterpreter<String> get dataValidator => const StringDataInterpreter();
+}
+
+/// IR visitor for computing inference data for a member.
+class TypePromotionIrComputer extends IrDataExtractor<String> {
+  final Map<ir.Expression, TypeMap> typeMaps;
+
+  TypePromotionIrComputer(DiagnosticReporter reporter,
+      Map<Id, ActualData<String>> actualMap, this.typeMaps)
+      : super(reporter, actualMap);
+
+  @override
+  String computeMemberValue(Id id, ir.Member node) {
+    return null;
+  }
+
+  @override
+  String computeNodeValue(Id id, ir.TreeNode node) {
+    if (node is ir.VariableGet) {
+      TypeMap type = typeMaps[node];
+      return type.getText(typesToText);
+    }
+    return null;
+  }
+}