Support stepping over/out Kotlin inline functions

Bug: 63608278
Change-Id: I5904eb873fdf191e9ef32ad88b5002909ca05088
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 c89e792..9a28fac 100644
--- a/src/test/java/com/android/tools/r8/debug/DebugTestBase.java
+++ b/src/test/java/com/android/tools/r8/debug/DebugTestBase.java
@@ -12,8 +12,10 @@
 import com.android.tools.r8.dex.Constants;
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.IteratorUtils;
 import com.android.tools.r8.utils.OffOrAuto;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
 import it.unimi.dsi.fastutil.longs.LongArrayList;
 import it.unimi.dsi.fastutil.longs.LongList;
 import java.io.File;
@@ -269,6 +271,12 @@
     return step(StepKind.INTO, stepFilter);
   }
 
+  protected static List<Variable> getVisibleKotlinInlineVariables(
+      JUnit3Wrapper.DebuggeeState debuggeeState) {
+    return debuggeeState.getVisibleVariables().stream()
+        .filter(v -> v.getName().matches("^\\$i\\$f\\$.*$")).collect(Collectors.toList());
+  }
+
   public enum StepKind {
     INTO(StepDepth.INTO),
     OVER(StepDepth.OVER),
@@ -317,10 +325,7 @@
   }
 
   protected final JUnit3Wrapper.Command checkNoLocal(String localName) {
-    return inspect(t -> {
-      List<String> localNames = t.getLocalNames();
-      Assert.assertFalse("Unexpected local: " + localName, localNames.contains(localName));
-    });
+    return inspect(t -> t.checkNoLocal(localName));
   }
 
   protected final JUnit3Wrapper.Command checkNoLocal() {
@@ -508,8 +513,9 @@
               .getMethodSignature(debuggeeState.getLocation().classID,
                   debuggeeState.getLocation().methodID);
           System.out.println(String
-              .format("Suspended in %s#%s%s@0x%x", classSig, methodName, methodSig,
-                  Long.valueOf(debuggeeState.getLocation().index)));
+              .format("Suspended in %s#%s%s@0x%x (line=%d)", classSig, methodName, methodSig,
+                  Long.valueOf(debuggeeState.getLocation().index),
+                  Integer.valueOf(debuggeeState.getLineNumber())));
         }
 
         // Handle event.
@@ -555,6 +561,16 @@
       return new ArtTestOptions(debuggeePath);
     }
 
+    public void enqueueCommandFirst(Command command) {
+      commandsQueue.addFirst(command);
+    }
+
+    public void enqueueCommandsFirst(List<Command> commands) {
+      for (int i = commands.size() - 1; i >= 0; --i) {
+        enqueueCommandFirst(commands.get(i));
+      }
+    }
+
     //
     // Inspection
     //
@@ -572,6 +588,8 @@
 
       // Locals
 
+      List<Variable> getVisibleVariables();
+
       /**
        * Returns the names of all local variables visible at the current location
        */
@@ -581,6 +599,7 @@
        * Returns the values of all locals visible at the current location.
        */
       Map<String, Value> getLocalValues();
+      void checkNoLocal(String localName);
       void checkLocal(String localName);
       void checkLocal(String localName, Value expectedValue);
     }
@@ -624,7 +643,7 @@
             // Code indices are in ascending order.
             assert currentLineCodeIndex >= startCodeIndex;
             assert currentLineCodeIndex <= endCodeIndex;
-            assert currentLineCodeIndex > previousLineCodeIndex;
+            assert currentLineCodeIndex >= previousLineCodeIndex;
             previousLineCodeIndex = currentLineCodeIndex;
 
             if (location.index >= currentLineCodeIndex) {
@@ -652,13 +671,19 @@
           }
         }
 
