Optional desugaring
Bug: 134732760
Change-Id: I54bda03a0a532af310efb3ba544ce6df536ded22
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 d7240eb..10e75e8 100644
--- a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
+++ b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
@@ -217,6 +217,8 @@
public final DexString comparatorDescriptor = createString("Ljava/util/Comparator;");
public final DexString callableDescriptor = createString("Ljava/util/concurrent/Callable;");
public final DexString supplierDescriptor = createString("Ljava/util/function/Supplier;");
+ public final DexString consumerDescriptor = createString("Ljava/util/function/Consumer;");
+ public final DexString runnableDescriptor = createString("Ljava/lang/Runnable;");
public final DexString throwableDescriptor = createString(throwableDescriptorString);
public final DexString illegalAccessErrorDescriptor =
@@ -297,6 +299,8 @@
public final DexType comparatorType = createType(comparatorDescriptor);
public final DexType callableType = createType(callableDescriptor);
public final DexType supplierType = createType(supplierDescriptor);
+ public final DexType consumerType = createType(consumerDescriptor);
+ public final DexType runnableType = createType(runnableDescriptor);
public final DexType throwableType = createType(throwableDescriptor);
public final DexType illegalAccessErrorType = createType(illegalAccessErrorDescriptor);
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/BackportedMethodRewriter.java b/src/main/java/com/android/tools/r8/ir/desugar/BackportedMethodRewriter.java
index c74391d..b2bcbc4 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/BackportedMethodRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/BackportedMethodRewriter.java
@@ -39,6 +39,7 @@
import com.android.tools.r8.ir.desugar.backports.LongMethods;
import com.android.tools.r8.ir.desugar.backports.MathMethods;
import com.android.tools.r8.ir.desugar.backports.ObjectsMethods;
+import com.android.tools.r8.ir.desugar.backports.OptionalMethods;
import com.android.tools.r8.ir.desugar.backports.ShortMethods;
import com.android.tools.r8.ir.desugar.backports.StringMethods;
import com.android.tools.r8.ir.synthetic.TemplateMethodCode;
@@ -236,6 +237,15 @@
initializeAndroidOMethodProviders(factory);
}
+ if (options.rewritePrefix.containsKey("java.util.Optional")
+ || options.minApiLevel >= AndroidApiLevel.N.getLevel()) {
+ // These are currently not implemented at any API level in Android.
+ // They however require the Optional class to be present, either through
+ // desugared libraries or natively. If Optional class is not present,
+ // we do not desugar to avoid confusion in error messages.
+ initializeOptionalMethodProviders(factory);
+ }
+
// These are currently not implemented at any API level in Android.
initializeJava9MethodProviders(factory);
initializeJava11MethodProviders(factory);
@@ -851,6 +861,56 @@
new MethodGenerator(clazz, method, proto, CharacterMethods::new, "toStringCodepoint"));
}
+ private void initializeOptionalMethodProviders(DexItemFactory factory) {
+ // Optional
+ DexString clazz = factory.createString("Ljava/util/Optional;");
+ DexType optionalType = factory.createType(clazz);
+
+ // Optional.or(supplier)
+ DexString method = factory.createString("or");
+ DexProto proto = factory.createProto(optionalType, factory.supplierType);
+ addProvider(
+ new StatifyingMethodGenerator(
+ clazz, method, proto, OptionalMethods::new, "or", optionalType));
+
+ // Optional.stream()
+ method = factory.createString("stream");
+ proto = factory.createProto(factory.createType("Ljava/util/stream/Stream;"));
+ addProvider(
+ new StatifyingMethodGenerator(
+ clazz, method, proto, OptionalMethods::new, "stream", optionalType));
+
+ // Optional{void,Int,Long,Double}.ifPresentOrElse(consumer,runnable)
+ DexString[] optionalClasses =
+ new DexString[] {
+ clazz,
+ factory.createString("Ljava/util/OptionalDouble;"),
+ factory.createString("Ljava/util/OptionalLong;"),
+ factory.createString("Ljava/util/OptionalInt;")
+ };
+ DexType[] consumerClasses =
+ new DexType[] {
+ factory.consumerType,
+ factory.createType("Ljava/util/function/DoubleConsumer;"),
+ factory.createType("Ljava/util/function/LongConsumer;"),
+ factory.createType("Ljava/util/function/IntConsumer;")
+ };
+ for (int i = 0; i < optionalClasses.length; i++) {
+ clazz = optionalClasses[i];
+ DexType consumer = consumerClasses[i];
+ method = factory.createString("ifPresentOrElse");
+ proto = factory.createProto(factory.voidType, consumer, factory.runnableType);
+ addProvider(
+ new StatifyingMethodGenerator(
+ clazz,
+ method,
+ proto,
+ OptionalMethods::new,
+ "ifPresentOrElse",
+ factory.createType(clazz)));
+ }
+ }
+
private void warnMissingRetargetCoreLibraryMember(DexType type, AppView<?> appView) {
StringDiagnostic warning =
new StringDiagnostic(
@@ -973,7 +1033,7 @@
public static class MethodGenerator extends MethodProvider {
private final TemplateMethodFactory factory;
- private final String methodName;
+ final String methodName;
public MethodGenerator(
DexString clazz, DexString method, DexProto proto, TemplateMethodFactory factory) {
@@ -1027,6 +1087,36 @@
}
}
+ // Specific subclass to transform virtual methods into static desugared methods.
+ // To be correct, the method has to be on a final class, and be implemented directly
+ // on the class (no overrides).
+ public static class StatifyingMethodGenerator extends MethodGenerator {
+
+ private final DexType receiverType;
+
+ public StatifyingMethodGenerator(
+ DexString clazz,
+ DexString method,
+ DexProto proto,
+ TemplateMethodFactory factory,
+ String methodName,
+ DexType receiverType) {
+ super(clazz, method, proto, factory, methodName);
+ this.receiverType = receiverType;
+ }
+
+ @Override
+ public DexMethod provideMethod(AppView<?> appView) {
+ if (dexMethod != null) {
+ return dexMethod;
+ }
+ super.provideMethod(appView);
+ DexProto newProto = appView.dexItemFactory().prependTypeToProto(receiverType, proto);
+ dexMethod = appView.dexItemFactory().createMethod(dexMethod.holder, newProto, method);
+ return dexMethod;
+ }
+ }
+
private interface TemplateMethodFactory {
TemplateMethodCode create(InternalOptions options, DexMethod method, String name);
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/backports/OptionalMethods.java b/src/main/java/com/android/tools/r8/ir/desugar/backports/OptionalMethods.java
new file mode 100644
index 0000000..87e6053
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/desugar/backports/OptionalMethods.java
@@ -0,0 +1,83 @@
+// 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.ir.desugar.backports;
+
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.ir.synthetic.TemplateMethodCode;
+import com.android.tools.r8.utils.InternalOptions;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.OptionalDouble;
+import java.util.OptionalInt;
+import java.util.OptionalLong;
+import java.util.function.Consumer;
+import java.util.function.DoubleConsumer;
+import java.util.function.IntConsumer;
+import java.util.function.LongConsumer;
+import java.util.function.Supplier;
+import java.util.stream.Stream;
+
+public class OptionalMethods extends TemplateMethodCode {
+
+ public OptionalMethods(InternalOptions options, DexMethod method, String methodName) {
+ super(options, method, methodName, method.proto.toDescriptorString());
+ }
+
+ public static <T> Optional<T> or(
+ Optional<T> receiver, Supplier<? extends Optional<? extends T>> supplier) {
+ Objects.requireNonNull(supplier);
+ if (receiver.isPresent()) {
+ return receiver;
+ } else {
+ @SuppressWarnings("unchecked")
+ Optional<T> r = (Optional<T>) supplier.get();
+ return Objects.requireNonNull(r);
+ }
+ }
+
+ public static <T> void ifPresentOrElse(
+ Optional<T> receiver, Consumer<? super T> action, Runnable emptyAction) {
+ if (receiver.isPresent()) {
+ action.accept(receiver.get());
+ } else {
+ emptyAction.run();
+ }
+ }
+
+ public static void ifPresentOrElse(
+ OptionalInt receiver, IntConsumer action, Runnable emptyAction) {
+ if (receiver.isPresent()) {
+ action.accept(receiver.getAsInt());
+ } else {
+ emptyAction.run();
+ }
+ }
+
+ public static void ifPresentOrElse(
+ OptionalLong receiver, LongConsumer action, Runnable emptyAction) {
+ if (receiver.isPresent()) {
+ action.accept(receiver.getAsLong());
+ } else {
+ emptyAction.run();
+ }
+ }
+
+ public static void ifPresentOrElse(
+ OptionalDouble receiver, DoubleConsumer action, Runnable emptyAction) {
+ if (receiver.isPresent()) {
+ action.accept(receiver.getAsDouble());
+ } else {
+ emptyAction.run();
+ }
+ }
+
+ public static <T> Stream<T> stream(Optional<T> receiver) {
+ if (receiver.isPresent()) {
+ return Stream.of(receiver.get());
+ } else {
+ return Stream.empty();
+ }
+ }
+}
diff --git a/src/test/examplesJava9/backport/OptionalBackportJava9Main.java b/src/test/examplesJava9/backport/OptionalBackportJava9Main.java
new file mode 100644
index 0000000..b337875
--- /dev/null
+++ b/src/test/examplesJava9/backport/OptionalBackportJava9Main.java
@@ -0,0 +1,109 @@
+// 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 backport;
+
+import java.util.Optional;
+import java.util.OptionalDouble;
+import java.util.OptionalInt;
+import java.util.OptionalLong;
+
+public final class OptionalBackportJava9Main {
+
+ public static void main(String[] args) {
+ testOr();
+ testOrNull();
+ testIfPresentOrElse();
+ testIfPresentOrElseInt();
+ testIfPresentOrElseLong();
+ testIfPresentOrElseDouble();
+ testStream();
+ }
+
+ private static void testOr() {
+ Optional<String> value = Optional.of("value");
+ Optional<String> defaultValue = Optional.of("default");
+ Optional<String> emptyValue = Optional.empty();
+ Optional<String> result;
+
+ result = value.or(() -> defaultValue);
+ assertTrue(value == result);
+ result = emptyValue.or(() -> defaultValue);
+ assertTrue(result == defaultValue);
+ }
+
+ private static void testOrNull() {
+ Optional<String> value = Optional.of("value");
+ Optional<String> emptyValue = Optional.empty();
+
+ try {
+ value.or(null);
+ fail();
+ } catch (NullPointerException e) {
+ }
+
+ try {
+ emptyValue.or(null);
+ fail();
+ } catch (NullPointerException e) {
+ }
+
+ try {
+ value.or(() -> null);
+ } catch (NullPointerException e) {
+ fail();
+ }
+
+ try {
+ emptyValue.or(() -> null);
+ fail();
+ } catch (NullPointerException e) {
+ }
+ }
+
+ private static void testIfPresentOrElse() {
+ Optional<String> value = Optional.of("value");
+ Optional<String> emptyValue = Optional.empty();
+ value.ifPresentOrElse(val -> {}, () -> assertTrue(false));
+ emptyValue.ifPresentOrElse(val -> assertTrue(false), () -> {});
+ }
+
+ private static void testIfPresentOrElseInt() {
+ OptionalInt value = OptionalInt.of(1);
+ OptionalInt emptyValue = OptionalInt.empty();
+ value.ifPresentOrElse(val -> {}, () -> assertTrue(false));
+ emptyValue.ifPresentOrElse(val -> assertTrue(false), () -> {});
+ }
+
+ private static void testIfPresentOrElseLong() {
+ OptionalLong value = OptionalLong.of(1L);
+ OptionalLong emptyValue = OptionalLong.empty();
+ value.ifPresentOrElse(val -> {}, () -> assertTrue(false));
+ emptyValue.ifPresentOrElse(val -> assertTrue(false), () -> {});
+ }
+
+ private static void testIfPresentOrElseDouble() {
+ OptionalDouble value = OptionalDouble.of(1.0d);
+ OptionalDouble emptyValue = OptionalDouble.empty();
+ value.ifPresentOrElse(val -> {}, () -> assertTrue(false));
+ emptyValue.ifPresentOrElse(val -> assertTrue(false), () -> {});
+ }
+
+ private static void testStream() {
+ Optional<String> value = Optional.of("value");
+ Optional<String> emptyValue = Optional.empty();
+ assertTrue(value.stream().count() == 1);
+ assertTrue(emptyValue.stream().count() == 0);
+ }
+
+ private static void assertTrue(boolean value) {
+ if (!value) {
+ throw new AssertionError("Expected <true> but was <false>");
+ }
+ }
+
+ private static void fail() {
+ throw new AssertionError("Failure.");
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/backports/OptionalBackportJava9Test.java b/src/test/java/com/android/tools/r8/desugar/backports/OptionalBackportJava9Test.java
new file mode 100644
index 0000000..e6105d3
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/backports/OptionalBackportJava9Test.java
@@ -0,0 +1,35 @@
+// 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.desugar.backports;
+
+import static com.android.tools.r8.utils.FileUtils.JAR_EXTENSION;
+
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestRuntime.CfVm;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.ToolHelper.DexVm.Version;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public final class OptionalBackportJava9Test extends AbstractBackportTest {
+ @Parameters(name = "{0}")
+ public static Iterable<?> data() {
+ return getTestParameters()
+ .withDexRuntimesStartingFromIncluding(Version.V7_0_0)
+ .withCfRuntimesStartingFromIncluding(CfVm.JDK9)
+ .build();
+ }
+
+ private static final Path TEST_JAR =
+ Paths.get(ToolHelper.EXAMPLES_JAVA9_BUILD_DIR).resolve("backport" + JAR_EXTENSION);
+
+ public OptionalBackportJava9Test(TestParameters parameters) {
+ super(parameters, Short.class, TEST_JAR, "backport.OptionalBackportJava9Main");
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/corelib/JavaUtilOptionalTest.java b/src/test/java/com/android/tools/r8/desugar/corelib/JavaUtilOptionalTest.java
index 9465aee..46f6418 100644
--- a/src/test/java/com/android/tools/r8/desugar/corelib/JavaUtilOptionalTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/corelib/JavaUtilOptionalTest.java
@@ -4,6 +4,7 @@
package com.android.tools.r8.desugar.corelib;
+import static com.android.tools.r8.utils.FileUtils.JAR_EXTENSION;
import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.assertTrue;
@@ -17,6 +18,7 @@
import com.android.tools.r8.utils.codeinspector.CodeInspector;
import com.android.tools.r8.utils.codeinspector.InstructionSubject;
import com.android.tools.r8.utils.codeinspector.InvokeInstructionSubject;
+import java.nio.file.Paths;
import java.util.Iterator;
import java.util.Optional;
import java.util.OptionalDouble;
@@ -88,6 +90,20 @@
.assertSuccessWithOutput(expectedOutput);
}
+ @Test
+ public void testJavaOptionalJava9() throws Exception {
+ testForD8()
+ .addProgramFiles(
+ Paths.get(ToolHelper.EXAMPLES_JAVA9_BUILD_DIR).resolve("backport" + JAR_EXTENSION))
+ .addLibraryFiles(ToolHelper.getAndroidJar(AndroidApiLevel.P))
+ .setMinApi(parameters.getApiLevel())
+ .enableCoreLibraryDesugaring(parameters.getApiLevel())
+ .compile()
+ .addRunClasspathFiles(buildDesugaredLibrary(parameters.getApiLevel()))
+ .run(parameters.getRuntime(), "backport.OptionalBackportJava9Main")
+ .assertSuccess();
+ }
+
static class TestClass {
public static void main(String[] args) {