Make lambda instance methods when possible.

This ensures a valid 'this' reference that can be seen by the debugger.

Bug: 123068053
Change-Id: I04698f09bb657a17ec4a1660b3c4d6424a85d4ed
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 f8fb8e1..cea5e4d 100644
--- a/src/main/java/com/android/tools/r8/graph/DexCode.java
+++ b/src/main/java/com/android/tools/r8/graph/DexCode.java
@@ -10,6 +10,7 @@
 import com.android.tools.r8.dex.MixedSectionCollection;
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.DexCode.TryHandler.TypeAddrPair;
+import com.android.tools.r8.graph.DexDebugEvent.StartLocal;
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.Position;
 import com.android.tools.r8.ir.code.ValueNumberGenerator;
@@ -19,6 +20,7 @@
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.StringUtils;
+import com.google.common.base.Strings;
 import it.unimi.dsi.fastutil.ints.Int2IntMap;
 import java.util.Arrays;
 import java.util.Collections;
@@ -31,6 +33,9 @@
 // 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";
+
   public final int registerSize;
   public final int incomingRegisterSize;
   public final int outgoingRegisterSize;
@@ -103,17 +108,43 @@
     }
   }
 
-  public DexDebugInfo debugInfoWithAdditionalFirstParameter(DexString name) {
+  public DexDebugInfo debugInfoWithFakeThisParameter(DexItemFactory factory) {
     if (debugInfo == null) {
       return null;
     }
+    // User code may already have variables named '_*this'. Use one more than the largest number of
+    // underscores present as a prefix to 'this'.
+    int largestPrefix = 0;
+    for (DexString parameter : debugInfo.parameters) {
+      largestPrefix = Integer.max(largestPrefix, getLargestPrefix(factory, parameter));
+    }
+    for (DexDebugEvent event : debugInfo.events) {
+      if (event instanceof DexDebugEvent.StartLocal) {
+        DexString name = ((StartLocal) event).name;
+        largestPrefix = Integer.max(largestPrefix, getLargestPrefix(factory, name));
+      }
+    }
+
+    String fakeThisName = Strings.repeat(FAKE_THIS_PREFIX, largestPrefix + 1) + FAKE_THIS_SUFFIX;
     DexString[] parameters = debugInfo.parameters;
     DexString[] newParameters = new DexString[parameters.length + 1];
-    newParameters[0] = name;
+    newParameters[0] = factory.createString(fakeThisName);
     System.arraycopy(parameters, 0, newParameters, 1, parameters.length);
     return new DexDebugInfo(debugInfo.startLine, newParameters, debugInfo.events);
   }
 
+  private 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++) {
+        if (string.charAt(i) != '_') {
+          return i;
+        }
+      }
+    }
+    return 0;
+  }
+
   public DexDebugInfo debugInfoWithoutFirstParameter() {
     if (debugInfo == null) {
       return null;
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/InterfaceProcessor.java b/src/main/java/com/android/tools/r8/ir/desugar/InterfaceProcessor.java
index 9e18c94..054b950 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/InterfaceProcessor.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/InterfaceProcessor.java
@@ -90,10 +90,7 @@
         newFlags.unsetBridge();
         newFlags.promoteToStatic();
         DexCode dexCode = code.asDexCode();
-        // We cannot name the parameter "this" because the debugger may omit it due to the method
-        // actually being static. Instead we prepend it with a special character.
-        dexCode.setDebugInfo(dexCode.debugInfoWithAdditionalFirstParameter(
-            rewriter.factory.createString("-this")));
+        dexCode.setDebugInfo(dexCode.debugInfoWithFakeThisParameter(rewriter.factory));
         assert (dexCode.getDebugInfo() == null)
             || (companionMethod.getArity() == dexCode.getDebugInfo().parameters.length);
 
@@ -153,8 +150,7 @@
                 + "interface method: " + oldMethod.toSourceString(), iface.origin);
           }
           DexCode dexCode = code.asDexCode();
