Merge "Temporarily ignore jctf tests on Windows"
diff --git a/src/main/java/com/android/tools/r8/graph/ObjectToOffsetMapping.java b/src/main/java/com/android/tools/r8/graph/ObjectToOffsetMapping.java
index 1d5f38c..7dab70a 100644
--- a/src/main/java/com/android/tools/r8/graph/ObjectToOffsetMapping.java
+++ b/src/main/java/com/android/tools/r8/graph/ObjectToOffsetMapping.java
@@ -79,6 +79,7 @@
 
   private static DexProgramClass[] sortClasses(
       DexApplication application, DexProgramClass[] classes) {
+    Arrays.sort(classes, (o1, o2) -> o1.type.descriptor.slowCompareTo(o2.type.descriptor));
     SortingProgramClassVisitor classVisitor = new SortingProgramClassVisitor(application, classes);
     classVisitor.run(classes);
     return classVisitor.getSortedClasses();
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/DexSourceCode.java b/src/main/java/com/android/tools/r8/ir/conversion/DexSourceCode.java
index b0ab940..118d0eb 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/DexSourceCode.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/DexSourceCode.java
@@ -268,7 +268,7 @@
   }
 
   @Override
-  public boolean traceInstruction(int index, IRBuilder builder) {
+  public int traceInstruction(int index, IRBuilder builder) {
     Instruction dex = code.instructions[index];
     int offset = dex.getOffset();
     assert !dex.isPayload();
@@ -279,7 +279,7 @@
       for (int relativeOffset : targets) {
         builder.ensureNormalSuccessorBlock(offset, offset + relativeOffset);
       }
-      return true;
+      return index;
     }
     if (dex.canThrow()) {
       // If the instruction can throw and is in a try block, add edges to its catch successors.
@@ -309,10 +309,10 @@
         if (!(dex instanceof Throw)) {
           builder.ensureNormalSuccessorBlock(offset, dex.getOffset() + dex.getSize());
         }
-        return true;
+        return index;
       }
       // Close the block if the instruction is a throw, otherwise the block remains open.
-      return dex instanceof Throw;
+      return dex instanceof Throw ? index : -1;
     }
     if (dex.isSwitch()) {
       // TODO(zerny): Remove this from block computation.
@@ -322,14 +322,14 @@
         builder.ensureNormalSuccessorBlock(offset, target);
       }
       builder.ensureNormalSuccessorBlock(offset, offset + dex.getSize());
-      return true;
+      return index;
     }
     // TODO(zerny): Remove this from block computation.
     if (dex.hasPayload()) {
       arrayFilledDataPayloadResolver.addPayloadUser((FillArrayData) dex);
     }
     // This instruction does not close the block.
-    return false;
+    return -1;
   }
 
   private boolean inTryRange(Try tryItem, int offset) {
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java b/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java
index d24c8b4..8cd9a2a 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java
@@ -106,7 +106,7 @@
  */
 public class IRBuilder {
 
-  private static final int INITIAL_BLOCK_OFFSET = -1;
+  public static final int INITIAL_BLOCK_OFFSET = -1;
 
   // SSA construction uses a worklist of basic blocks reachable from the entry and their
   // instruction offsets.
@@ -165,7 +165,7 @@
     }
   }
 
