Support CONSTANT_Dynamic in interface default methods (part 1)

This is first step for D8 only.

Bug: 178172809
Bug: 210485236
Bug: 210148618
Change-Id: I2a02607f04e577896f0730f02a34726eaa1bee8c
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/constantdynamic/ConstantDynamicClass.java b/src/main/java/com/android/tools/r8/ir/desugar/constantdynamic/ConstantDynamicClass.java
index 1eb43e3..a332828 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/constantdynamic/ConstantDynamicClass.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/constantdynamic/ConstantDynamicClass.java
@@ -53,6 +53,7 @@
 import com.android.tools.r8.ir.code.ValueType;
 import com.android.tools.r8.ir.desugar.FreshLocalProvider;
 import com.android.tools.r8.ir.desugar.LocalStackAllocator;
+import com.android.tools.r8.ir.desugar.itf.InterfaceDesugaringSyntheticHelper;
 import com.android.tools.r8.ir.optimize.UtilityMethodsForCodeOptimizations;
 import com.android.tools.r8.ir.optimize.UtilityMethodsForCodeOptimizations.MethodSynthesizerConsumer;
 import com.android.tools.r8.ir.optimize.UtilityMethodsForCodeOptimizations.UtilityMethodForCodeOptimizations;
@@ -84,8 +85,9 @@
   public final DexField constantValueField;
   private final DexMethod getConstMethod;
   private final Behaviour behaviour;
-  private DexEncodedMethod bootstrapMethodImpl;
+  private DexMethod bootstrapMethodReference;
   private DexMethod finalBootstrapMethodReference;
+  private boolean isFinalBootstrapMethodReferenceOnInterface;
 
   // Considered final but is set after due to circularity in allocation.
   private DexProgramClass clazz = null;
@@ -114,7 +116,7 @@
             factory.createString("get"));
 
     DexMethodHandle bootstrapMethodHandle = reference.getBootstrapMethod();
-    DexMethod bootstrapMethodReference = bootstrapMethodHandle.asMethod();
+    bootstrapMethodReference = bootstrapMethodHandle.asMethod();
     MethodResolutionResult resolution =
         appView
             .appInfoForDesugaring()
@@ -122,22 +124,34 @@
     if (resolution.isSingleResolution()
         && resolution.asSingleResolution().getResolvedMethod().isStatic()) {
       SingleResolutionResult result = resolution.asSingleResolution();
-      bootstrapMethodImpl = result.getResolvedMethod();
+      if (bootstrapMethodHandle.isInterface
+          && appView.options().isInterfaceMethodDesugaringEnabled()) {
+        bootstrapMethodReference =
+            bootstrapMethodReference.withHolder(
+                InterfaceDesugaringSyntheticHelper.getCompanionClassType(
+                    bootstrapMethodReference.getHolderType(), factory),
+                factory);
+        isFinalBootstrapMethodReferenceOnInterface = false;
+      } else {
+        assert bootstrapMethodReference.getHolderType() == resolution.getResolvedHolder().getType();
+        isFinalBootstrapMethodReferenceOnInterface = bootstrapMethodHandle.isInterface;
+      }
       if (shouldRewriteBootstrapMethodSignature()) {
         // The bootstrap method will have its signature modified to have type Object as its first
         // argument.
         this.finalBootstrapMethodReference =
             factory.createMethod(
-                result.getResolvedHolder().getType(),
+                bootstrapMethodReference.getHolderType(),
                 factory.createProto(
                     bootstrapMethodReference.getReturnType(),
                     factory.objectType,
                     factory.stringType,
                     factory.classType),
-                bootstrapMethodImpl.getName());
+                bootstrapMethodReference.getName());
       } else {
         this.finalBootstrapMethodReference = bootstrapMethodReference;
         // Ensure that the bootstrap method is accessible from the generated class.
+        DexEncodedMethod bootstrapMethodImpl = result.getResolvedMethod();
         MethodAccessFlags flags = bootstrapMethodImpl.getAccessFlags();
         flags.unsetPrivate();
         flags.setPublic();
@@ -248,7 +262,11 @@
     instructions.add(new CfConstNull());
     instructions.add(new CfConstString(reference.getName()));
     instructions.add(new CfConstClass(reference.getType()));
-    instructions.add(new CfInvoke(INVOKESTATIC, finalBootstrapMethodReference, false));
+    instructions.add(
+        new CfInvoke(
+            INVOKESTATIC,
+            finalBootstrapMethodReference,
+            isFinalBootstrapMethodReferenceOnInterface));
     instructions.add(new CfCheckCast(reference.getType()));
   }
 
@@ -351,12 +369,12 @@
       return;
     }
     DexProgramClass bootstrapMethodHolder =