-          // TODO(ager): Should we give the new first parameter an actual name? Maybe 'this'?
-          dexCode.setDebugInfo(dexCode.debugInfoWithAdditionalFirstParameter(null));
+          dexCode.setDebugInfo(dexCode.debugInfoWithFakeThisParameter(rewriter.factory));
           assert (dexCode.getDebugInfo() == null)
               || (companionMethod.getArity() == dexCode.getDebugInfo().parameters.length);
 
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 a0bc184..865a62c 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
@@ -27,6 +27,7 @@
 import com.android.tools.r8.graph.MethodAccessFlags;
 import com.android.tools.r8.graph.ParameterAnnotationsList;
 import com.android.tools.r8.ir.code.Invoke;
+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.google.common.base.Suppliers;
@@ -343,17 +344,28 @@
 
     assert implHandle.type.isInvokeInstance() || implHandle.type.isInvokeDirect();
 
-    // If lambda$ method is an instance method we convert it into a static methods and
-    // relax its accessibility.
-    DexProto implProto = implMethod.proto;
-    DexType[] implParams = implProto.parameters.values;
-    DexType[] newParams = new DexType[implParams.length + 1];
-    newParams[0] = implMethod.holder;
-    System.arraycopy(implParams, 0, newParams, 1, implParams.length);
+    // If the lambda$ method is an instance-private method on an interface we convert it into a
+    // public static method as it will be placed on the companion class.
+    if (implHandle.type.isInvokeDirect()
+        && rewriter.appInfo.definitionFor(implMethod.holder).isInterface()) {
+      DexProto implProto = implMethod.proto;
+      DexType[] implParams = implProto.parameters.values;
+      DexType[] newParams = new DexType[implParams.length + 1];
+      newParams[0] = implMethod.holder;
+      System.arraycopy(implParams, 0, newParams, 1, implParams.length);
 
-    DexProto newProto = rewriter.factory.createProto(implProto.returnType, newParams);
-    return new InstanceLambdaImplTarget(
-        rewriter.factory.createMethod(implMethod.holder, newProto, implMethod.name));
+      DexProto newProto = rewriter.factory.createProto(implProto.returnType, newParams);
+      return new InterfaceLambdaImplTarget(
+          rewriter.factory.createMethod(implMethod.holder, newProto, implMethod.name));
+    } else {
+      // Otherwise we need to ensure the method can be reached publicly by virtual dispatch.
+      // To avoid potential conflicts on the name of the lambda method once dispatch becomes virtual
+      // we add the method-holder name as suffix to the lambda-method name.
+      return new InstanceLambdaImplTarget(
+            rewriter.factory.createMethod(implMethod.holder, implMethod.proto,
+                rewriter.factory.createString(
+                    implMethod.name.toString() + "$" + implMethod.holder.getName())));
+    }
   }
 
   // Create targets for instance method referenced directly without
@@ -498,17 +510,17 @@
     }
   }
 
