Version 1.4.44

Cherry pick: Make lambda instance methods when possible.
CL: https://r8-review.googlesource.com/c/r8/+/34305

Cherry pick: Re-apply "Add tests showing issue with missing classes when desugaring."
CL: https://r8-review.googlesource.com/c/r8/+/33881

Cherry pick: Add regression test for missing locals in desugared lambdas.
CL: https://r8-review.googlesource.com/c/r8/+/33241/

Cherry pick: Set min-api thresholds in some misconfigured tests.
CL: https://r8-review.googlesource.com/c/r8/+/34120

Bug: 123068053
Change-Id: I8c2432aaef0cfcd4d06b5b386ed9b554c680a155
diff --git a/src/main/java/com/android/tools/r8/Version.java b/src/main/java/com/android/tools/r8/Version.java
index c9938d3..112d49a 100644
--- a/src/main/java/com/android/tools/r8/Version.java
+++ b/src/main/java/com/android/tools/r8/Version.java
@@ -11,7 +11,7 @@
 
   // This field is accessed from release scripts using simple pattern matching.
   // Therefore, changing this field could break our release scripts.
-  public static final String LABEL = "1.4.43";
+  public static final String LABEL = "1.4.44";
 
   private Version() {
   }
diff --git a/src/main/java/com/android/tools/r8/graph/DexByteCodeWriter.java b/src/main/java/com/android/tools/r8/graph/DexByteCodeWriter.java
index 19d7e24..60893c7 100644
--- a/src/main/java/com/android/tools/r8/graph/DexByteCodeWriter.java
+++ b/src/main/java/com/android/tools/r8/graph/DexByteCodeWriter.java
@@ -61,7 +61,8 @@
   private void write(ThrowingFunction<DexClass, PrintStream, IOException> outputStreamProvider,
       Consumer<PrintStream> closer)
       throws IOException {
-    for (DexProgramClass clazz : application.classes()) {
+    Iterable<DexProgramClass> classes = application.classesWithDeterministicOrder();
+    for (DexProgramClass clazz : classes) {
       if (anyMethodMatches(clazz)) {
         PrintStream ps = outputStreamProvider.apply(clazz);
         try {
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 fab4a83..b189f6f 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;
@@ -99,17 +104,43 @@
     this.debugInfo = debugInfo;
   }
 
-  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 9fe23de..9d5e5fc 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.setStatic();
         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);
 
@@ -154,8 +151,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/D8TestBuilder.java b/src/test/java/com/android/tools/r8/D8TestBuilder.java
index 37e4dec..c807a5d 100644
--- a/src/test/java/com/android/tools/r8/D8TestBuilder.java
+++ b/src/test/java/com/android/tools/r8/D8TestBuilder.java
@@ -87,8 +87,8 @@
     return self();
   }
 
-  public D8TestBuilder setIntermediate(boolean b) {
-    builder.setIntermediate(true);
+  public D8TestBuilder setIntermediate(boolean intermediate) {
+    builder.setIntermediate(intermediate);
     return self();
   }
 }
diff --git a/src/test/java/com/android/tools/r8/R8RunExamplesAndroidOTest.java b/src/test/java/com/android/tools/r8/R8RunExamplesAndroidOTest.java
index 003607b..fb69657 100644
--- a/src/test/java/com/android/tools/r8/R8RunExamplesAndroidOTest.java
+++ b/src/test/java/com/android/tools/r8/R8RunExamplesAndroidOTest.java
@@ -100,7 +100,7 @@
   @Test
   public void lambdaDesugaring() throws Throwable {
     test("lambdadesugaring", "lambdadesugaring", "LambdaDesugaring")
-        .withMinApiLevel(AndroidApiLevel.K)
+        .withMinApiLevel(ToolHelper.getMinApiLevelForDexVmNoHigherThan(AndroidApiLevel.K))
         .withOptionConsumer(opts -> opts.enableClassInlining = false)
         .withBuilderTransformation(
             b -> b.addProguardConfiguration(PROGUARD_OPTIONS, Origin.unknown()))
@@ -108,7 +108,7 @@
         .run();
 
     test("lambdadesugaring", "lambdadesugaring", "LambdaDesugaring")
-        .withMinApiLevel(AndroidApiLevel.K)
+        .withMinApiLevel(ToolHelper.getMinApiLevelForDexVmNoHigherThan(AndroidApiLevel.K))
         .withOptionConsumer(opts -> opts.enableClassInlining = true)
         .withBuilderTransformation(
             b -> b.addProguardConfiguration(PROGUARD_OPTIONS, Origin.unknown()))
@@ -144,7 +144,7 @@
   @Test
   public void lambdaDesugaringNPlus() throws Throwable {
     test("lambdadesugaringnplus", "lambdadesugaringnplus", "LambdasWithStaticAndDefaultMethods")
-        .withMinApiLevel(AndroidApiLevel.K)
+        .withMinApiLevel(ToolHelper.getMinApiLevelForDexVmNoHigherThan(AndroidApiLevel.K))
         .withInterfaceMethodDesugaring(OffOrAuto.Auto)
         .withOptionConsumer(opts -> opts.enableClassInlining = false)
         .withBuilderTransformation(
@@ -153,7 +153,7 @@
         .run();
 
     test("lambdadesugaringnplus", "lambdadesugaringnplus", "LambdasWithStaticAndDefaultMethods")
-        .withMinApiLevel(AndroidApiLevel.K)
+        .withMinApiLevel(ToolHelper.getMinApiLevelForDexVmNoHigherThan(AndroidApiLevel.K))
         .withInterfaceMethodDesugaring(OffOrAuto.Auto)
         .withOptionConsumer(opts -> opts.enableClassInlining = true)
         .withBuilderTransformation(
diff --git a/src/test/java/com/android/tools/r8/RunExamplesAndroidOTest.java b/src/test/java/com/android/tools/r8/RunExamplesAndroidOTest.java
index 4f98b6f..d103efc 100644
--- a/src/test/java/com/android/tools/r8/RunExamplesAndroidOTest.java
+++ b/src/test/java/com/android/tools/r8/RunExamplesAndroidOTest.java
@@ -362,7 +362,7 @@
   @Test
   public void lambdaDesugaring() throws Throwable {
     test("lambdadesugaring", "lambdadesugaring", "LambdaDesugaring")
-        .withMinApiLevel(AndroidApiLevel.K)
+        .withMinApiLevel(ToolHelper.getMinApiLevelForDexVmNoHigherThan(AndroidApiLevel.K))
         .withKeepAll()
         .run();
   }
@@ -370,7 +370,7 @@
   @Test
   public void lambdaDesugaringNPlus() throws Throwable {
     test("lambdadesugaringnplus", "lambdadesugaringnplus", "LambdasWithStaticAndDefaultMethods")
-        .withMinApiLevel(AndroidApiLevel.K)
+        .withMinApiLevel(ToolHelper.getMinApiLevelForDexVmNoHigherThan(AndroidApiLevel.K))
         .withInterfaceMethodDesugaring(OffOrAuto.Auto)
         .withKeepAll()
         .run();
@@ -399,7 +399,7 @@
   @Test
   public void lambdaDesugaringValueAdjustments() throws Throwable {
     test("lambdadesugaring-value-adjustments", "lambdadesugaring", "ValueAdjustments")
-        .withMinApiLevel(AndroidApiLevel.K)
+        .withMinApiLevel(ToolHelper.getMinApiLevelForDexVmNoHigherThan(AndroidApiLevel.K))
         .withKeepAll()
         .run();
   }
@@ -631,7 +631,7 @@
             output.replace("\r", "").equals(javaResult.stdout.replace("\r", "")));
       }
     } catch (Throwable t) {
-      assert expectedToFail;
+      assertTrue("Test was not expected to fail. Failed with " + t.getMessage(), expectedToFail);
     }
   }
 
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/TestDiagnosticMessagesImpl.java b/src/test/java/com/android/tools/r8/TestDiagnosticMessagesImpl.java
index 43d71b1..a0ed928 100644
--- a/src/test/java/com/android/tools/r8/TestDiagnosticMessagesImpl.java
+++ b/src/test/java/com/android/tools/r8/TestDiagnosticMessagesImpl.java
@@ -8,6 +8,7 @@
 import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.fail;
 
+import com.android.tools.r8.utils.ListUtils;
 import java.util.ArrayList;
 import java.util.List;
 import org.hamcrest.Matcher;
@@ -44,25 +45,34 @@
     return errors;
   }
 
+  private void assertEmpty(String type, List<Diagnostic> messages) {
+    assertEquals(
+        "Expected no "
+            + type
+            + " messages, got:\n"
+            + String.join("\n", ListUtils.map(messages, m -> m.getDiagnosticMessage())),
+        0,
+        messages.size());
+  }
 
   public TestDiagnosticMessages assertNoMessages() {
-    assertEquals(0, getInfos().size());
-    assertEquals(0, getWarnings().size());
-    assertEquals(0, getErrors().size());
+    assertEmpty("info", getInfos());
+    assertEmpty("warning", getWarnings());
+    assertEmpty("error", getErrors());
     return this;
   }
 
   public TestDiagnosticMessages assertOnlyInfos() {
     assertNotEquals(0, getInfos().size());
-    assertEquals(0, getWarnings().size());
-    assertEquals(0, getErrors().size());
+    assertEmpty("warning", getWarnings());
+    assertEmpty("error", getErrors());
     return this;
   }
 
   public TestDiagnosticMessages assertOnlyWarnings() {
-    assertEquals(0, getInfos().size());
+    assertEmpty("info", getInfos());
     assertNotEquals(0, getWarnings().size());
-    assertEquals(0, getErrors().size());
+    assertEmpty("error", getErrors());
     return this;
   }
 
diff --git a/src/test/java/com/android/tools/r8/ToolHelper.java b/src/test/java/com/android/tools/r8/ToolHelper.java
index dcfda2c..f53b8e6 100644
--- a/src/test/java/com/android/tools/r8/ToolHelper.java
+++ b/src/test/java/com/android/tools/r8/ToolHelper.java
@@ -735,6 +735,11 @@
     }
   }
 
