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) {