Add debugging test with Kotlin nested inlining

This new test verifies that D8 generates correct debug information
to support Kotlin inlining features. In fact, Kotlin adds unused local
variables into the variable table to encode different scopes of
inlined code.

In this test, we verify nested inlining (an inline function calling
another inline function) along with lambda block (also inlined). It
generates a more complex setup where multiple special variables
need to be preserved in debug information.

Change-Id: I5da6cde5f1f6c6e7411ccb1777b26fafaab925be
diff --git a/src/test/debugTestResourcesKotlin/KotlinInline.kt b/src/test/debugTestResourcesKotlin/KotlinInline.kt
index 7f914e4..4b55c57 100644
--- a/src/test/debugTestResourcesKotlin/KotlinInline.kt
+++ b/src/test/debugTestResourcesKotlin/KotlinInline.kt
@@ -47,6 +47,30 @@
         emptyMethod(-1)
     }
 
+    // Double inlining
+    fun testNestedInlining() {
+        val l1 = Int.MAX_VALUE
+        val l2 = Int.MIN_VALUE
+        inlinee1(l1, l2)
+    }
+    inline fun inlinee1(a: Int, b: Int) {
+        val c = a - 2
+        inlinee2(1) {
+            val left = a + b
+            val right = a - b
+            foo(left, right)
+        }
+        inlinee2(c) {
+            foo(b, a)
+        }
+    }
+
+    inline fun inlinee2(p: Int, block: () -> Unit) {
+        if (p > 0) {
+            block()
+        }
+    }
+
     companion object {
         @JvmStatic fun main(args: Array<String>) {
             println("Hello world!")
@@ -54,6 +78,7 @@
             instance.processObject(instance, instance::printObject)
             instance.invokeInlinedFunctions()
             instance.singleInline()
+            instance.testNestedInlining()
         }
     }
 }
\ No newline at end of file
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 e8d21f2..25356ae 100644
--- a/src/test/java/com/android/tools/r8/debug/DebugTestBase.java
+++ b/src/test/java/com/android/tools/r8/debug/DebugTestBase.java
@@ -390,6 +390,14 @@
     return inspect(t -> t.checkLocal(localName));
   }
 
+  protected final JUnit3Wrapper.Command checkLocals(String... localNames) {
+    return inspect(t -> {
+      for (String str : localNames) {
+        t.checkLocal(str);
+      }
+    });
+  }
+
   protected final JUnit3Wrapper.Command checkLocal(String localName, Value expectedValue) {
     return inspect(t -> t.checkLocal(localName, expectedValue));
   }
@@ -398,6 +406,14 @@
     return inspect(t -> t.checkNoLocal(localName));
   }
 