+  public static AndroidApiLevel getMinApiLevelForDexVmNoHigherThan(AndroidApiLevel threshold) {
+    AndroidApiLevel minApiLevelForDexVm = getMinApiLevelForDexVm();
+    return minApiLevelForDexVm.getLevel() < threshold.getLevel() ? minApiLevelForDexVm : threshold;
+  }
+
   public static AndroidApiLevel getMinApiLevelForDexVm() {
     return getMinApiLevelForDexVm(ToolHelper.getDexVm());
   }
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 fb61f45..13d59dc 100644
--- a/src/test/java/com/android/tools/r8/debug/DebugTestBase.java
+++ b/src/test/java/com/android/tools/r8/debug/DebugTestBase.java
@@ -15,6 +15,7 @@
 import com.android.tools.r8.naming.MemberNaming;
 import com.android.tools.r8.naming.MemberNaming.MethodSignature;
 import com.android.tools.r8.naming.MemberNaming.Signature;
+import com.android.tools.r8.references.MethodReference;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.TestDescriptionWatcher;
@@ -50,6 +51,7 @@
 import org.apache.harmony.jpda.tests.framework.jdwp.EventPacket;
 import org.apache.harmony.jpda.tests.framework.jdwp.Frame.Variable;
 import org.apache.harmony.jpda.tests.framework.jdwp.JDWPCommands;