-  private static class BlockInfo {
+  public static class BlockInfo {
     BasicBlock block = new BasicBlock();
     IntSet normalPredecessors = new IntArraySet();
     IntSet normalSuccessors = new IntArraySet();
@@ -282,6 +282,10 @@
     this.options = options;
   }
 
+  public Int2ReferenceSortedMap<BlockInfo> getCFG() {
+    return targets;
+  }
+
   private void addToWorklist(BasicBlock block, int firstInstructionIndex) {
     // TODO(ager): Filter out the ones that are already in the worklist, mark bit in block?
     if (!block.isFilled()) {
@@ -318,8 +322,11 @@
       // Process each instruction until the block is closed.
       for (int index = startOfBlockIndex; index < source.instructionCount(); ++index) {
         markIndexProcessed(index);
-        boolean closed = source.traceInstruction(index, this);
-        if (closed) {
+        int closedAt = source.traceInstruction(index, this);
+        if (closedAt != -1) {
+          if (closedAt + 1 < source.instructionCount()) {
+            ensureBlockWithoutEnqueuing(source.instructionOffset(closedAt + 1));
+          }
           break;
         }
         // If the next instruction starts a block, fall through to it.
@@ -1079,6 +1086,7 @@
 
   public void addMoveException(int dest) {
     Value out = writeRegister(dest, MoveType.OBJECT, ThrowingInfo.NO_THROW);
+    assert out.getDebugInfo() == null;
     MoveException instruction = new MoveException(out);
     assert !instruction.instructionTypeCanThrow();
     if (!currentBlock.getInstructions().isEmpty()) {
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/JarSourceCode.java b/src/main/java/com/android/tools/r8/ir/conversion/JarSourceCode.java
index 1d8f18d..12be2c2 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/JarSourceCode.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/JarSourceCode.java
@@ -28,17 +28,21 @@
 import com.android.tools.r8.ir.code.MoveType;
 import com.android.tools.r8.ir.code.NumericType;
 import com.android.tools.r8.ir.code.Switch;
+import com.android.tools.r8.ir.conversion.IRBuilder.BlockInfo;
 import com.android.tools.r8.ir.conversion.JarState.Local;
 import com.android.tools.r8.ir.conversion.JarState.Slot;
 import com.android.tools.r8.logging.Log;
+import it.unimi.dsi.fastutil.ints.Int2ReferenceSortedMap;
 import java.io.PrintWriter;
 import java.io.StringWriter;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
+import java.util.Queue;
 import java.util.Set;
 import java.util.function.BiConsumer;
 import org.objectweb.asm.Handle;
@@ -109,6 +113,16 @@
     }
   }
 
+  private static class JarStateWorklistItem {
+    BlockInfo blockInfo;
+    int instructionIndex;
+
+    public JarStateWorklistItem(BlockInfo blockInfo, int instructionIndex) {
+      this.blockInfo = blockInfo;
+      this.instructionIndex = instructionIndex;
+    }
+  }
+
   // Various descriptors.
   private static final String INT_ARRAY_DESC = "[I";
   private static final String REFLECT_ARRAY_DESC = "Ljava/lang/reflect/Array;";
@@ -294,6 +308,70 @@
         builder.addDebugUninitialized(localRegister, constType(localType));
       }
     }
+    computeBlockEntryJarStates(builder);
+  }
+
+  private void computeBlockEntryJarStates(IRBuilder builder) {
+    Int2ReferenceSortedMap<BlockInfo> CFG = builder.getCFG();
+    Queue<JarStateWorklistItem> worklist = new LinkedList<>();
+    BlockInfo entry = CFG.get(IRBuilder.INITIAL_BLOCK_OFFSET);
+    if (CFG.get(0) != null) {
+      entry = CFG.get(0);
+    }
+    worklist.add(new JarStateWorklistItem(entry, 0));
+    state.recordStateForTarget(0, this);
+    for (JarStateWorklistItem item = worklist.poll(); item != null; item = worklist.poll()) {
+      state.restoreState(item.instructionIndex);
+      // If the block being restored is a try-catch handler push the exception on the stack.
+      for (int i = 0; i < node.tryCatchBlocks.size(); i++) {
+        TryCatchBlockNode tryCatchBlockNode = (TryCatchBlockNode) node.tryCatchBlocks.get(i);
+        if (tryCatchBlockNode.handler == getInstruction(item.instructionIndex)) {
+          state.push(THROWABLE_TYPE);
+          break;
+        }
+      }
+      // Iterate each of the instructions in the block to compute the outgoing JarState.
+      for (int i = item.instructionIndex; i <= instructionCount(); ++i) {
+        // If we are at the end of the instruction stream or if we have reached the start
+        // of a new block, propagate the state to all successors and add the ones
+        // that changed to the worklist.
+        if (i == instructionCount() || (i != item.instructionIndex && CFG.containsKey(i))) {
+          item.blockInfo.normalSuccessors.iterator().forEachRemaining(offset -> {
+            if (state.recordStateForTarget(offset, this)) {
+              if (offset >= 0) {
+                worklist.add(new JarStateWorklistItem(CFG.get(offset.intValue()), offset));
+              }
+            }
+          });
+          item.blockInfo.exceptionalSuccessors.iterator().forEachRemaining(offset -> {
+            if (state.recordStateForExceptionalTarget(offset, this)) {
+              if (offset >= 0) {
+                worklist.add(new JarStateWorklistItem(CFG.get(offset.intValue()), offset));
+              }
+            }
+          });
+          break;
+        }
+
+        AbstractInsnNode insn = getInstruction(i);
+        updateState(insn);
+
+        if (!isControlFlowInstruction(insn)) {
+          updateStateForLocalVariableEnd(insn);
+        }
+      }
+    }
+  }
+
+  private void updateStateForLocalVariableEnd(AbstractInsnNode insn) {
+    assert !isControlFlowInstruction(insn);
+    if (!(insn.getNext() instanceof LabelNode)) {
+      return;
+    }
+    // If the label is the end of any local-variable scopes end the locals.
+    LabelNode label = (LabelNode) insn.getNext();
+    List<Local> locals = state.getLocalsToClose(label);
+    state.closeLocals(locals);
   }
 
   @Override
@@ -307,7 +385,6 @@
 
   private void buildExceptionalPostlude(IRBuilder builder) {
     assert isSynchronized();
-    assert state.isInvalid();
     generatingMethodSynchronization = true;
     int exceptionRegister = 0; // We are exiting the method so we just overwrite register 0.
     builder.addMoveException(exceptionRegister);
@@ -323,14 +400,10 @@
 
   @Override
   public void closedCurrentBlockWithFallthrough(int fallthroughInstructionIndex) {
-    assert !state.isInvalid();
-    state.recordStateForTarget(fallthroughInstructionIndex, this);
-    state.invalidateState();
   }
 
   @Override
   public void closedCurrentBlock() {
-    assert state.isInvalid();
   }
 
   @Override
@@ -343,8 +416,8 @@
     currentInstruction = insn;
     assert verifyExceptionEdgesAreRecorded(insn);
 
-    // Restore the state if invalid.
-    if (state.isInvalid()) {
+    // If a new block is starting here, we restore the computed JarState.
+    if (builder.getCFG().containsKey(instructionIndex) || instructionIndex == 0) {
       state.restoreState(instructionIndex);
       // If the block being restored is a try-catch handler push the exception on the stack.
       for (int i = 0; i < node.tryCatchBlocks.size(); i++) {
@@ -405,7 +478,7 @@
 
   @Override
   public DebugLocalInfo getCurrentLocal(int register) {
-    return state.getLocalInfoForRegister(register);
+    return generatingMethodSynchronization ? null : state.getLocalInfoForRegister(register);
   }
 
   @Override
@@ -518,15 +591,15 @@
   }
 
   @Override
-  public boolean traceInstruction(int index, IRBuilder builder) {
+  public int traceInstruction(int index, IRBuilder builder) {
     AbstractInsnNode insn = getInstruction(index);
     // Exit early on no-op instructions.
     if (insn instanceof LabelNode || insn instanceof LineNumberNode) {
-      return false;
+      return -1;
     }
     // If this instruction exits, close this block.
     if (isReturn(insn)) {
-      return true;
+      return index;
     }
     // For each target ensure a basic block and close this block.
     int[] targets = getTargets(insn);
@@ -535,7 +608,7 @@
       for (int target : targets) {
         builder.ensureNormalSuccessorBlock(index, target);
       }
-      return true;
+      return index;
     }
     if (canThrow(insn)) {
       List<TryCatchBlock> tryCatchBlocks = getTryHandlers(insn);
@@ -555,13 +628,13 @@
         if (!isThrow(insn)) {
           builder.ensureNormalSuccessorBlock(index, getOffset(insn.getNext()));
         }
-        return true;
+        return index;
       }
       // If the throwable instruction is "throw" it closes the block.
-      return isThrow(insn);
+      return isThrow(insn) ? index : -1;
     }
     // This instruction does not close the block.
-    return false;
+    return -1;
   }
 
   private List<TryCatchBlock> getPotentialTryHandlers(AbstractInsnNode insn) {
@@ -987,6 +1060,674 @@
     }
   }
 
+  // State updating procedures.
+
+  private void updateState(AbstractInsnNode insn) {
+    switch (insn.getType()) {
+      case AbstractInsnNode.INSN:
+        updateState((InsnNode) insn);
+        break;
+      case AbstractInsnNode.INT_INSN:
+        updateState((IntInsnNode) insn);
+        break;
+      case AbstractInsnNode.VAR_INSN:
+        updateState((VarInsnNode) insn);
+        break;
+      case AbstractInsnNode.TYPE_INSN:
+        updateState((TypeInsnNode) insn);
+        break;
+      case AbstractInsnNode.FIELD_INSN:
+        updateState((FieldInsnNode) insn);
+        break;
+      case AbstractInsnNode.METHOD_INSN:
+        updateState((MethodInsnNode) insn);
+        break;
+      case AbstractInsnNode.INVOKE_DYNAMIC_INSN:
+        updateState((InvokeDynamicInsnNode) insn);
+        break;
+      case AbstractInsnNode.JUMP_INSN:
+        updateState((JumpInsnNode) insn);
+        break;
+      case AbstractInsnNode.LABEL:
+        updateState((LabelNode) insn);
+        break;
+      case AbstractInsnNode.LDC_INSN:
+        updateState((LdcInsnNode) insn);
+        break;
+      case AbstractInsnNode.IINC_INSN:
+        updateState((IincInsnNode) insn);
+        break;
+      case AbstractInsnNode.TABLESWITCH_INSN:
+        updateState((TableSwitchInsnNode) insn);
+        break;
+      case AbstractInsnNode.LOOKUPSWITCH_INSN:
+        updateState((LookupSwitchInsnNode) insn);
+        break;
+      case AbstractInsnNode.MULTIANEWARRAY_INSN:
+        updateState((MultiANewArrayInsnNode) insn);
+        break;
+      case AbstractInsnNode.LINE:
+        updateState((LineNumberNode) insn);
+        break;
+      default:
+        throw new Unreachable("Unexpected instruction " + insn);
+    }
+  }
+
+  private void updateState(InsnNode insn) {
+    int opcode = insn.getOpcode();
+    switch (opcode) {
+      case Opcodes.NOP:
+        // Intentionally left empty.
+        break;
+      case Opcodes.ACONST_NULL:
+        state.push(JarState.NULL_TYPE);
+        break;
+      case Opcodes.ICONST_M1:
+      case Opcodes.ICONST_0:
+      case Opcodes.ICONST_1:
+      case Opcodes.ICONST_2:
+      case Opcodes.ICONST_3:
+      case Opcodes.ICONST_4:
+      case Opcodes.ICONST_5:
+        state.push(Type.INT_TYPE);
+        break;
+      case Opcodes.LCONST_0:
+      case Opcodes.LCONST_1:
+        state.push(Type.LONG_TYPE);
+        break;
+      case Opcodes.FCONST_0:
+      case Opcodes.FCONST_1:
+      case Opcodes.FCONST_2:
+        state.push(Type.FLOAT_TYPE);
+        break;
+      case Opcodes.DCONST_0:
+      case Opcodes.DCONST_1:
+        state.push(Type.DOUBLE_TYPE);
+        break;
+      case Opcodes.IALOAD:
+      case Opcodes.LALOAD:
+      case Opcodes.FALOAD:
+      case Opcodes.DALOAD:
+      case Opcodes.AALOAD:
+      case Opcodes.BALOAD:
+      case Opcodes.CALOAD:
+      case Opcodes.SALOAD: {
+        state.pop();
+        Slot array = state.pop(JarState.ARRAY_TYPE);
+        Type elementType = getArrayElementType(array.type);
+        if (elementType == null) {
+          // We propagate the null type, which will then get resolved to an
+          // actual type if we have a non-null type on another flow edge.
+          elementType = JarState.NULL_TYPE;
+        }
+        state.push(elementType);
+        break;
+      }
+      case Opcodes.IASTORE:
+      case Opcodes.LASTORE:
+      case Opcodes.FASTORE:
+      case Opcodes.DASTORE:
+      case Opcodes.AASTORE:
+      case Opcodes.BASTORE:
+      case Opcodes.CASTORE:
+      case Opcodes.SASTORE: {
+        state.pop();
+        state.pop();
+        state.pop();
+        break;
+      }
+      case Opcodes.POP: {
+        Slot value = state.pop();
+        assert value.isCategory1();
+        break;
+      }
+      case Opcodes.POP2: {
+        Slot value = state.pop();
+        if (value.isCategory1()) {
+          Slot value2 = state.pop();
+          assert value2.isCategory1();
+        }
+        break;
+      }
+      case Opcodes.DUP: {
+        Slot value = state.peek();
+        assert value.isCategory1();
+        state.push(value.type);
+        break;
+      }
+      case Opcodes.DUP_X1: {
+        // Stack transformation: ..., v2, v1 -> ..., v1, v2, v1
+        Slot value1 = state.pop();
+        Slot value2 = state.pop();
+        assert value1.isCategory1() && value2.isCategory1();
+        int stack2 = state.push(value1.type);
+        int stack1 = state.push(value2.type);
+        state.push(value1.type);
+        assert value2.register == stack2;
+        assert value1.register == stack1;
+        break;
+      }
+      case Opcodes.DUP_X2: {
+        Slot value1 = state.pop();
+        Slot value2 = state.pop();
+        assert value1.isCategory1();
+        if (value2.isCategory1()) {
+          Slot value3 = state.pop();
+          assert value3.isCategory1();
+          // Stack transformation: ..., v3, v2, v1 -> ..., v1, v3, v2, v1
+          updateStateForDupOneBelowTwo(value3, value2, value1);
+        } else {
+          // Stack transformation: ..., w2, v1 -> ..., v1, w2, v1
+          updateStateForDupOneBelowOne(value2, value1);
+        }
+        break;
+      }
+      case Opcodes.DUP2: {
+        Slot value1 = state.pop();
+        if (value1.isCategory1()) {
+          Slot value2 = state.pop();
+          // Stack transformation: ..., v2, v1 -> ..., v2, v1, v2, v1
+          assert value2.isCategory1();
+          state.push(value2.type);
+          state.push(value1.type);
+          state.push(value2.type);
+          state.push(value1.type);
+        } else {
+          // Stack transformation: ..., w1 -> ..., w1, w1
+          state.push(value1.type);
+          state.push(value1.type);
+        }
+        break;
+      }
+      case Opcodes.DUP2_X1: {
+        Slot value1 = state.pop();
+        Slot value2 = state.pop();
+        assert value2.isCategory1();
+        if (value1.isCategory1()) {
+          // Stack transformation: ..., v3, v2, v1 -> v2, v1, v3, v2, v1
+          Slot value3 = state.pop();
+          assert value3.isCategory1();
+          updateStateForDupTwoBelowOne(value3, value2, value1);
+        } else {
+          // Stack transformation: ..., v2, w1 -> ..., w1, v2, w1
+          updateStateForDupOneBelowOne(value2, value1);
+        }
+        break;
+      }
+      case Opcodes.DUP2_X2: {
+        Slot value1 = state.pop();
+        Slot value2 = state.pop();
+        if (!value1.isCategory1() && !value2.isCategory1()) {
+          // State transformation: ..., w2, w1 -> w1, w2, w1
+          updateStateForDupOneBelowOne(value2, value1);
+        } else {
+          Slot value3 = state.pop();
+          if (!value1.isCategory1()) {
+            assert value2.isCategory1();
+            assert value3.isCategory1();
+            // State transformation: ..., v3, v2, w1 -> w1, v3, v2, w1
+            updateStateForDupOneBelowTwo(value3, value2, value1);
+          } else if (!value3.isCategory1()) {
+            assert value1.isCategory1();
+            assert value2.isCategory1();
+            // State transformation: ..., w3, v2, v1 -> v2, v1, w3, v2, v1
+            updateStateForDupTwoBelowOne(value3, value2, value1);
+          } else {
+            Slot value4 = state.pop();
+            assert value1.isCategory1();
+            assert value2.isCategory1();
+            assert value3.isCategory1();
+            assert value4.isCategory1();
+            // State transformation: ..., v4, v3, v2, v1 -> v2, v1, v4, v3, v2, v1
+            updateStateForDupTwoBelowTwo(value4, value3, value2, value1);
+          }
+        }
+        break;
+      }
+      case Opcodes.SWAP: {
+        Slot value1 = state.pop();
+        Slot value2 = state.pop();
+        assert value1.isCategory1() && value2.isCategory1();
+        state.push(value1.type);
+        state.push(value2.type);
+        break;
+      }
+      case Opcodes.IADD:
+      case Opcodes.LADD:
+      case Opcodes.FADD:
+      case Opcodes.DADD:
+      case Opcodes.ISUB:
+      case Opcodes.LSUB:
+      case Opcodes.FSUB:
+      case Opcodes.DSUB:
+      case Opcodes.IMUL:
+      case Opcodes.LMUL:
+      case Opcodes.FMUL:
+      case Opcodes.DMUL:
+      case Opcodes.IDIV:
+      case Opcodes.LDIV:
+      case Opcodes.FDIV:
+      case Opcodes.DDIV:
+      case Opcodes.IREM:
+      case Opcodes.LREM:
+      case Opcodes.FREM:
+      case Opcodes.DREM: {
+        Type type = opType(opcode);
+        state.pop();
+        state.pop();
+        state.push(type);
+        break;
+      }
+      case Opcodes.INEG:
+      case Opcodes.LNEG:
+      case Opcodes.FNEG:
+      case Opcodes.DNEG: {
+        Type type = opType(opcode);
+        state.pop();
+        state.push(type);
+        break;
+      }
+      case Opcodes.ISHL:
+      case Opcodes.LSHL:
+      case Opcodes.ISHR:
+      case Opcodes.LSHR:
+      case Opcodes.IUSHR:
+      case Opcodes.LUSHR: {
+        Type type = opType(opcode);
+        state.pop();
+        state.pop();
+        state.push(type);
+        break;
+      }
+      case Opcodes.IAND:
+      case Opcodes.LAND: {
+        Type type = opcode == Opcodes.IAND ? Type.INT_TYPE : Type.LONG_TYPE;
+        state.pop();
+        state.pop();
+        state.push(type);
+        break;
+      }
+      case Opcodes.IOR:
+      case Opcodes.LOR: {
+        Type type = opcode == Opcodes.IOR ? Type.INT_TYPE : Type.LONG_TYPE;
+        state.pop();
+        state.pop();
+        state.push(type);
+        break;
+      }
+      case Opcodes.IXOR:
+      case Opcodes.LXOR: {
+        Type type = opcode == Opcodes.IXOR ? Type.INT_TYPE : Type.LONG_TYPE;
+        state.pop();
+        state.pop();
+        state.push(type);
+        break;
+      }
+      case Opcodes.I2L:
+        updateStateForConversion(Type.INT_TYPE, Type.LONG_TYPE);
+        break;
+      case Opcodes.I2F:
+        updateStateForConversion(Type.INT_TYPE, Type.FLOAT_TYPE);
+        break;
+      case Opcodes.I2D:
+        updateStateForConversion(Type.INT_TYPE, Type.DOUBLE_TYPE);
+        break;
+      case Opcodes.L2I:
+        updateStateForConversion(Type.LONG_TYPE, Type.INT_TYPE);
+        break;
+      case Opcodes.L2F:
+        updateStateForConversion(Type.LONG_TYPE, Type.FLOAT_TYPE);
+        break;
+      case Opcodes.L2D:
+        updateStateForConversion(Type.LONG_TYPE, Type.DOUBLE_TYPE);
+        break;
+      case Opcodes.F2I:
+        updateStateForConversion(Type.FLOAT_TYPE, Type.INT_TYPE);
+        break;
+      case Opcodes.F2L:
+        updateStateForConversion(Type.FLOAT_TYPE, Type.LONG_TYPE);
+        break;
+      case Opcodes.F2D:
+        updateStateForConversion(Type.FLOAT_TYPE, Type.DOUBLE_TYPE);
+        break;
+      case Opcodes.D2I:
+        updateStateForConversion(Type.DOUBLE_TYPE, Type.INT_TYPE);
+        break;
+      case Opcodes.D2L:
+        updateStateForConversion(Type.DOUBLE_TYPE, Type.LONG_TYPE);
+        break;
+      case Opcodes.D2F:
+        updateStateForConversion(Type.DOUBLE_TYPE, Type.FLOAT_TYPE);
+        break;
+      case Opcodes.I2B:
+        updateStateForConversion(Type.INT_TYPE, Type.BYTE_TYPE);
+        break;
+      case Opcodes.I2C:
+        updateStateForConversion(Type.INT_TYPE, Type.CHAR_TYPE);
+        break;
+      case Opcodes.I2S:
+        updateStateForConversion(Type.INT_TYPE, Type.SHORT_TYPE);
+        break;
+      case Opcodes.LCMP: {
+        state.pop();
+        state.pop();
+        state.push(Type.INT_TYPE);
+        break;
+      }
+      case Opcodes.FCMPL:
+      case Opcodes.FCMPG: {
+        state.pop();
+        state.pop();
+        state.push(Type.INT_TYPE);
+        break;
+      }
+      case Opcodes.DCMPL:
+      case Opcodes.DCMPG: {
+        state.pop();
+        state.pop();
+        state.push(Type.INT_TYPE);
+        break;
+      }
+      case Opcodes.IRETURN: {
+        state.pop();
+        break;
+      }
+      case Opcodes.LRETURN: {
+        state.pop();
+        break;
+      }
+      case Opcodes.FRETURN: {
+        state.pop();
+        break;
+      }
+      case Opcodes.DRETURN: {
+        state.pop();
+        break;
+      }
+      case Opcodes.ARETURN: {
+        state.pop(JarState.REFERENCE_TYPE);
+        break;
+      }
+      case Opcodes.RETURN: {
+        break;
+      }
+      case Opcodes.ARRAYLENGTH: {
+        state.pop(JarState.ARRAY_TYPE);
+        state.push(Type.INT_TYPE);
+        break;
+      }
+      case Opcodes.ATHROW: {
+        state.pop(JarState.OBJECT_TYPE);
+        break;
+      }
+      case Opcodes.MONITORENTER: {
+        state.pop(JarState.REFERENCE_TYPE);
+        break;
+      }
+      case Opcodes.MONITOREXIT: {
+        state.pop(JarState.REFERENCE_TYPE);
+        break;
+      }
+      default:
+        throw new Unreachable("Unexpected Insn opcode: " + insn.getOpcode());
+    }
+  }
+
+  private void updateStateForDupOneBelowTwo(Slot value3, Slot value2, Slot value1) {
+    state.push(value1.type);
+    state.push(value3.type);
+    state.push(value2.type);
+    state.push(value1.type);
+  }
+
+  private void updateStateForDupOneBelowOne(Slot value2, Slot value1) {
+    state.push(value1.type);
+    state.push(value2.type);
+    state.push(value1.type);
+  }
+
+  private void updateStateForDupTwoBelowOne(Slot value3, Slot value2, Slot value1) {
+    state.push(value2.type);
+    state.push(value1.type);
+    state.push(value3.type);
+    state.push(value2.type);
+    state.push(value1.type);
+  }
+
+  private void updateStateForDupTwoBelowTwo(Slot value4, Slot value3, Slot value2, Slot value1) {
+    state.push(value2.type);
+    state.push(value1.type);
+    state.push(value4.type);
+    state.push(value3.type);
+    state.push(value2.type);
+    state.push(value1.type);
+  }
+
+  private void updateState(IntInsnNode insn) {
+    switch (insn.getOpcode()) {
+      case Opcodes.BIPUSH:
+      case Opcodes.SIPUSH: {
+        state.push(Type.INT_TYPE);
+        break;
+      }
+      case Opcodes.NEWARRAY: {
+        String desc = arrayTypeDesc(insn.operand);
+        Type type = Type.getType(desc);
+        state.pop();
+        state.push(type);
+        break;
+      }
+      default:
+        throw new Unreachable("Unexpected IntInsn opcode: " + insn.getOpcode());
+    }
+  }
+
+  private void updateState(VarInsnNode insn) {
+    int opcode = insn.getOpcode();
+    Type expectedType;
+    switch (opcode) {
+      case Opcodes.ILOAD:
+      case Opcodes.ISTORE:
+        expectedType = Type.INT_TYPE;
+        break;
+      case Opcodes.FLOAD:
+      case Opcodes.FSTORE:
+        expectedType = Type.FLOAT_TYPE;
+        break;
+      case Opcodes.LLOAD:
+      case Opcodes.LSTORE:
+        expectedType = Type.LONG_TYPE;
+        break;
+      case Opcodes.DLOAD:
+      case Opcodes.DSTORE:
+        expectedType = Type.DOUBLE_TYPE;
+        break;
+      case Opcodes.ALOAD:
+      case Opcodes.ASTORE:
+        expectedType = JarState.REFERENCE_TYPE;
+        break;
+      case Opcodes.RET: {
+        throw new Unreachable("RET should be handled by the ASM jsr inliner");
+      }
+      default:
+        throw new Unreachable("Unexpected VarInsn opcode: " + insn.getOpcode());
+    }
+    if (Opcodes.ILOAD <= opcode && opcode <= Opcodes.ALOAD) {
+      Slot src = state.readLocal(insn.var, expectedType);
+      state.push(src.type);
+    } else {
+      assert Opcodes.ISTORE <= opcode && opcode <= Opcodes.ASTORE;
+      Slot slot = state.pop();
+      if (slot.type == JarState.NULL_TYPE && expectedType != JarState.REFERENCE_TYPE) {
+        state.writeLocal(insn.var, expectedType);
+      } else {
+        state.writeLocal(insn.var, slot.type);
+      }
+    }
+  }
+
+  private void updateState(TypeInsnNode insn) {
+    Type type = Type.getObjectType(insn.desc);
+    switch (insn.getOpcode()) {
+      case Opcodes.NEW: {
+        state.push(type);
+        break;
+      }
+      case Opcodes.ANEWARRAY: {
+        Type arrayType = makeArrayType(type);
+        state.pop();
+        state.push(arrayType);
+        break;
+      }
+      case Opcodes.CHECKCAST: {
+        // Pop the top value and push it back on with the checked type.
+        state.pop(type);
+        state.push(type);
+        break;
+      }
+      case Opcodes.INSTANCEOF: {
+        state.pop(JarState.REFERENCE_TYPE);
+        state.push(Type.INT_TYPE);
+        break;
+      }
+      default:
+        throw new Unreachable("Unexpected TypeInsn opcode: " + insn.getOpcode());
+    }
+
+  }
+
+  private void updateState(FieldInsnNode insn) {
+    Type type = Type.getType(insn.desc);
+    switch (insn.getOpcode()) {
+      case Opcodes.GETSTATIC:
+        state.push(type);
+        break;
+      case Opcodes.PUTSTATIC:
+        state.pop();
+        break;
+      case Opcodes.GETFIELD: {
+        state.pop(JarState.OBJECT_TYPE);
+        state.push(type);
+        break;
+      }
+      case Opcodes.PUTFIELD: {
+        state.pop();
+        state.pop(JarState.OBJECT_TYPE);
+        break;
+      }
+      default:
+        throw new Unreachable("Unexpected FieldInsn opcode: " + insn.getOpcode());
+    }
+  }
+
+  private void updateState(MethodInsnNode insn) {
+    updateStateForInvoke(insn.desc, insn.getOpcode() != Opcodes.INVOKESTATIC);
+  }
+
+  private void updateState(InvokeDynamicInsnNode insn) {
+    updateStateForInvoke(insn.desc, false /* receiver passed explicitly */);
+  }
+
+  private void updateStateForInvoke(String desc, boolean implicitReceiver) {
+    // Pop arguments.
+    Type[] parameterTypes = Type.getArgumentTypes(desc);
+    state.popReverse(parameterTypes.length);
+    // Pop implicit receiver if needed.
+    if (implicitReceiver) {
+      state.pop();
+    }
+    // Push return value if needed.
+    Type returnType = Type.getReturnType(desc);
+    if (returnType != Type.VOID_TYPE) {
+      state.push(returnType);
+    }
+  }
+
+  private void updateState(JumpInsnNode insn) {
+    int[] targets = getTargets(insn);
+    int opcode = insn.getOpcode();
+    if (Opcodes.IFEQ <= opcode && opcode <= Opcodes.IF_ACMPNE) {
+      assert targets.length == 2;
+      if (opcode <= Opcodes.IFLE) {
+        state.pop();
+      } else {
+        state.pop();
+        state.pop();
+      }
+    } else {
+      switch (opcode) {
+        case Opcodes.GOTO: {
+          assert targets.length == 1;
+          break;
+        }
+        case Opcodes.IFNULL:
+        case Opcodes.IFNONNULL: {
+          state.pop();
+          break;
+        }
+        case Opcodes.JSR: {
+          throw new Unreachable("JSR should be handled by the ASM jsr inliner");
+        }
+        default:
+          throw new Unreachable("Unexpected JumpInsn opcode: " + insn.getOpcode());
+      }
+    }
+  }
+
+  private void updateState(LabelNode insn) {
+    // Open the scope of locals starting at this point.
+    if (insn != initialLabel) {
+      state.openLocals(insn);
+    }
+  }
+
+  private void updateState(LdcInsnNode insn) {
+    if (insn.cst instanceof Type) {
+      Type type = (Type) insn.cst;
+      state.push(type);
+    } else if (insn.cst instanceof String) {
+      state.push(STRING_TYPE);
+    } else if (insn.cst instanceof Long) {
+      state.push(Type.LONG_TYPE);
+    } else if (insn.cst instanceof Double) {
+      state.push(Type.DOUBLE_TYPE);
+    } else if (insn.cst instanceof Integer) {
+      state.push(Type.INT_TYPE);
+    } else {
+      assert insn.cst instanceof Float;
+      state.push(Type.FLOAT_TYPE);
+    }
+  }
+
+  private void updateState(IincInsnNode insn) {
+    state.readLocal(insn.var, Type.INT_TYPE);
+  }
+
+  private void updateState(TableSwitchInsnNode insn) {
+    state.pop();
+  }
+
+  private void updateState(LookupSwitchInsnNode insn) {
+    state.pop();
+  }
+
+  private void updateState(MultiANewArrayInsnNode insn) {
+    // Type of the full array.
+    Type arrayType = Type.getObjectType(insn.desc);
+    state.popReverse(insn.dims, Type.INT_TYPE);
+    state.push(arrayType);
+  }
+
+  private void updateState(LineNumberNode insn) {
+    // Intentionally empty.
+  }
+
+  private void updateStateForConversion(Type from, Type to) {
+    state.pop();
+    state.push(to);
+  }
+
   // IR instruction building procedures.
 
   private void build(AbstractInsnNode insn, IRBuilder builder) {
@@ -1509,7 +2250,6 @@
       processLocalVariablesAtControlEdge(insn, builder);
     }
     builder.addThrow(register);
-    state.invalidateState();
   }
 
   private void addReturn(InsnNode insn, MoveType type, int register, IRBuilder builder) {
@@ -1520,7 +2260,6 @@
     } else {
       builder.addReturn(type, register);
     }
-    state.invalidateState();
   }
 
   private void dupOneBelowTwo(Slot value3, Slot value2, Slot value1, IRBuilder builder) {
@@ -1913,10 +2652,6 @@
           throw new Unreachable("Unexpected JumpInsn opcode: " + insn.getOpcode());
       }
     }
