Support maintain_prefix for function types

- Enqueuer support with multiple definition of the same class
- Revert change to alternative_3 file (so it can be released to google3)

Bug: 222647019
Change-Id: I2417b1dff182c401e9482e72db197ec90740bf3f
diff --git a/src/library_desugar/jdk11/desugar_jdk_libs_minimal.json b/src/library_desugar/jdk11/desugar_jdk_libs_minimal.json
new file mode 100644
index 0000000..73fa330
--- /dev/null
+++ b/src/library_desugar/jdk11/desugar_jdk_libs_minimal.json
@@ -0,0 +1,24 @@
+{
+  "identifier": "com.tools.android:desugar_jdk_libs_minimal:2.0.0",
+  "configuration_format_version": 100,
+  "required_compilation_api_level": 24,
+  "synthesized_library_classes_package_prefix": "j$.",
+  "support_all_callbacks_from_library": false,
+  "common_flags": [
+    {
+      "api_level_below_or_equal": 23,
+      "maintain_prefix": [
+        "java.util.function.",
+        "java.util.Optional"
+      ]
+    }
+  ],
+  "program_flags": [],
+  "library_flags": [],
+  "shrinker_config": [
+    "-keeppackagenames java.**",
+    "-keepattributes Signature",
+    "-keepattributes EnclosingMethod",
+    "-keepattributes InnerClasses"
+  ]
+}
\ No newline at end of file
diff --git a/src/main/java/com/android/tools/r8/R8Command.java b/src/main/java/com/android/tools/r8/R8Command.java
index f259f26..8d8b855 100644
--- a/src/main/java/com/android/tools/r8/R8Command.java
+++ b/src/main/java/com/android/tools/r8/R8Command.java
@@ -983,6 +983,8 @@
 
     internal.setDesugaredLibrarySpecification(desugaredLibrarySpecification);
     internal.synthesizedClassPrefix = synthesizedClassPrefix;
+    // TODO(b/214382176): Enable all the time.
+    internal.loadAllClassDefinitions = !synthesizedClassPrefix.isEmpty();
     internal.desugaredLibraryKeepRuleConsumer = desugaredLibraryKeepRuleConsumer;
 
     // Set up the map and source file providers.
diff --git a/src/main/java/com/android/tools/r8/dex/CodeToKeep.java b/src/main/java/com/android/tools/r8/dex/CodeToKeep.java
index 7bfd947..6b77d5b 100644
--- a/src/main/java/com/android/tools/r8/dex/CodeToKeep.java
+++ b/src/main/java/com/android/tools/r8/dex/CodeToKeep.java
@@ -11,6 +11,7 @@
 import com.android.tools.r8.graph.DexReference;
 import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.desugar.itf.InterfaceDesugaringSyntheticHelper;
 import com.android.tools.r8.naming.NamingLens;
 import com.android.tools.r8.utils.CollectionUtils;
 import com.android.tools.r8.utils.DescriptorUtils;
@@ -66,17 +67,26 @@
       this.options = options;
     }
 
-    private boolean shouldKeep(DexType type) {
-      return namingLens.prefixRewrittenType(type) != null
-          || options.machineDesugaredLibrarySpecification.getMaintainType().contains(type)
-          || options.machineDesugaredLibrarySpecification.isCustomConversionRewrittenType(type)
-          || options.machineDesugaredLibrarySpecification.isEmulatedInterfaceRewrittenType(type)
+    private boolean shouldKeep(DexType givenType) {
+      if (namingLens.prefixRewrittenType(givenType) != null
+          || options.machineDesugaredLibrarySpecification.isCustomConversionRewrittenType(givenType)
+          || options.machineDesugaredLibrarySpecification.isEmulatedInterfaceRewrittenType(
+              givenType)
           // TODO(b/158632510): This should prefix match on DexString.
-          || type.toDescriptorString()
+          || givenType
+              .toDescriptorString()
               .startsWith(
                   "L"
                       + options.machineDesugaredLibrarySpecification
-                          .getSynthesizedLibraryClassesPackagePrefix());
+                          .getSynthesizedLibraryClassesPackagePrefix())) {
+        return true;
+      }
+      DexType type =
+          InterfaceDesugaringSyntheticHelper.isCompanionClassType(givenType)
+              ? InterfaceDesugaringSyntheticHelper.getInterfaceClassType(
+                  givenType, options.dexItemFactory())
+              : givenType;
+      return options.machineDesugaredLibrarySpecification.getMaintainType().contains(type);
     }
 
     @Override
