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);
}