-        public List<String> getLocalNames() {
-          Location location = getLocation();
-          return JUnit3Wrapper.getVariablesAt(mirror, location).stream()
-              .map(v -> v.getName())
+        @Override
+        public List<Variable> getVisibleVariables() {
+          // Get variable table and keep only variables visible at this location.
+          Location frameLocation = getLocation();
+          return getVariables(getMirror(), frameLocation.classID, frameLocation.methodID).stream()
+              .filter(v -> inScope(frameLocation.index, v))
               .collect(Collectors.toList());
         }
 
+        public List<String> getLocalNames() {
+          return getVisibleVariables().stream().map(v -> v.getName()).collect(Collectors.toList());
+        }
+
         @Override
         public Map<String, Value> getLocalValues() {
           return JUnit3Wrapper.getVariablesAt(mirror, location).stream()
@@ -687,6 +712,13 @@
               "line " + getLineNumber() + ": Expected local '" + localName + "' not present");
         }
 
+        @Override
+        public void checkNoLocal(String localName) {
+          Optional<Variable> localVar = JUnit3Wrapper
+              .getVariableAt(mirror, getLocation(), localName);
+          Assert.assertFalse("Unexpected local: " + localName, localVar.isPresent());
+        }
+
         public void checkLocal(String localName) {
           Optional<Variable> localVar = JUnit3Wrapper
               .getVariableAt(mirror, getLocation(), localName);
@@ -780,6 +812,10 @@
         return threadId;
       }
 
+      public int getFrameDepth() {
+        return frames.size();
+      }
+
       public FrameInspector getFrame(int index) {
         return frames.get(index);
       }
@@ -799,6 +835,11 @@
       }
 
       @Override
+      public void checkNoLocal(String localName) {
+        getTopFrame().checkNoLocal(localName);
+      }
+
+      @Override
       public void checkLocal(String localName) {
         getTopFrame().checkLocal(localName);
       }
@@ -848,6 +889,11 @@
         return getTopFrame().getMethodSignature();
       }
 
+      @Override
+      public List<Variable> getVisibleVariables() {
+        return getTopFrame().getVisibleVariables();
+      }
+
       public Value getStaticField(String className, String fieldName, String fieldSignature) {
         String classSignature = DescriptorUtils.javaTypeToDescriptor(className);
         byte typeTag = TypeTag.CLASS;
@@ -920,19 +966,19 @@
       }
     }
 
-    private static boolean inScope(long index, Variable var) {
-      long varStart = var.getCodeIndex();
-      long varEnd = varStart + var.getLength();
-      return index >= varStart && index < varEnd;
-    }
-
-    private static Optional<Variable> getVariableAt(VmMirror mirror, Location location,
+    public static Optional<Variable> getVariableAt(VmMirror mirror, Location location,
         String localName) {
       return getVariablesAt(mirror, location).stream()
           .filter(v -> localName.equals(v.getName()))
           .findFirst();
     }
 
+    protected static boolean inScope(long index, Variable var) {
+      long varStart = var.getCodeIndex();
+      long varEnd = varStart + var.getLength();
+      return index >= varStart && index < varEnd;
+    }
+
     private static List<Variable> getVariablesAt(VmMirror mirror, Location location) {
       // Get variable table and keep only variables visible at this location.
       return getVariables(mirror, location.classID, location.methodID).stream()
@@ -1059,10 +1105,9 @@
               wrapper.getMirror().clearEvent(JDWPConstants.EventKind.CLASS_PREPARE,
                   classPrepareRequestId);
 
-              // Breakpoint then resume. Note: we add them at the beginning of the queue (to be the
-              // next commands to be processed), thus they need to be pushed in reverse order.
-              wrapper.commandsQueue.addFirst(new JUnit3Wrapper.Command.RunCommand());
-              wrapper.commandsQueue.addFirst(BreakpointCommand.this);
+              // Breakpoint then resume.
+              wrapper.enqueueCommandsFirst(
+                  Arrays.asList(BreakpointCommand.this, new JUnit3Wrapper.Command.RunCommand()));
 
               // Set wrapper ready to process next command.
               wrapper.setState(State.ProcessCommand);
@@ -1262,7 +1307,7 @@
         }
         if (repeatStep) {
           // In order to repeat the step now, we need to add it at the beginning of the queue.
-          testBase.commandsQueue.addFirst(stepCommand);
+          testBase.enqueueCommandFirst(stepCommand);
         }
         super.handle(testBase);
       }
