Add debug local table to LIR.

Bug: b/225838009
Change-Id: I65d662b36fe818a4b569975a562fb79a4f10b2eb
diff --git a/src/main/java/com/android/tools/r8/ir/code/DebugLocalWrite.java b/src/main/java/com/android/tools/r8/ir/code/DebugLocalWrite.java
index b17536e..834fd24 100644
--- a/src/main/java/com/android/tools/r8/ir/code/DebugLocalWrite.java
+++ b/src/main/java/com/android/tools/r8/ir/code/DebugLocalWrite.java
@@ -10,6 +10,7 @@
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.analysis.VerifyTypesHelper;
 import com.android.tools.r8.ir.conversion.CfBuilder;
+import com.android.tools.r8.lightir.LIRBuilder;
 
 /**
  * Instruction introducing an SSA value with attached local information.
@@ -87,4 +88,9 @@
     assert verifyTypesHelper.isAssignable(src().getType(), getOutType());
     return true;
   }
+
+  @Override
+  public void buildLIR(LIRBuilder<Value, BasicBlock> builder) {
+    builder.addDebugLocalWrite(src());
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/lightir/IR2LIRConverter.java b/src/main/java/com/android/tools/r8/lightir/IR2LIRConverter.java
index 4e148ee..9bdc15a 100644
--- a/src/main/java/com/android/tools/r8/lightir/IR2LIRConverter.java
+++ b/src/main/java/com/android/tools/r8/lightir/IR2LIRConverter.java
@@ -27,35 +27,45 @@
 
 public class IR2LIRConverter {
 
-  private IR2LIRConverter() {}
+  private final DexItemFactory factory;
+  private final IRCode irCode;
+  private final Reference2IntMap<BasicBlock> blocks = new Reference2IntOpenHashMap<>();
+  private final Reference2IntMap<Value> values = new Reference2IntOpenHashMap<>();
+  private final LIRBuilder<Value, BasicBlock> builder;
 
-  public static LIRCode translate(IRCode irCode, DexItemFactory factory) {
-    irCode.traceBlocks();
-    Reference2IntMap<BasicBlock> blocks = new Reference2IntOpenHashMap<>();
-    Reference2IntMap<Value> values = new Reference2IntOpenHashMap<>();
-    int instructionIndex = 0;
-    int valueIndex = 0;
-    for (BasicBlock block : irCode.blocks) {
-      blocks.put(block, instructionIndex);
-      for (Phi phi : block.getPhis()) {
-        values.put(phi, valueIndex);
-        valueIndex++;
-        instructionIndex++;
-      }
-      for (Instruction instruction : block.getInstructions()) {
-        if (instruction.hasOutValue()) {
-          values.put(instruction.outValue(), valueIndex);
-        }
-        valueIndex++;
-        if (!instruction.isArgument()) {
-          instructionIndex++;
-        }
-      }
-    }
-    LIRBuilder<Value, BasicBlock> builder =
+  private IR2LIRConverter(DexItemFactory factory, IRCode irCode) {
+    this.factory = factory;
+    this.irCode = irCode;
+    this.builder =
         new LIRBuilder<Value, BasicBlock>(
                 irCode.context().getReference(), values::getInt, blocks::getInt, factory)
             .setMetadata(irCode.metadata());
+  }
+
+  public static LIRCode translate(IRCode irCode, DexItemFactory factory) {
+    return new IR2LIRConverter(factory, irCode).internalTranslate();
+  }
+
+  private void recordBlock(BasicBlock block, int blockIndex) {
+    blocks.put(block, blockIndex);
+  }
+
+  private void recordValue(Value value, int valueIndex) {
+    values.put(value, valueIndex);
+    if (value.hasLocalInfo()) {
+      builder.setDebugValue(value.getLocalInfo(), valueIndex);
+    }
+  }
+
+  private LIRCode internalTranslate() {
+    irCode.traceBlocks();
+    computeBlockAndValueTables();
+    computeInstructions();
+    return builder.build();
+  }
+
+  private void computeInstructions() {
+    int currentInstructionIndex = 0;
     BasicBlockIterator blockIt = irCode.listIterator();
     while (blockIt.hasNext()) {
       BasicBlock block = blockIt.next();
@@ -67,6 +77,7 @@
         for (Phi phi : block.getPhis()) {
           permuteOperands(phi.getOperands(), permutation, operands);
           builder.addPhi(phi.getType(), Arrays.asList(operands));
+          currentInstructionIndex++;
         }
       }
       if (block.hasCatchHandlers()) {
@@ -79,20 +90,47 @@
       InstructionIterator it = block.iterator();
       while (it.hasNext()) {
         Instruction instruction = it.next();
+        assert !instruction.hasOutValue()
+            || currentInstructionIndex == values.getInt(instruction.outValue());
         builder.setCurrentPosition(instruction.getPosition());
+        if (!instruction.getDebugValues().isEmpty()) {
+          builder.setDebugLocalEnds(currentInstructionIndex, instruction.getDebugValues());
+        }
+
         if (instruction.isGoto()) {
           BasicBlock nextBlock = blockIt.peekNext();
           if (instruction.asGoto().getTarget() == nextBlock) {
             builder.addFallthrough();
-          } else {
-            instruction.buildLIR(builder);
+            currentInstructionIndex++;
+            continue;
           }
-        } else {
-          instruction.buildLIR(builder);
+        }
+        instruction.buildLIR(builder);
+        currentInstructionIndex++;
+      }
+    }
+  }
+
+  private void computeBlockAndValueTables() {
+    int instructionIndex = 0;
+    int valueIndex = 0;
+    for (BasicBlock block : irCode.blocks) {
+      recordBlock(block, instructionIndex);
+      for (Phi phi : block.getPhis()) {
+        recordValue(phi, valueIndex);
+        valueIndex++;
+        instructionIndex++;
+      }
+      for (Instruction instruction : block.getInstructions()) {
+        if (instruction.hasOutValue()) {
+          recordValue(instruction.outValue(), valueIndex);
+        }
+        valueIndex++;
+        if (!instruction.isArgument()) {
+          instructionIndex++;
         }
       }
     }
-    return builder.build();
   }
 
   private static void permuteOperands(List<Value> operands, int[] permutation, Value[] output) {
diff --git a/src/main/java/com/android/tools/r8/lightir/LIR2IRConverter.java b/src/main/java/com/android/tools/r8/lightir/LIR2IRConverter.java
index 056129b..28547c0 100644
--- a/src/main/java/com/android/tools/r8/lightir/LIR2IRConverter.java
+++ b/src/main/java/com/android/tools/r8/lightir/LIR2IRConverter.java
@@ -17,6 +17,7 @@
 import com.android.tools.r8.ir.code.CatchHandlers;
 import com.android.tools.r8.ir.code.ConstNumber;
 import com.android.tools.r8.ir.code.ConstString;
+import com.android.tools.r8.ir.code.DebugLocalWrite;
 import com.android.tools.r8.ir.code.DebugPosition;
 import com.android.tools.r8.ir.code.Div;
 import com.android.tools.r8.ir.code.Goto;
@@ -192,18 +193,21 @@
       return arguments;
     }
 
+    public int toInstructionIndexInIR(int lirIndex) {
+      return lirIndex + code.getArgumentCount();
+    }
+
     public int peekNextInstructionIndex() {
       return nextInstructionIndex;
     }
 
     public Value getOutValueForNextInstruction(TypeElement type) {
-      // TODO(b/225838009): Support debug locals.
-      DebugLocalInfo localInfo = null;
-      int index = peekNextInstructionIndex() + code.getArgumentCount();
-      Value value = values[index];
+      int valueIndex = toInstructionIndexInIR(peekNextInstructionIndex());
+      DebugLocalInfo localInfo = code.getDebugLocalInfo(valueIndex);
+      Value value = values[valueIndex];
       if (value == null) {
         value = new Value(valueNumberGenerator.next(), type, localInfo);
-        values[index] = value;
+        values[valueIndex] = value;
       } else {
         value.setType(type);
         if (localInfo != null) {
@@ -214,17 +218,17 @@
     }
 
     public Phi getPhiForNextInstructionAndAdvanceState(TypeElement type) {
-      int index = peekNextInstructionIndex() + code.getArgumentCount();
+      int valueIndex = toInstructionIndexInIR(peekNextInstructionIndex());
+      DebugLocalInfo localInfo = code.getDebugLocalInfo(valueIndex);
       // TODO(b/225838009): The phi constructor implicitly adds to the block, so we need to ensure
       //  the block. However, we must grab the index above. Find a way to clean this up so it is
       //  uniform with instructions.
       advanceInstructionState();
       // Creating the phi implicitly adds it to currentBlock.
-      DebugLocalInfo localInfo = null;
       Phi phi =
           new Phi(
               valueNumberGenerator.next(), currentBlock, type, localInfo, RegisterReadType.NORMAL);
-      Value value = values[index];
+      Value value = values[valueIndex];
       if (value != null) {
         // A fake ssa value has already been created, replace the users by the actual phi.
         // TODO(b/225838009): We could consider encoding the value type as a bit in the value index
@@ -232,7 +236,7 @@
         assert !value.isPhi();
         value.replaceUsers(phi);
       }
-      values[index] = phi;
+      values[valueIndex] = phi;
       return phi;
     }
 
@@ -243,10 +247,18 @@
     }
 
     private void addInstruction(Instruction instruction) {
+      int index = toInstructionIndexInIR(peekNextInstructionIndex());
       advanceInstructionState();
       instruction.setPosition(currentPosition);
       currentBlock.getInstructions().add(instruction);
       instruction.setBlock(currentBlock);
+      int[] debugEndIndices = code.getDebugLocalEnds(index);
+      if (debugEndIndices != null) {
+        for (int debugEndIndex : debugEndIndices) {
+          Value debugValue = getValue(debugEndIndex);
+          debugValue.addDebugLocalEnd(instruction);
+        }
+      }
     }
 
     private void addThisArgument(DexType type) {
@@ -259,6 +271,10 @@
       // which would otherwise advance the state.
       Value dest = getValue(index);
       dest.setType(type.toTypeElement(appView));
+      DebugLocalInfo localInfo = code.getDebugLocalInfo(index);
+      if (localInfo != null) {
+        dest.setLocalInfo(localInfo);
+      }
       Argument argument = new Argument(dest, index, type.isBooleanType());
       assert currentBlock != null;
       assert currentPosition.isSyntheticPosition();
@@ -380,5 +396,15 @@
       Value dest = getOutValueForNextInstruction(exceptionType.toTypeElement(appView));
       addInstruction(new MoveException(dest, exceptionType, appView.options()));
     }
+
+    @Override
+    public void onDebugLocalWrite(int srcIndex) {
+      Value src = getValue(srcIndex);
+      // The type is in the local table, so initialize it with bottom and reset with the local info.
+      Value dest = getOutValueForNextInstruction(TypeElement.getBottom());
+      TypeElement type = dest.getLocalInfo().type.toTypeElement(appView);
+      dest.setType(type);
+      addInstruction(new DebugLocalWrite(dest, src));
+    }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/lightir/LIRBuilder.java b/src/main/java/com/android/tools/r8/lightir/LIRBuilder.java
index 2971d2e..44876ed 100644
--- a/src/main/java/com/android/tools/r8/lightir/LIRBuilder.java
+++ b/src/main/java/com/android/tools/r8/lightir/LIRBuilder.java
@@ -5,6 +5,7 @@
 
 import com.android.tools.r8.errors.Unimplemented;
 import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.graph.DebugLocalInfo;
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexItem;
 import com.android.tools.r8.graph.DexItemFactory;
@@ -19,6 +20,7 @@
 import com.android.tools.r8.ir.code.Position;
 import com.android.tools.r8.ir.code.Position.SyntheticPosition;
 import com.android.tools.r8.ir.code.ValueType;
+import com.android.tools.r8.lightir.LIRCode.DebugLocalInfoTable;
 import com.android.tools.r8.lightir.LIRCode.PositionEntry;
 import com.android.tools.r8.lightir.LIRCode.TryCatchTable;
 import com.android.tools.r8.utils.ListUtils;
@@ -29,7 +31,9 @@
 import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.Iterator;
 import java.util.List;
+import java.util.Set;
 
 /**
  * Builder for constructing LIR code from IR.
@@ -68,6 +72,11 @@
   private final Int2ReferenceMap<CatchHandlers<Integer>> tryCatchRanges =
       new Int2ReferenceOpenHashMap<>();
 
+  // Mapping from SSA value definition to the local name index in the constant pool.
+  private final Int2ReferenceMap<DebugLocalInfo> debugLocals = new Int2ReferenceOpenHashMap<>();
+  // Mapping from instruction to the end usage of SSA values with debug local info.
+  private final Int2ReferenceMap<int[]> debugLocalEnds = new Int2ReferenceOpenHashMap();
+
   // TODO(b/225838009): Reconsider this fixed space as the operand count for phis is much larger.
   // Pre-allocated space for caching value indexes when writing instructions.
   private static final int MAX_VALUE_COUNT = 10;
@@ -87,6 +96,16 @@
     flushedPosition = currentPosition;
   }
 
+  public DexType toDexType(TypeElement typeElement) {
+    if (typeElement.isPrimitiveType()) {
+      return typeElement.asPrimitiveType().toDexType(factory);
+    }
+    if (typeElement.isReferenceType()) {
+      return typeElement.asReferenceType().toDexType(factory);
+    }
+    throw new Unreachable("Unexpected type element: " + typeElement);
+  }
+
   public void addTryCatchHanders(int blockIndex, CatchHandlers<Integer> handlers) {
     tryCatchRanges.put(blockIndex, handlers);
   }
@@ -149,6 +168,23 @@
     return this;
   }
 
+  public LIRBuilder<V, B> setDebugValue(DebugLocalInfo debugInfo, int valueIndex) {
+    DebugLocalInfo old = debugLocals.put(valueIndex, debugInfo);
+    assert old == null;
+    return this;
+  }
+
+  public LIRBuilder<V, B> setDebugLocalEnds(int instructionIndex, Set<V> endValues) {
+    int size = endValues.size();
+    int[] indices = new int[size];
+    Iterator<V> iterator = endValues.iterator();
+    for (int i = 0; i < size; i++) {
+      indices[i] = getValueIndex(iterator.next());
+    }
+    debugLocalEnds.put(instructionIndex, indices);
+    return this;
+  }
+
   public LIRBuilder<V, B> addArgument(int index, boolean knownToBeBoolean) {
     // Arguments are implicitly given by method descriptor and not an actual instruction.
     assert argumentCount == index;
@@ -387,18 +423,21 @@
   }
 
   public LIRBuilder<V, B> addPhi(TypeElement type, List<V> operands) {
-    DexType dexType =
-        type.isPrimitiveType()
-            ? type.asPrimitiveType().toDexType(factory)
-            : type.asReferenceType().toDexType(factory);
+    DexType dexType = toDexType(type);
     return addInstructionTemplate(LIROpcodes.PHI, Collections.singletonList(dexType), operands);
   }
 
+  public LIRBuilder<V, B> addDebugLocalWrite(V src) {
+    return addOneValueInstruction(LIROpcodes.DEBUGLOCALWRITE, src);
+  }
+
   public LIRCode build() {
     assert metadata != null;
     int constantsCount = constants.size();
     DexItem[] constantTable = new DexItem[constantsCount];
     constants.forEach((item, index) -> constantTable[index] = item);
+    DebugLocalInfoTable debugTable =
+        debugLocals.isEmpty() ? null : new DebugLocalInfoTable(debugLocals, debugLocalEnds);
     return new LIRCode(
         metadata,
         constantTable,
@@ -406,6 +445,7 @@
         argumentCount,
         byteWriter.toByteArray(),
         instructionCount,
-        new TryCatchTable(tryCatchRanges));
+        new TryCatchTable(tryCatchRanges),
+        debugTable);
   }
 }
diff --git a/src/main/java/com/android/tools/r8/lightir/LIRCode.java b/src/main/java/com/android/tools/r8/lightir/LIRCode.java
index 8caa8d6..09619f4 100644
--- a/src/main/java/com/android/tools/r8/lightir/LIRCode.java
+++ b/src/main/java/com/android/tools/r8/lightir/LIRCode.java
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.lightir;
 
+import com.android.tools.r8.graph.DebugLocalInfo;
 import com.android.tools.r8.graph.DexItem;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexMethod;
@@ -40,6 +41,20 @@
     }
   }
 
+  public static class DebugLocalInfoTable {
+    private final Int2ReferenceMap<DebugLocalInfo> valueToLocalMap;
+    private final Int2ReferenceMap<int[]> instructionToEndUseMap;
+
+    public DebugLocalInfoTable(
+        Int2ReferenceMap<DebugLocalInfo> valueToLocalMap,
+        Int2ReferenceMap<int[]> instructionToEndUseMap) {
+      assert !valueToLocalMap.isEmpty();
+      assert !instructionToEndUseMap.isEmpty();
+      this.valueToLocalMap = valueToLocalMap;
+      this.instructionToEndUseMap = instructionToEndUseMap;
+    }
+  }
+
   private final IRMetadata metadata;
 
   /** Constant pool of items. */
