Desugar two-arg AssertionError constructor on pre-API 19

Turns into a one-arg AssertionError constructor call and a call to initCause. On API 15 and below, a bug in the Apache Harmony implementation of AssertionError prevents propagating the cause so it is dropped.

Bug: 137449279
Test: tools/test.py --dex_vm all --no-internal -v *AssertionErrorRewriteTest*
Change-Id: I3cab39cbf5f94ccf0b2d9c82713ac58af88c7d1b
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 faa7862..7fb524d 100644
--- a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
+++ b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
@@ -179,6 +179,7 @@
   public final DexString invokeMethodName = createString("invoke");
   public final DexString invokeExactMethodName = createString("invokeExact");
 
+  public final DexString assertionErrorDescriptor = createString("Ljava/lang/AssertionError;");
   public final DexString charSequenceDescriptor = createString("Ljava/lang/CharSequence;");
   public final DexString charSequenceArrayDescriptor = createString("[Ljava/lang/CharSequence;");
   public final DexString stringDescriptor = createString("Ljava/lang/String;");
@@ -316,6 +317,7 @@
   public final LongMethods longMethods = new LongMethods();
   public final JavaUtilArraysMethods utilArraysMethods = new JavaUtilArraysMethods();
   public final ThrowableMethods throwableMethods = new ThrowableMethods();
+  public final AssertionErrorMethods assertionErrorMethods = new AssertionErrorMethods();
   public final ClassMethods classMethods = new ClassMethods();
   public final ConstructorMethods constructorMethods = new ConstructorMethods();
   public final EnumMethods enumMethods = new EnumMethods();
@@ -556,12 +558,29 @@
 
     public final DexMethod addSuppressed;
     public final DexMethod getSuppressed;
+    public final DexMethod initCause;
 
     private ThrowableMethods() {
       addSuppressed = createMethod(throwableDescriptor,
           createString("addSuppressed"), voidDescriptor, new DexString[]{throwableDescriptor});
       getSuppressed = createMethod(throwableDescriptor,
           createString("getSuppressed"), throwableArrayDescriptor, DexString.EMPTY_ARRAY);
+      initCause = createMethod(throwableDescriptor, createString("initCause"), throwableDescriptor,
+          new DexString[] { throwableDescriptor });
+    }
+  }
+
+  public class AssertionErrorMethods {
+    public final DexMethod initMessage;
+    public final DexMethod initMessageAndCause;
+
+    private AssertionErrorMethods() {
+      this.initMessage =
+          createMethod(assertionErrorDescriptor, constructorMethodName, voidDescriptor,
+              new DexString[] { objectDescriptor });
+      this.initMessageAndCause =
+          createMethod(assertionErrorDescriptor, constructorMethodName, voidDescriptor,
+              new DexString[] { stringDescriptor, throwableDescriptor });
     }
   }
 
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 854ed81..36e3b1c 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
@@ -1088,6 +1088,7 @@
     }
 
     codeRewriter.rewriteLongCompareAndRequireNonNull(code, options);
+    codeRewriter.rewriteAssertionErrorTwoArgumentConstructor(code, options);
     codeRewriter.commonSubexpressionElimination(code);
     codeRewriter.simplifyArrayConstruction(code);
     codeRewriter.rewriteMoveResult(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 b73c702..6352269 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
@@ -126,6 +126,7 @@
 import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap;
 import java.util.ArrayDeque;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.Comparator;
 import java.util.Deque;
@@ -3735,6 +3736,43 @@
     assert code.isConsistentSSA();
   }
 