diff --git a/src/test/java/com/android/tools/r8/debug/KotlinDebugTestBase.java b/src/test/java/com/android/tools/r8/debug/KotlinDebugTestBase.java
new file mode 100644
index 0000000..a0a9c3f
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/debug/KotlinDebugTestBase.java
@@ -0,0 +1,83 @@
+// Copyright (c) 2017, 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 java.util.Arrays;
+import java.util.List;
+import org.apache.harmony.jpda.tests.framework.jdwp.Frame.Variable;
+import org.apache.harmony.jpda.tests.framework.jdwp.Location;
+
+/**
+ * A specialization for Kotlin-based tests which provides extra commands.
+ */
+public abstract class KotlinDebugTestBase extends DebugTestBase {
+
+  protected final JUnit3Wrapper.Command kotlinStepOver() {
+    return testBaseBeforeStep -> {
+      final JUnit3Wrapper.DebuggeeState debuggeeStateBeforeStep = testBaseBeforeStep
+          .getDebuggeeState();
+      final int frameDepthBeforeStep = debuggeeStateBeforeStep.getFrameDepth();
+      final Location locationBeforeStep = debuggeeStateBeforeStep.getLocation();
+      final List<Variable> kotlinLvsBeforeStep = getVisibleKotlinInlineVariables(
+          debuggeeStateBeforeStep);
+
+      // This is the command that will be executed after the initial (normal) step over. If we
+      // reach an inlined location, this command will step until reaching a non-inlined location.
+      JUnit3Wrapper.Command commandAfterStep = testBaseAfterStep -> {
+        // Get the new debuggee state (previous one is stale).
+        JUnit3Wrapper.DebuggeeState debuggeeStateAfterStep = testBaseBeforeStep.getDebuggeeState();
+
+        // Are we in the same frame ?
+        final int frameDepthAfterStep = debuggeeStateAfterStep.getFrameDepth();
+        final Location locationAfterStep = debuggeeStateAfterStep.getLocation();
+        if (frameDepthBeforeStep == frameDepthAfterStep
+            && locationBeforeStep.classID == locationAfterStep.classID
+            && locationBeforeStep.methodID == locationAfterStep.methodID) {
+          // We remain in the same method. Do we step into an inlined section ?
+          List<Variable> kotlinLvsAfterStep = getVisibleKotlinInlineVariables(
+              debuggeeStateAfterStep);
+          if (kotlinLvsBeforeStep.isEmpty() && !kotlinLvsAfterStep.isEmpty()) {
+            assert kotlinLvsAfterStep.size() == 1;
+
+            // We're located in an inlined section. Instead of doing a classic step out, we must
+            // jump out of the inlined section.
+            Variable inlinedSectionLv = kotlinLvsAfterStep.get(0);
+            testBaseAfterStep.enqueueCommandFirst(stepUntilOutOfInlineScope(inlinedSectionLv));
+          }
+        }
+      };
+
+      // Step over then check whether we need to continue stepping.
+      testBaseBeforeStep.enqueueCommandsFirst(Arrays.asList(stepOver(), commandAfterStep));
+    };
+  }
+
+  protected final JUnit3Wrapper.Command kotlinStepOut() {
+    return wrapper -> {
+      final List<Variable> kotlinLvsBeforeStep = getVisibleKotlinInlineVariables(
+          wrapper.getDebuggeeState());
+
+      JUnit3Wrapper.Command nextCommand;
+      if (!kotlinLvsBeforeStep.isEmpty()) {
+        // We are in an inline section. We need to step until being out of inline scope.
+        assert kotlinLvsBeforeStep.size() == 1;
+        final Variable inlinedSectionLv = kotlinLvsBeforeStep.get(0);
+        nextCommand = stepUntilOutOfInlineScope(inlinedSectionLv);
+      } else {
+        nextCommand = stepOut();
+      }
+      wrapper.enqueueCommandFirst(nextCommand);
+    };
+  }
+
+  private JUnit3Wrapper.Command stepUntilOutOfInlineScope(Variable inlineScopeLv) {
+    return stepUntil(StepKind.OVER, StepLevel.LINE, debuggeeState -> {
+      boolean inInlineScope = JUnit3Wrapper
+          .inScope(debuggeeState.getLocation().index, inlineScopeLv);
+      return !inInlineScope;
+    });
+  }
+
+}
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 e62a17b..8e9d145 100644
--- a/src/test/java/com/android/tools/r8/debug/KotlinInlineTest.java
+++ b/src/test/java/com/android/tools/r8/debug/KotlinInlineTest.java
@@ -10,9 +10,9 @@
 import org.junit.Ignore;
 import org.junit.Test;
 
