Initial support for desugaring lambdas when compiling to CF

Except for adding an extra flag a few places, the main change is to
maintain proper debug information by replacing the this local variable
with a fake one, since the method is now static.

Bug: 146031650
Change-Id: I69d30284caed247665482fd0ec25efb073bd9f15
diff --git a/src/main/java/com/android/tools/r8/graph/CfCode.java b/src/main/java/com/android/tools/r8/graph/CfCode.java
index b9e8757..2753232 100644
--- a/src/main/java/com/android/tools/r8/graph/CfCode.java
+++ b/src/main/java/com/android/tools/r8/graph/CfCode.java
@@ -3,6 +3,9 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.graph;
 
+import static com.android.tools.r8.graph.DexCode.FAKE_THIS_PREFIX;
+import static com.android.tools.r8.graph.DexCode.FAKE_THIS_SUFFIX;
+
 import com.android.tools.r8.cf.CfPrinter;
 import com.android.tools.r8.cf.code.CfFrame;
 import com.android.tools.r8.cf.code.CfIinc;
@@ -26,6 +29,7 @@
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.InternalOptions;
+import com.google.common.base.Strings;
 import it.unimi.dsi.fastutil.ints.Int2IntArrayMap;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceArrayMap;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceMap;
@@ -518,4 +522,32 @@
     }
     return constraint;
   }