-        appView.definitionFor(bootstrapMethodImpl.getHolderType()).asProgramClass();
+        appView.definitionFor(bootstrapMethodReference.getHolderType()).asProgramClass();
     DexEncodedMethod replacement =
         bootstrapMethodHolder
             .getMethodCollection()
             .replaceDirectMethod(
-                bootstrapMethodImpl.getReference(),
+                bootstrapMethodReference,
                 encodedMethod -> {
                   MethodAccessFlags newAccessFlags = encodedMethod.accessFlags.copy();
                   // Ensure that the bootstrap method is accessible from the generated class.
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/itf/InterfaceDesugaringSyntheticHelper.java b/src/main/java/com/android/tools/r8/ir/desugar/itf/InterfaceDesugaringSyntheticHelper.java
index 616cc07..77d99e5 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/itf/InterfaceDesugaringSyntheticHelper.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/itf/InterfaceDesugaringSyntheticHelper.java
@@ -155,7 +155,7 @@
   }
 
   // Gets the companion class for the interface `type`.
-  static DexType getCompanionClassType(DexType type, DexItemFactory factory) {
+  public static DexType getCompanionClassType(DexType type, DexItemFactory factory) {
     assert type.isClassType();
     String descriptor = type.descriptor.toString();
     String ccTypeDescriptor = getCompanionClassDescriptor(descriptor);
diff --git a/src/test/java/com/android/tools/r8/TestBuilder.java b/src/test/java/com/android/tools/r8/TestBuilder.java
index aec53db..ae8a263 100644
--- a/src/test/java/com/android/tools/r8/TestBuilder.java
+++ b/src/test/java/com/android/tools/r8/TestBuilder.java
@@ -61,6 +61,23 @@
     return self;
   }
 
+  public T applyIf(
+      boolean value,
+      ThrowableConsumer<T> trueConsumer,
+      boolean value2,
+      ThrowableConsumer<T> trueConsumer2,
+      ThrowableConsumer<T> falseConsumer) {
+    T self = self();
+    if (value) {
+      trueConsumer.acceptWithRuntimeException(self);
+    } else if (value2) {
+      trueConsumer2.acceptWithRuntimeException(self);
+    } else {
+      falseConsumer.acceptWithRuntimeException(self);
+    }
+    return self;
+  }
+
   @Deprecated
   public RR run(String mainClass)
       throws CompilationFailedException, ExecutionException, IOException {
diff --git a/src/test/java/com/android/tools/r8/desugar/constantdynamic/ConstantDynamicInDefaultInterfaceMethodTest.java b/src/test/java/com/android/tools/r8/desugar/constantdynamic/ConstantDynamicInDefaultInterfaceMethodTest.java
new file mode 100644
index 0000000..a09ae6a
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/constantdynamic/ConstantDynamicInDefaultInterfaceMethodTest.java
@@ -0,0 +1,151 @@
+// 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.constantdynamic;
+
+import static com.android.tools.r8.DiagnosticsMatcher.diagnosticMessage;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.DesugarTestConfiguration;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestRuntime.CfVm;
+import com.android.tools.r8.cf.CfVersion;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.StringUtils;
+import java.lang.invoke.MethodHandles;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class ConstantDynamicInDefaultInterfaceMethodTest extends TestBase {
+
+  @Parameter() public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        getTestParameters().withAllRuntimes().withAllApiLevelsAlsoForCf().build());
+  }
+
+  private static final String EXPECTED_OUTPUT = StringUtils.lines("true", "true");
+  private static final Class<?> MAIN_CLASS = A.class;
+
+  @Test
+  public void testReference() throws Exception {
+    assumeTrue(parameters.isCfRuntime());
+    assumeTrue(parameters.getRuntime().asCf().isNewerThanOrEqual(CfVm.JDK11));
+    assumeTrue(parameters.getApiLevel().isEqualTo(AndroidApiLevel.B));
+
+    testForJvm()
+        .addProgramClasses(MAIN_CLASS)
+        .addProgramClassFileData(getTransformedClasses())
+        .run(parameters.getRuntime(), MAIN_CLASS)
+        .assertSuccessWithOutput(EXPECTED_OUTPUT);
+  }
+
+  @Test
+  public void testDesugaring() throws Exception {
+    testForDesugaring(parameters)
+        .addProgramClasses(MAIN_CLASS)
+        .addProgramClassFileData(getTransformedClasses())
+        .run(parameters.getRuntime(), MAIN_CLASS)
+        .applyIf(
+            // When not desugaring the CF code requires JDK 11.
+            DesugarTestConfiguration::isNotDesugared,
+            r -> {
+              if (parameters.isCfRuntime()
+                  && parameters.getRuntime().asCf().isNewerThanOrEqual(CfVm.JDK11)) {
+                r.assertSuccessWithOutput(EXPECTED_OUTPUT);
+              } else {
+                r.assertFailureWithErrorThatThrows(UnsupportedClassVersionError.class);
+              }
+            })
+        .applyIf(
+            DesugarTestConfiguration::isDesugared, r -> r.assertSuccessWithOutput(EXPECTED_OUTPUT));
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    assumeTrue(parameters.isDexRuntime() || parameters.getApiLevel().isEqualTo(AndroidApiLevel.B));
+
+    testForR8(parameters.getBackend())
+        .addProgramClasses(MAIN_CLASS)
+        .addProgramClassFileData(getTransformedClasses())
+        .setMinApi(parameters.getApiLevel())
+        .addKeepMainRule(MAIN_CLASS)
+        // TODO(b/198142613): There should not be a warnings on class references which are
+        //  desugared away.
+        .applyIf(
+            parameters.getApiLevel().isLessThan(AndroidApiLevel.O),
+            b -> b.addDontWarn("java.lang.invoke.MethodHandles$Lookup"))
+        // TODO(b/198142625): Support CONSTANT_Dynamic output for class files.
+        .applyIf(
+            parameters.isCfRuntime(),
+            b -> {
+              assertThrows(
+                  CompilationFailedException.class,
+                  () ->
+                      b.compileWithExpectedDiagnostics(
+                          diagnostics -> {
+                            diagnostics.assertOnlyErrors();
+                            diagnostics.assertErrorsMatch(
+                                diagnosticMessage(
+                                    containsString(
+                                        "Unsupported dynamic constant (not desugaring)")));
+                          }));
+            },
+            // TODO(b/210485236): This should not fail for R8.
+            !parameters.canUseDefaultAndStaticInterfaceMethodsWhenDesugaring(),
+            b ->
+                b.run(parameters.getRuntime(), MAIN_CLASS)
+                    .assertFailureWithErrorThatThrows(NoSuchMethodError.class),
+            b ->
+                b.run(parameters.getRuntime(), MAIN_CLASS)
+                    .assertSuccessWithOutput(EXPECTED_OUTPUT));
+  }
+
+  private byte[] getTransformedClasses() throws Exception {
+    return transformer(I.class)
+        .setVersion(CfVersion.V11)
+        .transformConstStringToConstantDynamic(
+            "condy1", I.class, "myConstant", "constantName", Object.class)
+        .transformConstStringToConstantDynamic(
+            "condy2", I.class, "myConstant", "constantName", Object.class)
+        .setPrivate(
+            I.class.getDeclaredMethod(
+                "myConstant", MethodHandles.Lookup.class, String.class, Class.class))
+        .transform();
+  }
+
+  public interface I {
+
+    default Object f() {
+      return "condy1"; // Will be transformed to Constant_DYNAMIC.
+    }
+
+    default Object g() {
+      return "condy2"; // Will be transformed to Constant_DYNAMIC.
+    }
+
+    /* private */ static Object myConstant(
+        MethodHandles.Lookup lookup, String name, Class<?> type) {
+      return new Object();
+    }
+  }
+
+  public static class A implements I {
+    public static void main(String[] args) {
+      A a = new A();
+      System.out.println(a.f() != null);
+      System.out.println(a.f() == a.g());
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/constantdynamic/JacocoConstantDynamicTest.java b/src/test/java/com/android/tools/r8/desugar/constantdynamic/JacocoConstantDynamicTest.java
index 9c33f50..46261e5 100644
--- a/src/test/java/com/android/tools/r8/desugar/constantdynamic/JacocoConstantDynamicTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/constantdynamic/JacocoConstantDynamicTest.java
@@ -16,6 +16,7 @@
 import com.android.tools.r8.jacoco.JacocoClasses;
 import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.StringUtils;
+import com.google.common.collect.ImmutableList;
 import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
@@ -50,7 +51,7 @@
   public JacocoClasses testClasses;
 
   private static final String MAIN_CLASS = TestRunner.class.getTypeName();
-  private static final String EXPECTED_OUTPUT = StringUtils.lines("Hello, world!");
+  private static final String EXPECTED_OUTPUT = StringUtils.lines("Hello, world!", "Hello from I!");
 
   @BeforeClass
   public static void setUpInput() throws IOException {
@@ -85,7 +86,7 @@
         .run(parameters.getRuntime(), MAIN_CLASS)
         .assertSuccessWithOutput(EXPECTED_OUTPUT);
     List<String> onTheFlyReport = testClasses.generateReport(agentOutputOnTheFly);
-    assertEquals(2, onTheFlyReport.size());
+    assertEquals(3, onTheFlyReport.size());
 
     // Run the instrumented code.
     Path agentOutputOffline = output.resolve("offline");
@@ -114,7 +115,7 @@
       // TODO(sgjesse): Need to figure out why there is no instrumentation output for newer VMs.
       if (parameters.getRuntime().asDex().getVm().isOlderThanOrEqual(DexVm.ART_4_4_4_HOST)) {
         List<String> report = testClasses.generateReport(agentOutput);
-        assertEquals(2, report.size());
+        assertEquals(3, report.size());
       } else {
         assertFalse(Files.exists(agentOutput));
       }
@@ -130,16 +131,23 @@
   private static JacocoClasses testClasses(TemporaryFolder temp, CfVersion version)
       throws IOException {
     return new JacocoClasses(
-        transformer(TestRunner.class)
-            .setVersion(version) /*.setClassDescriptor("LTestRunner;")*/
-            .transform(),
+        ImmutableList.of(
+            transformer(TestRunner.class).setVersion(version).transform(),
+            transformer(I.class).setVersion(version).transform()),
         temp);
   }
 
-  static class TestRunner {
+  interface I {
+    default void m() {
+      System.out.println("Hello from I!");
+    }
+  }
+
+  static class TestRunner implements I {
 
     public static void main(String[] args) {
       System.out.println("Hello, world!");
+      new TestRunner().m();
     }
   }
 }
diff --git a/src/test/java/com/android/tools/r8/jacoco/JacocoClasses.java b/src/test/java/com/android/tools/r8/jacoco/JacocoClasses.java
index b98cc63..dd2700f 100644
--- a/src/test/java/com/android/tools/r8/jacoco/JacocoClasses.java
+++ b/src/test/java/com/android/tools/r8/jacoco/JacocoClasses.java
@@ -12,6 +12,7 @@
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.ProcessResult;
 import com.android.tools.r8.utils.ZipUtils;
+import com.google.common.collect.ImmutableList;
 import java.io.File;
 import java.io.IOException;
 import java.nio.file.Files;
@@ -28,22 +29,29 @@
   private final Path originalJar;
   private final Path instrumentedJar;
 
-  // Create JacocoClasses with just one class provided as bytes.
+  // Create JacocoClasses with just one class provided as class file bytes.
   public JacocoClasses(byte[] clazz, TemporaryFolder temp) throws IOException {
+    this(ImmutableList.of(clazz), temp);
+  }
+
+  // Create JacocoClasses with multiple classes provided as class file bytes.
+  public JacocoClasses(List<byte[]> classes, TemporaryFolder temp) throws IOException {
     this.temp = temp;
     dir = temp.newFolder().toPath();
 
     // Write the class to a .class file with package sub-directories.
-    String typeName = TestBase.extractClassName(clazz);
-    int lastDotIndex = typeName.lastIndexOf('.');
-    String pkg = typeName.substring(0, lastDotIndex);
-    String baseFileName = typeName.substring(lastDotIndex + 1) + CLASS_EXTENSION;
     Path original = dir.resolve("original");
-    Files.createDirectories(original);
-    Path packageDir = original.resolve(pkg.replace(JAVA_PACKAGE_SEPARATOR, File.separatorChar));
-    Files.createDirectories(packageDir);
-    Path classFile = packageDir.resolve(baseFileName);
-    Files.write(classFile, clazz);
+    for (byte[] clazz : classes) {
+      String typeName = TestBase.extractClassName(clazz);
+      int lastDotIndex = typeName.lastIndexOf('.');
+      String pkg = typeName.substring(0, lastDotIndex);
+      String baseFileName = typeName.substring(lastDotIndex + 1) + CLASS_EXTENSION;
+      Files.createDirectories(original);
+      Path packageDir = original.resolve(pkg.replace(JAVA_PACKAGE_SEPARATOR, File.separatorChar));
+      Files.createDirectories(packageDir);
+      Path classFile = packageDir.resolve(baseFileName);
+      Files.write(classFile, clazz);
+    }
 
     // Run offline instrumentation.
     Path instrumented = dir.resolve("instrumented");
diff --git a/src/test/java/com/android/tools/r8/transformers/ClassFileTransformer.java b/src/test/java/com/android/tools/r8/transformers/ClassFileTransformer.java
index 296004d..8644e9a 100644
--- a/src/test/java/com/android/tools/r8/transformers/ClassFileTransformer.java
+++ b/src/test/java/com/android/tools/r8/transformers/ClassFileTransformer.java
@@ -996,7 +996,7 @@
                           DescriptorUtils.getClassBinaryName(bootstrapMethodHolder),
                           bootstrapMethodName,
                           bootstrapMethodSignature,
-                          false),
+                          bootstrapMethodHolder.isInterface()),
                       new Object[] {}));
             } else {
               super.visitLdcInsn(value);