Add infrastructure to optimize line numbers, not enabled yet.

Bug:
Change-Id: I04722643c69a7425998fd427c51501271540c748
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index 76a6ed1..c1b1096 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -45,6 +45,8 @@
 import com.android.tools.r8.utils.AndroidAppOutputSink;
 import com.android.tools.r8.utils.CfgPrinter;
 import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.InternalOptions.LineNumberOptimization;
+import com.android.tools.r8.utils.LineNumberOptimizer;
 import com.android.tools.r8.utils.StringDiagnostic;
 import com.android.tools.r8.utils.ThreadUtils;
 import com.android.tools.r8.utils.Timing;
@@ -314,6 +316,15 @@
               : new Minifier(appInfo.withLiveness(), rootSet, options).run(timing);
       timing.end();
 
+      if (options.lineNumberOptimization != LineNumberOptimization.OFF) {
+        timing.begin("Line number remapping");
+        LineNumberOptimizer.run(
+            application,
+            namingLens,
+            options.lineNumberOptimization == LineNumberOptimization.IDENTITY_MAPPING);
+        timing.end();
+      }
+
       // If a method filter is present don't produce output since the application is likely partial.
       if (options.hasMethodsFilter()) {
         System.out.println("Finished compilation with method filter: ");
diff --git a/src/main/java/com/android/tools/r8/graph/DexDebugEvent.java b/src/main/java/com/android/tools/r8/graph/DexDebugEvent.java
index 9d42c82..58d8473 100644
--- a/src/main/java/com/android/tools/r8/graph/DexDebugEvent.java
+++ b/src/main/java/com/android/tools/r8/graph/DexDebugEvent.java
@@ -446,7 +446,7 @@
 
     @Override
     public String toString() {
-      return String.format("DEFAULT %d (dpc %d, %dline %d)", value, getPCDelta(), getLineDelta());
+      return String.format("DEFAULT %d (dpc %d, dline %d)", value, getPCDelta(), getLineDelta());
     }
 
     @Override
diff --git a/src/main/java/com/android/tools/r8/graph/DexDebugEventBuilder.java b/src/main/java/com/android/tools/r8/graph/DexDebugEventBuilder.java
index 1c71b9b..83e01a2 100644
--- a/src/main/java/com/android/tools/r8/graph/DexDebugEventBuilder.java
+++ b/src/main/java/com/android/tools/r8/graph/DexDebugEventBuilder.java
@@ -29,7 +29,7 @@
  */
 public class DexDebugEventBuilder {
 
-  private static final int NO_PC_INFO = -1;
+  public static final int NO_PC_INFO = -1;
   private static final int NO_LINE_INFO = -1;
 
   private final DexEncodedMethod method;
@@ -234,7 +234,7 @@
     }
   }
 