-public class KotlinInlineTest extends DebugTestBase {
+// TODO check double-depth inline (an inline in another inline)
+public class KotlinInlineTest extends KotlinDebugTestBase {
 
-  @Ignore("Requires kotlin-specific stepping behavior")
   @Test
   public void testStepOverInline() throws Throwable {
     String methodName = "singleInline";
@@ -26,7 +26,6 @@
           assertEquals(41, s.getLineNumber());
           s.checkLocal("this");
         }),
-        // TODO(shertz) stepping over must take kotlin inline range into account.
         stepOver(),
         inspect(s -> {
           assertEquals("KotlinInline", s.getClassName());
@@ -35,7 +34,7 @@
           assertEquals(42, s.getLineNumber());
           s.checkLocal("this");
         }),
-        stepOver(),
+        kotlinStepOver(),
         inspect(s -> {
           assertEquals("KotlinInline", s.getClassName());
           assertEquals(methodName, s.getMethodName());
@@ -46,7 +45,6 @@
         run());
   }
 
-  @Ignore("Requires kotlin-specific stepping behavior")
   @Test
   public void testStepIntoInline() throws Throwable {
     String methodName = "singleInline";
@@ -60,7 +58,14 @@
           assertEquals(41, s.getLineNumber());
           s.checkLocal("this");
         }),
-        // TODO(shertz) stepping over must take kotlin inline range into account.
+        stepOver(),
+        inspect(s -> {
+          assertEquals("KotlinInline", s.getClassName());
+          assertEquals(methodName, s.getMethodName());
+          assertEquals("KotlinInline.kt", s.getSourceFile());
+          assertEquals(42, s.getLineNumber());
+          s.checkLocal("this");
+        }),
         stepInto(),
         inspect(s -> {
           assertEquals("KotlinInline", s.getClassName());
@@ -76,7 +81,6 @@
         run());
   }
 
-  @Ignore("Requires kotlin-specific stepping behavior")
   @Test
   public void testStepOutInline() throws Throwable {
     String methodName = "singleInline";
@@ -90,13 +94,20 @@
           assertEquals(41, s.getLineNumber());
           s.checkLocal("this");
         }),
-        // TODO(shertz) stepping out must take kotlin inline range into account.
+        stepOver(),
+        inspect(s -> {
+          assertEquals("KotlinInline", s.getClassName());
+          assertEquals(methodName, s.getMethodName());
+          assertEquals("KotlinInline.kt", s.getSourceFile());
+          assertEquals(42, s.getLineNumber());
+          s.checkLocal("this");
+        }),
         stepInto(),
         inspect(s -> {
           assertEquals("KotlinInline", s.getClassName());
           assertEquals(methodName, s.getMethodName());
         }),
-        stepOut(),
+        kotlinStepOut(),
         inspect(s -> {
           assertEquals("KotlinInline", s.getClassName());
           assertEquals(methodName, s.getMethodName());
diff --git a/src/test/java/com/android/tools/r8/debug/KotlinTest.java b/src/test/java/com/android/tools/r8/debug/KotlinTest.java
index a6b57b8..2baa426 100644
--- a/src/test/java/com/android/tools/r8/debug/KotlinTest.java
+++ b/src/test/java/com/android/tools/r8/debug/KotlinTest.java
@@ -8,7 +8,7 @@
 import org.apache.harmony.jpda.tests.framework.jdwp.Value;
 import org.junit.Test;
 
-public class KotlinTest extends DebugTestBase {
+public class KotlinTest extends KotlinDebugTestBase {
 
   // TODO(shertz) simplify test
   // TODO(shertz) add more variables ?