-  // Used for instance private lambda$ methods. Needs to be converted to
-  // a package-private static method.
-  private class InstanceLambdaImplTarget extends Target {
+  // Used for instance private lambda$ methods on interfaces which need to be converted to public
+  // static methods. They can't remain instance methods as they will end up on the companion class.
+  private class InterfaceLambdaImplTarget extends Target {
 
-    InstanceLambdaImplTarget(DexMethod staticMethod) {
-      super(staticMethod, Invoke.Type.STATIC);
+    InterfaceLambdaImplTarget(DexMethod staticMethod) {
+      super(staticMethod, Type.STATIC);
     }
 
     @Override
     boolean ensureAccessibility() {
-      // For all instantiation points for which compiler creates lambda$
+      // For all instantiation points for which the compiler creates lambda$
       // methods, it creates these methods in the same class/interface.
       DexMethod implMethod = descriptor.implHandle.asMethod();
       DexClass implMethodHolder = definitionFor(implMethod.holder);
@@ -534,15 +546,58 @@
                   encodedMethod.getCode());
           newMethod.copyMetadata(encodedMethod);
           rewriter.methodMapping.put(encodedMethod.method, callTarget);
-          // TODO(ager): Should we give the new first parameter an actual name? Maybe 'this'?
           DexCode dexCode = newMethod.getCode().asDexCode();
-          dexCode.setDebugInfo(dexCode.debugInfoWithAdditionalFirstParameter(null));
+          dexCode.setDebugInfo(dexCode.debugInfoWithFakeThisParameter(rewriter.factory));
           assert (dexCode.getDebugInfo() == null)
               || (callTarget.getArity() == dexCode.getDebugInfo().parameters.length);
           implMethodHolder.setDirectMethod(i, newMethod);
           return true;
         }
       }
+      assert false
+          : "Unexpected failure to find direct lambda target for: " + implMethod.qualifiedName();
+      return false;
+    }
+  }
+
+  // Used for instance private lambda$ methods which need to be converted to public methods.
+  private class InstanceLambdaImplTarget extends Target {
+
+    InstanceLambdaImplTarget(DexMethod staticMethod) {
+      super(staticMethod, Type.VIRTUAL);
+    }
+
+    @Override
+    boolean ensureAccessibility() {
+      // For all instantiation points for which the compiler creates lambda$
+      // methods, it creates these methods in the same class/interface.
+      DexMethod implMethod = descriptor.implHandle.asMethod();
+      DexClass implMethodHolder = definitionFor(implMethod.holder);
+
+      List<DexEncodedMethod> oldDirectMethods = implMethodHolder.directMethods();
+      for (int i = 0; i < oldDirectMethods.size(); i++) {
+        DexEncodedMethod encodedMethod = oldDirectMethods.get(i);
+        if (implMethod.match(encodedMethod)) {
+          // We need to create a new method with the same code to be able to safely relax its
+          // accessibility and make it virtual.
+          MethodAccessFlags newAccessFlags = encodedMethod.accessFlags.copy();
+          newAccessFlags.unsetPrivate();
+          newAccessFlags.setPublic();
+          DexEncodedMethod newMethod =
+              new DexEncodedMethod(
+                  callTarget,
+                  newAccessFlags,
+                  encodedMethod.annotations,
+                  encodedMethod.parameterAnnotationsList,
+                  encodedMethod.getCode());
+          newMethod.copyMetadata(encodedMethod);
+          rewriter.methodMapping.put(encodedMethod.method, callTarget);
+          // Move the method from the direct methods to the virtual methods set.
+          implMethodHolder.removeDirectMethod(i);
+          implMethodHolder.appendVirtualMethod(newMethod);
+          return true;
+        }
+      }
       return false;
     }
   }
diff --git a/src/test/java/com/android/tools/r8/TestCompilerBuilder.java b/src/test/java/com/android/tools/r8/TestCompilerBuilder.java
index 77dc0df..9e8f74a 100644
--- a/src/test/java/com/android/tools/r8/TestCompilerBuilder.java
+++ b/src/test/java/com/android/tools/r8/TestCompilerBuilder.java
@@ -113,6 +113,12 @@
     return setMode(CompilationMode.RELEASE);
   }
 
+  public T setMinApiThreshold(AndroidApiLevel minApiThreshold) {
+    assert backend == Backend.DEX;
+    AndroidApiLevel minApi = ToolHelper.getMinApiLevelForDexVmNoHigherThan(minApiThreshold);
+    return setMinApi(minApi);
+  }
+
   public T setMinApi(AndroidApiLevel minApiLevel) {
     // Should we ignore min-api calls when backend == CF?
     this.defaultMinApiLevel = null;
diff --git a/src/test/java/com/android/tools/r8/debug/DebugTestBase.java b/src/test/java/com/android/tools/r8/debug/DebugTestBase.java
index 1c0287e..13d59dc 100644
--- a/src/test/java/com/android/tools/r8/debug/DebugTestBase.java
+++ b/src/test/java/com/android/tools/r8/debug/DebugTestBase.java
@@ -336,6 +336,10 @@
     return breakpoint(method.getHolderClass().getTypeName(), method.getMethodName());
   }
 
