Desugaring of try-with-resources.

Adds desugaring of try-with-resources for pre-19 min-sdk
version, it essentially boils down to removing all calls to
Throwable::addSuppressed(...) and replacing all calls to
Throwable::getSuppressed() with empty array of Throwable.

The desugaring requires library and class path since it needs
to inspect/check exception class hierarchy. Disabling it by
default until we measure performance impact.

Bug:37744723

BUG=

Change-Id: If2781ec68cab3a2b38127ee20fffb94ceac3157a
diff --git a/src/test/examplesAndroidO/trywithresources/TryWithResources.java b/src/test/examplesAndroidO/trywithresources/TryWithResources.java
new file mode 100644
index 0000000..ac779d6
--- /dev/null
+++ b/src/test/examplesAndroidO/trywithresources/TryWithResources.java
@@ -0,0 +1,237 @@
+// 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 trywithresources;
+
+import java.io.Closeable;
+import java.io.IOException;
+
+public abstract class TryWithResources {
+  // --- TEST SUPPORT ---
+
+  interface Test {
+    void test() throws Throwable;
+  }
+
+  private void test(Test test) {
+    try {
+      test.test();
+    } catch (Throwable e) {
+      dumpException(e);
+    }
+  }
+
+  private void dumpException(Throwable e) {
+    dumpException(e, "Exception: ");
+  }
+
+  private void dumpException(Throwable e, String indent) {
+    assert e != null;
+    System.out.println(indent + e.getMessage());
+
+    indent = indent.replaceAll("[^:]", " ");
+
+    Throwable cause = e.getCause();
+    if (cause != null) {
+      dumpException(cause, indent + "  cause: ");
+    }
+
+    // Dump suppressed UNLESS it is a desugared code running
+    // on JVM, in which case we avoid dumping suppressed, since
+    // the output will be used for comparison with desugared code
+    // running on device.
+    if (!desugaredCodeRunningOnJvm()) {
+      Throwable[] suppressed = e.getSuppressed();
+      for (int i = 0; i < suppressed.length; i++) {
+        dumpException(suppressed[i], indent + "supp[" + i + "]: ");
+      }
+    }
+  }
+
+  abstract boolean desugaredCodeRunningOnJvm();
+
+  // --- TEST SYMBOLS ---
+
+  static class Resource implements Closeable {
+    final String tag;
+
+    Resource(String tag) {
+      this.tag = tag;
+    }
+
+    @Override
+    public void close() throws IOException {
+      Class<? extends Resource> cls = this.getClass();
+      System.out.println("Closing " + tag + " (" +
+          cls.getName().substring(TryWithResources.class.getName().length() + 1) + ")");
+    }
+  }
+
+  // --- TEST ---
+
+  class RegularTryWithResources {
+    class RegularResource extends Resource {
+      RegularResource(String tag) {
+        super(tag);
+      }
+    }
+
+    private void test() throws Throwable {
+      test(2);
+    }
+
+    private void test(int level) throws Throwable {
+      try (RegularResource a = new RegularResource("a" + level);
+           RegularResource b = new RegularResource("b" + level)) {
+        if (level > 0) {
+          try {
+            test(level - 1);
+          } catch (Throwable e) {
+            throw new RuntimeException("e" + level, e);
+          }
+        }
+        throw new RuntimeException("primary cause");
+      }
+    }
+  }
+
+  // --- TEST ---
+
+  class FailingTryWithResources {
+    class FailingResource extends Resource {
+      FailingResource(String tag) {
+        super(tag);
+      }
+
+      @Override
+      public void close() throws IOException {
+        super.close();
+        throw new RuntimeException("failed to close '" + tag + "'");
+      }
+    }
+
+    private void test() throws Throwable {
+      test(2);
+    }
+
+    private void test(int level) throws Throwable {
+      try (FailingResource a = new FailingResource("a" + level);
+           FailingResource b = new FailingResource("b" + level)) {
+        if (level > 0) {
+          try {
+            test(level - 1);
+          } catch (Throwable e) {
+            throw new RuntimeException("e" + level, e);
+          }
+        }
+        throw new RuntimeException("primary cause");
+      }
+    }
+  }
+
+  // --- TEST ---
+
+  class ExplicitAddGetSuppressed {
+    class RegularResource extends Resource {
+      RegularResource(String tag) {
+        super(tag);
+      }
+
+      @Override
+      public void close() throws IOException {
+        super.close();
+        throw new RuntimeException("failed to close '" + tag + "'");
+      }
+    }
+
+    private void test() throws Throwable {
+      test(2);
+    }
+
+    private void test(int level) throws RuntimeException {
+      try (RegularResource a = new RegularResource("a" + level);
+           RegularResource b = new RegularResource("b" + level)) {
+        if (level > 0) {
+          try {
+            test(level - 1);
+          } catch (RuntimeException e) {
+            // Just collect suppressed, but throw away the exception.
+            RuntimeException re = new RuntimeException("e" + level);
+            for (Throwable suppressed : e.getSuppressed()) {
+              re.addSuppressed(suppressed);
+            }
+            throw re;
+          }
+        }
+        throw new RuntimeException("primary cause");
+      } catch (IOException e) {
+        throw new RuntimeException(e);
+      }
+    }
+  }
+
+  // --- TEST ---
+
+  interface Consumer {
+    void act(RuntimeException re);
+  }
+
+  interface Supplier {
+    Throwable[] get();
+  }
+
+  class AddGetSuppressedRoundTrip {
+    private void test() throws Throwable {
+      RuntimeException carrier = new RuntimeException("carrier");
+      Consumer packer = carrier::addSuppressed;
+      Supplier unpacker = carrier::getSuppressed;
+
+      packer.act(new RuntimeException("original exception A"));
+      packer.act(new RuntimeException("original exception Z"));
+
+      for (Throwable unpacked : unpacker.get()) {
+        if (!desugaredCodeRunningOnJvm()) {
+          dumpException(unpacked);
+        }
+      }
+    }
+  }
+
+  // --- TEST ---
+
+  class UnreachableCatchAfterCallsRemoved {
+    private void test() throws Throwable {
+      RuntimeException main = new RuntimeException("main");
+      RuntimeException origA = new RuntimeException("original exception A");
+      RuntimeException origB = new RuntimeException("original exception Z");
+
+      try {
+        // After both calls below are removed, the whole catch
+        // handler should be removed.
+        main.addSuppressed(origA);
+        main.addSuppressed(origB);
+      } catch (Throwable t) {
+        throw new RuntimeException("UNREACHABLE");
+      }
+
+      // Return value not used.
+      main.getSuppressed();
+    }
+  }
+
+  // --- MAIN TEST ---
+
+  void test() throws Exception {
+    System.out.println("----- TEST 1 -----");
+    test(new RegularTryWithResources()::test);
+    System.out.println("----- TEST 2 -----");
+    test(new FailingTryWithResources()::test);
+    System.out.println("----- TEST 3 -----");
+    test(new ExplicitAddGetSuppressed()::test);
+    System.out.println("----- TEST 4 -----");
+    test(new AddGetSuppressedRoundTrip()::test);
+    System.out.println("----- TEST 5 -----");
+    test(new UnreachableCatchAfterCallsRemoved()::test);
+    System.out.println("------------------");
+  }
+}
diff --git a/src/test/examplesAndroidO/trywithresources/TryWithResourcesDesugaredTests.java b/src/test/examplesAndroidO/trywithresources/TryWithResourcesDesugaredTests.java
new file mode 100644
index 0000000..10a96f9
--- /dev/null
+++ b/src/test/examplesAndroidO/trywithresources/TryWithResourcesDesugaredTests.java
@@ -0,0 +1,24 @@
+// 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 trywithresources;
+
+public class TryWithResourcesDesugaredTests extends TryWithResources {
+  private boolean isAndroid() {
+    try {
+      Class.forName("dalvik.system.VMRuntime");
+      return true;
+    } catch (Exception ignored) {
+    }
+    return false;
+  }
+
+  @Override
+  boolean desugaredCodeRunningOnJvm() {
+    return !isAndroid();
+  }
+
+  public static void main(String[] args) throws Exception {
+    new TryWithResourcesDesugaredTests().test();
+  }
+}
diff --git a/src/test/examplesAndroidO/trywithresources/TryWithResourcesNotDesugaredTests.java b/src/test/examplesAndroidO/trywithresources/TryWithResourcesNotDesugaredTests.java
new file mode 100644
index 0000000..d2a05c3
--- /dev/null
+++ b/src/test/examplesAndroidO/trywithresources/TryWithResourcesNotDesugaredTests.java
@@ -0,0 +1,15 @@
+// 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 trywithresources;
+
+public class TryWithResourcesNotDesugaredTests extends TryWithResources {
+  @Override
+  boolean desugaredCodeRunningOnJvm() {
+    return false;
+  }
+
+  public static void main(String[] args) throws Exception {
+    new TryWithResourcesNotDesugaredTests().test();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/RunExamplesAndroidOTest.java b/src/test/java/com/android/tools/r8/RunExamplesAndroidOTest.java
index 3f63319..457e7f4 100644
--- a/src/test/java/com/android/tools/r8/RunExamplesAndroidOTest.java
+++ b/src/test/java/com/android/tools/r8/RunExamplesAndroidOTest.java
@@ -13,6 +13,11 @@
 
 import com.android.tools.r8.ToolHelper.DexVm;
 import com.android.tools.r8.errors.CompilationError;
+import com.android.tools.r8.utils.DexInspector;
+import com.android.tools.r8.utils.DexInspector.FoundClassSubject;
+import com.android.tools.r8.utils.DexInspector.FoundMethodSubject;
+import com.android.tools.r8.utils.DexInspector.InstructionSubject;
+import com.android.tools.r8.utils.DexInspector.InvokeInstructionSubject;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.OffOrAuto;
 import com.google.common.collect.ImmutableList;
@@ -20,10 +25,13 @@
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.ArrayList;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.function.Consumer;
+import java.util.function.Predicate;
 import java.util.function.UnaryOperator;
+import org.junit.Assert;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.ExpectedException;
@@ -38,6 +46,7 @@
     final String mainClass;
 
     final List<Consumer<InternalOptions>> optionConsumers = new ArrayList<>();
+    final List<Consumer<DexInspector>> dexInspectorChecks = new ArrayList<>();
     final List<UnaryOperator<B>> builderTransformations = new ArrayList<>();
 
     TestRunner(String testName, String packageName, String mainClass) {
@@ -46,6 +55,35 @@
       this.mainClass = mainClass;
     }
 
+    TestRunner withDexCheck(Consumer<DexInspector> check) {
+      dexInspectorChecks.add(check);
+      return this;
+    }
+
+    TestRunner withClassCheck(Consumer<FoundClassSubject> check) {
+      withDexCheck(inspector -> inspector.forAllClasses(check));
+      return this;
+    }
+
+    TestRunner withMethodCheck(Consumer<FoundMethodSubject> check) {
+      withClassCheck(clazz -> clazz.forAllMethods(check));
+      return this;
+    }
+
+    <T extends InstructionSubject> TestRunner
+    withInstructionCheck(Predicate<InstructionSubject> filter, Consumer<T> check) {
+      withMethodCheck(method -> {
+        if (method.isAbstract()) {
+          return;
+        }
+        Iterator<T> iterator = method.iterateInstructions(filter);
+        while (iterator.hasNext()) {
+          check.accept(iterator.next());
+        }
+      });
+      return this;
+    }
+
     TestRunner withOptionConsumer(Consumer<InternalOptions> consumer) {
       optionConsumers.add(consumer);
       return this;
@@ -55,6 +93,10 @@
       return withOptionConsumer(o -> o.interfaceMethodDesugaring = behavior);
     }
 
+    TestRunner withTryWithResourcesDesugaring(OffOrAuto behavior) {
+      return withOptionConsumer(o -> o.tryWithResourcesDesugaring = behavior);
+    }
+
     TestRunner withBuilderTransformation(UnaryOperator<B> builderTransformation) {
       builderTransformations.add(builderTransformation);
       return this;
@@ -86,6 +128,13 @@
         thrown.expect(Throwable.class);
       }
 
+      if (!dexInspectorChecks.isEmpty()) {
+        DexInspector inspector = new DexInspector(out);
+        for (Consumer<DexInspector> check : dexInspectorChecks) {
+          check.accept(inspector);
+        }
+      }
+
       String output = ToolHelper.runArtNoVerificationErrors(out.toString(), qualifiedMainClass);
       if (!expectedToFail) {
         ToolHelper.ProcessResult javaResult =
@@ -238,5 +287,24 @@
     test("repeat_annotations", "repeat_annotations", "RepeatAnnotations").run();
   }
 
+  @Test
+  public void testTryWithResources() throws Throwable {
+    test("try-with-resources-simplified", "trywithresources", "TryWithResourcesNotDesugaredTests")
+        .withTryWithResourcesDesugaring(OffOrAuto.Off)
+        .run();
+  }
+
+  @Test
+  public void testTryWithResourcesDesugared() throws Throwable {
+    test("try-with-resources-simplified", "trywithresources", "TryWithResourcesDesugaredTests")
+        .withTryWithResourcesDesugaring(OffOrAuto.Auto)
+        .withInstructionCheck(InstructionSubject::isInvoke,
+            (InvokeInstructionSubject invoke) -> {
+              Assert.assertFalse(invoke.invokedMethod().name.toString().equals("addSuppressed"));
+              Assert.assertFalse(invoke.invokedMethod().name.toString().equals("getSuppressed"));
+            })
+        .run();
+  }
+
   abstract TestRunner test(String testName, String packageName, String mainClass);
 }