diff --git a/src/main/java/com/android/tools/r8/graph/ClassResolutionResult.java b/src/main/java/com/android/tools/r8/graph/ClassResolutionResult.java
index 4eca4e1..f11571a 100644
--- a/src/main/java/com/android/tools/r8/graph/ClassResolutionResult.java
+++ b/src/main/java/com/android/tools/r8/graph/ClassResolutionResult.java
@@ -14,6 +14,11 @@
 
   DexClass toSingleClassWithProgramOverLibrary();
 
+  // The alternative class is:
+  // - the other class than the single class if the resolution resolves into multiple classes,
+  // - null if the resolution resolves into a single class.
+  DexClass toAlternativeClassWithProgramOverLibrary();
+
   void forEachClassResolutionResult(Consumer<DexClass> consumer);
 
   static Builder builder() {
@@ -86,6 +91,11 @@
     }
 
     @Override
+    public DexClass toAlternativeClassWithProgramOverLibrary() {
+      return null;
+    }
+
+    @Override
     public void forEachClassResolutionResult(Consumer<DexClass> consumer) {
       // Intentionally empty
     }
@@ -126,6 +136,11 @@
     public DexClass toSingleClassWithProgramOverLibrary() {
       return programOrClasspathClass;
     }
+
+    @Override
+    public DexClass toAlternativeClassWithProgramOverLibrary() {
+      return libraryClass;
+    }
   }
 
   class ClasspathAndLibraryClassResolutionResult
@@ -140,5 +155,10 @@
     public DexClass toSingleClassWithProgramOverLibrary() {
       return libraryClass;
     }
+
+    @Override
+    public DexClass toAlternativeClassWithProgramOverLibrary() {
+      return programOrClasspathClass;
+    }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/graph/DexClass.java b/src/main/java/com/android/tools/r8/graph/DexClass.java
index 4fac278..a615b1f 100644
--- a/src/main/java/com/android/tools/r8/graph/DexClass.java
+++ b/src/main/java/com/android/tools/r8/graph/DexClass.java
@@ -150,6 +150,11 @@
     return this;
   }
 