+  protected final JUnit3Wrapper.Command breakpoint(MethodReference method, int line) {
+    return breakpoint(method.getHolderClass().getTypeName(), method.getMethodName(), line);
+  }
+
   protected final JUnit3Wrapper.Command breakpoint(String className, String methodName) {
     return breakpoint(className, methodName, null);
   }
diff --git a/src/test/java/com/android/tools/r8/debug/LambdaOuterContextTestRunner.java b/src/test/java/com/android/tools/r8/debug/LambdaOuterContextTestRunner.java
index 8af2a6e..99a19f5 100644
--- a/src/test/java/com/android/tools/r8/debug/LambdaOuterContextTestRunner.java
+++ b/src/test/java/com/android/tools/r8/debug/LambdaOuterContextTestRunner.java
@@ -27,7 +27,6 @@
   }
 
   @Test
-  @Ignore("b/123068053")
   public void testD8() throws Throwable {
     D8TestCompileResult compileResult =
         testForD8().addProgramClassesAndInnerClasses(CLASS).compile();
diff --git a/src/test/java/com/android/tools/r8/desugar/DefaultLambdaWithInvokeInterfaceTest.java b/src/test/java/com/android/tools/r8/desugar/DefaultLambdaWithInvokeInterfaceTest.java
index e9b1b8c..9e8741d 100644
--- a/src/test/java/com/android/tools/r8/desugar/DefaultLambdaWithInvokeInterfaceTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/DefaultLambdaWithInvokeInterfaceTest.java
@@ -6,7 +6,7 @@
 public class DefaultLambdaWithInvokeInterfaceTest {
 
   public interface I {
-    int run();
+    String run();
   }
 
   public interface J {
@@ -15,11 +15,17 @@
     }
 
     default I stateful() {
-      return () -> stateless().length();
+      return () -> {
+        String stateless = stateless();
+        return "stateful(" + stateless + ")";
+      };
     }
   }
 
   public static void main(String[] args) {
-    System.out.println(new J() {}.stateful().run());
+    J j = new J() {};
+    I stateful = j.stateful();
+    String run = stateful.run();
+    System.out.println(run);
   }
 }
diff --git a/src/test/java/com/android/tools/r8/desugar/DefaultLambdaWithInvokeInterfaceTestRunner.java b/src/test/java/com/android/tools/r8/desugar/DefaultLambdaWithInvokeInterfaceTestRunner.java
index 732612d..b89f09a 100644
--- a/src/test/java/com/android/tools/r8/desugar/DefaultLambdaWithInvokeInterfaceTestRunner.java
+++ b/src/test/java/com/android/tools/r8/desugar/DefaultLambdaWithInvokeInterfaceTestRunner.java
@@ -6,30 +6,83 @@
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static org.hamcrest.MatcherAssert.assertThat;
 
+import com.android.tools.r8.D8TestCompileResult;
+import com.android.tools.r8.R8TestCompileResult;
 import com.android.tools.r8.TestBase;
+import com.android.tools.r8.debug.DebugTestBase;
+import com.android.tools.r8.debug.DebugTestBase.JUnit3Wrapper.Command;
+import com.android.tools.r8.debug.DebugTestConfig;
+import com.android.tools.r8.references.MethodReference;
+import com.android.tools.r8.references.Reference;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.StringUtils;
+import com.google.common.collect.ImmutableList;
+import java.util.Collections;
 import org.junit.Test;
 