@@ -59,6 +74,9 @@
   /** Table of try-catch handlers for each basic block. */
   private final TryCatchTable tryCatchTable;
 
+  /** Table of debug local information for each SSA value (if present). */
+  private final DebugLocalInfoTable debugLocalInfoTable;
+
   public static <V, B> LIRBuilder<V, B> builder(
       DexMethod method,
       ValueIndexGetter<V> valueIndexGetter,
@@ -75,7 +93,8 @@
       int argumentCount,
       byte[] instructions,
       int instructionCount,
-      TryCatchTable tryCatchTable) {
+      TryCatchTable tryCatchTable,
+      DebugLocalInfoTable debugLocalInfoTable) {
     this.metadata = metadata;
     this.constants = constants;
     this.positionTable = positions;
@@ -83,6 +102,7 @@
     this.instructions = instructions;
     this.instructionCount = instructionCount;
     this.tryCatchTable = tryCatchTable;
+    this.debugLocalInfoTable = debugLocalInfoTable;
   }
 
   public int getArgumentCount() {
@@ -113,6 +133,20 @@
     return tryCatchTable;
   }
 
+  public DebugLocalInfoTable getDebugLocalInfoTable() {
+    return debugLocalInfoTable;
+  }
+
+  public DebugLocalInfo getDebugLocalInfo(int valueIndex) {
+    return debugLocalInfoTable == null ? null : debugLocalInfoTable.valueToLocalMap.get(valueIndex);
+  }
+
+  public int[] getDebugLocalEnds(int instructionIndex) {
+    return debugLocalInfoTable == null
+        ? null
+        : debugLocalInfoTable.instructionToEndUseMap.get(instructionIndex);
+  }
+
   @Override
   public LIRIterator iterator() {
     return new LIRIterator(new ByteArrayIterator(instructions));
diff --git a/src/main/java/com/android/tools/r8/lightir/LIROpcodes.java b/src/main/java/com/android/tools/r8/lightir/LIROpcodes.java
index 0b0dfc8..abfea89 100644
--- a/src/main/java/com/android/tools/r8/lightir/LIROpcodes.java
+++ b/src/main/java/com/android/tools/r8/lightir/LIROpcodes.java
@@ -186,6 +186,7 @@
   int PHI = 206;
   int FALLTHROUGH = 207;
   int MOVEEXCEPTION = 208;
+  int DEBUGLOCALWRITE = 209;
 
   static String toString(int opcode) {
     switch (opcode) {
@@ -498,6 +499,8 @@
         return "FALLTHROUGH";
       case MOVEEXCEPTION:
         return "MOVEEXCEPTION";
+      case DEBUGLOCALWRITE:
+        return "DEBUGLOCALWRITE";
 
       default:
         throw new Unreachable("Unexpected LIR opcode: " + opcode);
diff --git a/src/main/java/com/android/tools/r8/lightir/LIRParsedInstructionCallback.java b/src/main/java/com/android/tools/r8/lightir/LIRParsedInstructionCallback.java
index 5c87faf..f0bc3f3 100644
--- a/src/main/java/com/android/tools/r8/lightir/LIRParsedInstructionCallback.java
+++ b/src/main/java/com/android/tools/r8/lightir/LIRParsedInstructionCallback.java
@@ -57,6 +57,8 @@
 
   public void onMoveException(DexType exceptionType) {}
 
+  public void onDebugLocalWrite(int srcIndex) {}
+
   public void onInvokeMethodInstruction(DexMethod method, IntList arguments) {}
 
   public void onInvokeDirect(DexMethod method, IntList arguments) {
@@ -199,6 +201,12 @@
           onMoveException(type);
           return;
         }
+      case LIROpcodes.DEBUGLOCALWRITE:
+        {
+          int srcIndex = view.getNextValueOperand();
+          onDebugLocalWrite(srcIndex);
+          return;
+        }
       default:
         throw new Unimplemented("No dispatch for opcode " + LIROpcodes.toString(opcode));
     }
diff --git a/src/test/java/com/android/tools/r8/lightir/LIRRoundtripTest.java b/src/test/java/com/android/tools/r8/lightir/LIRRoundtripTest.java
index 1a8d281..2188ba6 100644
--- a/src/test/java/com/android/tools/r8/lightir/LIRRoundtripTest.java
+++ b/src/test/java/com/android/tools/r8/lightir/LIRRoundtripTest.java
@@ -5,17 +5,17 @@
 
 import static org.junit.Assume.assumeTrue;
 
-import com.android.tools.r8.CompilationFailedException;
-import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.debug.DebugTestBase;
+import com.android.tools.r8.debug.DebugTestConfig;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
 
 @RunWith(Parameterized.class)
-public class LIRRoundtripTest extends TestBase {
+public class LIRRoundtripTest extends DebugTestBase {
 
   static class TestClass {
     public static void main(String[] args) {
@@ -64,9 +64,8 @@
         .assertSuccessWithOutputLines("Hello, world!");
   }
 
-  // TODO(b/225838009): Support debug local info.
-  @Test(expected = CompilationFailedException.class)
-  public void testRoundtripDebug() throws Exception {
+  @Test
+  public void testRoundtripDebug() throws Throwable {
     testForD8(parameters.getBackend())
         .debug()
         .setMinApi(AndroidApiLevel.B)
@@ -77,6 +76,17 @@
               o.testing.roundtripThroughLIR = true;
             })
         .run(parameters.getRuntime(), TestClass.class)
-        .assertSuccessWithOutputLines("Hello, world!");
+        .assertSuccessWithOutputLines("Hello, world!")
+        .debugger(this::runDebugger);
+  }
+
+  private void runDebugger(DebugTestConfig config) throws Throwable {
+    runDebugTest(
+        config,
+        TestClass.class,
+        breakOnException(typeName(TestClass.class), "main", true, true),
+        run(),
+        checkLocals("args", "message"),
+        run());
   }
 }