-  private static void emitAdvancementEvents(
+  public static void emitAdvancementEvents(
       int previousPc,
       Position previousPosition,
       int nextPc,
diff --git a/src/main/java/com/android/tools/r8/graph/DexDebugPositionState.java b/src/main/java/com/android/tools/r8/graph/DexDebugPositionState.java
index 280b27e..a02e546 100644
--- a/src/main/java/com/android/tools/r8/graph/DexDebugPositionState.java
+++ b/src/main/java/com/android/tools/r8/graph/DexDebugPositionState.java
@@ -17,8 +17,6 @@
  * the current state using the getters after a Default event.
  */
 public class DexDebugPositionState implements DexDebugEventVisitor {
-  private final DexMethod method;
-
   private int currentPc = 0;
   private int currentLine;
   private DexString currentFile = null;
@@ -26,7 +24,6 @@
   private Position currentCallerPosition = null;
 
   public DexDebugPositionState(int startLine, DexMethod method) {
-    this.method = method;
     currentLine = startLine;
     currentMethod = method;
   }
@@ -44,9 +41,6 @@
 
   @Override
   public void visit(SetInlineFrame setInlineFrame) {
-    assert (setInlineFrame.caller == null && setInlineFrame.callee == method)
-        || (setInlineFrame.caller != null
-            && setInlineFrame.caller.getOutermostCaller().method == method);
     currentMethod = setInlineFrame.callee;
     currentCallerPosition = setInlineFrame.caller;
   }
diff --git a/src/main/java/com/android/tools/r8/utils/InternalOptions.java b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
index 1616a71..24df0e4 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -4,13 +4,13 @@
 package com.android.tools.r8.utils;
 
 import com.android.tools.r8.DiagnosticsHandler;
-import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.dex.Marker;
 import com.android.tools.r8.errors.InvalidDebugInfoException;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.shaking.ProguardConfiguration;
 import com.android.tools.r8.shaking.ProguardConfigurationRule;
 import com.google.common.collect.ImmutableList;
@@ -25,6 +25,12 @@
 
 public class InternalOptions {
 
+  public enum LineNumberOptimization {
+    OFF,
+    ON,
+    IDENTITY_MAPPING
+  }
+
   public final DexItemFactory itemFactory;
   public final ProguardConfiguration proguardConfiguration;
 
@@ -125,6 +131,8 @@
   public ImmutableList<ProguardConfigurationRule> mainDexKeepRules = ImmutableList.of();
   public boolean minimalMainDex;
 
+  public LineNumberOptimization lineNumberOptimization = LineNumberOptimization.OFF;
+
   public static class InvalidParameterAnnotationInfo {
 
     final DexMethod method;
diff --git a/src/main/java/com/android/tools/r8/utils/LineNumberOptimizer.java b/src/main/java/com/android/tools/r8/utils/LineNumberOptimizer.java
new file mode 100644
index 0000000..85741e2
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/LineNumberOptimizer.java
@@ -0,0 +1,305 @@
+// 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.utils;
+
+import com.android.tools.r8.graph.Code;
+import com.android.tools.r8.graph.DexApplication;
+import com.android.tools.r8.graph.DexCode;
+import com.android.tools.r8.graph.DexDebugEvent;
+import com.android.tools.r8.graph.DexDebugEventBuilder;
+import com.android.tools.r8.graph.DexDebugEventVisitor;
+import com.android.tools.r8.graph.DexDebugInfo;
+import com.android.tools.r8.graph.DexDebugPositionState;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexString;
+import com.android.tools.r8.ir.code.Position;
+import com.android.tools.r8.naming.NamingLens;
+import java.util.ArrayList;
+import java.util.IdentityHashMap;
+import java.util.List;
+
+public class LineNumberOptimizer {
+
+  // EventFilter is a visitor for DebugEvents, splits events into two sinks:
+  // - Forwards non-positional events unchanged into a BypassedEventReceiver
+  // - Forwards positional events, accumulated into DexDebugPositionStates, into
+  //   positionEventReceiver.
+  private static class EventFilter implements DexDebugEventVisitor {
+    private final BypassedEventReceiver bypassedEventReceiver;
+    private final PositionEventReceiver positionEventReceiver;
+
+    private interface BypassedEventReceiver {
+      void receiveBypassedEvent(DexDebugEvent event);
+    }
+
+    private interface PositionEventReceiver {
+      void receivePositionEvent(DexDebugPositionState positionState);
+    }
+
+    private DexDebugPositionState positionState;
+
+    private EventFilter(
+        int startLine,
+        DexMethod method,
+        BypassedEventReceiver bypassedEventReceiver,
+        PositionEventReceiver positionEventReceiver) {
+      positionState = new DexDebugPositionState(startLine, method);
+      this.bypassedEventReceiver = bypassedEventReceiver;
+      this.positionEventReceiver = positionEventReceiver;
+    }
+
+    @Override
+    public void visit(DexDebugEvent.SetPrologueEnd event) {
+      bypassedEventReceiver.receiveBypassedEvent(event);
+    }
+
+    @Override
+    public void visit(DexDebugEvent.SetEpilogueBegin event) {
+      bypassedEventReceiver.receiveBypassedEvent(event);
+    }
+
+    @Override
+    public void visit(DexDebugEvent.StartLocal event) {
+      bypassedEventReceiver.receiveBypassedEvent(event);
+    }
+
+    @Override
+    public void visit(DexDebugEvent.EndLocal event) {
+      bypassedEventReceiver.receiveBypassedEvent(event);
+    }
+
+    @Override
+    public void visit(DexDebugEvent.RestartLocal event) {
+      bypassedEventReceiver.receiveBypassedEvent(event);
+    }
+
+    @Override
+    public void visit(DexDebugEvent.AdvancePC advancePC) {
+      positionState.visit(advancePC);
+    }
+
+    @Override
+    public void visit(DexDebugEvent.AdvanceLine advanceLine) {
+      positionState.visit(advanceLine);
+    }
+
+    @Override
+    public void visit(DexDebugEvent.SetInlineFrame setInlineFrame) {
+      positionState.visit(setInlineFrame);
+    }
+
+    @Override
+    public void visit(DexDebugEvent.Default defaultEvent) {
+      positionState.visit(defaultEvent);
+      positionEventReceiver.receivePositionEvent(positionState);
+    }
+
+    @Override
+    public void visit(DexDebugEvent.SetFile setFile) {
+      positionState.visit(setFile);
+    }
+  }
+
+  // PositionRemapper is a stateful function which takes a position (represented by a
+  // DexDebugPositionState) and returns a remapped Position.
+  private static class PositionRemapper {
+    private int nextLineNumber;
+
+    PositionRemapper(int nextLineNumber) {
+      this.nextLineNumber = nextLineNumber;
+    }
+
+    private Position createRemappedPosition(DexDebugPositionState positionState) {
+      // TODO(tamaskenez) Actual remapping is to be implemented here.
+      // For now this is only identity-mapping.
+      return new Position(
+          positionState.getCurrentLine(),
+          positionState.getCurrentFile(),
+          positionState.getCurrentMethod(),
+          positionState.getCurrentCallerPosition());
+    }
+
+    int getNextLineNumber() {
+      return nextLineNumber;
+    }
+  }
+
+  // PositionEventEmitter is a stateful function which converts a Position into series of
+  // position-related DexDebugEvents and puts them into a processedEvents list.
+  private static class PositionEventEmitter {
+    private final DexItemFactory dexItemFactory;
+    private int startLine = -1;
+    private DexMethod method;
+    private int previousPc = DexDebugEventBuilder.NO_PC_INFO;
+    private Position previousPosition = null;
+    private List<DexDebugEvent> processedEvents;
+
+    private PositionEventEmitter(
+        DexItemFactory dexItemFactory, DexMethod method, List<DexDebugEvent> processedEvents) {
+      this.dexItemFactory = dexItemFactory;
+      this.method = method;
+      this.processedEvents = processedEvents;
+    }
+
+    private void emitPositionEvents(int currentPc, Position currentPosition) {
+      if (previousPosition == null) {
+        startLine = currentPosition.line;
+        previousPosition = new Position(startLine, null, method, null);
+      }
+      DexDebugEventBuilder.emitAdvancementEvents(
+          previousPc,
+          previousPosition,
+          currentPc,
+          currentPosition,
+          processedEvents,
+          dexItemFactory);
+      previousPc = currentPc;
+      previousPosition = currentPosition;
+    }
+
+    private int getStartLine() {
+      assert (startLine >= 0);
+      return startLine;
+    }
+  }
+
+  public static void run(
+      DexApplication application, NamingLens namingLens, boolean identityMapping) {
+    IdentityHashMap<DexString, List<DexProgramClass>> classesOfFiles = new IdentityHashMap<>();
+    // Collect which files contain which classes that need to have their line numbers optimized.
+    for (DexProgramClass clazz : application.classes()) {
+
+      // TODO(tamaskenez) fix b/69356670 and remove the conditional skipping.
+      if (!clazz.getSynthesizedFrom().isEmpty()) {
+        continue;
+      }
+
+      // Group methods by name
+      IdentityHashMap<DexString, List<DexEncodedMethod>> methodsByName =
+          new IdentityHashMap<>(clazz.directMethods().length + clazz.virtualMethods().length);
+      clazz.forEachMethod(
+          method -> {
+            if (doesContainPositions(method)) {
+              methodsByName.compute(
+                  method.method.name,
+                  (name, methods) -> {
+                    if (methods == null) {
+                      methods = new ArrayList<>();
+                    }
+                    methods.add(method);
+                    return methods;
+                  });
+            }
+          });
+      for (List<DexEncodedMethod> methods : methodsByName.values()) {
+        if (methods.size() > 1) {
+          // If there are multiple methods with the same name (overloaded) then sort them for
+          // deterministic behaviour: the algorithm will assign new line numbers in this order.
+          // Methods with different names can share the same line numbers, that's why they don't
+          // need to be sorted.
+          methods.sort(
+              (lhs, rhs) -> {
+                int startLineDiff =
+                    lhs.getCode().asDexCode().getDebugInfo().startLine
+                        - rhs.getCode().asDexCode().getDebugInfo().startLine;
+                if (startLineDiff != 0) return startLineDiff;
+                return DexEncodedMethod.slowCompare(lhs, rhs);
+              });
+        }
+        int nextLineNumber = 1;
+        for (DexEncodedMethod method : methods) {
+          // Do the actual processing for each method.
+          DexCode dexCode = method.getCode().asDexCode();
+          DexDebugInfo debugInfo = dexCode.getDebugInfo();
+          List<DexDebugEvent> processedEvents = new ArrayList<>();
+
+          // Our pipeline will be:
+          // [debugInfo.events] -> eventFilter -> positionRemapper -> positionEventEmitter ->
+          // [processedEvents]
+          PositionEventEmitter positionEventEmitter =
+              new PositionEventEmitter(application.dexItemFactory, method.method, processedEvents);
+          PositionRemapper positionRemapper = new PositionRemapper(nextLineNumber);
+
+          EventFilter eventFilter =
+              new EventFilter(
+                  debugInfo.startLine,
+                  method.method,
+                  processedEvents::add,
+                  positionState -> {
+                    Position position = positionRemapper.createRemappedPosition(positionState);
+                    positionEventEmitter.emitPositionEvents(positionState.getCurrentPc(), position);
+                  });
+          for (DexDebugEvent event : debugInfo.events) {
+            event.accept(eventFilter);
+          }
+          nextLineNumber = positionRemapper.getNextLineNumber();
+          DexDebugInfo optimizedDebugInfo =
+              new DexDebugInfo(
+                  positionEventEmitter.getStartLine(),
+                  debugInfo.parameters,
+                  processedEvents.toArray(new DexDebugEvent[processedEvents.size()]));
+
+          // TODO(tamaskenez) Remove this as soon as we have external tests testing not only the
+          // remapping but whether the non-positional debug events remain intact.
+          if (identityMapping) {
+            assert (optimizedDebugInfo.startLine == debugInfo.startLine);
+            assert (optimizedDebugInfo.events.length == debugInfo.events.length);
+            for (int i = 0; i < debugInfo.events.length; ++i) {
+              assert optimizedDebugInfo.events[i].equals(debugInfo.events[i]);
+            }
+          }
+          dexCode.setDebugInfo(optimizedDebugInfo);
+        }
+      }
+    }
+  }
+
+  // Return true if any of the methods' debug infos describe a Position which lists the containing
+  // method as the outermost caller.
+  private static boolean checkMethodsForSelfReferenceInPositions(DexEncodedMethod[] methods) {
+    for (DexEncodedMethod method : methods) {
+      if (!doesContainPositions(method)) {
+        continue;
+      }
+      DexDebugInfo debugInfo = method.getCode().asDexCode().getDebugInfo();
+
+      DexDebugPositionState positionState =
+          new DexDebugPositionState(debugInfo.startLine, method.method);
+      for (DexDebugEvent event : debugInfo.events) {
+        event.accept(positionState);
+        if (event instanceof DexDebugEvent.Default) {
+          Position caller = positionState.getCurrentCallerPosition();
+          DexMethod outermostMethod =
+              caller == null
+                  ? positionState.getCurrentMethod()
+                  : caller.getOutermostCaller().method;
+          if (outermostMethod == method.method) {
+            return true;
+          }
+        }
+      }
+    }
+    return false;
+  }
+
+  private static boolean doesContainPositions(DexEncodedMethod method) {
+    Code code = method.getCode();
+    if (code == null || !code.isDexCode()) {
+      return false;
+    }
+    DexDebugInfo debugInfo = code.asDexCode().getDebugInfo();
+    if (debugInfo == null) {
+      return false;
+    }
+    for (DexDebugEvent event : debugInfo.events) {
+      if (event instanceof DexDebugEvent.Default) {
+        return true;
+      }
+    }
+    return false;
+  }
+}
diff --git a/src/test/debugTestResources/LineNumberOptimization1.java b/src/test/debugTestResources/LineNumberOptimization1.java
new file mode 100644
index 0000000..c25ecc6
--- /dev/null
+++ b/src/test/debugTestResources/LineNumberOptimization1.java
@@ -0,0 +1,14 @@
+// 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.
+
+public class LineNumberOptimization1 {
+  private static void callThisFromSameFile() {
+    System.out.println("callThisFromSameFile");
+    LineNumberOptimization2.callThisFromAnotherFile();
+  }
+
+  public static void main(String[] args) {
+    callThisFromSameFile();
+  }
+}
diff --git a/src/test/debugTestResources/LineNumberOptimization2.java b/src/test/debugTestResources/LineNumberOptimization2.java
new file mode 100644
index 0000000..0f07ae9
--- /dev/null
+++ b/src/test/debugTestResources/LineNumberOptimization2.java
@@ -0,0 +1,30 @@
+// 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.
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+public class LineNumberOptimization2 {
+  public static void callThisFromAnotherFile() {
+    System.out.println("callThisFromAnotherFile");
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/debug/LineNumberOptimizationTest.java b/src/test/java/com/android/tools/r8/debug/LineNumberOptimizationTest.java
new file mode 100644
index 0000000..ed6b315
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/debug/LineNumberOptimizationTest.java
@@ -0,0 +1,89 @@
+// 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 com.android.tools.r8.CompilationMode;
+import com.android.tools.r8.utils.InternalOptions.LineNumberOptimization;
+import java.util.Collections;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+/** Tests source file and line numbers on inlined methods. */
+public class LineNumberOptimizationTest extends DebugTestBase {
+
+  public static final String SOURCE_FILE = "LineNumberOptimization1.java";
+  private static DebuggeePath debuggeePathOptimized;
+  private static DebuggeePath debuggeePathNotOptimized;
+  private static DebuggeePath debuggeePathIdentityTest;
+
+  private final String class1 = "LineNumberOptimization1";
+  private final String class2 = "LineNumberOptimization2";
+  private final String file1 = class1 + ".java";
+  private final String file2 = class2 + ".java";
+  private final String mainSignature = "([Ljava/lang/String;)V";
+
+  private static DebuggeePath makeDex(LineNumberOptimization lineNumberOptimization)
+      throws Exception {
+    return DebuggeePath.makeDex(
+        compileToDexViaR8(
+            oc -> {
+              oc.lineNumberOptimization = lineNumberOptimization;
+              oc.inlineAccessors = false;
+            },
+            null,
+            DEBUGGEE_JAR,
+            Collections.<String>emptyList(),
+            true,
+            CompilationMode.RELEASE));
+  }
+
+  @BeforeClass
+  public static void initDebuggeePath() throws Exception {
+    debuggeePathNotOptimized = makeDex(LineNumberOptimization.OFF);
+    debuggeePathOptimized = makeDex(LineNumberOptimization.ON);
+    debuggeePathIdentityTest = makeDex(LineNumberOptimization.IDENTITY_MAPPING);
+  }
+
+  @Test
+  public void testNotOptimized() throws Throwable {
+    runDebugTest(
+        debuggeePathNotOptimized,
+        class1,
+        breakpoint(class1, "main", mainSignature),
+        run(),
+        checkMethod(class1, "main", mainSignature),
+        checkLine(file1, 12),
+        stepInto(),
+        checkMethod(class1, "callThisFromSameFile", "()V"),
+        checkLine(file1, 7),
+        stepOver(),
+        checkMethod(class1, "callThisFromSameFile", "()V"),
+        checkLine(file1, 8),
+        stepInto(),
+        checkMethod(class2, "callThisFromAnotherFile", "()V"),
+        checkLine(file2, 28),
+        run());
+  }
+
+  @Test
+  public void testOptimized() throws Throwable {
+    runDebugTest(
+        debuggeePathOptimized,
+        class1,
+        breakpoint(class1, "main", mainSignature),
+        run(),
+        checkMethod(class1, "main", mainSignature),
+        checkLine(file1, 12),
+        stepInto(),
+        checkMethod(class1, "callThisFromSameFile", "()V"),
+        checkLine(file1, 7),
+        stepOver(),
+        checkMethod(class1, "callThisFromSameFile", "()V"),
+        checkLine(file1, 8),
+        stepInto(),
+        checkMethod(class2, "callThisFromAnotherFile", "()V"),
+        checkLine(file2, 28),
+        run());
+  }
+}