CF backend bugfix for duplicate frames

Certain code patterns, e.g. empty catch clauses, generate empty blocks which
require explicit stack frames.

Ensure that only the last frame in a sequence of consecutive frames is output
to asm.

Change-Id: Ic340528c7bdf2eed5c61625fb6343e9343f8892e
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/CfBuilder.java b/src/main/java/com/android/tools/r8/ir/conversion/CfBuilder.java
index d709f49..ff37507 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/CfBuilder.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/CfBuilder.java
@@ -196,10 +196,11 @@
     labels = new HashMap<>(code.blocks.size());
     emittedLabels = new HashSet<>(code.blocks.size());
     instructions = new ArrayList<>();
-    Iterator<BasicBlock> blockIterator = code.listIterator();
+    ListIterator<BasicBlock> blockIterator = code.listIterator();
     BasicBlock block = blockIterator.next();
     CfLabel tryCatchStart = null;
     CatchHandlers<BasicBlock> tryCatchHandlers = CatchHandlers.EMPTY_BASIC_BLOCK;
+    BasicBlock pendingFrame = null;
     do {
       CatchHandlers<BasicBlock> handlers = block.getCatchHandlers();
       if (!tryCatchHandlers.equals(handlers)) {
@@ -229,10 +230,26 @@
         pendingLocalChanges = true;
       }
       buildCfInstructions(block, fallthrough, stack);
-      if (nextBlock != null && (!fallthrough || nextBlock.getPredecessors().size() > 1)) {
-        assert stack.isEmpty();
-        emitLabel(getLabel(nextBlock));
-        addFrame(nextBlock, Collections.emptyList());
+      if (nextBlock != null) {
+        if (!fallthrough || nextBlock.getPredecessors().size() > 1) {
+          assert stack.isEmpty();
+          pendingFrame = nextBlock;
+          emitLabel(getLabel(nextBlock));
+        }
+        if (pendingFrame != null) {
+          BasicBlock nextNextBlock = null;
+          if (blockIterator.hasNext()) {
+            nextNextBlock = blockIterator.next();
+            blockIterator.previous();
+          }
+          boolean advancesPC = hasMaterializingInstructions(nextBlock, nextNextBlock);
+          // If nextBlock has no materializing instructions, then nextNextBlock must be non-null
+          // (or we would fall off the edge of the method).
+          assert advancesPC || nextNextBlock != null;
+          if (advancesPC) {
+            addFrame(pendingFrame, Collections.emptyList());
+          }
+        }
       }
       block = nextBlock;
     } while (block != null);
@@ -251,6 +268,25 @@
         localVariablesTable);
   }
 
+  private static boolean isNopInstruction(Instruction instruction, BasicBlock nextBlock) {
+    // From DexBuilder
+    return instruction.isArgument()
+        || instruction.isDebugLocalsChange()
+        || (instruction.isGoto() && instruction.asGoto().getTarget() == nextBlock);
+  }
+
+  private boolean hasMaterializingInstructions(BasicBlock block, BasicBlock nextBlock) {
+    if (block == null) {
+      return false;
+    }
+    for (Instruction instruction : block.getInstructions()) {
+      if (!isNopInstruction(instruction, nextBlock)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
   private void buildCfInstructions(BasicBlock block, boolean fallthrough, Stack stack) {
     InstructionIterator it = block.iterator();
     while (it.hasNext()) {
diff --git a/src/test/java/com/android/tools/r8/cf/NestedExceptionTest.java b/src/test/java/com/android/tools/r8/cf/NestedExceptionTest.java
new file mode 100644
index 0000000..f7823a9
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/cf/NestedExceptionTest.java
@@ -0,0 +1,19 @@
+// Copyright (c) 2018, 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.cf;
+
+public class NestedExceptionTest {
+
+  public static void main(String[] args) {
+    try {
+      new Integer(1);
+    } catch (Exception t) {
+      try {
+        new Integer(1);
+      } catch (Exception tInner) {
+        // This block must be empty
+      }
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/cf/NestedExceptionTestRunner.java b/src/test/java/com/android/tools/r8/cf/NestedExceptionTestRunner.java
new file mode 100644
index 0000000..4c7effd
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/cf/NestedExceptionTestRunner.java
@@ -0,0 +1,36 @@
+// Copyright (c) 2018, 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.cf;
+
+import com.android.tools.r8.CompilationMode;
+import com.android.tools.r8.OutputMode;
+import com.android.tools.r8.R8;
+import com.android.tools.r8.R8Command;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.utils.AndroidAppConsumers;
+import java.nio.file.Path;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+public class NestedExceptionTestRunner {
+  static final Class CLASS = NestedExceptionTest.class;
+  @Rule public TemporaryFolder temp = ToolHelper.getTemporaryFolderForTest();
+
+  @Test
+  public void testNestedException() throws Exception {
+    AndroidAppConsumers a = new AndroidAppConsumers();
+    R8.run(
+        R8Command.builder()
+            .setMode(CompilationMode.DEBUG)
+            .addClassProgramData(ToolHelper.getClassAsBytes(CLASS), Origin.unknown())
+            .addLibraryFiles(ToolHelper.getAndroidJar(ToolHelper.getMinApiLevelForDexVm()))
+            .setProgramConsumer(a.wrapClassFileConsumer(null))
+            .build());
+    Path out = temp.newFolder().toPath();
+    a.build().writeToDirectory(out, OutputMode.ClassFile);
+    ToolHelper.runJava(out, CLASS.getCanonicalName());
+  }
+}