+  protected final JUnit3Wrapper.Command checkNoLocals(String... localNames) {
+    return inspect(t -> {
+      for (String str : localNames) {
+        t.checkNoLocal(str);
+      }
+    });
+  }
+
   protected final JUnit3Wrapper.Command checkNoLocal() {
     return inspect(t -> {
       List<String> localNames = t.getLocalNames();
diff --git a/src/test/java/com/android/tools/r8/debug/KotlinDebugTestBase.java b/src/test/java/com/android/tools/r8/debug/KotlinDebugTestBase.java
index 5ddd98c..63bc007 100644
--- a/src/test/java/com/android/tools/r8/debug/KotlinDebugTestBase.java
+++ b/src/test/java/com/android/tools/r8/debug/KotlinDebugTestBase.java
@@ -4,9 +4,6 @@
 
 package com.android.tools.r8.debug;
 
-import com.android.tools.r8.ToolHelper;
-import java.nio.file.Path;
-import java.nio.file.Paths;
 import java.util.Arrays;
 import java.util.List;
 import org.apache.harmony.jpda.tests.framework.jdwp.Frame.Variable;
diff --git a/src/test/java/com/android/tools/r8/debug/KotlinInlineTest.java b/src/test/java/com/android/tools/r8/debug/KotlinInlineTest.java
index cdba21b..cebfb1a 100644
--- a/src/test/java/com/android/tools/r8/debug/KotlinInlineTest.java
+++ b/src/test/java/com/android/tools/r8/debug/KotlinInlineTest.java
@@ -6,40 +6,46 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 
+import com.android.tools.r8.ToolHelper;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
 import org.apache.harmony.jpda.tests.framework.jdwp.Value;
 import org.junit.Test;
 
-// TODO check double-depth inline (an inline in another inline)
 public class KotlinInlineTest extends KotlinDebugTestBase {
 
+  public static final String DEBUGGEE_CLASS = "KotlinInline";
+  public static final String SOURCE_FILE = "KotlinInline.kt";
+
   @Test
   public void testStepOverInline() throws Throwable {
     String methodName = "singleInline";
     runDebugTest(
         getD8Config(),
-        "KotlinInline",
-        breakpoint("KotlinInline", methodName),
+        DEBUGGEE_CLASS,
+        breakpoint(DEBUGGEE_CLASS, methodName),
         run(),
         inspect(s -> {
-          assertEquals("KotlinInline", s.getClassName());
+          assertEquals(DEBUGGEE_CLASS, s.getClassName());
           assertEquals(methodName, s.getMethodName());
-          assertEquals("KotlinInline.kt", s.getSourceFile());
+          assertEquals(SOURCE_FILE, s.getSourceFile());
           assertEquals(41, s.getLineNumber());
           s.checkLocal("this");
         }),
         stepOver(),
         inspect(s -> {
-          assertEquals("KotlinInline", s.getClassName());
+          assertEquals(DEBUGGEE_CLASS, s.getClassName());
           assertEquals(methodName, s.getMethodName());
-          assertEquals("KotlinInline.kt", s.getSourceFile());
+          assertEquals(SOURCE_FILE, s.getSourceFile());
           assertEquals(42, s.getLineNumber());
           s.checkLocal("this");
         }),
         kotlinStepOver(),
         inspect(s -> {
-          assertEquals("KotlinInline", s.getClassName());
+          assertEquals(DEBUGGEE_CLASS, s.getClassName());
           assertEquals(methodName, s.getMethodName());
-          assertEquals("KotlinInline.kt", s.getSourceFile());
+          assertEquals(SOURCE_FILE, s.getSourceFile());
           assertEquals(43, s.getLineNumber());
           s.checkLocal("this");
         }),
@@ -51,29 +57,29 @@
     String methodName = "singleInline";
     runDebugTest(
         getD8Config(),
-        "KotlinInline",
-        breakpoint("KotlinInline", methodName),
+        DEBUGGEE_CLASS,
+        breakpoint(DEBUGGEE_CLASS, methodName),
         run(),
         inspect(s -> {
-          assertEquals("KotlinInline", s.getClassName());
+          assertEquals(DEBUGGEE_CLASS, s.getClassName());
           assertEquals(methodName, s.getMethodName());
-          assertEquals("KotlinInline.kt", s.getSourceFile());
+          assertEquals(SOURCE_FILE, s.getSourceFile());
           assertEquals(41, s.getLineNumber());
           s.checkLocal("this");
         }),
         stepOver(),
         inspect(s -> {
-          assertEquals("KotlinInline", s.getClassName());
+          assertEquals(DEBUGGEE_CLASS, s.getClassName());
           assertEquals(methodName, s.getMethodName());
-          assertEquals("KotlinInline.kt", s.getSourceFile());
+          assertEquals(SOURCE_FILE, s.getSourceFile());
           assertEquals(42, s.getLineNumber());
           s.checkLocal("this");
         }),
         stepInto(),
         inspect(s -> {
-          assertEquals("KotlinInline", s.getClassName());
+          assertEquals(DEBUGGEE_CLASS, s.getClassName());
           assertEquals(methodName, s.getMethodName());
-          assertEquals("KotlinInline.kt", s.getSourceFile());
+          assertEquals(SOURCE_FILE, s.getSourceFile());
           // The actual line number (the one encoded in debug information) is different than the
           // source file one.
           // TODO(shertz) extract original line number from JSR-45's SMAP (only supported on
@@ -89,47 +95,65 @@
     String methodName = "singleInline";
     runDebugTest(
         getD8Config(),
-        "KotlinInline",
-        breakpoint("KotlinInline", methodName),
+        DEBUGGEE_CLASS,
+        breakpoint(DEBUGGEE_CLASS, methodName),
         run(),
         inspect(s -> {
-          assertEquals("KotlinInline", s.getClassName());
+          assertEquals(DEBUGGEE_CLASS, s.getClassName());
           assertEquals(methodName, s.getMethodName());
-          assertEquals("KotlinInline.kt", s.getSourceFile());
+          assertEquals(SOURCE_FILE, s.getSourceFile());
           assertEquals(41, s.getLineNumber());
           s.checkLocal("this");
         }),
         stepOver(),
         inspect(s -> {
-          assertEquals("KotlinInline", s.getClassName());
+          assertEquals(DEBUGGEE_CLASS, s.getClassName());
           assertEquals(methodName, s.getMethodName());
-          assertEquals("KotlinInline.kt", s.getSourceFile());
+          assertEquals(SOURCE_FILE, s.getSourceFile());
           assertEquals(42, s.getLineNumber());
           s.checkLocal("this");
         }),
         stepInto(),
         inspect(s -> {
-          assertEquals("KotlinInline", s.getClassName());
+          assertEquals(DEBUGGEE_CLASS, s.getClassName());
           assertEquals(methodName, s.getMethodName());
         }),
         kotlinStepOut(),
         inspect(s -> {
-          assertEquals("KotlinInline", s.getClassName());
+          assertEquals(DEBUGGEE_CLASS, s.getClassName());
           assertEquals(methodName, s.getMethodName());
-          assertEquals("KotlinInline.kt", s.getSourceFile());
+          assertEquals(SOURCE_FILE, s.getSourceFile());
           assertEquals(43, s.getLineNumber());
           s.checkLocal("this");
         }),
         run());
   }
 
+  private static String mangleFunctionNameFromInlineScope(String functionName) {
+    return "$i$f$" + functionName;
+  }
+
+  private static String mangleLambdaNameFromInlineScope(String functionName, int lambdaId) {
+    assert lambdaId > 0;
+    return "$i$a$" + lambdaId + "$" + functionName;
+  }
+
+  private static String mangleLvNameFromInlineScope(String lvName, int inlineDepth) {
+    assert inlineDepth > 0;
+    StringBuilder builder = new StringBuilder(lvName);
+    for (int i = 0; i < inlineDepth; ++i) {
+      builder.append("$iv");
+    }
+    return builder.toString();
+  }
+
   @Test
   public void testKotlinInline() throws Throwable {
     final String inliningMethodName = "invokeInlinedFunctions";
     runDebugTest(
         getD8Config(),
-        "KotlinInline",
-        breakpoint("KotlinInline", inliningMethodName),
+        DEBUGGEE_CLASS,
+        breakpoint(DEBUGGEE_CLASS, inliningMethodName),
         run(),
         inspect(s -> {
           assertEquals(inliningMethodName, s.getMethodName());
@@ -157,8 +181,8 @@
           s.checkLocal("this");
           s.checkLocal("inA", Value.createInt(1));
           // This is a "hidden" lv added by Kotlin (which is neither initialized nor used).
-          s.checkLocal("$i$f$inlinedA");
-          s.checkLocal("$i$a$1$inlinedA");
+          s.checkLocal(mangleFunctionNameFromInlineScope("inlinedA"));
+          s.checkLocal(mangleLambdaNameFromInlineScope("inlinedA", 1));
         }),
         stepInto(),
         inspect(s -> {
@@ -181,10 +205,143 @@
           s.checkLocal("this");
           s.checkLocal("inB", Value.createInt(2));
           // This is a "hidden" lv added by Kotlin (which is neither initialized nor used).
-          s.checkLocal("$i$f$inlinedB");
-          s.checkLocal("$i$a$1$inlinedB");
+          s.checkLocal(mangleFunctionNameFromInlineScope("inlinedB"));
+          s.checkLocal(mangleLambdaNameFromInlineScope("inlinedB", 1));
         }),
         run());
   }
 
+  @Test
+  public void testNestedInlining() throws Throwable {
+    // Count the number of lines in the source file. This is needed to check that inlined code
+    // refers to non-existing line numbers.
+    Path sourceFilePath = Paths.get(ToolHelper.TESTS_DIR, "debugTestResourcesKotlin", SOURCE_FILE);
+    assert sourceFilePath.toFile().exists();
+    final int maxLineNumber = Files.readAllLines(sourceFilePath).size();
+    final String inliningMethodName = "testNestedInlining";
+
+    // Local variables that represent the scope (start,end) of function's code that has been
+    // inlined.
+    final String inlinee1_inlineScope = mangleFunctionNameFromInlineScope("inlinee1");
+    final String inlinee2_inlineScope = mangleFunctionNameFromInlineScope("inlinee2");
+
+    // Local variables that represent the scope (start,end) of lambda's code that has been inlined.
+    final String inlinee2_lambda1_inlineScope = mangleLambdaNameFromInlineScope("inlinee2", 1);
+    final String inlinee2_lambda2_inlineScope = mangleLambdaNameFromInlineScope("inlinee2", 2);
+    final String c_mangledLvName = mangleLvNameFromInlineScope("c", 1);
+    final String left_mangledLvName = mangleLvNameFromInlineScope("left", 1);
+    final String right_mangledLvName = mangleLvNameFromInlineScope("right", 1);
+    final String p_mangledLvName = mangleLvNameFromInlineScope("p", 2);
+
+    runDebugTest(
+        getD8Config(),
+        DEBUGGEE_CLASS,
+        breakpoint(DEBUGGEE_CLASS, inliningMethodName),
+        run(),
+        inspect(s -> {
+          assertEquals(inliningMethodName, s.getMethodName());
+          assertEquals(52, s.getLineNumber());
+          s.checkLocal("this");
+        }),
+        checkLocal("this"),
+        checkNoLocals("l1", "l2"),
+        stepOver(),
+        checkLine(SOURCE_FILE, 53),
+        checkLocals("this", "l1"),
+        checkNoLocal("l2"),
+        stepOver(),
+        checkLine(SOURCE_FILE, 54),
+        checkLocals("this", "l1", "l2"),
+        stepInto(),
+        // We jumped into 1st inlinee but the current method is the same
+        checkMethod(DEBUGGEE_CLASS, inliningMethodName),
+        checkLocal(inlinee1_inlineScope),
+        inspect(state -> {
+          assertEquals(SOURCE_FILE, state.getSourceFile());
+          assertTrue(state.getLineNumber() > maxLineNumber);
+        }),
+        checkNoLocal(c_mangledLvName),
+        stepInto(),
+        checkLocal(c_mangledLvName),
+        stepInto(),
+        // We jumped into 2nd inlinee which is nested in the 1st inlinee
+        checkLocal(inlinee2_inlineScope),
+        checkLocal(inlinee1_inlineScope),
+        inspect(state -> {
+          assertEquals(SOURCE_FILE, state.getSourceFile());
+          assertTrue(state.getLineNumber() > maxLineNumber);
+        }),
+        // We must see the local variable "p" with a 2-level inline depth.
+        checkLocal(p_mangledLvName),
+        checkNoLocals(left_mangledLvName, right_mangledLvName),
+        // Enter the if block of inlinee2
+        stepInto(),
+        checkLocal(p_mangledLvName),
+        checkNoLocals(left_mangledLvName, right_mangledLvName),
+        // Enter the inlined lambda
+        stepInto(),
+        checkLocal(p_mangledLvName),
+        checkLocal(inlinee2_lambda1_inlineScope),
+        checkNoLocals(left_mangledLvName, right_mangledLvName),
+        stepInto(),
+        checkLocal(inlinee2_lambda1_inlineScope),
+        checkLocal(left_mangledLvName),
+        checkNoLocal(right_mangledLvName),
+        stepInto(),
+        checkLocals(left_mangledLvName, right_mangledLvName),
+        // Enter "foo"
+        stepInto(),
+        checkMethod(DEBUGGEE_CLASS, "foo"),
+        checkLine(SOURCE_FILE, 34),
+        stepOut(),
+        // We're back to the inline section, at the end of the lambda
+        inspect(state -> {
+          assertEquals(SOURCE_FILE, state.getSourceFile());
+          assertTrue(state.getLineNumber() > maxLineNumber);
+        }),
+        checkLocal(inlinee1_inlineScope),
+        checkLocal(inlinee2_inlineScope),
+        checkLocal(inlinee2_lambda1_inlineScope),
+        checkNoLocal(inlinee2_lambda2_inlineScope),
+        stepInto(),
+        // We're in inlinee2, after the call to the inlined lambda.
+        checkLocal(inlinee1_inlineScope),
+        checkLocal(inlinee2_inlineScope),
+        checkNoLocal(inlinee2_lambda1_inlineScope),
+        checkNoLocal(inlinee2_lambda2_inlineScope),
+        stepInto(),
+        // We're out of inlinee2
+        checkMethod(DEBUGGEE_CLASS, inliningMethodName),
+        checkLocal(inlinee1_inlineScope),
+        checkNoLocal(inlinee2_inlineScope),
+        checkNoLocal(inlinee2_lambda1_inlineScope),
+        // Enter the new call to "inlinee2"
+        stepInto(),
+        checkMethod(DEBUGGEE_CLASS, inliningMethodName),
+        checkLocal(inlinee1_inlineScope),
+        checkLocal(inlinee2_inlineScope),
+        checkNoLocal(inlinee2_lambda1_inlineScope),
+        checkNoLocal(inlinee2_lambda2_inlineScope),
+        checkLocal(p_mangledLvName),
+        stepInto(),
+        checkMethod(DEBUGGEE_CLASS, inliningMethodName),
+        checkLocal(inlinee1_inlineScope),
+        checkLocal(inlinee2_inlineScope),
+        checkNoLocal(inlinee2_lambda1_inlineScope),
+        checkNoLocal(inlinee2_lambda2_inlineScope),
+        checkLocal(p_mangledLvName),
+        // We enter the 2nd lambda
+        stepInto(),
+        checkMethod(DEBUGGEE_CLASS, inliningMethodName),
+        checkLocal(inlinee1_inlineScope),
+        checkLocal(inlinee2_inlineScope),
+        checkNoLocal(inlinee2_lambda1_inlineScope),
+        checkLocal(inlinee2_lambda2_inlineScope),
+        // Enter the call to "foo"
+        stepInto(),
+        checkMethod(DEBUGGEE_CLASS, "foo"),
+        checkLine(SOURCE_FILE, 34),
+        run());
+  }
+
 }