+  @Override
+  public DexClass toAlternativeClassWithProgramOverLibrary() {
+    return null;
+  }
+
   public abstract void accept(
       Consumer<DexProgramClass> programClassConsumer,
       Consumer<DexClasspathClass> classpathClassConsumer,
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/TrivialFieldAccessReprocessor.java b/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/TrivialFieldAccessReprocessor.java
index d6ca793..d2b1d3b 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/TrivialFieldAccessReprocessor.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/TrivialFieldAccessReprocessor.java
@@ -331,8 +331,8 @@
 
       if (definition.isStatic() != isStatic
           || appView.isCfByteCodePassThrough(getContext().getDefinition())
-          || resolutionResult.isAccessibleFrom(getContext(), appView).isPossiblyFalse()
           || !resolutionResult.isSingleProgramFieldResolutionResult()
+          || resolutionResult.isAccessibleFrom(getContext(), appView).isPossiblyFalse()
           || appView.appInfo().isNeverReprocessMethod(getContext())) {
         recordAccessThatCannotBeOptimized(field, definition);
         return;
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 eb5a30b..c039f9d 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
@@ -249,7 +249,7 @@
               return;
             }
             AndroidApiLevel theApi = apiLevel.asKnownApiLevel().getApiLevel();
-            if (appView.typeRewriter.hasRewrittenType(type, appView)) {
+            if (typeIsInDesugaredLibrary(type)) {
               assert theApi.equals(appView.options().getMinApiLevel());
               return;
             }
@@ -258,6 +258,15 @@
       return true;
     }
 
+    private boolean typeIsInDesugaredLibrary(DexType type) {
+      return appView.typeRewriter.hasRewrittenType(type, appView)
+          || appView
+              .options()
+              .machineDesugaredLibrarySpecification
+              .getMaintainType()
+              .contains(type);
+    }
+
     private boolean typeIsAbsentOrPresentWithoutBackportsFrom(
         DexType type, AndroidApiLevel apiLevel) {
       return !typeIsPresent(type) || typeIsPresentWithoutBackportsFrom(type, apiLevel);
@@ -268,7 +277,7 @@
     }
 
     private boolean typeIsPresentWithoutBackportsFrom(DexType type, AndroidApiLevel methodsMinAPI) {
-      if (appView.typeRewriter.hasRewrittenType(type, appView)) {
+      if (typeIsInDesugaredLibrary(type)) {
         // Desugared library is enabled, the methods are present if desugared library specifies it.
         return methodsMinAPI.isGreaterThan(AndroidApiLevel.N)
             && !appView.options().machineDesugaredLibrarySpecification.includesJDK11Methods();
@@ -284,7 +293,7 @@
     private boolean typeIsPresent(DexType type) {
       // TODO(b/224954240): Always use the apiDatabase when always available.
       return appView.options().getMinApiLevel().isGreaterThanOrEqualTo(typeMinApi.get(type))
-          || appView.typeRewriter.hasRewrittenType(type, appView);
+          || typeIsInDesugaredLibrary(type);
     }
 
     boolean isEmpty() {
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/humanspecification/HumanRewritingFlags.java b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/humanspecification/HumanRewritingFlags.java
index b0c7920..50e98e1 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/humanspecification/HumanRewritingFlags.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/humanspecification/HumanRewritingFlags.java
@@ -172,6 +172,7 @@
   public boolean isEmpty() {
     return rewritePrefix.isEmpty()
         && rewriteDerivedPrefix.isEmpty()
+        && maintainPrefix.isEmpty()
         && emulatedInterfaces.isEmpty()
         && retargetMethod.isEmpty()
         && retargetMethodEmulatedDispatch.isEmpty()
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/machinespecification/MachineDesugaredLibrarySpecification.java b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/machinespecification/MachineDesugaredLibrarySpecification.java
index d974c56..0fafbac 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/machinespecification/MachineDesugaredLibrarySpecification.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/machinespecification/MachineDesugaredLibrarySpecification.java
@@ -52,6 +52,10 @@
     this.rewritingFlags = rewritingFlags;
   }
 
+  public boolean isEmpty() {
+    return rewritingFlags.isEmpty();
+  }
+
   public boolean isLibraryCompilation() {
     return libraryCompilation;
   }
@@ -167,7 +171,10 @@
 
   public boolean isSupported(DexReference reference) {
     // Support through type rewriting.
-    if (rewritingFlags.getRewriteType().containsKey(reference.getContextType())) {
+    if (getRewriteType().containsKey(reference.getContextType())) {
+      return true;
+    }
+    if (getMaintainType().contains(reference.getContextType())) {
       return true;
     }
     if (!reference.isDexMethod()) {
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/machinespecification/MachineRewritingFlags.java b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/machinespecification/MachineRewritingFlags.java
index dcaa744..1a301da 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/machinespecification/MachineRewritingFlags.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/machinespecification/MachineRewritingFlags.java
@@ -196,6 +196,15 @@
     return emulatedInterfaces.get(method.getHolderType()).getEmulatedMethods().get(method);
   }
 
+  public boolean isEmpty() {
+    return rewriteType.isEmpty()
+        && maintainType.isEmpty()
+        && rewriteDerivedTypeOnly.isEmpty()
+        && !hasRetargeting()
+        && emulatedInterfaces.isEmpty()
+        && legacyBackport.isEmpty();
+  }
+
   public static class Builder {
 
     Builder() {}
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/specificationconversion/LibraryValidator.java b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/specificationconversion/LibraryValidator.java
index ef19eb6..567cc79 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/specificationconversion/LibraryValidator.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/specificationconversion/LibraryValidator.java
@@ -26,6 +26,8 @@
       levelType = app.dexItemFactory.createType("Ljava/time/LocalTime;");
     } else if (requiredCompilationAPILevel.isEqualTo(AndroidApiLevel.R)) {
       levelType = app.dexItemFactory.createType("Ljava/util/concurrent/Flow;");
+    } else if (requiredCompilationAPILevel.isEqualTo(AndroidApiLevel.N)) {
+      levelType = app.dexItemFactory.createType("Ljava/util/function/Supplier;");
     } else {
       app.options.reporter.warning(
           "Unsupported requiredCompilationAPILevel: " + requiredCompilationAPILevel);
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/itf/ClassProcessor.java b/src/main/java/com/android/tools/r8/ir/desugar/itf/ClassProcessor.java
index 4446275..266b1c5 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/itf/ClassProcessor.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/itf/ClassProcessor.java
@@ -377,7 +377,8 @@
     this.dexItemFactory = appView.dexItemFactory();
     this.helper = new InterfaceDesugaringSyntheticHelper(appView);
     needsLibraryInfo =
-        appView.options().machineDesugaredLibrarySpecification.hasEmulatedInterfaces();
+        !appView.options().canUseDefaultAndStaticInterfaceMethods()
+            && !appView.options().machineDesugaredLibrarySpecification.isEmpty();
     this.isLiveMethod = isLiveMethod;
   }
 
diff --git a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
index 7d54b09..a26f167 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -27,6 +27,7 @@
 import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.ClassDefinition;
+import com.android.tools.r8.graph.ClassResolutionResult;
 import com.android.tools.r8.graph.ClasspathOrLibraryClass;
 import com.android.tools.r8.graph.ClasspathOrLibraryDefinition;
 import com.android.tools.r8.graph.Definition;
@@ -160,6 +161,7 @@
 import com.google.common.collect.Sets.SetView;
 import it.unimi.dsi.fastutil.objects.Object2BooleanArrayMap;
 import it.unimi.dsi.fastutil.objects.Object2BooleanMap;
+import java.lang.reflect.InvocationHandler;
 import java.util.ArrayDeque;
 import java.util.ArrayList;
 import java.util.Collections;
@@ -667,26 +669,39 @@
     return definitionFor(type, context, this::recordNonProgramClass, this::reportMissingClass);
   }
 
+  public boolean hasAlternativeLibraryDefinition(DexProgramClass programClass) {
+    ClassResolutionResult classResolutionResult =
+        internalDefinitionFor(
+            programClass.type, programClass, this::recordNonProgramClass, this::reportMissingClass);
+    assert classResolutionResult.hasClassResolutionResult();
+    DexClass alternativeClass = classResolutionResult.toAlternativeClassWithProgramOverLibrary();
+    assert alternativeClass == null || alternativeClass.isLibraryClass();
+    return alternativeClass != null;
+  }
+
   private DexClass definitionFor(
       DexType type,
       ProgramDerivedContext context,
       BiConsumer<DexClass, ProgramDerivedContext> foundClassConsumer,
       BiConsumer<DexType, ProgramDerivedContext> missingClassConsumer) {
-    return internalDefinitionFor(type, context, foundClassConsumer, missingClassConsumer);
+    return internalDefinitionFor(type, context, foundClassConsumer, missingClassConsumer)
+        .toSingleClassWithProgramOverLibrary();
   }
 
-  private DexClass internalDefinitionFor(
+  private ClassResolutionResult internalDefinitionFor(
       DexType type,
       ProgramDerivedContext context,
       BiConsumer<DexClass, ProgramDerivedContext> foundClassConsumer,
       BiConsumer<DexType, ProgramDerivedContext> missingClassConsumer) {
-    DexClass clazz = appInfo().definitionFor(type);
-    if (clazz == null) {
+    ClassResolutionResult classResolutionResult =
+        appInfo().contextIndependentDefinitionForWithResolutionResult(type);
+    if (classResolutionResult.hasClassResolutionResult()) {
+      classResolutionResult.forEachClassResolutionResult(
+          clazz -> foundClassConsumer.accept(clazz, context));
+    } else {
       missingClassConsumer.accept(type, context);
-      return null;
     }
-    foundClassConsumer.accept(clazz, context);
-    return clazz;
+    return classResolutionResult;
   }
 
   public FieldAccessInfoCollectionImpl getFieldAccessInfoCollection() {
@@ -783,12 +798,18 @@
     if (!type.isClassType()) {
       return;
     }
-    DexClass clazz = appView.definitionFor(type);
-    if (clazz == null) {
+    ClassResolutionResult classResolutionResult =
+        appView.contextIndependentDefinitionForWithResolutionResult(type);
+    if (!classResolutionResult.hasClassResolutionResult()) {
       missingClassConsumer.accept(type, context);
-    } else if (!clazz.isProgramClass()) {
-      classAdder.accept(clazz.asClasspathOrLibraryClass());
+      return;
     }
+    classResolutionResult.forEachClassResolutionResult(
+        clazz -> {
+          if (!clazz.isProgramClass()) {
+            classAdder.accept(clazz.asClasspathOrLibraryClass());
+          }
+        });
   }
 
   private DexProgramClass getProgramClassOrNull(DexType type, ProgramDefinition context) {
@@ -2086,6 +2107,9 @@
     // Update keep info.
     applyMinimumKeepInfo(clazz);
     applyMinimumKeepInfoDependentOn(new LiveClassEnqueuerEvent(clazz));
+    if (hasAlternativeLibraryDefinition(clazz)) {
+      getKeepInfo().keepClass(clazz);
+    }
 
     processAnnotations(clazz);
 
@@ -2472,6 +2496,16 @@
     if (clazz == null) {
       return;
     }
+    DexClass alternativeResolutionResult =
+        appInfo()
+            .contextIndependentDefinitionForWithResolutionResult(type)
+            .toAlternativeClassWithProgramOverLibrary();
+    if (alternativeResolutionResult != null && alternativeResolutionResult.isLibraryClass()) {
+      // We are in a situation where a library class inherits from a library class, which has a
+      // program class duplicated version for low API levels.
+      recordNonProgramClass(alternativeResolutionResult, clazz);
+      return;
+    }
     if (forceProguardCompatibility) {
       // To ensure that the program works correctly we have to pin all super types and members
       // in the tree.
@@ -3000,6 +3034,9 @@
 
     // Update keep info.
     applyMinimumKeepInfo(field);
+    if (hasAlternativeLibraryDefinition(field.getHolder()) && !field.getDefinition().isPrivate()) {
+      getKeepInfo().keepField(field);
+    }
 
     // Notify analyses.
     analyses.forEach(analysis -> analysis.processNewlyLiveField(field, context, workList));
@@ -4561,6 +4598,10 @@
 
     // Update keep info.
     applyMinimumKeepInfo(method);
+    if (hasAlternativeLibraryDefinition(method.getHolder())
+        && !method.getDefinition().isPrivateMethod()) {
+      getKeepInfo().keepMethod(method);
+    }
   }
 
   private void traceNonDesugaredCode(ProgramMethod method) {
diff --git a/src/test/java/com/android/tools/r8/ToolHelper.java b/src/test/java/com/android/tools/r8/ToolHelper.java
index 5b8bb7a..7bcdfb6 100644
--- a/src/test/java/com/android/tools/r8/ToolHelper.java
+++ b/src/test/java/com/android/tools/r8/ToolHelper.java
@@ -196,6 +196,10 @@
     return System.getProperty("desugar_jdk_json_dir", "src/library_desugar");
   }
 
+  public static Path getDesugarLibJsonMinimalForTesting() {
+    return Paths.get(getDesugarLibraryJsonDir(), "desugar_jdk_libs_minimal.json");
+  }
+
   public static Path getDesugarLibJsonForTesting() {
     return Paths.get(getDesugarLibraryJsonDir(), "desugar_jdk_libs.json");
   }
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/FunctionOnlyTest.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/FunctionOnlyTest.java
new file mode 100644
index 0000000..0b443a8
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/FunctionOnlyTest.java
@@ -0,0 +1,277 @@
+// Copyright (c) 2022, 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.desugaredlibrary;
+
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.CompilationMode;
+import com.android.tools.r8.L8Command;
+import com.android.tools.r8.LibraryDesugaringTestConfiguration;
+import com.android.tools.r8.OutputMode;
+import com.android.tools.r8.StringResource;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.ir.desugar.desugaredlibrary.DesugaredLibrarySpecificationParser;
+import com.android.tools.r8.utils.BooleanUtils;
+import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.StringUtils;
+import com.google.common.collect.ImmutableList;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.function.BiFunction;
+import java.util.function.BinaryOperator;
+import java.util.function.BooleanSupplier;
+import java.util.function.Consumer;
+import java.util.function.DoublePredicate;
+import java.util.function.DoubleSupplier;
+import java.util.function.Function;
+import java.util.function.IntSupplier;
+import java.util.function.LongConsumer;
+import java.util.function.LongSupplier;
+import java.util.function.Supplier;
+import org.junit.Assume;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class FunctionOnlyTest extends DesugaredLibraryTestBase {
+
+  private final TestParameters parameters;
+  private final boolean shrinkDesugaredLibrary;
+  private static final String EXPECTED_RESULT =
+      StringUtils.lines(" true true true", "2", "false", "3", "true", "5", "42.0");
+
+  @Parameters(name = "{0}, shrinkDesugaredLibrary: {1}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        getTestParameters().withDexRuntimes().withAllApiLevelsAlsoForCf().build(),
+        BooleanUtils.values());
+  }
+
+  public FunctionOnlyTest(TestParameters parameters, boolean shrinkDesugaredLibrary) {
+    this.shrinkDesugaredLibrary = shrinkDesugaredLibrary;
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testFunctionCompositionD8() throws Exception {
+    testForD8()
+        .addLibraryFiles(getLibraryFile())
+        .setMinApi(parameters.getApiLevel())
+        .addInnerClasses(getClass())
+        .enableCoreLibraryDesugaring(minimalConfiguration())
+        .compile()
+        .run(parameters.getRuntime(), Executor.class)
+        .assertSuccessWithOutput(EXPECTED_RESULT);
+  }
+
+  private LibraryDesugaringTestConfiguration minimalConfiguration() {
+    if (!isJDK11DesugaredLibrary()) {
+      return LibraryDesugaringTestConfiguration.builder()
+          .setMinApi(parameters.getApiLevel())
+          .setMode(CompilationMode.RELEASE)
+          .withKeepRuleConsumer()
+          .build();
+    }
+    return LibraryDesugaringTestConfiguration.builder()
+        .setMinApi(parameters.getApiLevel())
+        .addDesugaredLibraryConfiguration(
+            StringResource.fromFile(ToolHelper.getDesugarLibJsonMinimalForTesting()))
+        .setMode(shrinkDesugaredLibrary ? CompilationMode.RELEASE : CompilationMode.DEBUG)
+        .withKeepRuleConsumer()
+        .build();
+  }
+
+  @Test
+  public void testFunctionCompositionD8Cf2Cf() throws Exception {
+    Assume.assumeTrue(isJDK11DesugaredLibrary());
+    KeepRuleConsumer keepRuleConsumer = createKeepRuleConsumer(parameters);
+    Path jar =
+        testForD8(Backend.CF)
+            .addLibraryFiles(getLibraryFile())
+            .setMinApi(parameters.getApiLevel())
+            .addInnerClasses(FunctionOnlyTest.class)
+            .enableCoreLibraryDesugaring(
+                parameters.getApiLevel(),
+                keepRuleConsumer,
+                StringResource.fromFile(ToolHelper.getDesugarLibJsonMinimalForTesting()))
+            .compile()
+            .writeToZip();
+    String desugaredLibraryKeepRules = "";
+    if (shrinkDesugaredLibrary && keepRuleConsumer.get() != null) {
+      // Collection keep rules is only implemented in the DEX writer.
+      assertEquals(0, keepRuleConsumer.get().length());
+      desugaredLibraryKeepRules = "-keep class java.util.function.* { *; }";
+    }
+    if (parameters.getRuntime().isDex()) {
+      testForD8()
+          .addProgramFiles(jar)
+          .setMinApi(parameters.getApiLevel())
+          .disableDesugaring()
+          .compile()
+          .addDesugaredCoreLibraryRunClassPath(
+              (api, keep, shrink) ->
+                  buildDesugaredLibrary(
+                      api,
+                      keep,
+                      shrink,
+                      ImmutableList.of(),
+                      opt ->
+                          opt.setDesugaredLibrarySpecification(
+                              DesugaredLibrarySpecificationParser
+                                  .parseDesugaredLibrarySpecification(
+                                      StringResource.fromFile(
+                                          ToolHelper.getDesugarLibJsonMinimalForTesting()),
+                                      opt.itemFactory,
+                                      opt.reporter,
+                                      true,
+                                      parameters.getApiLevel().getLevel()))),
+              parameters.getApiLevel(),
+              desugaredLibraryKeepRules,
+              shrinkDesugaredLibrary)
+          .run(parameters.getRuntime(), Executor.class)
+          .assertSuccessWithOutput(EXPECTED_RESULT);
+
+    } else {
+      testForJvm()
+          .addProgramFiles(jar)
+          .addRunClasspathFiles(getDesugaredLibraryInCFMinimal(parameters, options -> {}))
+          .run(parameters.getRuntime(), Executor.class)
+          .assertSuccessWithOutput(EXPECTED_RESULT);
+    }
+  }
+
+  public Path getDesugaredLibraryInCFMinimal(
+      TestParameters parameters, Consumer<InternalOptions> configurationForLibraryCompilation)
+      throws IOException, CompilationFailedException {
+    Path desugaredLib = temp.newFolder().toPath().resolve("desugar_jdk_libs.jar");
+    L8Command.Builder l8Builder =
+        L8Command.builder()
+            .addLibraryFiles(getLibraryFile())
+            .addProgramFiles(ToolHelper.getDesugarJDKLibs())
+            .addProgramFiles(ToolHelper.DESUGAR_LIB_CONVERSIONS)
+            .setMode(CompilationMode.DEBUG)
+            .addDesugaredLibraryConfiguration(
+                StringResource.fromFile(ToolHelper.getDesugarLibJsonMinimalForTesting()))
+            .setMinApiLevel(parameters.getApiLevel().getLevel())
+            .setOutput(desugaredLib, OutputMode.ClassFile);
+    ToolHelper.runL8(l8Builder.build(), configurationForLibraryCompilation);
+    return desugaredLib;
+  }
+
+  @Test
+  public void testFunctionCompositionR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addLibraryFiles(getLibraryFile())
+        .setMinApi(parameters.getApiLevel())
+        .addKeepMainRule(Executor.class)
+        .addInnerClasses(getClass())
+        .enableCoreLibraryDesugaring(minimalConfiguration())
+        .allowStdoutMessages()
+        .compile()
+        .run(parameters.getRuntime(), Executor.class)
+        .assertSuccessWithOutput(EXPECTED_RESULT);
+  }
+
+  static class Executor {
+
+    public static void main(String[] args) {
+      Function<Executor.Object1, Executor.Object3> function =
+          FunctionClass.composeFunction(Executor.Object2::new, Executor.Object3::new);
+      System.out.println(function.apply(new Executor.Object1()).toString());
+      BiFunction<String, String, Character> biFunction =
+          FunctionClass.composeBiFunctions(
+              (String i, String j) -> i + j, (String s) -> s.charAt(1));
+      System.out.println(biFunction.apply("1", "2"));
+      BooleanSupplier booleanSupplier =
+          () -> FunctionClass.composeBoolSuppliers(() -> true, () -> false).get();
+      System.out.println(booleanSupplier.getAsBoolean());
+      LongConsumer longConsumer = FunctionClass.composeLong(() -> 1L, System.out::println);
+      longConsumer.accept(2L);
+      DoublePredicate doublePredicate =
+          FunctionClass.composePredicate(d -> d > 1.0, d -> d == 2.0, d -> d < 3.0);
+      System.out.println(doublePredicate.test(2.0));
+      System.out.println(FunctionClass.extractInt(() -> 5));
+      System.out.println(FunctionClass.getDoubleSupplier().get());
+    }
+
+    static class Object1 {}
+
+    static class Object2 {
+
+      private Executor.Object1 field;
+
+      Object2(Executor.Object1 o) {
+        this.field = o;
+      }
+    }
+
+    static class Object3 {
+
+      private Executor.Object2 field;
+
+      Object3(Executor.Object2 o) {
+        this.field = o;
+      }
+
+      @Override
+      public String toString() {
+        return " "
+            + (field.field.getClass() == Executor.Object1.class)
+            + " "
+            + (field.getClass() == Executor.Object2.class)
+            + " "
+            + (getClass() == Executor.Object3.class);
+      }
+    }
+  }
+
+  static class FunctionClass {
+
+    public static <T, Q, R> Function<T, R> composeFunction(Function<T, Q> f1, Function<Q, R> f2) {
+      return f1.andThen(f2);
+    }
+
+    public static <T, R> BiFunction<T, T, R> composeBiFunctions(
+        BinaryOperator<T> operator, Function<T, R> function) {
+      return operator.andThen(function);
+    }
+
+    // BooleanSupplier is not a wrapped type, so it can't be placed on the boundary.
+    public static Supplier<Boolean> composeBoolSuppliers(
+        Supplier<Boolean> supplier1, Supplier<Boolean> supplier2) {
+      BooleanSupplier wrap1 = supplier1::get;
+      BooleanSupplier wrap2 = supplier2::get;
+      return () -> wrap1.getAsBoolean() && wrap2.getAsBoolean();
+    }
+
+    // LongSupplier is not a wrapped type, so it can't be placed on the boundary.
+    public static LongConsumer composeLong(Supplier<Long> supplier, LongConsumer consumer) {
+      LongSupplier wrap = supplier::get;
+      return l -> consumer.accept(l + wrap.getAsLong());
+    }
+
+    public static DoublePredicate composePredicate(
+        DoublePredicate predicate1, DoublePredicate predicate2, DoublePredicate predicate3) {
+      return predicate1.and(predicate2).and(predicate3);
+    }
+
+    // IntSupplier is not a wrapped type, so it can't be placed on the boundary.
+    public static int extractInt(Supplier<Integer> supplier) {
+      IntSupplier wrap = supplier::get;
+      return wrap.getAsInt();
+    }
+
+    // DoubleSupplier is not a wrapped type, so it can't be placed on the boundary.
+    public static Supplier<Double> getDoubleSupplier() {
+      DoubleSupplier supplier = () -> 42.0;
+      return supplier::getAsDouble;
+    }
+  }
+}