+  public void rewriteAssertionErrorTwoArgumentConstructor(IRCode code, InternalOptions options) {
+    if (options.canUseAssertionErrorTwoArgumentConstructor()) {
+      return;
+    }
+
+    InstructionIterator iterator = code.instructionIterator();
+    while (iterator.hasNext()) {
+      Instruction current = iterator.next();
+      if (current.isInvokeMethod()) {
+        DexMethod invokedMethod = current.asInvokeMethod().getInvokedMethod();
+        if (invokedMethod == dexItemFactory.assertionErrorMethods.initMessageAndCause) {
+          // Rewrite calls to new AssertionError(message, cause) to new AssertionError(message)
+          // and then initCause(cause).
+          List<Value> inValues = current.inValues();
+          assert inValues.size() == 3; // receiver, message, cause
+
+          // Remove cause from the constructor call
+          List<Value> newInitInValues = inValues.subList(0, 2);
+          iterator.replaceCurrentInstruction(
+              new InvokeDirect(dexItemFactory.assertionErrorMethods.initMessage, null,
+                  newInitInValues));
+
+          // On API 15 and older we cannot use initCause because of a bug in AssertionError.
+          if (options.canInitCauseAfterAssertionErrorObjectConstructor()) {
+            // Add a call to Throwable.initCause(cause)
+            List<Value> initCauseArguments = Arrays.asList(inValues.get(0), inValues.get(2));
+            InvokeVirtual initCause = new InvokeVirtual(dexItemFactory.throwableMethods.initCause,
+                code.createValue(TypeLatticeElement.SINGLE), initCauseArguments);
+            initCause.setPosition(current.getPosition());
+            iterator.add(initCause);
+          }
+        }
+      }
+    }
+    assert code.isConsistentSSA();
+  }
+
   /**
    * Remove moves that are not actually used by instructions in exiting paths. These moves can arise
    * due to debug local info needing a particular value and the live-interval for it then moves it
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 ab297fb..8bed04c 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -1048,6 +1048,20 @@
     return isGeneratingClassFiles() || hasMinApi(AndroidApiLevel.K);
   }
 
+  public boolean canUseAssertionErrorTwoArgumentConstructor() {
+    return isGeneratingClassFiles() || hasMinApi(AndroidApiLevel.K);
+  }
+
+  // The Apache Harmony-based AssertionError constructor which takes an Object on API 15 and older
+  // calls the Error supertype constructor with null as the exception cause. This prevents
+  // subsequent calls to initCause() because its implementation checks that cause==this before
+  // allowing a cause to be set.
+  //
+  // https://android.googlesource.com/platform/libcore/+/refs/heads/ics-mr1/luni/src/main/java/java/lang/AssertionError.java#56
+  public boolean canInitCauseAfterAssertionErrorObjectConstructor() {
+    return isGeneratingClassFiles() || hasMinApi(AndroidApiLevel.J);
+  }
+
   // Dalvik x86-atom backend had a bug that made it crash on filled-new-array instructions for
   // arrays of objects. This is unfortunate, since this never hits arm devices, but we have
   // to disallow filled-new-array of objects for dalvik until kitkat. The buggy code was
diff --git a/src/test/java/com/android/tools/r8/GenerateMainDexListTestBuilder.java b/src/test/java/com/android/tools/r8/GenerateMainDexListTestBuilder.java
index 39a7dc6..619f96f 100644
--- a/src/test/java/com/android/tools/r8/GenerateMainDexListTestBuilder.java
+++ b/src/test/java/com/android/tools/r8/GenerateMainDexListTestBuilder.java
@@ -40,7 +40,7 @@
   }
 
   @Override
-  public GenerateMainDexListRunResult run(TestRuntime runtime, String mainClass)
+  public GenerateMainDexListRunResult run(TestRuntime runtime, String mainClass, String... args)
       throws IOException, CompilationFailedException {
     throw new Unimplemented("No support for running with a main class");
   }
diff --git a/src/test/java/com/android/tools/r8/JvmTestBuilder.java b/src/test/java/com/android/tools/r8/JvmTestBuilder.java
index c4b8b62..c6a5026 100644
--- a/src/test/java/com/android/tools/r8/JvmTestBuilder.java
+++ b/src/test/java/com/android/tools/r8/JvmTestBuilder.java
@@ -9,6 +9,7 @@
 import com.android.tools.r8.errors.Unimplemented;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.FileUtils;
+import com.google.common.collect.ObjectArrays;
 import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
@@ -43,9 +44,11 @@
     return new JvmTestRunResult(builder.build(), result);
   }
 
-  public JvmTestRunResult run(TestRuntime runtime, String mainClass) throws IOException {
+  public JvmTestRunResult run(TestRuntime runtime, String mainClass, String... args)
+      throws IOException {
     assert runtime.isCf();
-    ProcessResult result = ToolHelper.runJava(runtime.asCf().getVm(), classpath, mainClass);
+    ProcessResult result =
+        ToolHelper.runJava(runtime.asCf().getVm(), classpath, ObjectArrays.concat(mainClass, args));
     return new JvmTestRunResult(builder.build(), result);
   }
 
diff --git a/src/test/java/com/android/tools/r8/TestBuilder.java b/src/test/java/com/android/tools/r8/TestBuilder.java
index ab6533f..46e3814 100644
--- a/src/test/java/com/android/tools/r8/TestBuilder.java
+++ b/src/test/java/com/android/tools/r8/TestBuilder.java
@@ -40,7 +40,7 @@
   public abstract RR run(String mainClass)
       throws CompilationFailedException, ExecutionException, IOException;
 
-  public abstract RR run(TestRuntime runtime, String mainClass)
+  public abstract RR run(TestRuntime runtime, String mainClass, String... args)
       throws CompilationFailedException, ExecutionException, IOException;
 
   @Deprecated
@@ -49,9 +49,9 @@
     return run(mainClass.getTypeName());
   }
 
-  public RR run(TestRuntime runtime, Class<?> mainClass)
+  public RR run(TestRuntime runtime, Class<?> mainClass, String... args)
       throws CompilationFailedException, ExecutionException, IOException {
-    return run(runtime, mainClass.getTypeName());
+    return run(runtime, mainClass.getTypeName(), args);
   }
 
   public abstract DebugTestConfig debugConfig();
diff --git a/src/test/java/com/android/tools/r8/TestCompilerBuilder.java b/src/test/java/com/android/tools/r8/TestCompilerBuilder.java
index 1ec471c..ef770d5 100644
--- a/src/test/java/com/android/tools/r8/TestCompilerBuilder.java
+++ b/src/test/java/com/android/tools/r8/TestCompilerBuilder.java
@@ -111,9 +111,9 @@
   }
 
   @Override
-  public RR run(TestRuntime runtime, String mainClass)
+  public RR run(TestRuntime runtime, String mainClass, String... args)
       throws CompilationFailedException, ExecutionException, IOException {
-    return compile().run(runtime, mainClass);
+    return compile().run(runtime, mainClass, args);
   }
 
   @Override
diff --git a/src/test/java/com/android/tools/r8/rewrite/assertionerror/AssertionErrorRewriteTest.java b/src/test/java/com/android/tools/r8/rewrite/assertionerror/AssertionErrorRewriteTest.java
new file mode 100644
index 0000000..b8ee931
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/rewrite/assertionerror/AssertionErrorRewriteTest.java
@@ -0,0 +1,87 @@
+// Copyright (c) 2019, 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.rewrite.assertionerror;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+import static com.android.tools.r8.ToolHelper.getDefaultAndroidJar;
+import static org.junit.Assume.assumeTrue;
+
+@RunWith(Parameterized.class)
+public class AssertionErrorRewriteTest extends TestBase {
+  @Parameters(name = "{0}")
+  public static Iterable<?> data() {
+    return getTestParameters().withAllRuntimes().build();
+  }
+
+  private final TestParameters parameters;
+  private final boolean expectCause;
+
+  public AssertionErrorRewriteTest(TestParameters parameters) {
+    this.parameters = parameters;
+
+    // The exception cause is only preserved on API 16 and newer.
+    expectCause = parameters.isCfRuntime()
+        || parameters.getRuntime().asDex().getMinApiLevel().getLevel() >= 16;
+  }
+
+  @Test public void d8() throws Exception {
+    assumeTrue(parameters.isDexRuntime());
+
+    testForD8()
+        .addLibraryFiles(getDefaultAndroidJar())
+        .addProgramClasses(Main.class)
+        .setMinApi(parameters.getRuntime())
+        .run(parameters.getRuntime(), Main.class, String.valueOf(expectCause))
+        .assertSuccessWithOutputLines("OK", "OK");
+  }
+
+  @Test public void r8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addLibraryFiles(getDefaultAndroidJar())
+        .addProgramClasses(Main.class)
+        .addKeepMainRule(Main.class)
+        .enableInliningAnnotations()
+        .setMinApi(parameters.getRuntime())
+        .run(parameters.getRuntime(), Main.class, String.valueOf(expectCause))
+        .assertSuccessWithOutputLines("OK", "OK");
+  }
+
+  public static final class Main {
+    public static void main(String[] args) {
+      boolean expectCause = Boolean.parseBoolean(args[0]);
+
+      Throwable expectedCause = new RuntimeException("cause message");
+      try {
+        throwAssertionError(expectedCause);
+        System.out.println("unreachable");
+      } catch (AssertionError e) {
+        String message = e.getMessage();
+        if (!message.equals("message")) {
+          throw new RuntimeException("Incorrect AssertionError message: " + message);
+        } else {
+          System.out.println("OK");
+        }
+
+        Throwable cause = e.getCause();
+        if (expectCause && cause != expectedCause) {
+          throw new RuntimeException("Incorrect AssertionError cause", cause);
+        } else {
+          System.out.println("OK");
+        }
+      }
+    }
+
+    @NeverInline
+    private static void throwAssertionError(Throwable cause) {
+      throw new AssertionError("message", cause);
+    }
+  }
+}