-public class DefaultLambdaWithInvokeInterfaceTestRunner extends TestBase {
+public class DefaultLambdaWithInvokeInterfaceTestRunner extends DebugTestBase {
 
   final Class<?> CLASS = DefaultLambdaWithInvokeInterfaceTest.class;
-  final String EXPECTED = StringUtils.lines("4");
+  final String EXPECTED = StringUtils.lines("stateful(hest)");
 
   @Test
   public void testJvm() throws Exception {
     testForJvm().addTestClasspath().run(CLASS).assertSuccessWithOutput(EXPECTED);
   }
 
+  private void runDebugger(DebugTestConfig config) throws Throwable {
+    MethodReference main = Reference.methodFromMethod(CLASS.getMethod("main", String[].class));
+    Command checkThis = conditional((state) ->
+        state.isCfRuntime()
+            ? Collections.singletonList(checkLocal("this"))
+            : ImmutableList.of(
+                checkNoLocal("this"),
+                checkLocal("_this")));
+
+    runDebugTest(config, CLASS,
+        breakpoint(main, 27),
+        run(),
+        checkLine(27),
+        stepInto(INTELLIJ_FILTER),
+        checkLine(18),
+        checkThis,
+        breakpoint(main, 28),
+        run(),
+        checkLine(28),
+        checkLocal("stateful"),
+        stepInto(INTELLIJ_FILTER),
+        checkLine(19),
+        checkThis,
+        run());
+  }
+
   @Test
-  public void test() throws Exception {
-    testForD8()
+  public void testR8Cf() throws Throwable {
+    R8TestCompileResult compileResult = testForR8(Backend.CF)
         .addProgramClassesAndInnerClasses(CLASS)
-        .setMinApi(AndroidApiLevel.K)
-        .compile()
+        .noMinification()
+        .noTreeShaking()
+        .debug()
+        .compile();
+    compileResult
         // TODO(b/123506120): Add .assertNoMessages()
         .run(CLASS)
         .assertSuccessWithOutput(EXPECTED)
         .inspect(inspector -> assertThat(inspector.clazz(CLASS), isPresent()));
+    runDebugger(compileResult.debugConfig());
+  }
+
+  @Test
+  public void testD8() throws Throwable {
+    D8TestCompileResult compileResult = testForD8()
+        .addProgramClassesAndInnerClasses(CLASS)
+        .setMinApi(AndroidApiLevel.K)
+        .compile();
+    compileResult
+        // TODO(b/123506120): Add .assertNoMessages()
+        .run(CLASS)
+        .assertSuccessWithOutput(EXPECTED)
+        .inspect(inspector -> assertThat(inspector.clazz(CLASS), isPresent()));
+    runDebugger(compileResult.debugConfig());
   }
 }
diff --git a/src/test/java/com/android/tools/r8/desugar/DefaultLambdaWithSelfReferenceTest.java b/src/test/java/com/android/tools/r8/desugar/DefaultLambdaWithSelfReferenceTest.java
index eb4e75b..e122450 100644
--- a/src/test/java/com/android/tools/r8/desugar/DefaultLambdaWithSelfReferenceTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/DefaultLambdaWithSelfReferenceTest.java
@@ -13,11 +13,18 @@
     }
 
     default I stateful() {
-      return () -> "stateful(" + stateless().foo() + ")";
+      return () -> {
+        I stateless = stateless();
+        String foo = stateless.foo();
+        return "stateful(" + foo + ")";
+      };
     }
   }
 
   public static void main(String[] args) {
-    System.out.println(((I) () -> "foo").stateful().foo());
+    I i = () -> "foo";
+    I stateful = i.stateful();
+    String foo = stateful.foo();
+    System.out.println(foo);
   }
 }
diff --git a/src/test/java/com/android/tools/r8/desugar/DefaultLambdaWithSelfReferenceTestRunner.java b/src/test/java/com/android/tools/r8/desugar/DefaultLambdaWithSelfReferenceTestRunner.java
index bc193c8..e209e44 100644
--- a/src/test/java/com/android/tools/r8/desugar/DefaultLambdaWithSelfReferenceTestRunner.java
+++ b/src/test/java/com/android/tools/r8/desugar/DefaultLambdaWithSelfReferenceTestRunner.java
@@ -3,14 +3,24 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.desugar;
 
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertEquals;
 