+import org.apache.harmony.jpda.tests.framework.jdwp.JDWPCommands.ObjectReferenceCommandSet;
 import org.apache.harmony.jpda.tests.framework.jdwp.JDWPCommands.ReferenceTypeCommandSet;
 import org.apache.harmony.jpda.tests.framework.jdwp.JDWPCommands.StackFrameCommandSet;
 import org.apache.harmony.jpda.tests.framework.jdwp.JDWPConstants;
@@ -153,6 +155,12 @@
   }
 
   protected final void runDebugTest(
+      DebugTestConfig config, Class<?> debuggeeClass, JUnit3Wrapper.Command... commands)
+      throws Throwable {
+    runInternal(config, debuggeeClass.getTypeName(), Arrays.asList(commands));
+  }
+
+  protected final void runDebugTest(
       DebugTestConfig config, String debuggeeClass, JUnit3Wrapper.Command... commands)
       throws Throwable {
     runInternal(config, debuggeeClass, Arrays.asList(commands));
@@ -324,6 +332,14 @@
     return new JUnit3Wrapper.Command.RunCommand();
   }
 
+  protected final JUnit3Wrapper.Command breakpoint(MethodReference method) {
+    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);
   }
@@ -477,6 +493,10 @@
     });
   }
 
+  protected final JUnit3Wrapper.Command checkLine(int line) {
+    return inspect(t -> t.checkLine(null, line));
+  }
+
   protected final JUnit3Wrapper.Command checkLine(String sourceFile, int line) {
     return inspect(t -> t.checkLine(sourceFile, line));
   }
@@ -532,6 +552,16 @@
     });
   }
 
