Local Type Analysis.

Stolen from https://r8-review.googlesource.com/c/r8/+/4140
and then reinterpreted and fixed a couple nits.

Bug: 69964136
Change-Id: I84590d61325486d78312cfe4583391d283385c1f
diff --git a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
index e920751..d4a2ef9 100644
--- a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
+++ b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
@@ -174,6 +174,7 @@
   public final DexType enumType = createType(enumDescriptor);
   public final DexType annotationType = createType(annotationDescriptor);
   public final DexType throwableType = createType(throwableDescriptor);
+  public final DexType classType = createType(classDescriptor);
 
   public final DexType stringBuilderType = createType(stringBuilderDescriptor);
   public final DexType stringBufferType = createType(stringBufferDescriptor);
diff --git a/src/main/java/com/android/tools/r8/graph/DexType.java b/src/main/java/com/android/tools/r8/graph/DexType.java
index a9689e5..9983f29 100644
--- a/src/main/java/com/android/tools/r8/graph/DexType.java
+++ b/src/main/java/com/android/tools/r8/graph/DexType.java
@@ -348,7 +348,7 @@
     }
   }
 
-  private int getNumberOfLeadingSquareBrackets() {
+  public int getNumberOfLeadingSquareBrackets() {
     int leadingSquareBrackets = 0;
     while (descriptor.content[leadingSquareBrackets] == '[') {
       leadingSquareBrackets++;
@@ -468,4 +468,51 @@
     assert hierarchyLevel != UNKNOWN_LEVEL;
     return type.directSubtypes.contains(this);
   }
+
+  public DexType computeLeastUpperBound(AppInfoWithSubtyping appInfo, DexType other) {
+    DexType objectType = appInfo.dexItemFactory.objectType;
+    // If either one is interface,
+    //   1) one of them is a super type of the other
+    //   2) otherwise, the object type is the least upper bound.
+    if (isInterface() || other.isInterface()) {
+      if (isSubtypeOf(other, appInfo)) {
+        return other;
+      }
+      if (other.isSubtypeOf(this, appInfo)) {
+        return this;
+      }
+      return objectType;
+    }
+    // To make the logic simple, change the role if the other is lower than this.
+    if (other.hierarchyLevel < this.hierarchyLevel) {
+      return other.computeLeastUpperBound(appInfo, this);
+    }
+    DexClass dexClass;
+    // Make both of other and this in the same level.
+    while (other.hierarchyLevel > this.hierarchyLevel) {
+      dexClass = appInfo.definitionFor(other);
+      if (dexClass == null) {
+        return objectType;
+      }
+      other = dexClass.superType;
+    }
+    // At this point, they are at the same level.
+    // lub starts from this, and will move up; other starts from itself, and will move up, too.
+    // They move up in their own hierarchy tree, and will repeat the process until they meet.
+    // (It will stop at anytime when either one's definition is not found.)
+    DexType lub = this;
+    while (other != lub) {
+      dexClass = appInfo.definitionFor(other);
+      if (dexClass == null) {
+        return objectType;
+      }
+      other = dexClass.superType;
+      dexClass = appInfo.definitionFor(lub);
+      if (dexClass == null) {
+        return objectType;
+      }
+      lub = dexClass.superType;
+    }
+    return lub;
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/type/ArrayTypeLatticeElement.java b/src/main/java/com/android/tools/r8/ir/analysis/type/ArrayTypeLatticeElement.java
new file mode 100644
index 0000000..c388ec5
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/analysis/type/ArrayTypeLatticeElement.java
@@ -0,0 +1,65 @@
+// Copyright (c) 2017, the R8 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.
+package com.android.tools.r8.ir.analysis.type;
+
+import com.android.tools.r8.graph.AppInfoWithSubtyping;
+import com.android.tools.r8.graph.DexType;
+import com.google.common.base.Strings;
+
+public class ArrayTypeLatticeElement extends TypeLatticeElement {
+  final DexType elementType;
+  final int nesting;
+
+  ArrayTypeLatticeElement(DexType elementType, int nesting, boolean isNullable) {
+    super(isNullable);
+    this.elementType = elementType;
+    this.nesting = nesting;
+  }
+
+  @Override
+  TypeLatticeElement asNullable() {
+    return isNullable() ? this : new ArrayTypeLatticeElement(elementType, nesting, true);
+  }
+
+  @Override
+  public TypeLatticeElement arrayGet(AppInfoWithSubtyping appInfo) {
+    if (nesting == 1) {
+      return fromDexType(appInfo, elementType, true);
+    }
+    return new ArrayTypeLatticeElement(elementType, nesting - 1, true);
+  }
+
+  @Override
+  public TypeLatticeElement checkCast(AppInfoWithSubtyping appInfo, DexType castType) {
+    if (castType.getNumberOfLeadingSquareBrackets() == nesting) {
+      DexType base = castType.toBaseType(appInfo.dexItemFactory);
+      if (elementType.isSubtypeOf(base, appInfo)) {
+        return this;
+      }
+    }
+    return super.checkCast(appInfo, castType);
+  }
+
+  @Override
+  public String toString() {
+    return isNullableString() + elementType.toString() + Strings.repeat("[]", nesting);
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (!super.equals(o)) {
+      return false;
+    }
+    ArrayTypeLatticeElement other = (ArrayTypeLatticeElement) o;
+    return nesting == other.nesting && elementType == other.elementType;
+  }
+
+  @Override
+  public int hashCode() {
+    int result = super.hashCode();
+    result = 31 * result + elementType.hashCode();
+    result = 31 * result + nesting;
+    return result;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/type/Bottom.java b/src/main/java/com/android/tools/r8/ir/analysis/type/Bottom.java
new file mode 100644
index 0000000..3de4233
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/analysis/type/Bottom.java
@@ -0,0 +1,31 @@
+// Copyright (c) 2017, the R8 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.
+package com.android.tools.r8.ir.analysis.type;
+
+public class Bottom extends TypeLatticeElement {
+  private static final Bottom INSTANCE = new Bottom();
+
+  private Bottom() {
+    super(true);
+  }
+
+  @Override
+  TypeLatticeElement asNullable() {
+    return this;
+  }
+
+  static Bottom getInstance() {
+    return INSTANCE;
+  }
+
+  @Override
+  boolean isBottom() {
+    return true;
+  }
+
+  @Override
+  public String toString() {
+    return "BOTTOM (empty)";
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/type/ClassTypeLatticeElement.java b/src/main/java/com/android/tools/r8/ir/analysis/type/ClassTypeLatticeElement.java
new file mode 100644
index 0000000..2120669
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/analysis/type/ClassTypeLatticeElement.java
@@ -0,0 +1,56 @@
+// Copyright (c) 2017, the R8 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.
+package com.android.tools.r8.ir.analysis.type;
+
+import com.android.tools.r8.graph.AppInfoWithSubtyping;
+import com.android.tools.r8.graph.DexType;
+
+public class ClassTypeLatticeElement extends TypeLatticeElement {
+  final DexType classType;
+
+  ClassTypeLatticeElement(DexType classType, boolean isNullable) {
+    super(isNullable);
+    assert classType.isClassType();
+    this.classType = classType;
+  }
+
+  @Override
+  TypeLatticeElement asNullable() {
+    return isNullable() ? this : new ClassTypeLatticeElement(classType, true);
+  }
+
+  @Override
+  public TypeLatticeElement arrayGet(AppInfoWithSubtyping appInfo) {
+    return objectType(appInfo, true);
+  }
+
+  @Override
+  public TypeLatticeElement checkCast(AppInfoWithSubtyping appInfo, DexType castType) {
+    if (classType.isSubtypeOf(castType, appInfo)) {
+      return this;
+    }
+    return fromDexType(appInfo, castType, isNullable());
+  }
+
+  @Override
+  public String toString() {
+    return isNullableString() + classType.toString();
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (!super.equals(o)) {
+      return false;
+    }
+    ClassTypeLatticeElement other = (ClassTypeLatticeElement) o;
+    return classType == other.classType;
+  }
+
+  @Override
+  public int hashCode() {
+    int result = super.hashCode();
+    result = 31 * result + classType.hashCode();
+    return result;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/type/NullLatticeElement.java b/src/main/java/com/android/tools/r8/ir/analysis/type/NullLatticeElement.java
new file mode 100644
index 0000000..18ffad3
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/analysis/type/NullLatticeElement.java
@@ -0,0 +1,26 @@
+// Copyright (c) 2017, the R8 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.
+package com.android.tools.r8.ir.analysis.type;
+
+public class NullLatticeElement extends TypeLatticeElement {
+  private static final NullLatticeElement INSTANCE = new NullLatticeElement();
+
+  private NullLatticeElement() {
+    super(true);
+  }
+
+  @Override
+  TypeLatticeElement asNullable() {
+    return this;
+  }
+
+  static NullLatticeElement getInstance() {
+    return INSTANCE;
+  }
+
+  @Override
+  public String toString() {
+    return "NULL";
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/type/PrimitiveArrayTypeLatticeElement.java b/src/main/java/com/android/tools/r8/ir/analysis/type/PrimitiveArrayTypeLatticeElement.java
new file mode 100644
index 0000000..d92e891
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/analysis/type/PrimitiveArrayTypeLatticeElement.java
@@ -0,0 +1,50 @@
+// Copyright (c) 2017, the R8 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.
+package com.android.tools.r8.ir.analysis.type;
+
+import com.android.tools.r8.graph.AppInfoWithSubtyping;
+import com.google.common.base.Strings;
+
+public class PrimitiveArrayTypeLatticeElement extends TypeLatticeElement {
+  final int nesting;
+
+  PrimitiveArrayTypeLatticeElement(int nesting, boolean isNullable) {
+    super(isNullable);
+    this.nesting = nesting;
+  }
+
+  @Override
+  TypeLatticeElement asNullable() {
+    return isNullable() ? this : new PrimitiveArrayTypeLatticeElement(nesting, true);
+  }
+
+  @Override
+  public TypeLatticeElement arrayGet(AppInfoWithSubtyping appInfo) {
+    if (nesting == 1) {
+      return PrimitiveTypeLatticeElement.getInstance();
+    }
+    return new PrimitiveArrayTypeLatticeElement(nesting - 1, true);
+  }
+
+  @Override
+  public String toString() {
+    return isNullableString() + "PRIMITIVE" + Strings.repeat("[]", nesting);
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (!super.equals(o)) {
+      return false;
+    }
+    PrimitiveArrayTypeLatticeElement other = (PrimitiveArrayTypeLatticeElement) o;
+    return nesting == other.nesting;
+  }
+
+  @Override
+  public int hashCode() {
+    int result = super.hashCode();
+    result = 31 * result + nesting;
+    return result;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/type/PrimitiveTypeLatticeElement.java b/src/main/java/com/android/tools/r8/ir/analysis/type/PrimitiveTypeLatticeElement.java
new file mode 100644
index 0000000..ea06862
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/analysis/type/PrimitiveTypeLatticeElement.java
@@ -0,0 +1,31 @@
+// Copyright (c) 2017, the R8 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.
+package com.android.tools.r8.ir.analysis.type;
+
+/**
+ * A {@link TypeLatticeElement} that represents all primitive types, such as int, long, etc.
+ * All primitives are aggregated, since the main use of this type analysis is to help
+ * better understanding of the type of object references.
+ */
+public class PrimitiveTypeLatticeElement extends TypeLatticeElement {
+  private static final PrimitiveTypeLatticeElement INSTANCE = new PrimitiveTypeLatticeElement();
+
+  private PrimitiveTypeLatticeElement() {
+    super(false);
+  }
+
+  @Override
+  TypeLatticeElement asNullable() {
+    return Top.getInstance();
+  }
+
+  public static PrimitiveTypeLatticeElement getInstance() {
+    return INSTANCE;
+  }
+
+  @Override
+  public String toString() {
+    return "PRIMITIVE";
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/type/Top.java b/src/main/java/com/android/tools/r8/ir/analysis/type/Top.java
new file mode 100644
index 0000000..6b4745d
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/analysis/type/Top.java
@@ -0,0 +1,31 @@
+// Copyright (c) 2017, the R8 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.
+package com.android.tools.r8.ir.analysis.type;
+
+public class Top extends TypeLatticeElement {
+  private static final Top INSTANCE = new Top();
+
+  private Top() {
+    super(true);
+  }
+
+  @Override
+  TypeLatticeElement asNullable() {
+    return this;
+  }
+
+  static Top getInstance() {
+    return INSTANCE;
+  }
+
+  @Override
+  boolean isTop() {
+    return true;
+  }
+
+  @Override
+  public String toString() {
+    return "TOP (everything)";
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/type/TypeAnalysis.java b/src/main/java/com/android/tools/r8/ir/analysis/type/TypeAnalysis.java
new file mode 100644
index 0000000..91d53a2
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/analysis/type/TypeAnalysis.java
@@ -0,0 +1,138 @@
+// Copyright (c) 2017, the R8 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.
+package com.android.tools.r8.ir.analysis.type;
+
+import com.android.tools.r8.graph.AppInfoWithSubtyping;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.code.Argument;
+import com.android.tools.r8.ir.code.BasicBlock;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.ir.code.Phi;
+import com.android.tools.r8.ir.code.Value;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+
+import java.util.ArrayDeque;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Deque;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.BiConsumer;
+import java.util.function.BiFunction;
+
+public class TypeAnalysis {
+  private final AppInfoWithSubtyping appInfo;
+  private final DexEncodedMethod encodedMethod;
+  private final IRCode code;
+
+  private final Deque<BasicBlock> worklist = new ArrayDeque<>();
+  private final Map<Value, TypeLatticeElement> typeMap = Maps.newHashMap();
+  private final Map<Value, Set<BasicBlock>> users = Maps.newHashMap();
+
+  public TypeAnalysis(AppInfoWithSubtyping appInfo, DexEncodedMethod encodedMethod, IRCode code) {
+    this.appInfo = appInfo;
+    this.encodedMethod = encodedMethod;
+    this.code = code;
+  }
+
+  public void run() {
+    BasicBlock[] basicBlocks = code.topologicallySortedBlocks();
+    if (basicBlocks.length <= 0) {
+      return;
+    }
+    worklist.addAll(Arrays.asList(basicBlocks));
+    while (!worklist.isEmpty()) {
+      processBasicBlock(worklist.poll());
+    }
+  }
+
+  private void addToWorklist(BasicBlock block) {
+    if (!worklist.contains(block)) {
+      worklist.add(block);
+    }
+  }
+
+  private void processBasicBlock(BasicBlock block) {
+    int argumentsSeen = encodedMethod.accessFlags.isStatic() ? 0 : -1;
+    for (Instruction instruction : block.getInstructions()) {
+      TypeLatticeElement derived = Bottom.getInstance();
+      Value outValue = instruction.outValue();
+      // Argument, a quasi instruction, needs to be handled specially:
+      //   1) We can derive its type from the method signature; and
+      //   2) It does not have an out value, so we can skip the env updating.
+      if (instruction instanceof Argument) {
+        if (argumentsSeen < 0) {
+          derived = TypeLatticeElement.fromDexType(appInfo, encodedMethod.method.holder, false);
+        } else {
+          DexType argType = encodedMethod.method.proto.parameters.values[argumentsSeen];
+          derived = TypeLatticeElement.fromDexType(appInfo, argType, true);
+        }
+        argumentsSeen++;
+      } else {
+        // Register this block as a user for in values to revisit this if in values are updated.
+        instruction.inValues()
+            .forEach(v -> registerAsUserOfValue(v, block, Sets.newIdentityHashSet()));
+        if (outValue != null) {
+          derived = instruction.evaluate(appInfo, this::getLatticeElement);
+        }
+      }
+      if (outValue != null) {
+        TypeLatticeElement current = getLatticeElement(outValue);
+        if (!current.equals(derived)) {
+          updateTypeOfValue(outValue, derived);
+        }
+      }
+    }
+  }
+
+  private void registerAsUserOfValue(Value value, BasicBlock block, Set<Value> seenPhis) {
+    if (value.isPhi() && seenPhis.add(value)) {
+      for (Value operand : value.asPhi().getOperands()) {
+        registerAsUserOfValue(operand, block, seenPhis);
+      }
+    } else {
+      users.computeIfAbsent(value, k -> Sets.newIdentityHashSet()).add(block);
+    }
+  }
+
+  private void updateTypeOfValue(Value value, TypeLatticeElement type) {
+    setLatticeElement(value, type);
+    // Revisit the blocks that use the value whose type binding has just been updated.
+    users.getOrDefault(value, Collections.emptySet()).forEach(this::addToWorklist);
+    // Propagate the type change to phi users if any.
+    for (Phi phi : value.uniquePhiUsers()) {
+      TypeLatticeElement phiType = computePhiType(phi);
+      if (!getLatticeElement(phi).equals(phiType)) {
+        updateTypeOfValue(phi, phiType);
+      }
+    }
+  }
+
+  private TypeLatticeElement computePhiType(Phi phi) {
+    // Type of phi(v1, v2, ..., vn) is the least upper bound of all those n operands.
+    BiFunction<TypeLatticeElement, TypeLatticeElement, TypeLatticeElement> joiner =
+        TypeLatticeElement.joiner(appInfo);
+    return phi.getOperands().stream().reduce(
+        Bottom.getInstance(),
+        (acc, value) -> joiner.apply(acc, getLatticeElement(value)),
+        joiner::apply);
+  }
+
+  private void setLatticeElement(Value value, TypeLatticeElement type) {
+    typeMap.put(value, type);
+  }
+
+  TypeLatticeElement getLatticeElement(Value value) {
+    return typeMap.getOrDefault(value, Bottom.getInstance());
+  }
+
+  @VisibleForTesting
+  void forEach(BiConsumer<Value, TypeLatticeElement> consumer) {
+    typeMap.forEach(consumer);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/type/TypeLatticeElement.java b/src/main/java/com/android/tools/r8/ir/analysis/type/TypeLatticeElement.java
new file mode 100644
index 0000000..3d5738f
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/analysis/type/TypeLatticeElement.java
@@ -0,0 +1,214 @@
+// Copyright (c) 2017, the R8 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.
+package com.android.tools.r8.ir.analysis.type;
+
+import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.graph.AppInfo;
+import com.android.tools.r8.graph.AppInfoWithSubtyping;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.code.Value;
+import java.util.function.BiFunction;
+
+/**
+ * The base abstraction of lattice elements for local type analysis.
+ */
+abstract public class TypeLatticeElement {
+  private final boolean isNullable;
+
+  TypeLatticeElement(boolean isNullable) {
+    this.isNullable = isNullable;
+  }
+
+  boolean isNullable() {
+    return isNullable;
+  }
+
+  /**
+   * Defines how to join with null.
+   *
+   * @return {@link TypeLatticeElement} a result of joining with null.
+   */
+  abstract TypeLatticeElement asNullable();
+
+  String isNullableString() {
+    return isNullable() ? "" : "@NonNull ";
+  }
+
+  /**
+   * Computes the least upper bound of the current and the other elements.
+   *
+   * @param appInfo {@link AppInfoWithSubtyping} that contains subtype info.
+   * @param l1 {@link TypeLatticeElement} to join.
+   * @param l2 {@link TypeLatticeElement} to join.
+   * @return {@link TypeLatticeElement}, a least upper bound of {@param l1} and {@param l2}.
+   */
+  static TypeLatticeElement join(
+      AppInfoWithSubtyping appInfo, TypeLatticeElement l1, TypeLatticeElement l2) {
+    if (l1.isBottom()) {
+      return l2;
+    }
+    if (l2.isBottom()) {
+      return l1;
+    }
+    if (l1.isTop() || l2.isTop()) {
+      return Top.getInstance();
+    }
+    if (l1 instanceof NullLatticeElement) {
+      return l2.asNullable();
+    }
+    if (l2 instanceof NullLatticeElement) {
+      return l1.asNullable();
+    }
+    if (l1 instanceof PrimitiveTypeLatticeElement) {
+      return l2 instanceof PrimitiveTypeLatticeElement ? l1 : Top.getInstance();
+    }
+    if (l2 instanceof PrimitiveTypeLatticeElement) {
+      // !(l1 instanceof PrimitiveTypeLatticeElement)
+      return Top.getInstance();
+    }
+    // From now on, l1 and l2 are either PrimitiveArrayType, ArrayType, or ClassType.
+    boolean isNullable = l1.isNullable() || l2.isNullable();
+    if (l1.getClass() != l2.getClass()) {
+      return objectType(appInfo, isNullable);
+    }
+    // From now on, l1.getClass() == l2.getClass()
+    if (l1 instanceof PrimitiveArrayTypeLatticeElement) {
+      PrimitiveArrayTypeLatticeElement a1 = (PrimitiveArrayTypeLatticeElement) l1;
+      PrimitiveArrayTypeLatticeElement a2 = (PrimitiveArrayTypeLatticeElement) l2;
+      if (a1.nesting != a2.nesting) {
+        int min = Math.min(a1.nesting, a2.nesting);
+        if (min > 1) {
+          return objectArrayType(appInfo, min - 1, isNullable);
+        } else {
+          return objectType(appInfo, isNullable);
+        }
+      } else {
+        return l1;
+      }
+    }
+    if (l1 instanceof ArrayTypeLatticeElement) {
+      ArrayTypeLatticeElement a1 = (ArrayTypeLatticeElement) l1;
+      ArrayTypeLatticeElement a2 = (ArrayTypeLatticeElement) l2;
+      if (a1.nesting != a2.nesting) {
+        int min = Math.min(a1.nesting, a2.nesting);
+        return objectArrayType(appInfo, min, isNullable);
+      } else {
+        // Same nesting, same base type.
+        if (a1.elementType == a2.elementType) {
+          return a1.isNullable() ? a1 : a2;
+        } else if (a1.elementType.isClassType() && a2.elementType.isClassType()) {
+          // For different class element types, compute the least upper bound of element types.
+          DexType lub = a1.elementType.computeLeastUpperBound(appInfo, a2.elementType);
+          return new ArrayTypeLatticeElement(lub, a1.nesting, isNullable);
+        }
+        // Otherwise, fall through to the end, where TOP will be returned.
+      }
+    }
+    if (l1 instanceof ClassTypeLatticeElement) {
+      ClassTypeLatticeElement c1 = (ClassTypeLatticeElement) l1;
+      ClassTypeLatticeElement c2 = (ClassTypeLatticeElement) l2;
+      if (c1.classType == c2.classType) {
+        return c1.isNullable() ? c1 : c2;
+      } else {
+        DexType lub = c1.classType.computeLeastUpperBound(appInfo, c2.classType);
+        return new ClassTypeLatticeElement(lub, isNullable);
+      }
+    }
+    throw new Unreachable("unless a new type lattice is introduced.");
+  }
+
+  static BiFunction<TypeLatticeElement, TypeLatticeElement, TypeLatticeElement> joiner(
+      AppInfoWithSubtyping appInfo) {
+    return (l1, l2) -> join(appInfo, l1, l2);
+  }
+
+  /**
+   * Represents a type that can be everything.
+   *
+   * @return true if the corresponding {@link Value} could be any kinds.
+   */
+  boolean isTop() {
+    return false;
+  }
+
+  /**
+   * Represents an empty type.
+   *
+   * @return true if the type of corresponding {@link Value} is not determined yet.
+   */
+  boolean isBottom() {
+    return false;
+  }
+
+  static ClassTypeLatticeElement objectType(AppInfo appInfo, boolean isNullable) {
+    return new ClassTypeLatticeElement(appInfo.dexItemFactory.objectType, isNullable);
+  }
+
+  static ArrayTypeLatticeElement objectArrayType(AppInfo appInfo, int nesting, boolean isNullable) {
+    return new ArrayTypeLatticeElement(appInfo.dexItemFactory.objectType, nesting, isNullable);
+  }
+
+  public static TypeLatticeElement fromDexType(
+      AppInfoWithSubtyping appInfo, DexType type, boolean isNullable) {
+    if (type.isPrimitiveType()) {
+      return PrimitiveTypeLatticeElement.getInstance();
+    }
+    if (type.isPrimitiveArrayType()) {
+      return new PrimitiveArrayTypeLatticeElement(
+          type.getNumberOfLeadingSquareBrackets(), isNullable);
+    }
+    if (type.isClassType()) {
+      return new ClassTypeLatticeElement(type, isNullable);
+    }
+    assert type.isArrayType();
+    return new ArrayTypeLatticeElement(
+        type.toBaseType(appInfo.dexItemFactory),
+        type.getNumberOfLeadingSquareBrackets(),
+        isNullable);
+  }
+
+  public static TypeLatticeElement newArray(
+      AppInfoWithSubtyping appInfo, DexType arrayType, boolean isNullable) {
+    DexType baseType = arrayType.toBaseType(appInfo.dexItemFactory);
+    assert baseType != arrayType;
+    int nesting = arrayType.getNumberOfLeadingSquareBrackets();
+    if (baseType.isClassType()) {
+      return new ArrayTypeLatticeElement(baseType, nesting, isNullable);
+    } else {
+      return newPrimitiveArray(nesting, isNullable);
+    }
+  }
+
+  public static TypeLatticeElement newPrimitiveArray(int nesting, boolean isNullable) {
+    return new PrimitiveArrayTypeLatticeElement(nesting, isNullable);
+  }
+
+  public TypeLatticeElement arrayGet(AppInfoWithSubtyping appInfo) {
+    return Top.getInstance();
+  }
+
+  public TypeLatticeElement checkCast(AppInfoWithSubtyping appInfo, DexType castType) {
+    return fromDexType(appInfo, castType, isNullable());
+  }
+
+  @Override
+  abstract public String toString();
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (o == null || o.getClass() != this.getClass()) {
+      return false;
+    }
+    TypeLatticeElement otherElement = (TypeLatticeElement) o;
+    return otherElement.isNullable() == isNullable;
+  }
+
+  @Override
+  public int hashCode() {
+    return isNullable ? 1 : 0;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/code/ArrayGet.java b/src/main/java/com/android/tools/r8/ir/code/ArrayGet.java
index b226b70..2380ea2 100644
--- a/src/main/java/com/android/tools/r8/ir/code/ArrayGet.java
+++ b/src/main/java/com/android/tools/r8/ir/code/ArrayGet.java
@@ -16,11 +16,13 @@
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.AppInfoWithSubtyping;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.analysis.type.TypeLatticeElement;
 import com.android.tools.r8.ir.conversion.CfBuilder;
 import com.android.tools.r8.ir.conversion.DexBuilder;
 import com.android.tools.r8.ir.optimize.Inliner.Constraint;
 import com.android.tools.r8.ir.regalloc.RegisterAllocator;
 import java.util.Arrays;
+import java.util.function.Function;
 
 public class ArrayGet extends Instruction {
 
@@ -142,4 +144,10 @@
   public void buildCf(CfBuilder builder) {
     builder.add(new CfArrayLoad(type));
   }
+
+  @Override
+  public TypeLatticeElement evaluate(
+      AppInfoWithSubtyping appInfo, Function<Value, TypeLatticeElement> getLatticeElement) {
+    return getLatticeElement.apply(array()).arrayGet(appInfo);
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/code/ArrayLength.java b/src/main/java/com/android/tools/r8/ir/code/ArrayLength.java
index 742ccb5..f7689ab 100644
--- a/src/main/java/com/android/tools/r8/ir/code/ArrayLength.java
+++ b/src/main/java/com/android/tools/r8/ir/code/ArrayLength.java
@@ -9,10 +9,13 @@
 import com.android.tools.r8.dex.Constants;
 import com.android.tools.r8.graph.AppInfoWithSubtyping;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.analysis.type.PrimitiveTypeLatticeElement;
+import com.android.tools.r8.ir.analysis.type.TypeLatticeElement;
 import com.android.tools.r8.ir.conversion.CfBuilder;
 import com.android.tools.r8.ir.conversion.DexBuilder;
 import com.android.tools.r8.ir.optimize.Inliner.Constraint;
 import com.android.tools.r8.ir.regalloc.RegisterAllocator;
+import java.util.function.Function;
 
 public class ArrayLength extends Instruction {
 
@@ -101,4 +104,10 @@
   public void buildCf(CfBuilder builder) {
     builder.add(new CfArrayLength());
   }
+
+  @Override
+  public TypeLatticeElement evaluate(
+      AppInfoWithSubtyping appInfo, Function<Value, TypeLatticeElement> getLatticeElement) {
+    return PrimitiveTypeLatticeElement.getInstance();
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/code/Binop.java b/src/main/java/com/android/tools/r8/ir/code/Binop.java
index 8473f71..55b3ba5 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Binop.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Binop.java
@@ -11,9 +11,12 @@
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.AppInfoWithSubtyping;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.analysis.type.PrimitiveTypeLatticeElement;
+import com.android.tools.r8.ir.analysis.type.TypeLatticeElement;
 import com.android.tools.r8.ir.conversion.CfBuilder;
 import com.android.tools.r8.ir.optimize.Inliner.Constraint;
 import com.android.tools.r8.ir.regalloc.RegisterAllocator;
+import java.util.function.Function;
 
 public abstract class Binop extends Instruction {
 
@@ -132,4 +135,10 @@
   public void buildCf(CfBuilder builder) {
     builder.add(new CfBinop(getCfOpcode()));
   }
+
+  @Override
+  public TypeLatticeElement evaluate(
+      AppInfoWithSubtyping appInfo, Function<Value, TypeLatticeElement> getLatticeElement) {
+    return PrimitiveTypeLatticeElement.getInstance();
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/code/CheckCast.java b/src/main/java/com/android/tools/r8/ir/code/CheckCast.java
index 90723b2..f0e4d44 100644
--- a/src/main/java/com/android/tools/r8/ir/code/CheckCast.java
+++ b/src/main/java/com/android/tools/r8/ir/code/CheckCast.java
@@ -9,8 +9,10 @@
 import com.android.tools.r8.dex.Constants;
 import com.android.tools.r8.graph.AppInfoWithSubtyping;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.analysis.type.TypeLatticeElement;
 import com.android.tools.r8.ir.conversion.DexBuilder;
 import com.android.tools.r8.ir.optimize.Inliner.Constraint;
+import java.util.function.Function;
 
 public class CheckCast extends Instruction {
 
@@ -101,4 +103,10 @@
   public Constraint inliningConstraint(AppInfoWithSubtyping info, DexType invocationContext) {
     return Constraint.classIsVisible(invocationContext, type, info);
   }
+
+  @Override
+  public TypeLatticeElement evaluate(
+      AppInfoWithSubtyping appInfo, Function<Value, TypeLatticeElement> getLatticeElement) {
+    return getLatticeElement.apply(object()).checkCast(appInfo, type);
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/code/ConstClass.java b/src/main/java/com/android/tools/r8/ir/code/ConstClass.java
index bab1c2f..16239e2 100644
--- a/src/main/java/com/android/tools/r8/ir/code/ConstClass.java
+++ b/src/main/java/com/android/tools/r8/ir/code/ConstClass.java
@@ -7,9 +7,11 @@
 import com.android.tools.r8.dex.Constants;
 import com.android.tools.r8.graph.AppInfoWithSubtyping;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.analysis.type.TypeLatticeElement;
 import com.android.tools.r8.ir.conversion.DexBuilder;
 import com.android.tools.r8.ir.optimize.Inliner.Constraint;
 import com.android.tools.r8.utils.InternalOptions;
+import java.util.function.Function;
 
 public class ConstClass extends ConstInstruction {
 
@@ -94,4 +96,10 @@
   public Constraint inliningConstraint(AppInfoWithSubtyping info, DexType invocationContext) {
     return Constraint.classIsVisible(invocationContext, clazz, info);
   }
+
+  @Override
+  public TypeLatticeElement evaluate(
+      AppInfoWithSubtyping appInfo, Function<Value, TypeLatticeElement> getLatticeElement) {
+    return TypeLatticeElement.fromDexType(appInfo, appInfo.dexItemFactory.classType, false);
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/code/ConstNumber.java b/src/main/java/com/android/tools/r8/ir/code/ConstNumber.java
index f990d90..1c6161b 100644
--- a/src/main/java/com/android/tools/r8/ir/code/ConstNumber.java
+++ b/src/main/java/com/android/tools/r8/ir/code/ConstNumber.java
@@ -16,10 +16,13 @@
 import com.android.tools.r8.code.ConstWide32;
 import com.android.tools.r8.code.ConstWideHigh16;
 import com.android.tools.r8.dex.Constants;
+import com.android.tools.r8.graph.AppInfoWithSubtyping;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.analysis.constant.Bottom;
 import com.android.tools.r8.ir.analysis.constant.ConstLatticeElement;
 import com.android.tools.r8.ir.analysis.constant.LatticeElement;
+import com.android.tools.r8.ir.analysis.type.PrimitiveTypeLatticeElement;
+import com.android.tools.r8.ir.analysis.type.TypeLatticeElement;
 import com.android.tools.r8.ir.conversion.CfBuilder;
 import com.android.tools.r8.ir.conversion.DexBuilder;
 import com.android.tools.r8.utils.NumberUtils;
@@ -263,4 +266,10 @@
     }
     return new ConstLatticeElement(this);
   }
+
+  @Override
+  public TypeLatticeElement evaluate(
+      AppInfoWithSubtyping appInfo, Function<Value, TypeLatticeElement> getLatticeElement) {
+    return PrimitiveTypeLatticeElement.getInstance();
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/code/ConstString.java b/src/main/java/com/android/tools/r8/ir/code/ConstString.java
index a3a730d..9ff7fe4 100644
--- a/src/main/java/com/android/tools/r8/ir/code/ConstString.java
+++ b/src/main/java/com/android/tools/r8/ir/code/ConstString.java
@@ -7,12 +7,15 @@
 import com.android.tools.r8.cf.TypeVerificationHelper;
 import com.android.tools.r8.cf.code.CfConstString;
 import com.android.tools.r8.dex.Constants;
+import com.android.tools.r8.graph.AppInfoWithSubtyping;
 import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.analysis.type.TypeLatticeElement;
 import com.android.tools.r8.ir.conversion.CfBuilder;
 import com.android.tools.r8.ir.conversion.DexBuilder;
 import com.android.tools.r8.utils.InternalOptions;
 import java.io.UTFDataFormatException;
+import java.util.function.Function;
 
 public class ConstString extends ConstInstruction {
 
@@ -120,4 +123,10 @@
   public DexType computeVerificationType(TypeVerificationHelper helper) {
     return helper.getFactory().stringType;
   }
+
+  @Override
+  public TypeLatticeElement evaluate(
+      AppInfoWithSubtyping appInfo, Function<Value, TypeLatticeElement> getLatticeElement) {
+    return TypeLatticeElement.fromDexType(appInfo, appInfo.dexItemFactory.stringType, false);
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/code/InstanceGet.java b/src/main/java/com/android/tools/r8/ir/code/InstanceGet.java
index e1bc063..ddf2969 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InstanceGet.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InstanceGet.java
@@ -14,10 +14,13 @@
 import com.android.tools.r8.dex.Constants;
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.AppInfo;
+import com.android.tools.r8.graph.AppInfoWithSubtyping;
 import com.android.tools.r8.graph.DexEncodedField;
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.analysis.type.TypeLatticeElement;
 import com.android.tools.r8.ir.conversion.DexBuilder;
+import java.util.function.Function;
 
 public class InstanceGet extends FieldInstruction {
 
@@ -121,4 +124,10 @@
   public String toString() {
     return super.toString() + "; field: " + field.toSourceString();
   }
+
+  @Override
+  public TypeLatticeElement evaluate(
+      AppInfoWithSubtyping appInfo, Function<Value, TypeLatticeElement> getLatticeElement) {
+    return TypeLatticeElement.fromDexType(appInfo, field.type, true);
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/code/InstanceOf.java b/src/main/java/com/android/tools/r8/ir/code/InstanceOf.java
index 995a35e..0985ad1 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InstanceOf.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InstanceOf.java
@@ -7,8 +7,11 @@
 import com.android.tools.r8.dex.Constants;
 import com.android.tools.r8.graph.AppInfoWithSubtyping;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.analysis.type.PrimitiveTypeLatticeElement;
+import com.android.tools.r8.ir.analysis.type.TypeLatticeElement;
 import com.android.tools.r8.ir.conversion.DexBuilder;
 import com.android.tools.r8.ir.optimize.Inliner.Constraint;
+import java.util.function.Function;
 
 public class InstanceOf extends Instruction {
 
@@ -77,4 +80,10 @@
   public Constraint inliningConstraint(AppInfoWithSubtyping info, DexType invocationContext) {
     return Constraint.classIsVisible(invocationContext, type, info);
   }
+
+  @Override
+  public TypeLatticeElement evaluate(
+      AppInfoWithSubtyping appInfo, Function<Value, TypeLatticeElement> getLatticeElement) {
+    return PrimitiveTypeLatticeElement.getInstance();
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/code/Instruction.java b/src/main/java/com/android/tools/r8/ir/code/Instruction.java
index 70ffe8a..cf5190a 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Instruction.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Instruction.java
@@ -13,6 +13,7 @@
 import com.android.tools.r8.ir.analysis.constant.Bottom;
 import com.android.tools.r8.ir.analysis.constant.ConstRangeLatticeElement;
 import com.android.tools.r8.ir.analysis.constant.LatticeElement;
+import com.android.tools.r8.ir.analysis.type.TypeLatticeElement;
 import com.android.tools.r8.ir.conversion.CfBuilder;
 import com.android.tools.r8.ir.conversion.DexBuilder;
 import com.android.tools.r8.ir.optimize.Inliner.Constraint;
@@ -1018,4 +1019,10 @@
     }
     return Bottom.getInstance();
   }
+
+  public TypeLatticeElement evaluate(
+      AppInfoWithSubtyping appInfo, Function<Value, TypeLatticeElement> getLatticeElement) {
+    assert outValue == null;
+    throw new Unreachable("Instructions without outValue have no type.");
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/code/InvokeMethod.java b/src/main/java/com/android/tools/r8/ir/code/InvokeMethod.java
index 9e1494e..17b7b64 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InvokeMethod.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InvokeMethod.java
@@ -5,16 +5,19 @@
 
 import com.android.tools.r8.cf.LoadStoreHelper;
 import com.android.tools.r8.cf.TypeVerificationHelper;
+import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.AppInfoWithSubtyping;
 import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.analysis.type.TypeLatticeElement;
 import com.android.tools.r8.ir.optimize.Inliner.Constraint;
 import com.android.tools.r8.ir.optimize.Inliner.InlineAction;
 import com.android.tools.r8.ir.optimize.InliningOracle;
 import java.util.Collection;
 import java.util.List;
+import java.util.function.Function;
 
 public abstract class InvokeMethod extends Invoke {
 
@@ -144,4 +147,14 @@
   public DexType computeVerificationType(TypeVerificationHelper helper) {
     return getInvokedMethod().proto.returnType;
   }
+
+  @Override
+  public TypeLatticeElement evaluate(
+      AppInfoWithSubtyping appInfo, Function<Value, TypeLatticeElement> getLatticeElement) {
+    DexType returnType = method.proto.returnType;
+    if (returnType.isVoidType()) {
+      throw new Unreachable("void methods have no type.");
+    }
+    return TypeLatticeElement.fromDexType(appInfo, returnType, true);
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/code/InvokeNewArray.java b/src/main/java/com/android/tools/r8/ir/code/InvokeNewArray.java
index 440574a..f075633 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InvokeNewArray.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InvokeNewArray.java
@@ -8,9 +8,11 @@
 import com.android.tools.r8.graph.AppInfoWithSubtyping;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.analysis.type.TypeLatticeElement;
 import com.android.tools.r8.ir.conversion.DexBuilder;
 import com.android.tools.r8.ir.optimize.Inliner.Constraint;
 import java.util.List;
+import java.util.function.Function;
 
 public class InvokeNewArray extends Invoke {
 
@@ -104,4 +106,10 @@
   public Constraint inliningConstraint(AppInfoWithSubtyping info, DexType invocationContext) {
     return Constraint.classIsVisible(invocationContext, type, info);
   }
+
+  @Override
+  public TypeLatticeElement evaluate(
+      AppInfoWithSubtyping appInfo, Function<Value, TypeLatticeElement> getLatticeElement) {
+    return TypeLatticeElement.newArray(appInfo, type, false);
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/code/Move.java b/src/main/java/com/android/tools/r8/ir/code/Move.java
index 9d27eab..4187cab 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Move.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Move.java
@@ -7,8 +7,10 @@
 import com.android.tools.r8.dex.Constants;
 import com.android.tools.r8.graph.AppInfoWithSubtyping;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.analysis.type.TypeLatticeElement;
 import com.android.tools.r8.ir.conversion.DexBuilder;
 import com.android.tools.r8.ir.optimize.Inliner.Constraint;
+import java.util.function.Function;
 
 public class Move extends Instruction {
 
@@ -84,4 +86,10 @@
   public Constraint inliningConstraint(AppInfoWithSubtyping info, DexType invocationContext) {
     return Constraint.ALWAYS;
   }
+
+  @Override
+  public TypeLatticeElement evaluate(
+      AppInfoWithSubtyping appInfo, Function<Value, TypeLatticeElement> getLatticeElement) {
+    return getLatticeElement.apply(src());
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/code/MoveException.java b/src/main/java/com/android/tools/r8/ir/code/MoveException.java
index 8bfdcd2..dda7a30 100644
--- a/src/main/java/com/android/tools/r8/ir/code/MoveException.java
+++ b/src/main/java/com/android/tools/r8/ir/code/MoveException.java
@@ -8,6 +8,7 @@
 import com.android.tools.r8.dex.Constants;
 import com.android.tools.r8.graph.AppInfoWithSubtyping;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.analysis.type.TypeLatticeElement;
 import com.android.tools.r8.ir.conversion.CfBuilder;
 import com.android.tools.r8.ir.conversion.DexBuilder;
 import com.android.tools.r8.ir.optimize.Inliner.Constraint;
@@ -15,6 +16,7 @@
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
+import java.util.function.Function;
 
 public class MoveException extends Instruction {
 
@@ -105,4 +107,10 @@
     }
     return helper.join(exceptionTypes);
   }
+
+  @Override
+  public TypeLatticeElement evaluate(
+      AppInfoWithSubtyping appInfo, Function<Value, TypeLatticeElement> getLatticeElement) {
+    return TypeLatticeElement.fromDexType(appInfo, appInfo.dexItemFactory.throwableType, false);
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/code/NewArrayEmpty.java b/src/main/java/com/android/tools/r8/ir/code/NewArrayEmpty.java
index 8d32ca9..2c15de7 100644
--- a/src/main/java/com/android/tools/r8/ir/code/NewArrayEmpty.java
+++ b/src/main/java/com/android/tools/r8/ir/code/NewArrayEmpty.java
@@ -10,9 +10,11 @@
 import com.android.tools.r8.dex.Constants;
 import com.android.tools.r8.graph.AppInfoWithSubtyping;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.analysis.type.TypeLatticeElement;
 import com.android.tools.r8.ir.conversion.CfBuilder;
 import com.android.tools.r8.ir.conversion.DexBuilder;
 import com.android.tools.r8.ir.optimize.Inliner.Constraint;
+import java.util.function.Function;
 
 public class NewArrayEmpty extends Instruction {
 
@@ -101,4 +103,10 @@
     assert type.isArrayType();
     builder.add(new CfNewArray(type));
   }
+
+  @Override
+  public TypeLatticeElement evaluate(
+      AppInfoWithSubtyping appInfo, Function<Value, TypeLatticeElement> getLatticeElement) {
+    return TypeLatticeElement.newArray(appInfo, type, false);
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/code/NewArrayFilledData.java b/src/main/java/com/android/tools/r8/ir/code/NewArrayFilledData.java
index 765251f..4b53afb 100644
--- a/src/main/java/com/android/tools/r8/ir/code/NewArrayFilledData.java
+++ b/src/main/java/com/android/tools/r8/ir/code/NewArrayFilledData.java
@@ -8,10 +8,12 @@
 import com.android.tools.r8.dex.Constants;
 import com.android.tools.r8.graph.AppInfoWithSubtyping;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.analysis.type.TypeLatticeElement;
 import com.android.tools.r8.ir.conversion.DexBuilder;
 import com.android.tools.r8.ir.optimize.Inliner.Constraint;
 import com.android.tools.r8.utils.InternalOptions;
 import java.util.Arrays;
+import java.util.function.Function;
 
 public class NewArrayFilledData extends Instruction {
 
@@ -102,4 +104,10 @@
   public Constraint inliningConstraint(AppInfoWithSubtyping info, DexType invocationContext) {
     return Constraint.ALWAYS;
   }
+
+  @Override
+  public TypeLatticeElement evaluate(
+      AppInfoWithSubtyping appInfo, Function<Value, TypeLatticeElement> getLatticeElement) {
+    return TypeLatticeElement.newPrimitiveArray(1, false);
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/code/NewInstance.java b/src/main/java/com/android/tools/r8/ir/code/NewInstance.java
index b00eef0..6b45990 100644
--- a/src/main/java/com/android/tools/r8/ir/code/NewInstance.java
+++ b/src/main/java/com/android/tools/r8/ir/code/NewInstance.java
@@ -9,9 +9,11 @@
 import com.android.tools.r8.dex.Constants;
 import com.android.tools.r8.graph.AppInfoWithSubtyping;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.analysis.type.TypeLatticeElement;
 import com.android.tools.r8.ir.conversion.CfBuilder;
 import com.android.tools.r8.ir.conversion.DexBuilder;
 import com.android.tools.r8.ir.optimize.Inliner.Constraint;
+import java.util.function.Function;
 
 public class NewInstance extends Instruction {
 
@@ -95,4 +97,10 @@
   public DexType computeVerificationType(TypeVerificationHelper helper) {
     return clazz;
   }
+
+  @Override
+  public TypeLatticeElement evaluate(
+      AppInfoWithSubtyping appInfo, Function<Value, TypeLatticeElement> getLatticeElement) {
+    return TypeLatticeElement.fromDexType(appInfo, clazz, false);
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/code/StaticGet.java b/src/main/java/com/android/tools/r8/ir/code/StaticGet.java
index 32871b3..acb8977 100644
--- a/src/main/java/com/android/tools/r8/ir/code/StaticGet.java
+++ b/src/main/java/com/android/tools/r8/ir/code/StaticGet.java
@@ -16,11 +16,14 @@
 import com.android.tools.r8.dex.Constants;
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.AppInfo;
+import com.android.tools.r8.graph.AppInfoWithSubtyping;
 import com.android.tools.r8.graph.DexEncodedField;
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.analysis.type.TypeLatticeElement;
 import com.android.tools.r8.ir.conversion.CfBuilder;
 import com.android.tools.r8.ir.conversion.DexBuilder;
+import java.util.function.Function;
 
 public class StaticGet extends FieldInstruction {
 
@@ -135,4 +138,10 @@
   public DexType computeVerificationType(TypeVerificationHelper helper) {
     return field.type;
   }
+
+  @Override
+  public TypeLatticeElement evaluate(
+      AppInfoWithSubtyping appInfo, Function<Value, TypeLatticeElement> getLatticeElement) {
+    return TypeLatticeElement.fromDexType(appInfo, field.type, true);
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/code/Unop.java b/src/main/java/com/android/tools/r8/ir/code/Unop.java
index f161635..f1189b9 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Unop.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Unop.java
@@ -8,8 +8,11 @@
 import com.android.tools.r8.dex.Constants;
 import com.android.tools.r8.graph.AppInfoWithSubtyping;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.analysis.type.PrimitiveTypeLatticeElement;
+import com.android.tools.r8.ir.analysis.type.TypeLatticeElement;
 import com.android.tools.r8.ir.conversion.CfBuilder;
 import com.android.tools.r8.ir.optimize.Inliner.Constraint;
+import java.util.function.Function;
 
 abstract public class Unop extends Instruction {
 
@@ -62,4 +65,10 @@
   public void buildCf(CfBuilder builder) {
     builder.add(new CfUnop(getCfOpcode()));
   }
+
+  @Override
+  public TypeLatticeElement evaluate(
+      AppInfoWithSubtyping appInfo, Function<Value, TypeLatticeElement> getLatticeElement) {
+    return PrimitiveTypeLatticeElement.getInstance();
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/ToolHelper.java b/src/test/java/com/android/tools/r8/ToolHelper.java
index a6ab7e9..cf5e451 100644
--- a/src/test/java/com/android/tools/r8/ToolHelper.java
+++ b/src/test/java/com/android/tools/r8/ToolHelper.java
@@ -72,6 +72,7 @@
   public static final String EXAMPLES_ANDROID_O_BUILD_DIR = TESTS_BUILD_DIR + "examplesAndroidO/";
   public static final String EXAMPLES_ANDROID_P_BUILD_DIR = TESTS_BUILD_DIR + "examplesAndroidP/";
   public static final String EXAMPLES_JAVA9_BUILD_DIR = TESTS_BUILD_DIR + "examplesJava9/";
+  public static final String SMALI_DIR = TESTS_DIR + "smali/";
   public static final String SMALI_BUILD_DIR = TESTS_BUILD_DIR + "smali/";
 
   public static final String LINE_SEPARATOR = StringUtils.LINE_SEPARATOR;
diff --git a/src/test/java/com/android/tools/r8/ir/analysis/type/NullabilityTest.java b/src/test/java/com/android/tools/r8/ir/analysis/type/NullabilityTest.java
new file mode 100644
index 0000000..da8772f
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/analysis/type/NullabilityTest.java
@@ -0,0 +1,257 @@
+// Copyright (c) 2017, the R8 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.
+package com.android.tools.r8.ir.analysis.type;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.dex.ApplicationReader;
+import com.android.tools.r8.graph.AppInfoWithSubtyping;
+import com.android.tools.r8.graph.DexApplication;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.smali.SmaliBuilder;
+import com.android.tools.r8.smali.SmaliBuilder.MethodSignature;
+import com.android.tools.r8.smali.SmaliTestBase;
+import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.DexInspector;
+import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.Timing;
+import com.google.common.collect.ImmutableList;
+import java.util.function.BiConsumer;
+import org.junit.Test;
+
+public class NullabilityTest extends SmaliTestBase {
+  private final String CLASS_NAME = "Example";
+  private static final InternalOptions TEST_OPTIONS = new InternalOptions();
+
+  private void buildAndTest(
+      SmaliBuilder builder,
+      MethodSignature signature,
+      BiConsumer<AppInfoWithSubtyping, TypeAnalysis> inspector)
+      throws Exception {
+    AndroidApp app = builder.build();
+    DexApplication dexApplication =
+        new ApplicationReader(app, TEST_OPTIONS, new Timing("NullabilityTest.appReader"))
+            .read().toDirect();
+    AppInfoWithSubtyping appInfo = new AppInfoWithSubtyping(dexApplication);
+    DexInspector dexInspector = new DexInspector(appInfo.app);
+    DexEncodedMethod foo = dexInspector.clazz(CLASS_NAME).method(signature).getMethod();
+    IRCode irCode = foo.buildIR(TEST_OPTIONS);
+    TypeAnalysis analysis = new TypeAnalysis(appInfo, foo, irCode);
+    analysis.run();
+    inspector.accept(appInfo, analysis);
+  }
+
+  @Test
+  public void nonNullAfterSafeInvokes() throws Exception {
+    SmaliBuilder builder = new SmaliBuilder(CLASS_NAME);
+    MethodSignature signature =
+        builder.addInstanceMethod("void", "foo", ImmutableList.of("java.lang.String"), 1,
+            "invoke-virtual {p1}, Ljava/lang/String;->toString()Ljava/lang/String;",
+            // Successful invocation above means p1 is not null.
+            "if-nez p1, :not_null",
+            "new-instance v0, Ljava/lang/AssertionError;",
+            "throw v0",
+            ":not_null",
+            "return-void"
+    );
+    buildAndTest(builder, signature, (appInfo, typeAnalysis) -> {
+      typeAnalysis.forEach((v, l) -> {
+        assertTrue(l instanceof ClassTypeLatticeElement);
+        ClassTypeLatticeElement lattice = (ClassTypeLatticeElement) l;
+        if (lattice.classType.equals(appInfo.dexItemFactory.stringType)) {
+          // TODO(b/70795205): Can be refined by using control-flow info.
+          // parameter
+          assertTrue(lattice.isNullable());
+        } else {
+          // holder and newly created assertion error.
+          assertFalse(lattice.isNullable());
+        }
+      });
+    });
+  }
+
+  @Test
+  public void stillNullAfterExceptionCatch_invoke() throws Exception {
+    SmaliBuilder builder = new SmaliBuilder(CLASS_NAME);
+    MethodSignature signature =
+        builder.addInstanceMethod("void", "foo", ImmutableList.of("java.lang.String"), 1,
+            ":try_start",
+            "invoke-virtual {p1}, Ljava/lang/String;->toString()Ljava/lang/String;",
+            "if-nez p1, :return",
+            "new-instance v0, Ljava/lang/AssertionError;",
+            "throw v0",
+            ":try_end",
+            ".catch Ljava/lang/Throwable; {:try_start .. :try_end} :return",
+            ":return",
+            // p1 could be still null at the outside of try-catch.
+            "invoke-virtual {p1}, Ljava/lang/String;->hashCode()I",
+            "return-void"
+    );
+    buildAndTest(builder, signature, (appInfo, typeAnalysis) -> {
+      typeAnalysis.forEach((v, l) -> {
+        assertTrue(l instanceof ClassTypeLatticeElement);
+        ClassTypeLatticeElement lattice = (ClassTypeLatticeElement) l;
+        if (lattice.classType.equals(appInfo.dexItemFactory.stringType)) {
+          // parameter
+          assertTrue(lattice.isNullable());
+        } else {
+          // holder and newly created assertion error.
+          assertFalse(lattice.isNullable());
+        }
+      });
+    });
+  }
+
+  @Test
+  public void nonNullAfterSafeArrayAccess() throws Exception {
+    SmaliBuilder builder = new SmaliBuilder(CLASS_NAME);
+    MethodSignature signature =
+        builder.addInstanceMethod("void", "foo", ImmutableList.of("java.lang.String[]"), 1,
+            "const/4 v0, 0",
+            "aget-object v0, p1, v0",
+            // Successful array access above means p1 is not null.
+            "if-nez p1, :not_null",
+            "new-instance v0, Ljava/lang/AssertionError;",
+            "throw v0",
+            ":not_null",
+            "return-void"
+    );
+    buildAndTest(builder, signature, (appInfo, typeAnalysis) -> {
+      typeAnalysis.forEach((v, l) -> {
+        if (l instanceof ArrayTypeLatticeElement) {
+          ArrayTypeLatticeElement lattice = (ArrayTypeLatticeElement) l;
+          assertEquals(appInfo.dexItemFactory.stringType, lattice.elementType);
+          // TODO(b/70795205): Can be refined by using control-flow info.
+          assertTrue(l.isNullable());
+        }
+        if (l instanceof ClassTypeLatticeElement) {
+          ClassTypeLatticeElement lattice = (ClassTypeLatticeElement) l;
+          if (lattice.classType.equals(appInfo.dexItemFactory.stringType)) {
+            // An element inside a non-null array could be null.
+            assertTrue(lattice.isNullable());
+          } else {
+            // holder and newly created assertion error.
+            assertFalse(lattice.isNullable());
+          }
+        }
+      });
+    });
+  }
+
+  @Test
+  public void stillNullAfterExceptionCatch_aget() throws Exception {
+    SmaliBuilder builder = new SmaliBuilder(CLASS_NAME);
+    MethodSignature signature =
+        builder.addInstanceMethod("void", "foo", ImmutableList.of("java.lang.String[]"), 1,
+            ":try_start",
+            "const/4 v0, 0",
+            "aget-object v0, p1, v0",
+            "if-nez p1, :return",
+            "new-instance v0, Ljava/lang/AssertionError;",
+            "throw v0",
+            ":try_end",
+            ".catch Ljava/lang/Throwable; {:try_start .. :try_end} :return",
+            ":return",
+            // p1 could be still null at the outside of try-catch.
+            "invoke-virtual {p1}, [Ljava/lang/String;->hashCode()I",
+            "return-void"
+    );
+    buildAndTest(builder, signature, (appInfo, typeAnalysis) -> {
+      typeAnalysis.forEach((v, l) -> {
+        if (l instanceof ArrayTypeLatticeElement) {
+          ArrayTypeLatticeElement lattice = (ArrayTypeLatticeElement) l;
+          assertEquals(appInfo.dexItemFactory.stringType, lattice.elementType);
+          assertTrue(l.isNullable());
+        }
+        if (l instanceof ClassTypeLatticeElement) {
+          ClassTypeLatticeElement lattice = (ClassTypeLatticeElement) l;
+          if (lattice.classType.equals(appInfo.dexItemFactory.stringType)) {
+            // An element inside a non-null array could be null.
+            assertTrue(lattice.isNullable());
+          } else {
+            // holder and newly created assertion error.
+            assertFalse(lattice.isNullable());
+          }
+        }
+      });
+    });
+  }
+
+  @Test
+  public void nonNullAfterSafeFieldAccess() throws Exception {
+    SmaliBuilder builder = new SmaliBuilder(CLASS_NAME);
+    MethodSignature signature =
+        builder.addInstanceMethod("void", "foo", ImmutableList.of("Test"), 1,
+            "iget-object v0, p1, LTest;->bar:Ljava/lang/String;",
+            // Successful field access above means p1 is not null.
+            "if-nez p1, :not_null",
+            "new-instance v0, Ljava/lang/AssertionError;",
+            "throw v0",
+            ":not_null",
+            "return-void"
+    );
+    builder.addClass("Test");
+    builder.addInstanceField("bar", "Ljava/lang/String;");
+    buildAndTest(builder, signature, (appInfo, typeAnalysis) -> {
+      DexType testType = appInfo.dexItemFactory.createType("LTest;");
+      typeAnalysis.forEach((v, l) -> {
+        assertTrue(l instanceof ClassTypeLatticeElement);
+        ClassTypeLatticeElement lattice = (ClassTypeLatticeElement) l;
+        if (lattice.classType.equals(appInfo.dexItemFactory.stringType)) {
+          // instance may not be initialized.
+          assertTrue(lattice.isNullable());
+        } else if (lattice.classType.equals(testType)) {
+          // TODO(b/70795205): Can be refined by using control-flow info.
+          // parameter
+          assertTrue(lattice.isNullable());
+        } else {
+          // holder and newly created assertion error.
+          assertFalse(lattice.isNullable());
+        }
+      });
+    });
+  }
+
+  @Test
+  public void stillNullAfterExceptionCatch_iget() throws Exception {
+    SmaliBuilder builder = new SmaliBuilder(CLASS_NAME);
+    MethodSignature signature =
+        builder.addInstanceMethod("void", "foo", ImmutableList.of("Test"), 1,
+            ":try_start",
+            "iget-object v0, p1, LTest;->bar:Ljava/lang/String;",
+            "if-nez p1, :return",
+            "new-instance v0, Ljava/lang/AssertionError;",
+            "throw v0",
+            ":try_end",
+            ".catch Ljava/lang/Throwable; {:try_start .. :try_end} :return",
+            ":return",
+            // p1 could be still null at the outside of try-catch.
+            "invoke-virtual {p1}, LTest;->hashCode()I",
+            "return-void"
+    );
+    builder.addClass("Test");
+    builder.addInstanceField("bar", "Ljava/lang/String;");
+    buildAndTest(builder, signature, (appInfo, typeAnalysis) -> {
+      DexType testType = appInfo.dexItemFactory.createType("LTest;");
+      typeAnalysis.forEach((v, l) -> {
+        assertTrue(l instanceof ClassTypeLatticeElement);
+        ClassTypeLatticeElement lattice = (ClassTypeLatticeElement) l;
+        if (lattice.classType.equals(appInfo.dexItemFactory.stringType)) {
+          // instance may not be initialized.
+          assertTrue(lattice.isNullable());
+        } else if (lattice.classType.equals(testType)) {
+          // parameter
+          assertTrue(lattice.isNullable());
+        } else {
+          // holder and newly created assertion error.
+          assertFalse(lattice.isNullable());
+        }
+      });
+    });
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/analysis/type/TypeAnalysisTest.java b/src/test/java/com/android/tools/r8/ir/analysis/type/TypeAnalysisTest.java
new file mode 100644
index 0000000..9bd8af6
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/analysis/type/TypeAnalysisTest.java
@@ -0,0 +1,345 @@
+// Copyright (c) 2017, the R8 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.
+package com.android.tools.r8.ir.analysis.type;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.android.tools.r8.ApiLevelException;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.dex.ApplicationReader;
+import com.android.tools.r8.graph.AppInfoWithSubtyping;
+import com.android.tools.r8.graph.DexApplication;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.code.ArrayLength;
+import com.android.tools.r8.ir.code.CheckCast;
+import com.android.tools.r8.ir.code.ConstNumber;
+import com.android.tools.r8.ir.code.ConstString;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.InstanceOf;
+import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.ir.code.InstructionIterator;
+import com.android.tools.r8.ir.code.InvokeNewArray;
+import com.android.tools.r8.ir.code.NewArrayEmpty;
+import com.android.tools.r8.ir.code.NewInstance;
+import com.android.tools.r8.ir.code.StaticGet;
+import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.naming.MemberNaming.MethodSignature;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.smali.SmaliTestBase;
+import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.DexInspector;
+import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.Smali;
+import com.android.tools.r8.utils.Timing;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Maps;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Consumer;
+
+@RunWith(Parameterized.class)
+public class TypeAnalysisTest extends SmaliTestBase {
+  private static final InternalOptions TEST_OPTIONS = new InternalOptions();
+  private static final TypeLatticeElement PRIMITIVE = PrimitiveTypeLatticeElement.getInstance();
+
+  private final String dirName;
+  private final String smaliFileName;
+  private final Consumer<AppInfoWithSubtyping> inspection;
+
+  public TypeAnalysisTest(String test, Consumer<AppInfoWithSubtyping> inspection) {
+    dirName = test.substring(0, test.lastIndexOf('/'));
+    smaliFileName = test.substring(test.lastIndexOf('/') + 1) + ".smali";
+    this.inspection = inspection;
+  }
+
+  @Parameters(name = "{0}")
+  public static Collection<Object[]> data() throws Exception {
+    List<String> tests = Arrays.asList(
+        "arithmetic/Arithmetic",
+        "fibonacci/Fibonacci",
+        "fill-array-data/FillArrayData",
+        "filled-new-array/FilledNewArray",
+        "infinite-loop/InfiniteLoop1",
+        "try-catch/TryCatch",
+        "type-confusion-regression/TestObject",
+        "type-confusion-regression5/TestObject"
+    );
+
+    Map<String, Consumer<AppInfoWithSubtyping>> inspections = new HashMap<>();
+    inspections.put("arithmetic/Arithmetic", TypeAnalysisTest::arithmetic);
+    inspections.put("fibonacci/Fibonacci", TypeAnalysisTest::fibonacci);
+    inspections.put("fill-array-data/FillArrayData", TypeAnalysisTest::fillArrayData);
+    inspections.put("filled-new-array/FilledNewArray", TypeAnalysisTest::filledNewArray);
+    inspections.put("infinite-loop/InfiniteLoop1", TypeAnalysisTest::infiniteLoop);
+    inspections.put("try-catch/TryCatch", TypeAnalysisTest::tryCatch);
+    inspections.put("type-confusion-regression/TestObject", TypeAnalysisTest::typeConfusion);
+    inspections.put("type-confusion-regression5/TestObject", TypeAnalysisTest::typeConfusion5);
+
+    List<Object[]> testCases = new ArrayList<>();
+    for (String test : tests) {
+      Consumer<AppInfoWithSubtyping> inspection = inspections.get(test);
+      testCases.add(new Object[]{test, inspection});
+    }
+    return testCases;
+  }
+
+  @Test
+  public void typeAnalysisTest() throws Exception {
+    Path smaliPath = Paths.get(ToolHelper.SMALI_DIR, dirName, smaliFileName);
+    StringBuilder smaliStringBuilder = new StringBuilder();
+    Files.lines(smaliPath, StandardCharsets.UTF_8)
+        .forEach(s -> smaliStringBuilder.append(s).append(System.lineSeparator()));
+    byte[] content = Smali.compile(smaliStringBuilder.toString());
+    AndroidApp app = AndroidApp.builder().addDexProgramData(content, Origin.unknown()).build();
+    DexApplication dexApplication =
+        new ApplicationReader(app, TEST_OPTIONS, new Timing("TypeAnalysisTest.appReader"))
+            .read().toDirect();
+    inspection.accept(new AppInfoWithSubtyping(dexApplication));
+  }
+
+  // Simple one path with a lot of arithmetic operations.
+  private static void arithmetic(AppInfoWithSubtyping appInfo) {
+    DexInspector inspector = new DexInspector(appInfo.app);
+    DexEncodedMethod subtract =
+        inspector.clazz("Test")
+            .method(
+                new MethodSignature("subtractConstants8bitRegisters", "int", ImmutableList.of()))
+            .getMethod();
+    try {
+      IRCode irCode = subtract.buildIR(TEST_OPTIONS);
+      TypeAnalysis analysis = new TypeAnalysis(appInfo, subtract, irCode);
+      analysis.run();
+      analysis.forEach((v, l) -> {
+        assertEquals(l, PRIMITIVE);
+      });
+    } catch (ApiLevelException e) {
+      fail(e.getMessage());
+    }
+  }
+
+  // A couple branches, along with some recursive calls.
+  private static void fibonacci(AppInfoWithSubtyping appInfo) {
+    DexInspector inspector = new DexInspector(appInfo.app);
+    DexEncodedMethod fib =
+        inspector.clazz("Test")
+            .method(new MethodSignature("fibonacci", "int", ImmutableList.of("int")))
+            .getMethod();
+    try {
+      IRCode irCode = fib.buildIR(TEST_OPTIONS);
+      TypeAnalysis analysis = new TypeAnalysis(appInfo, fib, irCode);
+      analysis.run();
+      analysis.forEach((v, l) -> {
+        assertEquals(l, PRIMITIVE);
+      });
+    } catch (ApiLevelException e) {
+      fail(e.getMessage());
+    }
+  }
+
+  // fill-array-data
+  private static void fillArrayData(AppInfoWithSubtyping appInfo) {
+    DexInspector inspector = new DexInspector(appInfo.app);
+    DexEncodedMethod test1 =
+        inspector.clazz("Test")
+            .method(new MethodSignature("test1", "int[]", ImmutableList.of()))
+            .getMethod();
+    try {
+      IRCode irCode = test1.buildIR(TEST_OPTIONS);
+      TypeAnalysis analysis = new TypeAnalysis(appInfo, test1, irCode);
+      analysis.run();
+      Value array = null;
+      InstructionIterator iterator = irCode.instructionIterator();
+      while (iterator.hasNext()) {
+        Instruction instruction = iterator.next();
+        if (instruction instanceof NewArrayEmpty) {
+          array = instruction.outValue();
+          break;
+        }
+      }
+      assertNotNull(array);
+      final Value finalArray = array;
+      analysis.forEach((v, l) -> {
+        if (v == finalArray) {
+          assertTrue(l instanceof PrimitiveArrayTypeLatticeElement);
+          PrimitiveArrayTypeLatticeElement lattice = (PrimitiveArrayTypeLatticeElement) l;
+          assertEquals(1, lattice.nesting);
+          assertFalse(l.isNullable());
+        }
+      });
+    } catch (ApiLevelException e) {
+      fail(e.getMessage());
+    }
+  }
+
+  // filled-new-array
+  private static void filledNewArray(AppInfoWithSubtyping appInfo) {
+    DexInspector inspector = new DexInspector(appInfo.app);
+    DexEncodedMethod test4 =
+        inspector.clazz("Test")
+            .method(new MethodSignature("test4", "int[]", ImmutableList.of()))
+            .getMethod();
+    try {
+      IRCode irCode = test4.buildIR(TEST_OPTIONS);
+      TypeAnalysis analysis = new TypeAnalysis(appInfo, test4, irCode);
+      analysis.run();
+      Value array = null;
+      InstructionIterator iterator = irCode.instructionIterator();
+      while (iterator.hasNext()) {
+        Instruction instruction = iterator.next();
+        if (instruction instanceof InvokeNewArray) {
+          array = instruction.outValue();
+          break;
+        }
+      }
+      assertNotNull(array);
+      final Value finalArray = array;
+      analysis.forEach((v, l) -> {
+        if (v == finalArray) {
+          assertTrue(l instanceof PrimitiveArrayTypeLatticeElement);
+          PrimitiveArrayTypeLatticeElement lattice = (PrimitiveArrayTypeLatticeElement) l;
+          assertEquals(1, lattice.nesting);
+          assertFalse(l.isNullable());
+        }
+      });
+    } catch (ApiLevelException e) {
+      fail(e.getMessage());
+    }
+  }
+
+  // Make sure the analysis does not hang.
+  private static void infiniteLoop(AppInfoWithSubtyping appInfo) {
+    DexInspector inspector = new DexInspector(appInfo.app);
+    DexEncodedMethod loop2 =
+        inspector.clazz("Test")
+            .method(new MethodSignature("loop2", "void", ImmutableList.of()))
+            .getMethod();
+    try {
+      IRCode irCode = loop2.buildIR(TEST_OPTIONS);
+      TypeAnalysis analysis = new TypeAnalysis(appInfo, loop2, irCode);
+      analysis.run();
+      analysis.forEach((v, l) -> {
+        if (l instanceof ClassTypeLatticeElement) {
+          ClassTypeLatticeElement lattice = (ClassTypeLatticeElement) l;
+          assertEquals("Ljava/io/PrintStream;", lattice.classType.toDescriptorString());
+          // TODO(b/70795205): Can be refined by using control-flow info.
+          assertTrue(l.isNullable());
+        }
+      });
+    } catch (ApiLevelException e) {
+      fail(e.getMessage());
+    }
+  }
+
+  // move-exception
+  private static void tryCatch(AppInfoWithSubtyping appInfo) {
+    DexInspector inspector = new DexInspector(appInfo.app);
+    DexEncodedMethod test2 =
+        inspector.clazz("Test")
+            .method(new MethodSignature("test2_throw", "int", ImmutableList.of()))
+            .getMethod();
+    try {
+      IRCode irCode = test2.buildIR(TEST_OPTIONS);
+      TypeAnalysis analysis = new TypeAnalysis(appInfo, test2, irCode);
+      analysis.run();
+      analysis.forEach((v, l) -> {
+        if (l instanceof ClassTypeLatticeElement) {
+          ClassTypeLatticeElement lattice = (ClassTypeLatticeElement) l;
+          assertEquals("Ljava/lang/Throwable;", lattice.classType.toDescriptorString());
+          assertFalse(l.isNullable());
+        }
+      });
+    } catch (ApiLevelException e) {
+      fail(e.getMessage());
+    }
+  }
+
+  // One very complicated example.
+  private static void typeConfusion(AppInfoWithSubtyping appInfo) {
+    DexInspector inspector = new DexInspector(appInfo.app);
+    DexEncodedMethod method =
+        inspector.clazz("TestObject")
+            .method(
+                new MethodSignature("a", "void",
+                    ImmutableList.of("Test", "Test", "Test", "Test")))
+            .getMethod();
+
+    DexType test = appInfo.dexItemFactory.createType("LTest;");
+    Map<Class<? extends Instruction>, TypeLatticeElement> expectedLattices = Maps.newHashMap();
+    expectedLattices.put(ArrayLength.class, PRIMITIVE);
+    expectedLattices.put(ConstNumber.class, PRIMITIVE);
+    expectedLattices.put(
+        ConstString.class, new ClassTypeLatticeElement(appInfo.dexItemFactory.stringType, false));
+    expectedLattices.put(CheckCast.class, new ClassTypeLatticeElement(test, true));
+    expectedLattices.put(NewInstance.class, new ClassTypeLatticeElement(test, false));
+
+    try {
+      IRCode irCode = method.buildIR(TEST_OPTIONS);
+      TypeAnalysis analysis = new TypeAnalysis(appInfo, method, irCode);
+      analysis.run();
+      analysis.forEach((v, l) -> {
+        if (v.definition == null) {
+          return;
+        }
+        TypeLatticeElement expected = expectedLattices.get(v.definition.getClass());
+        if (expected != null) {
+          assertEquals(expected, l);
+        }
+      });
+    } catch (ApiLevelException e) {
+      fail(e.getMessage());
+    }
+  }
+
+  // One more complicated example.
+  private static void typeConfusion5(AppInfoWithSubtyping appInfo) {
+    DexInspector inspector = new DexInspector(appInfo.app);
+    DexEncodedMethod method =
+        inspector.clazz("TestObject")
+            .method(
+                new MethodSignature("onClick", "void", ImmutableList.of("Test")))
+            .getMethod();
+
+    DexType test = appInfo.dexItemFactory.createType("LTest;");
+    Map<Class<? extends Instruction>, TypeLatticeElement> expectedLattices = Maps.newHashMap();
+    expectedLattices.put(ConstNumber.class, PRIMITIVE);
+    expectedLattices.put(
+        ConstString.class, new ClassTypeLatticeElement(appInfo.dexItemFactory.stringType, false));
+    expectedLattices.put(InstanceOf.class, PRIMITIVE);
+    expectedLattices.put(StaticGet.class, new ClassTypeLatticeElement(test, true));
+
+    try {
+      IRCode irCode = method.buildIR(TEST_OPTIONS);
+      TypeAnalysis analysis = new TypeAnalysis(appInfo, method, irCode);
+      analysis.run();
+      analysis.forEach((v, l) -> {
+        if (v.definition == null) {
+          return;
+        }
+        TypeLatticeElement expected = expectedLattices.get(v.definition.getClass());
+        if (expected != null) {
+          assertEquals(expected, l);
+        }
+      });
+    } catch (ApiLevelException e) {
+      fail(e.getMessage());
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/utils/DexInspector.java b/src/test/java/com/android/tools/r8/utils/DexInspector.java
index 56ba5a5..bbd6fb5 100644
--- a/src/test/java/com/android/tools/r8/utils/DexInspector.java
+++ b/src/test/java/com/android/tools/r8/utils/DexInspector.java
@@ -63,6 +63,7 @@
 import com.android.tools.r8.naming.MemberNaming.FieldSignature;
 import com.android.tools.r8.naming.MemberNaming.MethodSignature;
 import com.android.tools.r8.naming.MemberNaming.Signature;
+import com.android.tools.r8.smali.SmaliBuilder;
 import com.google.common.collect.BiMap;
 import com.google.common.collect.ImmutableList;
 import java.io.IOException;
@@ -286,6 +287,11 @@
       return method(signature.type, signature.name, ImmutableList.copyOf(signature.parameters));
     }
 
+    public MethodSubject method(SmaliBuilder.MethodSignature signature) {
+      return method(
+          signature.returnType, signature.name, ImmutableList.copyOf(signature.parameterTypes));
+    }
+
     public abstract void forAllFields(Consumer<FoundFieldSubject> inspection);
 
     public abstract FieldSubject field(String type, String name);
diff --git a/src/test/smali/try-catch/TryCatch.smali b/src/test/smali/try-catch/TryCatch.smali
index 2becd5f..8e06c5a 100644
--- a/src/test/smali/try-catch/TryCatch.smali
+++ b/src/test/smali/try-catch/TryCatch.smali
@@ -32,6 +32,23 @@
     return v0
 .end method
 
+.method public static test2_throw()I
+    .locals 1
+    const v0, 1
+    :try_start
+    invoke-static {}, Ltest/X;->f()V
+    const v0, 0
+    goto :return
+    :try_end
+    .catch Ljava/lang/Exception; {:try_start .. :try_end} :return
+    .catch Ljava/lang/Throwable; {:try_start .. :try_end} :error
+    :error
+    move-exception v0
+    throw v0
+    :return
+    return v0
+.end method
+
 # Dead catch block.
 .method public test3()I
     .locals 1