Remove trivial fallthrough blocks before debug positions.

Fixes: b/230337727
Change-Id: If5324b353972ffa33b6cdb784bb4883dc1169052
diff --git a/src/main/java/com/android/tools/r8/code/Instruction.java b/src/main/java/com/android/tools/r8/code/Instruction.java
index 8671b68..580d5ef 100644
--- a/src/main/java/com/android/tools/r8/code/Instruction.java
+++ b/src/main/java/com/android/tools/r8/code/Instruction.java
@@ -343,6 +343,10 @@
   @Override
   public final int acceptCompareTo(Instruction other, CompareToVisitor visitor) {
     int opcodeDiff = visitor.visitInt(getCompareToId(), other.getCompareToId());
+    if (opcodeDiff != 0) {
+      return opcodeDiff;
+    }
+    opcodeDiff = visitor.visitInt(getOffset(), other.getOffset());
     return opcodeDiff != 0 ? opcodeDiff : internalAcceptCompareTo(other, visitor);
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/code/IRCode.java b/src/main/java/com/android/tools/r8/ir/code/IRCode.java
index bf8f7ad..ffc5782 100644
--- a/src/main/java/com/android/tools/r8/ir/code/IRCode.java
+++ b/src/main/java/com/android/tools/r8/ir/code/IRCode.java
@@ -516,7 +516,9 @@
   }
 
   public void removeBlocks(Collection<BasicBlock> blocksToRemove) {
-    blocks.removeAll(blocksToRemove);
+    if (!blocksToRemove.isEmpty()) {
+      blocks.removeAll(blocksToRemove);
+    }
   }
 
   /**
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/CfSourceCode.java b/src/main/java/com/android/tools/r8/ir/conversion/CfSourceCode.java
index 1a39011..5270193 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/CfSourceCode.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/CfSourceCode.java
@@ -412,6 +412,11 @@
     if (needsGeneratedMethodSynchronization) {
       buildMethodEnterSynchronization(builder);
     }
+    Position entryPosition = getCanonicalDebugPositionAtOffset(0);
+    if (!state.getPosition().equals(entryPosition)) {
+      state.setPosition(entryPosition);
+      builder.addDebugPosition(state.getPosition());
+    }
     recordStateForTarget(0, state.getSnapshot());
     inPrelude = false;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/DexBuilder.java b/src/main/java/com/android/tools/r8/ir/conversion/DexBuilder.java
index 2bd4cfa..de631d3 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/DexBuilder.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/DexBuilder.java
@@ -54,7 +54,6 @@
 import com.android.tools.r8.ir.code.InstructionIterator;
 import com.android.tools.r8.ir.code.InstructionListIterator;
 import com.android.tools.r8.ir.code.IntSwitch;
-import com.android.tools.r8.ir.code.JumpInstruction;
 import com.android.tools.r8.ir.code.Move;
 import com.android.tools.r8.ir.code.NewArrayFilledData;
 import com.android.tools.r8.ir.code.Position;
@@ -72,7 +71,7 @@
 import it.unimi.dsi.fastutil.ints.Int2ReferenceOpenHashMap;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.HashSet;
+import java.util.Collections;
 import java.util.List;
 import java.util.ListIterator;
 import java.util.Map;
@@ -351,6 +350,50 @@
         && currentBlock.getPredecessors().get(0) == previousBlock;
   }
 
+  private static void removeTrivialFallthroughBlocks(IRCode code) {
+    for (int blockIndex = 1; blockIndex < code.blocks.size() - 1; blockIndex++) {
+      // We skip the last block as it has no fallthrough. We also skip checking the entry block as
+      // it has no predecessors and must define the initial position. Any subsequent block must be
+      // statically reachable and thus have predecessors.
+      BasicBlock currentBlock = code.blocks.get(blockIndex);
+      assert !currentBlock.getPredecessors().isEmpty();
+      if (currentBlock.size() != 2) {
+        continue;
+      }
+      DebugPosition debugPosition = currentBlock.entry().asDebugPosition();
+      com.android.tools.r8.ir.code.Goto exit = currentBlock.exit().asGoto();
+      if (debugPosition == null || exit == null || debugPosition.getPosition().isNone()) {
+        continue;
+      }
+      BasicBlock nextBlock = code.blocks.get(blockIndex + 1);
+      if (exit.getTarget() != nextBlock) {
+        continue;
+      }
+      // The block is a trivial position block that falls through to the following.
+      // If the position is equal to each predecessor block then the line is already active on
+      // each entry to the fallthrough and it can be safely removed.
+      boolean allMatch = true;
+      Position position = debugPosition.getPosition();
+      for (BasicBlock pred : currentBlock.getPredecessors()) {
+        // Do to the target == next check this cannot be a trivial loop.
+        assert pred != currentBlock;
+        Position predExit = pred.exit().getPosition();
+        if (!position.equals(predExit)) {
+          allMatch = false;
+          break;
+        }
+      }
+      if (allMatch) {
+        currentBlock.removeInstruction(debugPosition);
+        CodeRewriter.unlinkTrivialGotoBlock(currentBlock, nextBlock);
+        code.removeBlocks(Collections.singleton(currentBlock));
+        // Having removed the block at blockIndex, the previous block may now be a trivial
+        // fallthrough. Rewind to that point and retry. This avoids iterating to a fixed point.
+        blockIndex = Math.max(0, blockIndex - 2);
+      }
+    }
+  }
+
   // Eliminates unneeded debug positions.
   //
   // After this pass all remaining debug positions mark places where we must ensure a materializing
@@ -359,6 +402,12 @@
     if (!code.metadata().mayHaveDebugPosition()) {
       return;
     }
+
+    // We must start by removing any blocks that are already trivial fallthrough blocks with no
+    // position change. With these removed it is then sound to make the fallthrough judgement when
+    // determining if a goto will materialize or not.
+    removeTrivialFallthroughBlocks(code);
+
     // Current position known to have a materializing instruction associated with it.
     Position currentMaterializedPosition = Position.none();
 
@@ -373,7 +422,6 @@
     // Compute the set of all positions that can be removed.
     // (Delaying removal to avoid ConcurrentModificationException).
     List<DebugPosition> toRemove = new ArrayList<>();
-    Set<BasicBlock> trivialBlocks = new HashSet<>();
 
     for (int blockIndex = 0; blockIndex < code.blocks.size(); blockIndex++) {
       BasicBlock currentBlock = code.blocks.get(blockIndex);
@@ -425,12 +473,11 @@
               && currentMaterializedPosition == instruction.getPosition()) {
             // Here we don't need to check locals state as the line is already active.
             toRemove.add(instruction.asDebugPosition());
-            if (currentBlock.getInstructions().size() == 2) {
-              JumpInstruction exit = currentBlock.exit();
-              if (exit.isGoto() && exit.getPosition() == currentMaterializedPosition) {
-                trivialBlocks.add(currentBlock);
-              }
-            }
+            assert currentBlock.size() != 2
+                    || currentBlock.exit().getPosition() != currentMaterializedPosition
+                    || !currentBlock.exit().isGoto()
+                    || currentBlock.exit().asGoto().getTarget() != nextBlock
+                : "Unexpected trivial fallthrough block. This should be removed already.";
           } else if (unresolvedPosition != null
               && unresolvedPosition.getPosition() == instruction.getPosition()
               && locals.equals(localsAtUnresolvedPosition)) {
@@ -474,29 +521,6 @@
       }
       assert i == toRemove.size();
     }
-
-    // Remove all trivial goto blocks that have a position known to be emitted in their predecessor.
-    if (!trivialBlocks.isEmpty()) {
-      List<BasicBlock> blocksToRemove = new ArrayList<>();
-      ListIterator<BasicBlock> iterator = code.listIterator();
-      // Skip the entry block.
-      assert code.blocks.size() > 1;
-      iterator.next();
-      BasicBlock nextBlock = iterator.next();
-      do {
-        BasicBlock block = nextBlock;
-        nextBlock = iterator.hasNext() ? iterator.next() : null;
-        if (block.isTrivialGoto()
-            && trivialBlocks.contains(block)
-            && block.exit().asGoto().getTarget() != block
-            && !CodeRewriter.isFallthroughBlock(block)) {
-          BasicBlock target = block.exit().asGoto().getTarget();
-          blocksToRemove.add(block);
-          CodeRewriter.unlinkTrivialGotoBlock(block, target);
-        }
-      } while (iterator.hasNext());
-      code.removeBlocks(blocksToRemove);
-    }
   }
 
   // Rewrite ifs with offsets that are too large for the if encoding. The rewriting transforms:
@@ -522,8 +546,10 @@
         BasicBlock trueTarget = theIf.getTrueTarget();
         BasicBlock newBlock =
             BasicBlock.createGotoBlock(
-                ir.blocks.size(), theIf.getPosition(), ir.metadata(), trueTarget);
+                ir.getNextBlockNumber(), theIf.getPosition(), ir.metadata(), trueTarget);
         theIf.setTrueTarget(newBlock);
+        newBlock.getMutablePredecessors().add(block);
+        trueTarget.replacePredecessor(block, newBlock);
         theIf.invert();
         it.add(newBlock);
       }
diff --git a/src/test/java/com/android/tools/r8/TestParametersBuilder.java b/src/test/java/com/android/tools/r8/TestParametersBuilder.java
index 48e94f0..cf33980 100644
--- a/src/test/java/com/android/tools/r8/TestParametersBuilder.java
+++ b/src/test/java/com/android/tools/r8/TestParametersBuilder.java
@@ -93,6 +93,10 @@
     return withCfRuntime(CfRuntime.getDefaultCfRuntime().getVm());
   }
 
+  public TestParametersBuilder withDefaultRuntimes() {
+    return withDefaultDexRuntime().withDefaultCfRuntime();
+  }
+
   /** Add all available CF runtimes. */
   public TestParametersBuilder withCfRuntimes() {
     return withCfRuntimeFilter(vm -> true);
diff --git a/src/test/java/com/android/tools/r8/debuginfo/NopLineRegression230337727Test.java b/src/test/java/com/android/tools/r8/debuginfo/NopLineRegression230337727Test.java
new file mode 100644
index 0000000..ccdac2d
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/debuginfo/NopLineRegression230337727Test.java
@@ -0,0 +1,141 @@
+// Copyright (c) 2022, 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.debuginfo;
+
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.cf.CfVersion;
+import com.android.tools.r8.transformers.ClassFileTransformer.MethodPredicate;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import java.util.Arrays;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.objectweb.asm.Label;
+import org.objectweb.asm.Opcodes;
+
+@RunWith(Parameterized.class)
+public class NopLineRegression230337727Test extends TestBase implements Opcodes {
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withDefaultRuntimes().withApiLevel(AndroidApiLevel.B).build();
+  }
+
+  private final TestParameters parameters;
+
+  public NopLineRegression230337727Test(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForRuntime(parameters)
+        .addProgramClassFileData(getTestClassTransformed())
+        .run(parameters.getRuntime(), TestClass.class)
+        .inspect(
+            inspector -> {
+              MethodSubject foo = inspector.clazz(TestClass.class).uniqueMethodWithName("foo");
+              assertTrue(
+                  foo.getLineNumberTable().getLines().toString(),
+                  foo.getLineNumberTable().getLines().contains(11));
+            });
+  }
+
+  static class TestClass {
+
+    public static boolean boo() {
+      return true;
+    }
+
+    public static void foo(List<Integer> args) {
+      System.nanoTime();
+      // Code added by transformer for the kotlin code:
+      //  if (boo()) {
+      //    l.forEach {}
+      //  }
+      //  if (boo()) {}
+    }
+
+    public static void main(String[] args) {
+      foo(Arrays.asList(1, 2, 3));
+    }
+  }
+
+  private static byte[] getTestClassTransformed() throws Exception {
+    // Code generated by kotlin but with frames, locals and checkNotNullParameter removed.
+    String holder = binaryName(TestClass.class);
+    return transformer(TestClass.class)
+        .setVersion(CfVersion.V1_5) // Avoid need of stack frames
+        .setMaxs(MethodPredicate.onName("foo"), 2, 7)
+        .transformMethodInsnInMethod(
+            "foo",
+            (opcode, owner, name, descriptor, isInterface, visitor) -> {
+              Label label1 = new Label();
+              visitor.visitLabel(label1);
+              visitor.visitLineNumber(4, label1);
+              visitor.visitMethodInsn(INVOKESTATIC, holder, "boo", "()Z", false);
+              Label label2 = new Label();
+              visitor.visitJumpInsn(IFEQ, label2);
+              Label label3 = new Label();
+              visitor.visitLabel(label3);
+              visitor.visitLineNumber(5, label3);
+              visitor.visitVarInsn(ALOAD, 0);
+              visitor.visitTypeInsn(CHECKCAST, "java/lang/Iterable");
+              visitor.visitVarInsn(ASTORE, 1);
+              visitor.visitInsn(ICONST_0);
+              visitor.visitVarInsn(ISTORE, 2);
+              Label label5 = new Label();
+              visitor.visitLabel(label5);
+              visitor.visitLineNumber(10, label5);
+              visitor.visitVarInsn(ALOAD, 1);
+              visitor.visitMethodInsn(
+                  INVOKEINTERFACE,
+                  "java/lang/Iterable",
+                  "iterator",
+                  "()Ljava/util/Iterator;",
+                  true);
+              visitor.visitVarInsn(ASTORE, 3);
+              Label label6 = new Label();
+              visitor.visitLabel(label6);
+              visitor.visitVarInsn(ALOAD, 3);
+              visitor.visitMethodInsn(
+                  INVOKEINTERFACE, "java/util/Iterator", "hasNext", "()Z", true);
+              Label label7 = new Label();
+              visitor.visitJumpInsn(IFEQ, label7);
+              visitor.visitVarInsn(ALOAD, 3);
+              visitor.visitMethodInsn(
+                  INVOKEINTERFACE, "java/util/Iterator", "next", "()Ljava/lang/Object;", true);
+              visitor.visitVarInsn(ASTORE, 4);
+              visitor.visitVarInsn(ALOAD, 4);
+              visitor.visitTypeInsn(CHECKCAST, "java/lang/Number");
+              visitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Number", "intValue", "()I", false);
+              visitor.visitVarInsn(ISTORE, 5);
+              visitor.visitInsn(ICONST_0);
+              visitor.visitVarInsn(ISTORE, 6);
+              Label label10 = new Label();
+              visitor.visitLabel(label10);
+              visitor.visitLineNumber(5, label10);
+              visitor.visitInsn(NOP);
+              visitor.visitJumpInsn(GOTO, label6);
+              visitor.visitLabel(label7);
+              visitor.visitLineNumber(11, label7);
+              visitor.visitInsn(NOP);
+              visitor.visitLabel(label2);
+              visitor.visitLineNumber(7, label2);
+              visitor.visitMethodInsn(INVOKESTATIC, holder, "boo", "()Z", false);
+              Label label12 = new Label();
+              visitor.visitJumpInsn(IFEQ, label12);
+              visitor.visitLabel(label12);
+              visitor.visitLineNumber(8, label12);
+              visitor.visitInsn(RETURN);
+            })
+        .transform();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/transformers/ClassFileTransformer.java b/src/test/java/com/android/tools/r8/transformers/ClassFileTransformer.java
index 889f7a7..9b4d076 100644
--- a/src/test/java/com/android/tools/r8/transformers/ClassFileTransformer.java
+++ b/src/test/java/com/android/tools/r8/transformers/ClassFileTransformer.java
@@ -1204,23 +1204,22 @@
   }
 
   public ClassFileTransformer setMaxStackHeight(MethodPredicate predicate, int newMaxStack) {
-    return addMethodTransformer(
-        new MethodTransformer() {
-          @Override
-          public void visitMaxs(int maxStack, int maxLocals) {
-            super.visitMaxs(
-                MethodPredicate.testContext(predicate, getContext()) ? newMaxStack : maxStack,
-                maxLocals);
-          }
-        });
+    return setMaxs(predicate, newMaxStack, null);
   }
 
-  public ClassFileTransformer computeMaxs() {
+  public ClassFileTransformer setMaxs(
+      MethodPredicate predicate, Integer newMaxStack, Integer newMaxLocals) {
     return addMethodTransformer(
         new MethodTransformer() {
           @Override
           public void visitMaxs(int maxStack, int maxLocals) {
-            super.visitMaxs(maxStack, maxLocals);
+            if (MethodPredicate.testContext(predicate, getContext())) {
+              super.visitMaxs(
+                  newMaxStack != null ? newMaxStack : maxStack,
+                  newMaxLocals != null ? newMaxLocals : maxLocals);
+            } else {
+              super.visitMaxs(maxStack, maxLocals);
+            }
           }
         });
   }