+
+  void addFakeThisParameter(DexItemFactory factory) {
+    if (localVariables == null || localVariables.isEmpty()) {
+      // We have no debugging info in the code.
+      return;
+    }
+    int largestPrefix = 1;
+    int existingThisIndex = -1;
+    for (int i = 0; i < localVariables.size(); i++) {
+      LocalVariableInfo localVariable = localVariables.get(i);
+      largestPrefix =
+          Math.max(largestPrefix, DexCode.getLargestPrefix(factory, localVariable.local.name));
+      if (localVariable.local.name.toString().equals("this")) {
+        existingThisIndex = i;
+      }
+    }
+    if (existingThisIndex < 0) {
+      return;
+    }
+    String fakeThisName = Strings.repeat(FAKE_THIS_PREFIX, largestPrefix + 1) + FAKE_THIS_SUFFIX;
+    DebugLocalInfo debugLocalInfo =
+        new DebugLocalInfo(factory.createString(fakeThisName), this.originalHolder, null);
+    LocalVariableInfo thisLocalInfo = localVariables.get(existingThisIndex);
+    this.localVariables.set(
+        existingThisIndex,
+        new LocalVariableInfo(
+            thisLocalInfo.index, debugLocalInfo, thisLocalInfo.start, thisLocalInfo.end));
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/graph/DexCode.java b/src/main/java/com/android/tools/r8/graph/DexCode.java
index a5b1097..d32abdf 100644
--- a/src/main/java/com/android/tools/r8/graph/DexCode.java
+++ b/src/main/java/com/android/tools/r8/graph/DexCode.java
@@ -32,8 +32,8 @@
 // DexCode corresponds to code item in dalvik/dex-format.html
 public class DexCode extends Code {
 
-  private static final String FAKE_THIS_PREFIX = "_";
-  private static final String FAKE_THIS_SUFFIX = "this";
+  static final String FAKE_THIS_PREFIX = "_";
+  static final String FAKE_THIS_SUFFIX = "this";
 
   public final int registerSize;
   public final int incomingRegisterSize;
@@ -132,7 +132,7 @@
     return new DexDebugInfo(debugInfo.startLine, newParameters, debugInfo.events);
   }
 
-  private static int getLargestPrefix(DexItemFactory factory, DexString name) {
+  public static int getLargestPrefix(DexItemFactory factory, DexString name) {
     if (name != null && name.endsWith(factory.thisName)) {
       String string = name.toString();
       for (int i = 0; i < string.length(); i++) {
diff --git a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
index b276b8b..e4fff90 100644
--- a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
@@ -758,8 +758,11 @@
       assert (dexCode.getDebugInfo() == null)
           || (arity == dexCode.getDebugInfo().parameters.length);
     } else {
-      // TODO(b/134732760): Patch Cf debug info.
-      assert appView.options().isDesugaredLibraryCompilation();
+      assert appView.options().isDesugaredLibraryCompilation()
+          || appView.options().enableCfInterfaceMethodDesugaring;
+      assert code.isCfCode();
+      CfCode cfCode = code.asCfCode();
+      cfCode.addFakeThisParameter(appView.dexItemFactory());
     }
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/LambdaClass.java b/src/main/java/com/android/tools/r8/ir/desugar/LambdaClass.java
index e7ba0b5..ce77849 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/LambdaClass.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/LambdaClass.java
@@ -29,6 +29,7 @@
 import com.android.tools.r8.ir.code.Invoke.Type;
 import com.android.tools.r8.ir.synthetic.SynthesizedCode;
 import com.android.tools.r8.origin.SynthesizedOrigin;
+import com.android.tools.r8.utils.InternalOptions;
 import com.google.common.base.Suppliers;
 import com.google.common.primitives.Longs;
 import java.nio.ByteBuffer;
@@ -494,7 +495,8 @@
     }
 
     boolean holderIsInterface() {
-      if (!rewriter.converter.appView.options().isGeneratingClassFiles()) {
+      InternalOptions options = rewriter.converter.appView.options();
+      if (!options.isGeneratingClassFiles()) {
         // When generating dex the value of this flag on invokes does not matter (unused).
         // We cannot know if definitionFor(implMethod.holder) is null or not in that case,
         // so we cannot set the flag and just return false.
@@ -503,14 +505,11 @@
       // The only case where we do Lambda desugaring with Cf to Cf is in L8.
       // If the compilation is not coreLibraryCompilation, then the assertion
       // implMethodHolder != null may fail, hence the assertion.
-      assert rewriter.converter.appView.options().isDesugaredLibraryCompilation();
+      assert options.isDesugaredLibraryCompilation() || options.enableCfInterfaceMethodDesugaring;
       DexMethod implMethod = descriptor.implHandle.asMethod();
       DexClass implMethodHolder = definitionFor(implMethod.holder);
       if (implMethodHolder == null) {
-        assert rewriter
-            .converter
-            .appView
-            .options()
+        assert options
             .desugaredLibraryConfiguration
             .getBackportCoreLibraryMember()
             .containsKey(implMethod.holder);
diff --git a/src/main/java/com/android/tools/r8/jar/CfApplicationWriter.java b/src/main/java/com/android/tools/r8/jar/CfApplicationWriter.java
index 6aee26c..1600057 100644
--- a/src/main/java/com/android/tools/r8/jar/CfApplicationWriter.java
+++ b/src/main/java/com/android/tools/r8/jar/CfApplicationWriter.java
@@ -113,7 +113,9 @@
     }
     String markerString = marker.toString();
     for (DexProgramClass clazz : application.classes()) {
-      if (clazz.getSynthesizedFrom().isEmpty() || options.isDesugaredLibraryCompilation()) {
+      if (clazz.getSynthesizedFrom().isEmpty()
+          || options.isDesugaredLibraryCompilation()
+          || options.enableCfInterfaceMethodDesugaring) {
         writeClass(clazz, consumer, markerString);
       } else {
         throw new Unimplemented("No support for synthetics in the Java bytecode backend.");
@@ -201,8 +203,11 @@
       // In this case bridges have been introduced for the Cf back-end,
       // which do not have class file version.
       assert options.testing.enableForceNestBasedAccessDesugaringForTest
-          || options.isDesugaredLibraryCompilation();
-      return 0;
+          || options.isDesugaredLibraryCompilation()
+          || options.enableCfInterfaceMethodDesugaring;
+      // TODO(b/146424042): We may call static methods on interface classes so we have to go for
+      //  version 52.
+      return options.enableCfInterfaceMethodDesugaring ? 52 : 0;
     }
     return method.getClassFileVersion();
   }
diff --git a/src/main/java/com/android/tools/r8/utils/InternalOptions.java b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
index 98ac732..c4815ae 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -217,6 +217,7 @@
   public boolean encodeChecksums = false;
   public BiPredicate<String, Long> dexClassChecksumFilter = (name, checksum) -> true;
   public boolean enableSourceDebugExtensionRewriter = false;
+  public boolean enableCfInterfaceMethodDesugaring = false;
 
   public int callGraphLikelySpuriousCallEdgeThreshold = 50;
 
@@ -1124,7 +1125,7 @@
     }
     return enableDesugaring
         && interfaceMethodDesugaring == OffOrAuto.Auto
-        && !canUseDefaultAndStaticInterfaceMethods();
+        && (!canUseDefaultAndStaticInterfaceMethods() || enableCfInterfaceMethodDesugaring);
   }
 
   public boolean isStringSwitchConversionEnabled() {
diff --git a/src/test/java/com/android/tools/r8/TestCompilerBuilder.java b/src/test/java/com/android/tools/r8/TestCompilerBuilder.java
index fa2c751..0f613d5 100644
--- a/src/test/java/com/android/tools/r8/TestCompilerBuilder.java
+++ b/src/test/java/com/android/tools/r8/TestCompilerBuilder.java
@@ -43,7 +43,7 @@
 
   // Default initialized setup. Can be overwritten if needed.
   private boolean useDefaultRuntimeLibrary = true;
-  final List<Path> additionalRunClassPath = new ArrayList<>();
+  private final List<Path> additionalRunClassPath = new ArrayList<>();
   private ProgramConsumer programConsumer;
   private StringConsumer mainDexListConsumer;
   private AndroidApiLevel defaultMinApiLevel = ToolHelper.getMinApiLevelForDexVm();
diff --git a/src/test/java/com/android/tools/r8/desugar/DefaultLambdaWithUnderscoreThisTestRunner.java b/src/test/java/com/android/tools/r8/desugar/DefaultLambdaWithUnderscoreThisTestRunner.java
index 10d74a8..feda70f 100644
--- a/src/test/java/com/android/tools/r8/desugar/DefaultLambdaWithUnderscoreThisTestRunner.java
+++ b/src/test/java/com/android/tools/r8/desugar/DefaultLambdaWithUnderscoreThisTestRunner.java
@@ -3,8 +3,15 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.desugar;
 
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.CompilationMode;
 import com.android.tools.r8.D8TestCompileResult;
 import com.android.tools.r8.JvmTestBuilder;
+import com.android.tools.r8.R8FullTestBuilder;
+import com.android.tools.r8.R8TestCompileResult;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.debug.DebugTestBase;
 import com.android.tools.r8.debug.DebugTestBase.JUnit3Wrapper.Command;
 import com.android.tools.r8.debug.DebugTestConfig;
@@ -17,25 +24,43 @@
 import java.util.Collections;
 import java.util.function.Function;
 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 DefaultLambdaWithUnderscoreThisTestRunner extends DebugTestBase {
 
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public DefaultLambdaWithUnderscoreThisTestRunner(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
   final Class<?> CLASS = DefaultLambdaWithUnderscoreThisTest.class;
 
   final String EXPECTED = StringUtils
       .lines("stateful(My _this variable foo Another ___this variable)");
 
-  private void runDebugger(DebugTestConfig config) throws Throwable {
+  private void runDebugger(DebugTestConfig config, boolean desugared) throws Throwable {
     MethodReference main = Reference.methodFromMethod(CLASS.getMethod("main", String[].class));
     MethodReference stateful = Reference.methodFromMethod(I.class.getMethod("stateful"));
-    Function<String, Command> checkThis = (String desugarThis) -> conditional((state) ->
-        state.isCfRuntime()
-            ? Collections.singletonList(checkLocal("this"))
-            : ImmutableList.of(
-                checkNoLocal("this"),
-                checkLocal(desugarThis)));
+    Function<String, Command> checkThis =
+        (String desugarThis) ->
+            conditional(
+                (state) ->
+                    !desugared
+                        ? Collections.singletonList(checkLocal("this"))
+                        : ImmutableList.of(checkNoLocal("this"), checkLocal(desugarThis)));
 
-    runDebugTest(config, CLASS,
+    runDebugTest(
+        config,
+        CLASS,
         breakpoint(main, 22),
         run(),
         checkLine(22),
@@ -55,27 +80,53 @@
         checkLine(15),
         // Desugaring will insert '____this' in place of 'this' here.
         checkThis.apply("____this"),
-        checkLocals("_this",  "___this"),
+        checkLocals("_this", "___this"),
         run());
   }
 
   @Test
   public void testJvm() throws Throwable {
+    assumeTrue(parameters.isCfRuntime());
     JvmTestBuilder builder = testForJvm().addTestClasspath();
-    builder.run(CLASS).assertSuccessWithOutput(EXPECTED);
-    runDebugger(builder.debugConfig());
+    builder.run(parameters.getRuntime(), CLASS).assertSuccessWithOutput(EXPECTED);
+    runDebugger(builder.debugConfig(), false);
   }
 
   @Test
   public void testD8() throws Throwable {
-    D8TestCompileResult compileResult = testForD8()
-        .addProgramClassesAndInnerClasses(CLASS)
-        .setMinApiThreshold(AndroidApiLevel.K)
-        .compile();
+    assumeTrue(parameters.isDexRuntime());
+    D8TestCompileResult compileResult =
+        testForD8()
+            .addProgramClassesAndInnerClasses(CLASS)
+            .setMinApiThreshold(AndroidApiLevel.K)
+            .compile();
     compileResult
         // TODO(b/123506120): Add .assertNoMessages()
-        .run(CLASS)
+        .run(parameters.getRuntime(), CLASS)
         .assertSuccessWithOutput(EXPECTED);
-    runDebugger(compileResult.debugConfig());
+    runDebugger(compileResult.debugConfig(), true);
+  }
+
+  @Test
+  public void testR8() throws Throwable {
+    R8FullTestBuilder r8FullTestBuilder =
+        testForR8(parameters.getBackend())
+            .addProgramClassesAndInnerClasses(CLASS)
+            .addKeepAllClassesRule()
+            .noMinification()
+            .setMode(CompilationMode.DEBUG)
+            .addOptionsModification(
+                internalOptions -> {
+                  if (parameters.isCfRuntime()) {
+                    internalOptions.enableDesugaring = true;
+                    internalOptions.enableCfInterfaceMethodDesugaring = true;
+                  }
+                });
+    if (parameters.isDexRuntime()) {
+      r8FullTestBuilder.setMinApiThreshold(AndroidApiLevel.K);
+    }
+    R8TestCompileResult compileResult = r8FullTestBuilder.compile();
+    compileResult.run(parameters.getRuntime(), CLASS).assertSuccessWithOutput(EXPECTED);
+    runDebugger(compileResult.debugConfig(), true);
   }
 }