Version 1.2.47

Merge: Insert a nop on any exceptional edges directly targeting a loop header.
CL: https://r8-review.googlesource.com/c/r8/+/26920
Bug: 111337896
Change-Id: Ib9f5cf5e75335b2c394a8f9a251f665cb1b26f6d
diff --git a/src/main/java/com/android/tools/r8/Version.java b/src/main/java/com/android/tools/r8/Version.java
index cc8472a..ddbe2a1 100644
--- a/src/main/java/com/android/tools/r8/Version.java
+++ b/src/main/java/com/android/tools/r8/Version.java
@@ -11,7 +11,7 @@
 
   // This field is accessed from release scripts using simple pattern matching.
   // Therefore, changing this field could break our release scripts.
-  public static final String LABEL = "1.2.46";
+  public static final String LABEL = "1.2.47";
 
   private Version() {
   }
diff --git a/src/main/java/com/android/tools/r8/ir/code/AlwaysMaterializingNop.java b/src/main/java/com/android/tools/r8/ir/code/AlwaysMaterializingNop.java
new file mode 100644
index 0000000..e5ba1de
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/code/AlwaysMaterializingNop.java
@@ -0,0 +1,66 @@
+// 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.ir.code;
+
+import com.android.tools.r8.cf.LoadStoreHelper;
+import com.android.tools.r8.cf.code.CfNop;
+import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.graph.DexType;
+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.shaking.Enqueuer.AppInfoWithLiveness;
+import com.android.tools.r8.utils.InternalOptions;
+
+public class AlwaysMaterializingNop extends Instruction {
+
+  public AlwaysMaterializingNop() {
+    super(null);
+  }
+
+  @Override
+  public boolean canBeDeadCode(IRCode code, InternalOptions options) {
+    return false;
+  }
+
+  @Override
+  public void buildDex(DexBuilder builder) {
+    builder.addNop(this);
+  }
+
+  @Override
+  public void buildCf(CfBuilder builder) {
+    builder.add(new CfNop());
+  }
+
+  @Override
+  public boolean identicalNonValueNonPositionParts(Instruction other) {
+    return other instanceof AlwaysMaterializingNop;
+  }
+
+  @Override
+  public int compareNonValueParts(Instruction other) {
+    return 0;
+  }
+
+  @Override
+  public int maxInValueRegister() {
+    throw new Unreachable();
+  }
+
+  @Override
+  public int maxOutValueRegister() {
+    throw new Unreachable();
+  }
+
+  @Override
+  public Constraint inliningConstraint(AppInfoWithLiveness info, DexType invocationContext) {
+    return Constraint.ALWAYS;
+  }
+
+  @Override
+  public void insertLoadAndStores(InstructionListIterator it, LoadStoreHelper helper) {
+    // Nothing to do.
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/code/AlwaysMaterializingUser.java b/src/main/java/com/android/tools/r8/ir/code/AlwaysMaterializingUser.java
index 4530040..4117cec 100644
--- a/src/main/java/com/android/tools/r8/ir/code/AlwaysMaterializingUser.java
+++ b/src/main/java/com/android/tools/r8/ir/code/AlwaysMaterializingUser.java
@@ -28,7 +28,7 @@
 
   @Override
   public void buildDex(DexBuilder builder) {
-    builder.addNop(this);
+    builder.addNothing(this);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java b/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java
index e10f789..df0c9f3 100644
--- a/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java
+++ b/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java
@@ -840,7 +840,7 @@
     if (!hasCatchHandlers()) {
       return false;
     }
-    return catchHandlers.getAllTargets().contains(successors.indexOf(block));
+    return catchHandlers.getUniqueTargets().contains(successors.indexOf(block));
   }
 
   public int guardsForCatchSuccessor(BasicBlock block) {
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 71f1bec..3a03afd 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
@@ -114,7 +114,7 @@
   public void buildDex(DexBuilder builder) {
     if (!dest().needsRegister()) {
       forceSetPosition(Position.none());
-      builder.addNop(this);
+      builder.addNothing(this);
       return;
     }
 
diff --git a/src/main/java/com/android/tools/r8/ir/code/DebugLocalsChange.java b/src/main/java/com/android/tools/r8/ir/code/DebugLocalsChange.java
index edf68f3..8ff3c64 100644
--- a/src/main/java/com/android/tools/r8/ir/code/DebugLocalsChange.java
+++ b/src/main/java/com/android/tools/r8/ir/code/DebugLocalsChange.java
@@ -55,7 +55,7 @@
 
   @Override
   public void buildDex(DexBuilder builder) {
-    builder.addNop(this);
+    builder.addNothing(this);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/DexBuilder.java b/src/main/java/com/android/tools/r8/ir/conversion/DexBuilder.java
index 8201562..07107a5 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/DexBuilder.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/DexBuilder.java
@@ -424,7 +424,7 @@
     if (jump.getTarget() != nextBlock) {
       add(jump, new GotoInfo(jump));
     } else {
-      addNop(jump);
+      addNothing(jump);
     }
   }
 
@@ -437,7 +437,7 @@
     add(move, new MoveInfo(move));
   }
 
-  public void addNop(com.android.tools.r8.ir.code.Instruction instruction) {
+  public void addNothing(com.android.tools.r8.ir.code.Instruction instruction) {
     assert instruction.getPosition().isNone();
     add(instruction, new FallThroughInfo(instruction));
   }
@@ -457,10 +457,14 @@
         && !(instruction.outValue().needsRegister());
   }
 
+  public void addNop(com.android.tools.r8.ir.code.Instruction ir) {
+    add(ir, new FixedSizeInfo(ir, new Nop()));
+  }
+
   public void addDebugPosition(DebugPosition position) {
     // Remaining debug positions always require we emit an actual nop instruction.
     // See removeRedundantDebugPositions.
-    add(position, new FixedSizeInfo(position, new Nop()));
+    addNop(position);
   }
 
   public void add(com.android.tools.r8.ir.code.Instruction ir, Instruction dex) {
@@ -493,7 +497,7 @@
     if (nextBlock != null
         && ret.identicalAfterRegisterAllocation(nextBlock.entry(), registerAllocator)) {
       ret.forceSetPosition(Position.none());
-      addNop(ret);
+      addNothing(ret);
     } else {
       add(ret, dex);
     }
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
index cc0a5f9..d2f1d60 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
@@ -841,6 +841,9 @@
     workaroundForwardingInitializerBug(code);
     LinearScanRegisterAllocator registerAllocator = new LinearScanRegisterAllocator(code, options);
     registerAllocator.allocateRegisters(options.debug);
+    if (options.canHaveExceptionTargetingLoopHeaderBug()) {
+      codeRewriter.workaroundExceptionTargetingLoopHeaderBug(code);
+    }
     printMethod(code, "After register allocation (non-SSA)");
     for (int i = 0; i < PEEPHOLE_OPTIMIZATION_PASSES; i++) {
       CodeRewriter.collapsTrivialGotos(method, code);
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java b/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java
index 67e779a..748c078 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java
@@ -32,6 +32,7 @@
 import com.android.tools.r8.graph.DexValue.DexValueString;
 import com.android.tools.r8.ir.analysis.type.TypeEnvironment;
 import com.android.tools.r8.ir.analysis.type.TypeLatticeElement;
+import com.android.tools.r8.ir.code.AlwaysMaterializingNop;
 import com.android.tools.r8.ir.code.ArrayPut;
 import com.android.tools.r8.ir.code.BasicBlock;
 import com.android.tools.r8.ir.code.Binop;
@@ -2985,4 +2986,28 @@
       }
     }
   }
+
+  // If an exceptional edge could target a conditional-loop header ensure that we have a
+  // materializing instruction on that path to work around a bug in some L x86_64 non-emulator VMs.
+  // See b/111337896.
+  public void workaroundExceptionTargetingLoopHeaderBug(IRCode code) {
+    for (BasicBlock block : code.blocks) {
+      if (block.hasCatchHandlers()) {
+        for (BasicBlock handler : block.getCatchHandlers().getUniqueTargets()) {
+          // We conservatively assume that a block with at least two normal predecessors is a loop
+          // header. If we ever end up computing exact loop headers, use that here instead.
+          // The loop is conditional if it has at least two normal successors.
+          BasicBlock target = handler.endOfGotoChain();
+          if (target.getPredecessors().size() > 2
+              && target.getNormalPredecessors().size() > 1
+              && target.getNormalSuccessors().size() > 1) {
+            Instruction fixit = new AlwaysMaterializingNop();
+            fixit.setBlock(handler);
+            fixit.setPosition(handler.getPosition());
+            handler.getInstructions().addFirst(fixit);
+          }
+        }
+      }
+    }
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/utils/InternalOptions.java b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
index 470f64a..97b870c 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -658,4 +658,16 @@
   public boolean canHaveForwardingInitInliningBug() {
     return minApiLevel < AndroidApiLevel.M.getLevel();
   }
+
+  // Some Lollipop x86_64 VMs have a bug causing a segfault if an exception handler directly targets
+  // a conditional-loop header. This cannot happen for debug builds as the existence of a
+  // move-exception instruction will ensure a non-direct target.
+  //
+  // To workaround this in release builds, we insert a materializing nop instruction in the
+  // exception handler forcing it not directly target any loop header.
+  //
+  // See b/111337896.
+  public boolean canHaveExceptionTargetingLoopHeaderBug() {
+    return isGeneratingDex() && !debug && minApiLevel < AndroidApiLevel.M.getLevel();
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/debuginfo/DebugInfoTestBase.java b/src/test/java/com/android/tools/r8/debuginfo/DebugInfoTestBase.java
index 6f85b18..da83ed9 100644
--- a/src/test/java/com/android/tools/r8/debuginfo/DebugInfoTestBase.java
+++ b/src/test/java/com/android/tools/r8/debuginfo/DebugInfoTestBase.java
@@ -35,7 +35,7 @@
   @Rule
   public TestDescriptionWatcher watcher = new TestDescriptionWatcher();
 
-  static AndroidApp compileWithD8(Class... classes) throws IOException, CompilationFailedException {
+  static AndroidApp compileWithD8(Class... classes) throws CompilationFailedException {
     D8Command.Builder builder = D8Command.builder();
     for (Class clazz : classes) {
       builder.addProgramFiles(ToolHelper.getClassFileForTestClass(clazz));
diff --git a/src/test/java/com/android/tools/r8/debuginfo/Regress111337896Test.java b/src/test/java/com/android/tools/r8/debuginfo/Regress111337896Test.java
new file mode 100644
index 0000000..bcb8215
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/debuginfo/Regress111337896Test.java
@@ -0,0 +1,40 @@
+// 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.debuginfo;
+
+import java.util.Arrays;
+import java.util.Iterator;
+
+public class Regress111337896Test {
+
+  public static void regress111337896() {
+    Iterator it = Arrays.asList(new Object()).iterator();
+    while (it.hasNext()) { // Loop must be a conditional to hit the issue.
+      it.next();
+      try {
+        noThrow();
+        doThrow();
+        noThrow();
+      } catch (Exception e) {
+        // Handler targeting the loop header causes segfault on some ART 5.0 x86 devices.
+        continue;
+      }
+      // The normal successor may differ from the exceptional one and still cause the issue.
+      it.hasNext();
+    }
+  }
+
+  public static void doThrow() throws Exception {
+    throw new Exception();
+  }
+
+  public static void noThrow() throws Exception {
+    // Intentionally empty.
+  }
+
+  public static void main(String[] args) {
+    regress111337896();
+    System.out.print("aok");
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/debuginfo/Regress111337896TestRunner.java b/src/test/java/com/android/tools/r8/debuginfo/Regress111337896TestRunner.java
new file mode 100644
index 0000000..1404f35
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/debuginfo/Regress111337896TestRunner.java
@@ -0,0 +1,71 @@
+// 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.debuginfo;
+
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.CompilationMode;
+import com.android.tools.r8.D8;
+import com.android.tools.r8.D8Command;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.code.Instruction;
+import com.android.tools.r8.code.Nop;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.AndroidAppConsumers;
+import java.util.Arrays;
+import org.junit.Test;
+
+public class Regress111337896TestRunner extends DebugInfoTestBase {
+
+  @Test
+  public void test() throws Exception {
+    Class clazz = Regress111337896Test.class;
+
+    // Check the regression test is valid code.
+    String expected = "aok";
+    assertEquals(expected, runOnJava(clazz));
+
+    for (AndroidApiLevel minApi : Arrays.asList(AndroidApiLevel.L, AndroidApiLevel.M)) {
+      for (CompilationMode mode : CompilationMode.values()) {
+        AndroidAppConsumers appSink = new AndroidAppConsumers();
+        D8.run(
+            D8Command.builder()
+                .addProgramFiles(ToolHelper.getClassFileForTestClass(clazz))
+                .setProgramConsumer(appSink.wrapDexIndexedConsumer(null))
+                .setMode(mode)
+                .setMinApiLevel(minApi.getLevel())
+                .build());
+        AndroidApp app = appSink.build();
+        assertEquals(expected, runOnArt(app, clazz.getCanonicalName()));
+
+        // Check that the compiled output contains a nop to workaround the issue.
+        // We can't really check much else as this only reproduces on some physical x86_64 devices.
+        check(inspectMethod(app, clazz, "void", "regress111337896"), mode, minApi);
+      }
+    }
+  }
+
+  private void check(DebugInfoInspector info, CompilationMode mode, AndroidApiLevel minApi) {
+    info.checkStartLine(12);
+    assertEquals(1, info.checkLineExists(18));
+    int nopsFound = 0;
+    for (Instruction instruction : info.getMethod().getCode().asDexCode().instructions) {
+      if (instruction instanceof Nop) {
+        nopsFound++;
+      }
+    }
+    if (mode == CompilationMode.DEBUG) {
+      // In debug mode a nop is used to preserve line 21.
+      info.checkLineExists(21);
+      assertEquals(1, nopsFound);
+    } else {
+      // Release mode will have removed the line.
+      info.checkNoLine(21);
+      // In release mode the workaround will have inserted a nop if below M.
+      int expectedNops = minApi.getLevel() < AndroidApiLevel.M.getLevel() ? 1 : 0;
+      assertEquals(expectedNops, nopsFound);
+    }
+  }
+}