Don't simplify branches with observable line changes
Bug: b/339064674
Change-Id: I92c40837b6625a0a27b48b07b14de9fb94b8541b
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/equivalence/BasicBlockBehavioralSubsumption.java b/src/main/java/com/android/tools/r8/ir/analysis/equivalence/BasicBlockBehavioralSubsumption.java
index c37be79..35619e1 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/equivalence/BasicBlockBehavioralSubsumption.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/equivalence/BasicBlockBehavioralSubsumption.java
@@ -18,6 +18,7 @@
import com.android.tools.r8.ir.code.InstructionIterator;
import com.android.tools.r8.ir.code.InvokeDirect;
import com.android.tools.r8.ir.code.Phi;
+import com.android.tools.r8.ir.code.Position;
import com.android.tools.r8.ir.code.Return;
import com.android.tools.r8.ir.code.Value;
import com.android.tools.r8.utils.SetUtils;
@@ -43,8 +44,10 @@
this.context = code.context();
}
- public boolean isSubsumedBy(Value conditionValue, BasicBlock block, BasicBlock other) {
- return isSubsumedBy(conditionValue, block.iterator(), other.iterator(), null);
+ public boolean isSubsumedBy(
+ Value conditionValue, Position conditionPosition, BasicBlock block, BasicBlock other) {
+ return isSubsumedBy(
+ conditionValue, conditionPosition, block.iterator(), other.iterator(), null);
}
private boolean dependsOnConditionValue(Value conditionValue, Instruction instruction) {
@@ -81,13 +84,21 @@
}
private Instruction skipNonDependentInstructionsUntil(
- InstructionIterator iterator, Value conditionValue, Predicate<Instruction> predicate) {
+ InstructionIterator iterator,
+ Value conditionValue,
+ Position position,
+ Predicate<Instruction> predicate) {
return iterator.nextUntil(
- predicate.or(i -> i.isJumpInstruction() || dependsOnConditionValue(conditionValue, i)));
+ predicate.or(
+ i ->
+ i.isJumpInstruction()
+ || (i.isDebugPosition() && !i.getPosition().equals(position))
+ || dependsOnConditionValue(conditionValue, i)));
}
private boolean isSubsumedBy(
Value conditionValue,
+ Position conditionPosition,
InstructionIterator iterator,
InstructionIterator otherIterator,
Set<BasicBlock> visited) {
@@ -95,7 +106,7 @@
// outside the block itself) that do not have side effects.
Instruction instruction =
skipNonDependentInstructionsUntil(
- iterator, conditionValue, this::isNonLocalDefinitionOrSideEffecting);
+ iterator, conditionValue, conditionPosition, this::isNonLocalDefinitionOrSideEffecting);
// If the instruction defines a value with non-local usages, then we would need a dominator
// analysis to verify that all these non-local usages can in fact be replaced by a value
@@ -107,7 +118,7 @@
// Skip over non-throwing instructions in the other block.
Instruction otherInstruction =
skipNonDependentInstructionsUntil(
- otherIterator, conditionValue, this::instructionMayHaveSideEffects);
+ otherIterator, conditionValue, conditionPosition, this::instructionMayHaveSideEffects);
assert otherInstruction != null;
if (instruction.isGoto()) {
@@ -139,7 +150,8 @@
visited = SetUtils.newIdentityHashSet(instruction.getBlock());
}
if (visited.add(targetBlock)) {
- return isSubsumedBy(conditionValue, targetBlock.iterator(), otherIterator, visited);
+ return isSubsumedBy(
+ conditionValue, conditionPosition, targetBlock.iterator(), otherIterator, visited);
}
// Guard against cycles in the control flow graph.
return false;
@@ -164,7 +176,10 @@
otherIterator = targetBlock.iterator();
otherInstruction =
skipNonDependentInstructionsUntil(
- otherIterator, conditionValue, this::instructionMayHaveSideEffects);
+ otherIterator,
+ conditionValue,
+ conditionPosition,
+ this::instructionMayHaveSideEffects);
// If following the first goto instruction leads to another goto instruction, then we need to
// keep track of the set of visited blocks to guard against cycles in the control flow graph.
@@ -188,7 +203,7 @@
|| otherSingleTarget.getDefinition().getOptimizationInfo().mayHaveSideEffects()) {
return false;
}
- return isSubsumedBy(conditionValue, iterator, otherIterator, visited);
+ return isSubsumedBy(conditionValue, conditionPosition, iterator, otherIterator, visited);
}
return false;
}
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/BranchSimplifier.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/BranchSimplifier.java
index 4696ec5..228eb0f 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/passes/BranchSimplifier.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/BranchSimplifier.java
@@ -137,7 +137,8 @@
// Unable to determine which branch will be taken. Check if the true target can safely be
// rewritten to the false target.
if (behavioralSubsumption.isSubsumedBy(
- theIf.inValues().get(0), theIf.getTrueTarget(), theIf.fallthroughBlock())) {
+ theIf.inValues().get(0), theIf.getPosition(),
+ theIf.getTrueTarget(), theIf.fallthroughBlock())) {
simplifyIfWithKnownCondition(code, block, theIf, theIf.fallthroughBlock());
simplified = true;
}
@@ -837,7 +838,7 @@
if (switchCaseAnalyzer.switchCaseIsUnreachable(theSwitch, switchAbstractValue, i)) {
eliminator.markSwitchCaseForRemoval(i);
} else if (behavioralSubsumption.isSubsumedBy(
- theSwitch.value(), targetBlock, defaultTarget)) {
+ theSwitch.value(), theSwitch.getPosition(), targetBlock, defaultTarget)) {
eliminator.markSwitchCaseForRemoval(i);
hasSwitchCaseToDefaultRewrite = true;
}
diff --git a/src/test/java/com/android/tools/r8/debug/RegressB339064674Test.java b/src/test/java/com/android/tools/r8/debug/RegressB339064674Test.java
new file mode 100644
index 0000000..e2a2b26
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/debug/RegressB339064674Test.java
@@ -0,0 +1,20 @@
+// Copyright (c) 2024, 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;
+
+public class RegressB339064674Test {
+
+ public static void main(String[] args) {
+ for (int i : Arrays.asList(0, 1)) {
+ int a1 = 1;
+ if (i == 0) {
+ continue;
+ }
+ int a2 = 2;
+ }
+ int a3 = 3;
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/debug/RegressB339064674TestRunner.java b/src/test/java/com/android/tools/r8/debug/RegressB339064674TestRunner.java
new file mode 100644
index 0000000..c8507ef
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/debug/RegressB339064674TestRunner.java
@@ -0,0 +1,106 @@
+// Copyright (c) 2024, 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 static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class RegressB339064674TestRunner extends DebugTestBase {
+
+ static final Class CLASS = RegressB339064674Test.class;
+ static final String NAME = CLASS.getCanonicalName();
+ static final String FILE = CLASS.getSimpleName() + ".java";
+ static final AndroidApiLevel minApi = AndroidApiLevel.B;
+ static final String EXPECTED = "";
+
+ private final TestParameters parameters;
+
+ @Parameters(name = "{0}")
+ public static TestParametersCollection setup() {
+ return TestParameters.builder().withAllRuntimes().build();
+ }
+
+ public RegressB339064674TestRunner(TestParameters parameters) {
+ this.parameters = parameters;
+ }
+
+ @Test
+ public void testReference() throws Throwable {
+ assumeTrue(parameters.isCfRuntime());
+ testForJvm(parameters)
+ .addProgramClasses(CLASS)
+ .run(parameters.getRuntime(), CLASS)
+ .assertSuccessWithOutput(EXPECTED)
+ .debugger(this::runDebugger);
+ }
+
+ @Test
+ public void testD8() throws Throwable {
+ testForD8(parameters.getBackend())
+ .setMinApi(minApi)
+ .addProgramClasses(CLASS)
+ .run(parameters.getRuntime(), CLASS)
+ .assertSuccessWithOutput(EXPECTED)
+ .debugger(this::runDebugger);
+ }
+
+ @Test
+ public void testR8() throws Throwable {
+ testForR8(parameters.getBackend())
+ .debug()
+ .addKeepMainRule(CLASS)
+ .addProgramClasses(CLASS)
+ .setMinApi(minApi)
+ .run(parameters.getRuntime(), CLASS)
+ .assertSuccessWithOutput(EXPECTED)
+ .debugger(this::runDebugger);
+ }
+
+ public void runDebugger(DebugTestConfig config) throws Throwable {
+ // The test ensures that when breaking inside a function and changing a local in the parent
+ // frame, that the new value is passed to the second invocation of the function.
+ // This ensures that no peephole optimizations will optimize if there is any debug information.
+ runDebugTest(
+ config,
+ NAME,
+ breakpoint(NAME, "main"),
+ run(),
+ checkLine(FILE, 11),
+ stepOver(),
+ checkLine(FILE, 12),
+ stepOver(),
+ checkLine(FILE, 13),
+ stepOver(),
+ checkLine(FILE, 14),
+ stepOver(),
+ checkLine(FILE, 11),
+ stepOver(),
+ checkLine(FILE, 12),
+ stepOver(),
+ checkLine(FILE, 13),
+ stepOver(),
+ checkLine(FILE, 16),
+ checkLocals("args", "i", "a1"),
+ checkNoLocal("a2"),
+ stepOver(),
+ checkLine(FILE, 17),
+ checkLocals("args"),
+ checkNoLocal("i"),
+ stepOver(),
+ checkLine(FILE, 11),
+ stepOver(),
+ checkLine(FILE, 18),
+ stepOver(),
+ checkLine(FILE, 19),
+ run());
+ }
+}