-    for (int target : targets) {
-      state.recordStateForTarget(target, this);
-    }
-    state.invalidateState();
   }
 
   private void build(LabelNode insn, IRBuilder builder) {
@@ -1926,11 +2661,6 @@
         builder.addDebugLocalStart(local.slot.register, local.info);
       }
     }
-    // Processing local-variable ends is done before closing potential control-flow edges.
-    // Record the state for all the try-catch handlers that are active at this label.
-    for (TryCatchBlock tryCatchBlock : getTryHandlers(insn)) {
-      state.recordStateForExceptionalTarget(tryCatchBlock.getHandler(), this);
-    }
   }
 
   private void build(LdcInsnNode insn, IRBuilder builder) {
@@ -1968,6 +2698,7 @@
   }
 
   private void build(LookupSwitchInsnNode insn, IRBuilder builder) {
+    processLocalVariablesAtControlEdge(insn, builder);
     int[] keys = new int[insn.keys.size()];
     for (int i = 0; i < insn.keys.size(); i++) {
       keys[i] = (int) insn.keys.get(i);
@@ -1979,15 +2710,12 @@
       IRBuilder builder) {
     int index = state.pop(Type.INT_TYPE).register;
     int fallthroughOffset = getOffset(dflt);
-    state.recordStateForTarget(fallthroughOffset, this);
     int[] labelOffsets = new int[labels.size()];
     for (int i = 0; i < labels.size(); i++) {
       int offset = getOffset((LabelNode) labels.get(i));
       labelOffsets[i] = offset;
-      state.recordStateForTarget(offset, this);
     }
     builder.addSwitch(type, index, keys, fallthroughOffset, labelOffsets);
-    state.invalidateState();
   }
 
   private void build(MultiANewArrayInsnNode insn, IRBuilder builder) {
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/JarState.java b/src/main/java/com/android/tools/r8/ir/conversion/JarState.java
index e45fcb2..9e59c48 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/JarState.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/JarState.java
@@ -40,13 +40,6 @@
   // Type representative for an address type (used by JSR/RET).
   public static final Type ADDR_TYPE = Type.getObjectType("<address>");
 
-  private static final Slot INVALID_SLOT = new Slot(-1, null);
-  private static final Local INVALID_LOCAL = new Local(INVALID_SLOT, null);
-
-  public boolean isInvalid() {
-    return stack.peek() == INVALID_SLOT;
-  }
-
   // Typed mapping from a local slot or stack slot to a virtual register.
   public static class Slot {
     public final int register;
@@ -54,7 +47,7 @@
 
     @Override
     public String toString() {
-      return this == INVALID_SLOT ? "<invalid slot>" : "r" + register + ":" + type;
+      return "r" + register + ":" + type;
     }
 
     public Slot(int register, Type type) {
@@ -356,15 +349,6 @@
 
   // State procedures.
 
-  public void invalidateState() {
-    for (int i = 0; i < locals.length; i++) {
-      locals[i] = INVALID_LOCAL;
-    }
-    stack.clear();
-    stack.push(INVALID_SLOT);
-    topOfStack = -1;
-  }
-
   public boolean hasState(int offset) {
     return targetStates.get(offset) != null;
   }
@@ -374,49 +358,100 @@
     assert snapshot != null;
     assert locals.length == snapshot.locals.length;
     for (int i = 0; i < locals.length; i++) {
-      assert locals[i] == INVALID_LOCAL;
       locals[i] = snapshot.locals[i];
     }
-    assert stack.peek() == INVALID_SLOT;
-    assert topOfStack == -1;
     stack.clear();
     stack.addAll(snapshot.stack);
     topOfStack = startOfStack + 2 * stack.size();
   }
 
-  public void recordStateForTarget(int target, JarSourceCode source) {
-    recordStateForTarget(target, locals.clone(), ImmutableList.copyOf(stack), source);
+  public boolean recordStateForTarget(int target, JarSourceCode source) {
+    return recordStateForTarget(target, locals.clone(), ImmutableList.copyOf(stack), source);
   }
 
-  public void recordStateForExceptionalTarget(int target, JarSourceCode source) {
-    recordStateForTarget(target, locals.clone(), ImmutableList.of(), source);
+  public boolean recordStateForExceptionalTarget(int target, JarSourceCode source) {
+    return recordStateForTarget(target, locals.clone(), ImmutableList.of(), source);
   }
 
-  private void recordStateForTarget(int target, Local[] locals, ImmutableList<Slot> stack,
+  private boolean recordStateForTarget(int target, Local[] locals, ImmutableList<Slot> stack,
       JarSourceCode source) {
-    Snapshot snapshot = targetStates.get(target);
-    if (snapshot == null) {
-      if (!localVariables.isEmpty()) {
-        for (int i = 0; i < locals.length; i++) {
-          if (locals[i] != null) {
-            locals[i] = new Local(locals[i].slot, null);
-          }
-        }
-        // TODO(zerny): Precompute and sort the local ranges.
-        for (LocalVariableNode node : localVariables.keySet()) {
-          int startOffset = source.getOffset(node.start);
-          int endOffset = source.getOffset(node.end);
-          if (startOffset <= target && target < endOffset) {
-            int register = getLocalRegister(node.index, Type.getType(node.desc));
-            Local local = locals[register];
-            locals[register] = new Local(local.slot, localVariables.get(node));
-          }
+    if (!localVariables.isEmpty()) {
+      for (int i = 0; i < locals.length; i++) {
+        if (locals[i] != null) {
+          locals[i] = new Local(locals[i].slot, null);
         }
       }
-      targetStates.put(target, new Snapshot(locals, stack));
-      return;
+      // TODO(zerny): Precompute and sort the local ranges.
+      for (LocalVariableNode node : localVariables.keySet()) {
+        int startOffset = source.getOffset(node.start);
+        int endOffset = source.getOffset(node.end);
+        if (startOffset <= target && target < endOffset) {
+          int register = getLocalRegister(node.index, Type.getType(node.desc));
+          Local local = locals[register];
+          locals[register] = new Local(local.slot, localVariables.get(node));
+        }
+      }
     }
-    assert verifyStack(stack, snapshot.stack);
+    Snapshot snapshot = targetStates.get(target);
+    if (snapshot != null) {
+      Local[] newLocals = mergeLocals(snapshot.locals, locals);
+      ImmutableList<Slot> newStack = mergeStacks(snapshot.stack, stack);
+      if (newLocals != snapshot.locals || newStack != snapshot.stack) {
+        targetStates.put(target, new Snapshot(newLocals, newStack));
+        return true;
+      }
+      // The snapshot is up to date - no new type information recoded.
+      return false;
+    }
+    targetStates.put(target, new Snapshot(locals, stack));
+    return true;
+  }
+
+  private ImmutableList<Slot> mergeStacks(
+      ImmutableList<Slot> currentStack, ImmutableList<Slot> newStack) {
+    assert currentStack.size() == newStack.size();
+    List<Slot> mergedStack = null;
+    for (int i = 0; i < currentStack.size(); i++) {
+      assert currentStack.get(i).isCompatibleWith(newStack.get(i).type);
+      if (currentStack.get(i).type == JarState.NULL_TYPE &&
+          newStack.get(i).type != JarState.NULL_TYPE) {
+        if (mergedStack == null) {
+          mergedStack = new ArrayList<>();
+          mergedStack.addAll(currentStack.subList(0, i));
+        }
+        mergedStack.add(newStack.get(i));
+      } else if (mergedStack != null) {
+        mergedStack.add(currentStack.get(i));
+      }
+    }
+    return mergedStack != null ? ImmutableList.copyOf(mergedStack) : currentStack;
+  }
+
+  private Local[] mergeLocals(Local[] currentLocals, Local[] newLocals) {
+    assert currentLocals.length == newLocals.length;
+    Local[] mergedLocals = null;
+    for (int i = 0; i < currentLocals.length; i++) {
+      Local currentLocal = currentLocals[i];
+      Local newLocal = newLocals[i];
+      if (currentLocal == null || newLocal == null) {
+        continue;
+      }
+      // If this assert triggers we can get different debug information for the same local
+      // on different control-flow paths and we will have to merge them.
+      assert currentLocal.info == newLocal.info;
+      if (currentLocal.slot.type == JarState.NULL_TYPE &&
+          newLocal.slot.type != JarState.NULL_TYPE) {
+        if (mergedLocals == null) {
+          mergedLocals = new Local[currentLocals.length];
+          System.arraycopy(currentLocals, 0, mergedLocals, 0, i);
+        }
+        Slot newSlot = new Slot(newLocal.slot.register, newLocal.slot.type);
+        mergedLocals[i] = new Local(newSlot, newLocal.info);
+      } else if (mergedLocals != null) {
+        mergedLocals[i] = currentLocals[i];
+      }
+    }
+    return mergedLocals != null ? mergedLocals : currentLocals;
   }
 
   private static boolean verifyStack(List<Slot> stack, List<Slot> other) {
@@ -446,9 +481,6 @@
   public static String stackToString(Collection<Slot> stack) {
     List<String> strings = new ArrayList<>(stack.size());
     for (Slot slot : stack) {
-      if (slot == INVALID_SLOT) {
-        return "<stack invalidated>";
-      }
       strings.add(slot.type.toString());
     }
     StringBuilder builder = new StringBuilder("{ ");
@@ -466,9 +498,6 @@
     StringBuilder builder = new StringBuilder("{ ");
     boolean first = true;
     for (Local local : locals) {
-      if (local == INVALID_LOCAL) {
-        return "<locals invalidated>";
-      }
       if (!first) {
         builder.append(", ");
       } else {
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/SourceCode.java b/src/main/java/com/android/tools/r8/ir/conversion/SourceCode.java
index ba03f82..a36091d 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/SourceCode.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/SourceCode.java
@@ -33,9 +33,10 @@
    * <p>The instruction at {@code index} is traced and its target blocks are marked by using
    * {@code IRBuilder.ensureSuccessorBlock} (and {@code ensureBlockWithoutEnqueuing}).
    *
-   * @return True if the instruction closes the current block, false otherwise.
+   * @return If the instruction closes the block, the last index of the block,
+   * otherwise -1.
    */
-  boolean traceInstruction(int instructionIndex, IRBuilder builder);
+  int traceInstruction(int instructionIndex, IRBuilder builder);
 
   void closedCurrentBlockWithFallthrough(int fallthroughInstructionIndex);
   void closedCurrentBlock();
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/Outliner.java b/src/main/java/com/android/tools/r8/ir/optimize/Outliner.java
index 42b5220..4ab72f6 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/Outliner.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/Outliner.java
@@ -812,9 +812,9 @@
     }
 
     @Override
-    public boolean traceInstruction(int instructionIndex, IRBuilder builder) {
+    public int traceInstruction(int instructionIndex, IRBuilder builder) {
       // There is just one block, and after the last instruction it is closed.
-      return instructionIndex == outline.templateInstructions.size();
+      return instructionIndex == outline.templateInstructions.size() ? instructionIndex : -1;
     }
 
     @Override
diff --git a/src/main/java/com/android/tools/r8/ir/synthetic/SingleBlockSourceCode.java b/src/main/java/com/android/tools/r8/ir/synthetic/SingleBlockSourceCode.java
index 8c97f8f..22cf6d5 100644
--- a/src/main/java/com/android/tools/r8/ir/synthetic/SingleBlockSourceCode.java
+++ b/src/main/java/com/android/tools/r8/ir/synthetic/SingleBlockSourceCode.java
@@ -124,8 +124,8 @@
   }
 
   @Override
-  public final boolean traceInstruction(int instructionIndex, IRBuilder builder) {
-    return instructionIndex == constructors.size() - 1;
+  public final int traceInstruction(int instructionIndex, IRBuilder builder) {
+    return (instructionIndex == constructors.size() - 1) ? instructionIndex : -1;
   }
 
   @Override
diff --git a/src/test/java/com/android/tools/r8/jasmin/BooleanByteConfusion.java b/src/test/java/com/android/tools/r8/jasmin/BooleanByteConfusion.java
index 2132b4e..548d9d0 100644
--- a/src/test/java/com/android/tools/r8/jasmin/BooleanByteConfusion.java
+++ b/src/test/java/com/android/tools/r8/jasmin/BooleanByteConfusion.java
@@ -6,7 +6,6 @@
 import static junit.framework.TestCase.assertEquals;
 
 import com.google.common.collect.ImmutableList;
-import org.junit.Ignore;
 import org.junit.Test;
 
 public class BooleanByteConfusion extends JasminTestBase {
@@ -21,7 +20,6 @@
   }
 
   @Test
-  @Ignore("b/62438166")
   public void booleanByteConfusion() throws Exception {
     JasminBuilder builder = new JasminBuilder();
     JasminBuilder.ClassBuilder clazz = builder.addClass("Test");
diff --git a/src/test/java/com/android/tools/r8/maindexlist/MainDexListTests.java b/src/test/java/com/android/tools/r8/maindexlist/MainDexListTests.java
index d05c800..f540bc0 100644
--- a/src/test/java/com/android/tools/r8/maindexlist/MainDexListTests.java
+++ b/src/test/java/com/android/tools/r8/maindexlist/MainDexListTests.java
@@ -11,6 +11,8 @@
 import static org.junit.Assert.fail;
 
 import com.android.tools.r8.CompilationException;
+import com.android.tools.r8.D8;
+import com.android.tools.r8.D8Command;
 import com.android.tools.r8.R8Command;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.dex.ApplicationWriter;
@@ -39,6 +41,7 @@
 import com.android.tools.r8.ir.regalloc.LinearScanRegisterAllocator;
 import com.android.tools.r8.ir.regalloc.RegisterAllocator;
 import com.android.tools.r8.ir.synthetic.SynthesizedCode;
+import com.android.tools.r8.jasmin.JasminBuilder;
 import com.android.tools.r8.naming.NamingLens;
 import com.android.tools.r8.shaking.ProguardRuleParserException;
 import com.android.tools.r8.utils.AndroidApp;
@@ -52,12 +55,14 @@
 import com.android.tools.r8.utils.ThreadUtils;
 import com.android.tools.r8.utils.Timing;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
 import com.google.common.io.ByteStreams;
 import com.google.common.io.Closer;
 import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Set;
 import java.util.concurrent.ExecutionException;
@@ -207,6 +212,113 @@
     MainDexList.parse(mainDexList, factory);
   }
 
+  @Test
+  public void checkDeterminism() throws Exception {
+    // Synthesize a dex containing a few empty classes including some in the default package.
+    // Everything can fit easaly in a single dex file.
+    String[] classes = {
+        "A",
+        "B",
+        "C",
+        "D",
+        "E",
+        "F",
+        "A1",
+        "A2",
+        "A3",
+        "A4",
+        "A5",
+        "maindexlist/A",
+        "maindexlist/B",
+        "maindexlist/C",
+        "maindexlist/D",
+        "maindexlist/E",
+        "maindexlist/F",
+        "maindexlist/A1",
+        "maindexlist/A2",
+        "maindexlist/A3",
+        "maindexlist/A4",
+        "maindexlist/A5"
+    };
+    JasminBuilder jasminBuilder = new JasminBuilder();
+    for (String name : classes) {
+      jasminBuilder.addClass(name);
+    }
+    Path input = temp.newFolder().toPath().resolve("input.zip");
+    ToolHelper.runR8(jasminBuilder.build()).writeToZip(input, OutputMode.Indexed);
+
+    // Prepare different main dex lists.
+    ArrayList<Path> mainLists = new ArrayList<>();
+    // Lets first without a main dex list.
+    mainLists.add(null);
+
+    // List with all classes.
+    List<String> mainList = new ArrayList<>();
+    for (int i = 0; i < classes.length; i++) {
+      mainList.add(classes[i] + ".class");
+    }
+    addMainListFile(mainLists, mainList);
+
+    // Full list in reverse order
+    addMainListFile(mainLists, Lists.reverse(mainList));
+
+    // Partial list without first entries (those in default package).
+    mainList.clear();
+    for (int i = classes.length / 2; i < classes.length; i++) {
+      mainList.add(classes[i] + ".class");
+    }
+    addMainListFile(mainLists, mainList);
+
+    // Same in reverese order
+    addMainListFile(mainLists, Lists.reverse(mainList));
+
+    // Mixed partial list.
+    mainList.clear();
+    for (int i = 0; i < classes.length; i += 2) {
+      mainList.add(classes[i] + ".class");
+    }
+    addMainListFile(mainLists, mainList);
+
+    // Another different mixed partial list.
+    mainList.clear();
+    for (int i = 1; i < classes.length; i += 2) {
+      mainList.add(classes[i] + ".class");
+    }
+    addMainListFile(mainLists, mainList);
+
+    // Build with all main dex lists.
+    Path tmp = temp.getRoot().toPath();
+    for (int i = 0; i < mainLists.size(); i++) {
+      Path out = tmp.resolve(String.valueOf(i));
+      Files.createDirectories(out);
+      D8Command.Builder builder = D8Command.builder()
+          .addProgramFiles(input)
+          .setOutputPath(out);
+      if (mainLists.get(i) != null) {
+        builder.setMainDexListFile(mainLists.get(i));
+      }
+      ToolHelper.runD8(builder.build());
+    }
+
+    // Check: no secondary dex and resulting dex is always the same.
+    assertFalse(Files.exists(tmp.resolve(String.valueOf(0)).resolve("classes2.dex")));
+    byte[] ref = Files.readAllBytes(
+        tmp.resolve(String.valueOf(0)).resolve(FileUtils.DEFAULT_DEX_FILENAME));
+    for (int i = 1; i < mainLists.size(); i++) {
+      assertFalse(Files.exists(tmp.resolve(String.valueOf(i)).resolve("classes2.dex")));
+      byte[] checked = Files.readAllBytes(
+          tmp.resolve(String.valueOf(i)).resolve(FileUtils.DEFAULT_DEX_FILENAME));
+      assertArrayEquals(ref, checked);
+    }
+  }
+
+  private void addMainListFile(ArrayList<Path> mainLists, List<String> content)
+      throws IOException {
+    Path listFile = temp.newFile().toPath();
+    FileUtils.writeTextFile(listFile, content);
+    mainLists.add(listFile);
+  }
+
   private static String typeToEntry(String type) {
     return type.replace(".", "/") + CLASS_EXTENSION;
   }
@@ -265,7 +377,7 @@
     for (String clazz : classes) {
       DexString desc = factory.createString(DescriptorUtils.javaTypeToDescriptor(clazz));
       DexType type = factory.createType(desc);
-      DexEncodedMethod[] virtualMethods = new DexEncodedMethod[methodCount];
+      DexEncodedMethod[] directMethods = new DexEncodedMethod[methodCount];
       for (int i = 0; i < methodCount; i++) {
         DexAccessFlags access = new DexAccessFlags();
         access.setPublic();
@@ -285,7 +397,7 @@
         IRCode ir = code.buildIR(method, options);
         RegisterAllocator allocator = new LinearScanRegisterAllocator(ir);
         method.setCode(ir, allocator, factory);
-        virtualMethods[i] = method;
+        directMethods[i] = method;
       }
       builder.addProgramClass(
           new DexProgramClass(
@@ -298,8 +410,8 @@
               DexAnnotationSet.empty(),
               DexEncodedField.EMPTY_ARRAY,
               DexEncodedField.EMPTY_ARRAY,
-              DexEncodedMethod.EMPTY_ARRAY,
-              virtualMethods));
+              directMethods,
+              DexEncodedMethod.EMPTY_ARRAY));
     }
     DexApplication application = builder.build();
     AppInfoWithSubtyping appInfo = new AppInfoWithSubtyping(application);
@@ -342,8 +454,8 @@
     }
 
     @Override
-    public boolean traceInstruction(int instructionIndex, IRBuilder builder) {
-      return true;
+    public int traceInstruction(int instructionIndex, IRBuilder builder) {
+      return instructionIndex;
     }
 
     @Override
diff --git a/src/test/java/com/android/tools/r8/maindexlist/many-classes.zip b/src/test/java/com/android/tools/r8/maindexlist/many-classes.zip
index 3c1530d..fd4ecdc 100644
--- a/src/test/java/com/android/tools/r8/maindexlist/many-classes.zip
+++ b/src/test/java/com/android/tools/r8/maindexlist/many-classes.zip
Binary files differ
diff --git a/src/test/java/com/android/tools/r8/maindexlist/two-large-classes.zip b/src/test/java/com/android/tools/r8/maindexlist/two-large-classes.zip
index 97b5fde..9ad745f 100644
--- a/src/test/java/com/android/tools/r8/maindexlist/two-large-classes.zip
+++ b/src/test/java/com/android/tools/r8/maindexlist/two-large-classes.zip
Binary files differ