+  protected final JUnit3Wrapper.Command checkFieldOnThis(
+      String fieldName, String fieldSignature, Value expectedValue) {
+    return inspect(
+        t -> {
+          Value value = t.getFieldOnThis(fieldName, fieldSignature);
+          Assert.assertEquals(
+              "Incorrect value for field 'this." + fieldName + "'", expectedValue, value);
+        });
+  }
+
   protected final JUnit3Wrapper.Command inspect(Consumer<JUnit3Wrapper.DebuggeeState> inspector) {
     return t -> inspector.accept(t.debuggeeState);
   }
@@ -1298,6 +1328,7 @@
 
         @Override
         public void checkLine(String sourceFile, int line) {
+          sourceFile = sourceFile != null ? sourceFile : getSourceFile();
           if (!Objects.equals(sourceFile, getSourceFile()) || line != getLineNumber()) {
             String locationString = convertCurrentLocationToString();
             Assert.fail(
@@ -1501,7 +1532,15 @@
 
         // The class is available, lookup and read the field.
         long fieldId = findField(getMirror(), classId, fieldName, fieldSignature);
-        return getField(getMirror(), classId, fieldId);
+        return internalStaticField(getMirror(), classId, fieldId);
+      }
+
+      public Value getFieldOnThis(String fieldName, String fieldSignature) {
+        long thisObjectId = getMirror().getThisObject(getThreadId(), getFrameId());
+        long classId = getMirror().getReferenceType(thisObjectId);
+        // TODO(zerny): Search supers too. This will only get the field if directly on the class.
+        long fieldId = findField(getMirror(), classId, fieldName, fieldSignature);
+        return internalInstanceField(getMirror(), thisObjectId, fieldId);
       }
 
       private long findField(VmMirror mirror, long classId, String fieldName,
@@ -1547,10 +1586,10 @@
         return matchingFieldIds.getLong(0);
       }
 
-      private Value getField(VmMirror mirror, long classId, long fieldId) {
-
-        CommandPacket commandPacket = new CommandPacket(ReferenceTypeCommandSet.CommandSetID,
-            ReferenceTypeCommandSet.GetValuesCommand);
+      private static Value internalStaticField(VmMirror mirror, long classId, long fieldId) {
+        CommandPacket commandPacket =
+            new CommandPacket(
+                ReferenceTypeCommandSet.CommandSetID, ReferenceTypeCommandSet.GetValuesCommand);
         commandPacket.setNextValueAsReferenceTypeID(classId);
         commandPacket.setNextValueAsInt(1);
         commandPacket.setNextValueAsFieldID(fieldId);
@@ -1565,6 +1604,23 @@
       }
     }
 
+    private static Value internalInstanceField(VmMirror mirror, long objectId, long fieldId) {
+      CommandPacket commandPacket =
+          new CommandPacket(
+              ObjectReferenceCommandSet.CommandSetID, ObjectReferenceCommandSet.GetValuesCommand);
+      commandPacket.setNextValueAsObjectID(objectId);
+      commandPacket.setNextValueAsInt(1);
+      commandPacket.setNextValueAsFieldID(fieldId);
+      ReplyPacket replyPacket = mirror.performCommand(commandPacket);
+      assert replyPacket.getErrorCode() == Error.NONE;
+
+      int fieldsCount = replyPacket.getNextValueAsInt();
+      assert fieldsCount == 1;
+      Value result = replyPacket.getNextValueAsValue();
+      Assert.assertTrue(replyPacket.isAllDataRead());
+      return result;
+    }
+
     public static Optional<Variable> getVariableAt(VmMirror mirror, Location location,
         String localName) {
       return getVariablesAt(mirror, location).stream()
diff --git a/src/test/java/com/android/tools/r8/debug/LambdaOuterContextTest.java b/src/test/java/com/android/tools/r8/debug/LambdaOuterContextTest.java
new file mode 100644
index 0000000..9991b57
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/debug/LambdaOuterContextTest.java
@@ -0,0 +1,32 @@
+// 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.debug;
+
+public class LambdaOuterContextTest {
+
+  public interface Converter {
+    String convert(int value);
+  }
+
+  public int outer;
+
+  public LambdaOuterContextTest(int outer) {
+    this.outer = outer;
+  }
+
+  public void foo(Converter converter) {
+    System.out.println(converter.convert(outer));
+  }
+
+  public void bar(int arg) {
+    foo(value -> {
+      // Ensure lambda uses parts of the outer context, otherwise javac will optimize it out.
+      return Integer.toString(outer + value + arg);
+    });
+  }
+
+  public static void main(String[] args) {
+    new LambdaOuterContextTest(args.length + 42).bar(args.length);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/debug/LambdaOuterContextTestRunner.java b/src/test/java/com/android/tools/r8/debug/LambdaOuterContextTestRunner.java
new file mode 100644
index 0000000..99a19f5
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/debug/LambdaOuterContextTestRunner.java
@@ -0,0 +1,66 @@
+// 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.debug;
+
+import static com.android.tools.r8.references.Reference.methodFromMethod;
+
+import com.android.tools.r8.D8TestCompileResult;
+import com.android.tools.r8.JvmTestBuilder;
+import com.android.tools.r8.R8TestCompileResult;
+import com.android.tools.r8.debug.LambdaOuterContextTest.Converter;
+import com.android.tools.r8.utils.StringUtils;
+import org.apache.harmony.jpda.tests.framework.jdwp.Value;
+import org.junit.Ignore;
+import org.junit.Test;
+
+public class LambdaOuterContextTestRunner extends DebugTestBase {
+
+  public static final Class<?> CLASS = LambdaOuterContextTest.class;
+  public static final String EXPECTED = StringUtils.lines("84");
+
+  @Test
+  public void testJvm() throws Throwable {
+    JvmTestBuilder jvmTestBuilder = testForJvm().addTestClasspath();
+    jvmTestBuilder.run(CLASS).assertSuccessWithOutput(EXPECTED);
+    runDebugger(jvmTestBuilder.debugConfig());
+  }
+
+  @Test
+  public void testD8() throws Throwable {
+    D8TestCompileResult compileResult =
+        testForD8().addProgramClassesAndInnerClasses(CLASS).compile();
+    compileResult.run(CLASS).assertSuccessWithOutput(EXPECTED);
+    runDebugger(compileResult.debugConfig());
+  }
+
+  @Test
+  public void testR8Cf() throws Throwable {
+    R8TestCompileResult compileResult =
+        testForR8(Backend.CF)
+            .addProgramClassesAndInnerClasses(CLASS)
+            .debug()
+            .noMinification()
+            .noTreeShaking()
+            .compile();
+    compileResult.run(CLASS).assertSuccessWithOutput(EXPECTED);
+    runDebugger(compileResult.debugConfig());
+  }
+
+  private void runDebugger(DebugTestConfig config) throws Throwable {
+    runDebugTest(
+        config,
+        CLASS,
+        breakpoint(methodFromMethod(CLASS.getMethod("foo", Converter.class))),
+        run(),
+        checkLine(19),
+        checkLocals("this", "converter"),
+        checkFieldOnThis("outer", null, Value.createInt(42)),
+        stepInto(INTELLIJ_FILTER),
+        checkLine(25),
+        checkLocals("this", "value", "arg"),
+        checkNoLocal("outer"),
+        checkFieldOnThis("outer", null, Value.createInt(42)),
+        run());
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/debug/LambdaTest.java b/src/test/java/com/android/tools/r8/debug/LambdaTest.java
index 1b413ba..f9e6c34 100644
--- a/src/test/java/com/android/tools/r8/debug/LambdaTest.java
+++ b/src/test/java/com/android/tools/r8/debug/LambdaTest.java
@@ -13,7 +13,6 @@
 import org.junit.runners.Parameterized;
 import org.junit.runners.Parameterized.Parameters;
 
-// TODO(shertz) test local variables
 @RunWith(Parameterized.class)
 public class LambdaTest extends DebugTestBase {
 
@@ -47,8 +46,11 @@
         run(),
         checkMethod(debuggeeClass, initialMethodName),
         checkLine(SOURCE_FILE, 12),
+        checkLocals("i"),
+        checkNoLocal("j"),
         stepInto(INTELLIJ_FILTER),
         checkLine(SOURCE_FILE, 16),
+        checkLocals("i", "j"),
         run());
   }
 
@@ -63,8 +65,10 @@
         run(),
         checkMethod(debuggeeClass, initialMethodName),
         checkLine(SOURCE_FILE, 32),
+        checkLocals("i", "a", "b"),
         stepInto(INTELLIJ_FILTER),
         checkLine(SOURCE_FILE, 37),
+        checkLocals("a", "b"),
         run());
   }
 
diff --git a/src/test/java/com/android/tools/r8/desugar/DefaultLambdaWithInvokeInterfaceTest.java b/src/test/java/com/android/tools/r8/desugar/DefaultLambdaWithInvokeInterfaceTest.java
new file mode 100644
index 0000000..9e8741d
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/DefaultLambdaWithInvokeInterfaceTest.java
@@ -0,0 +1,31 @@
+// 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 DefaultLambdaWithInvokeInterfaceTest {
+
+  public interface I {
+    String run();
+  }
+
+  public interface J {
+    default String stateless() {
+      return "hest";
+    }
+
+    default I stateful() {
+      return () -> {
+        String stateless = stateless();
+        return "stateful(" + stateless + ")";
+      };
+    }
+  }
+
+  public static void main(String[] args) {
+    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
new file mode 100644
index 0000000..b89f09a
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/DefaultLambdaWithInvokeInterfaceTestRunner.java
@@ -0,0 +1,88 @@
+// 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 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 DebugTestBase {
+
+  final Class<?> CLASS = DefaultLambdaWithInvokeInterfaceTest.class;
+  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 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 {
+    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
new file mode 100644
index 0000000..e122450
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/DefaultLambdaWithSelfReferenceTest.java
@@ -0,0 +1,30 @@
+// 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 DefaultLambdaWithSelfReferenceTest {
+
+  interface I {
+    String foo();
+
+    default I stateless() {
+      return () -> "stateless";
+    }
+
+    default I stateful() {
+      return () -> {
+        I stateless = stateless();
+        String foo = stateless.foo();
+        return "stateful(" + foo + ")";
+      };
+    }
+  }
+
+  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/DefaultLambdaWithSelfReferenceTestRunner.java b/src/test/java/com/android/tools/r8/desugar/DefaultLambdaWithSelfReferenceTestRunner.java
new file mode 100644
index 0000000..e209e44
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/DefaultLambdaWithSelfReferenceTestRunner.java
@@ -0,0 +1,153 @@
+// 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 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.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;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import org.junit.Test;
+
+public class DefaultLambdaWithSelfReferenceTestRunner extends DebugTestBase {
+
+  final Class<?> CLASS = DefaultLambdaWithSelfReferenceTest.class;
+  final String EXPECTED = StringUtils.lines("stateful(stateless)");
+
+  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 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)
+        .setMinApi(AndroidApiLevel.K)
+        .compile()
+        // TODO(b/123506120): Add .assertNoMessages()
+        .writeToZip(out1)
+        .run(CLASS)
+        .assertSuccessWithOutput(EXPECTED);
+
+    Path outPerClassDir = temp.newFolder().toPath();
+    Collection<Path> innerClasses =
+        ToolHelper.getClassFilesForInnerClasses(Collections.singleton(CLASS));
+
+    int i = 0;
+    List<Path> outs = new ArrayList<>();
+    {
+      Path mainOut = outPerClassDir.resolve("class" + i++ + ".zip");
+      outs.add(mainOut);
+      testForD8()
+          .addProgramClasses(CLASS)
+          .addClasspathFiles(ToolHelper.getClassPathForTests())
+          .setIntermediate(true)
+          .setMinApi(AndroidApiLevel.K)
+          .compile()
+          // TODO(b/123506120): Add .assertNoMessages()
+          .writeToZip(mainOut);
+    }
+    for (Path innerClass : innerClasses) {
+      Path out = outPerClassDir.resolve("class" + i++ + ".zip");
+      outs.add(out);
+      testForD8()
+          .addProgramFiles(innerClass)
+          .addClasspathFiles(ToolHelper.getClassPathForTests())
+          .setIntermediate(true)
+          .setMinApi(AndroidApiLevel.K)
+          .compile()
+          // TODO(b/123506120): Add .assertNoMessages()
+          .writeToZip(out);
+    }
+
+    Path out2 = temp.newFolder().toPath().resolve("out2.zip");
+    D8TestCompileResult compiledResult = testForD8()
+        .addProgramFiles(outs)
+        .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(
+        DisassembleCommand.builder().addProgramFiles(out1).setOutputPath(dissasemble1).build());
+    Disassemble.disassemble(
+        DisassembleCommand.builder().addProgramFiles(out2).setOutputPath(dissasemble2).build());
+    String content1 = StringUtils.join(Files.readAllLines(dissasemble1), "\n");
+    String content2 = StringUtils.join(Files.readAllLines(dissasemble2), "\n");
+    assertEquals(content1, content2);
+  }
+}
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());
+  }
+}