+import com.android.tools.r8.D8TestCompileResult;
 import com.android.tools.r8.Disassemble;
 import com.android.tools.r8.Disassemble.DisassembleCommand;
-import com.android.tools.r8.TestBase;
+import com.android.tools.r8.JvmTestBuilder;
+import com.android.tools.r8.R8TestCompileResult;
 import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.debug.DebugTestBase;
+import com.android.tools.r8.debug.DebugTestBase.JUnit3Wrapper.Command;
+import com.android.tools.r8.debug.DebugTestConfig;
+import com.android.tools.r8.references.MethodReference;
+import com.android.tools.r8.references.Reference;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.StringUtils;
+import com.google.common.collect.ImmutableList;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.ArrayList;
@@ -19,18 +29,63 @@
 import java.util.List;
 import org.junit.Test;
 
-public class DefaultLambdaWithSelfReferenceTestRunner extends TestBase {
+public class DefaultLambdaWithSelfReferenceTestRunner extends DebugTestBase {
 
   final Class<?> CLASS = DefaultLambdaWithSelfReferenceTest.class;
   final String EXPECTED = StringUtils.lines("stateful(stateless)");
 
-  @Test
-  public void testJvm() throws Exception {
-    testForJvm().addTestClasspath().run(CLASS).assertSuccessWithOutput(EXPECTED);
+  private void runDebugger(DebugTestConfig config) throws Throwable {
+    MethodReference main = Reference.methodFromMethod(CLASS.getMethod("main", String[].class));
+    Command checkThis = conditional((state) ->
+        state.isCfRuntime()
+            ? Collections.singletonList(checkLocal("this"))
+            : ImmutableList.of(
+                checkNoLocal("this"),
+                checkLocal("_this")));
+
+    runDebugTest(config, CLASS,
+        breakpoint(main, 26),
+        run(),
+        checkLine(26),
+        stepInto(INTELLIJ_FILTER),
+        checkLine(16),
+        // When desugaring, the InterfaceProcessor makes this static on the companion class.
+        checkThis,
+        breakpoint(main, 27),
+        run(),
+        checkLine(27),
+        stepInto(INTELLIJ_FILTER),
+        checkLine(17),
+        // When desugaring, the LambdaClass will change this to a static (later moved to companion).
+        checkThis,
+        run());
   }
 
   @Test
-  public void test() throws Exception {
+  public void testJvm() throws Throwable {
+    JvmTestBuilder builder = testForJvm().addTestClasspath();
+    builder.run(CLASS).assertSuccessWithOutput(EXPECTED);
+    runDebugger(builder.debugConfig());
+  }
+
+  @Test
+  public void testR8Cf() throws Throwable {
+    R8TestCompileResult compileResult = testForR8(Backend.CF)
+        .addProgramClassesAndInnerClasses(CLASS)
+        .noMinification()
+        .noTreeShaking()
+        .debug()
+        .compile();
+    compileResult
+        // TODO(b/123506120): Add .assertNoMessages()
+        .run(CLASS)
+        .assertSuccessWithOutput(EXPECTED)
+        .inspect(inspector -> assertThat(inspector.clazz(CLASS), isPresent()));
+    runDebugger(compileResult.debugConfig());
+  }
+
+  @Test
+  public void testD8() throws Throwable {
     Path out1 = temp.newFolder().toPath().resolve("out1.zip");
     testForD8()
         .addProgramClassesAndInnerClasses(CLASS)
@@ -73,14 +128,18 @@
     }
 
     Path out2 = temp.newFolder().toPath().resolve("out2.zip");
-    testForD8()
+    D8TestCompileResult compiledResult = testForD8()
         .addProgramFiles(outs)
-        .compile()
+        .compile();
+
+    compiledResult
         // TODO(b/123506120): Add .assertNoMessages()
         .writeToZip(out2)
         .run(CLASS)
         .assertSuccessWithOutput(EXPECTED);
 
+    runDebugger(compiledResult.debugConfig());
+
     Path dissasemble1 = temp.newFolder().toPath().resolve("disassemble1.txt");
     Path dissasemble2 = temp.newFolder().toPath().resolve("disassemble2.txt");
     Disassemble.disassemble(
diff --git a/src/test/java/com/android/tools/r8/desugar/DefaultLambdaWithUnderscoreThisTest.java b/src/test/java/com/android/tools/r8/desugar/DefaultLambdaWithUnderscoreThisTest.java
new file mode 100644
index 0000000..7df85c2
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/DefaultLambdaWithUnderscoreThisTest.java
@@ -0,0 +1,26 @@
+// 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;
+
+public class DefaultLambdaWithUnderscoreThisTest {
+
+  public interface I {
+    String foo();
+
+    default I stateful() {
+      String _this = "My _this variable";
+      return () -> {
+        String ___this = "Another ___this variable";
+        return "stateful(" + _this + " " + foo() + " " + ___this + ")";
+      };
+    }
+  }
+
+  public static void main(String[] args) {
+    I i = () -> "foo";
+    I stateful = i.stateful();
+    String foo = stateful.foo();
+    System.out.println(foo);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/DefaultLambdaWithUnderscoreThisTestRunner.java b/src/test/java/com/android/tools/r8/desugar/DefaultLambdaWithUnderscoreThisTestRunner.java
new file mode 100644
index 0000000..10d74a8
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/DefaultLambdaWithUnderscoreThisTestRunner.java
@@ -0,0 +1,81 @@
+// 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;
+
+import com.android.tools.r8.D8TestCompileResult;
+import com.android.tools.r8.JvmTestBuilder;
+import com.android.tools.r8.debug.DebugTestBase;
+import com.android.tools.r8.debug.DebugTestBase.JUnit3Wrapper.Command;
+import com.android.tools.r8.debug.DebugTestConfig;
+import com.android.tools.r8.desugar.DefaultLambdaWithUnderscoreThisTest.I;
+import com.android.tools.r8.references.MethodReference;
+import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.StringUtils;
+import com.google.common.collect.ImmutableList;
+import java.util.Collections;
+import java.util.function.Function;
+import org.junit.Test;
+
+public class DefaultLambdaWithUnderscoreThisTestRunner extends DebugTestBase {
+
+  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 {
+    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)));
+
+    runDebugTest(config, CLASS,
+        breakpoint(main, 22),
+        run(),
+        checkLine(22),
+        stepInto(INTELLIJ_FILTER),
+        checkLine(12),
+        stepInto(INTELLIJ_FILTER),
+        checkLine(13),
+        // Desugaring will insert '__this' in place of 'this' here.
+        checkThis.apply("__this"),
+        checkLocal("_this"),
+        breakpoint(main, 23),
+        run(),
+        checkLine(23),
+        stepInto(INTELLIJ_FILTER),
+        checkLine(14),
+        stepInto(INTELLIJ_FILTER),
+        checkLine(15),
+        // Desugaring will insert '____this' in place of 'this' here.
+        checkThis.apply("____this"),
+        checkLocals("_this",  "___this"),
+        run());
+  }
+
+  @Test
+  public void testJvm() throws Throwable {
+    JvmTestBuilder builder = testForJvm().addTestClasspath();
+    builder.run(CLASS).assertSuccessWithOutput(EXPECTED);
+    runDebugger(builder.debugConfig());
+  }
+
+  @Test
+  public void testD8() throws Throwable {
+    D8TestCompileResult compileResult = testForD8()
+        .addProgramClassesAndInnerClasses(CLASS)
+        .setMinApiThreshold(AndroidApiLevel.K)
+        .compile();
+    compileResult
+        // TODO(b/123506120): Add .assertNoMessages()
+        .run(CLASS)
+        .assertSuccessWithOutput(EXPECTED);
+    runDebugger(compileResult.debugConfig());
+  }
+}