Implement join of cf stacks
Change-Id: I116eeea8a89973c5c81803c5d08ad48925e451b6
diff --git a/src/main/java/com/android/tools/r8/optimize/interfaces/analysis/ConcreteCfFrameState.java b/src/main/java/com/android/tools/r8/optimize/interfaces/analysis/ConcreteCfFrameState.java
index b3aa677..867d37a 100644
--- a/src/main/java/com/android/tools/r8/optimize/interfaces/analysis/ConcreteCfFrameState.java
+++ b/src/main/java/com/android/tools/r8/optimize/interfaces/analysis/ConcreteCfFrameState.java
@@ -11,7 +11,6 @@
import com.android.tools.r8.cf.code.CfFrame.FrameType;
import com.android.tools.r8.cf.code.frame.SingleFrameType;
import com.android.tools.r8.cf.code.frame.WideFrameType;
-import com.android.tools.r8.errors.Unimplemented;
import com.android.tools.r8.graph.AppView;
import com.android.tools.r8.graph.DexMethod;
import com.android.tools.r8.graph.DexType;
@@ -24,6 +23,7 @@
import it.unimi.dsi.fastutil.objects.ObjectBidirectionalIterator;
import java.util.ArrayDeque;
import java.util.Deque;
+import java.util.Iterator;
import java.util.Objects;
import java.util.function.BiFunction;
@@ -217,7 +217,10 @@
public CfFrameState join(ConcreteCfFrameState state) {
CfFrame.Builder builder = CfFrame.builder();
joinLocals(state.locals, builder);
- joinStack(state.stack, builder);
+ ErroneousCfFrameState error = joinStack(state.stack, builder);
+ if (error != null) {
+ return error;
+ }
CfFrame frame = builder.buildMutable();
return new ConcreteCfFrameState(frame.getLocals(), frame.getStack());
}
@@ -399,9 +402,25 @@
setSingleLocalToTop(localIndex + 1, builder);
}
- private void joinStack(Deque<FrameType> stack, CfFrame.Builder builder) {
- // TODO(b/214496607): Implement this.
- throw new Unimplemented();
+ private ErroneousCfFrameState joinStack(Deque<FrameType> stack, CfFrame.Builder builder) {
+ Iterator<FrameType> iterator = this.stack.descendingIterator();
+ Iterator<FrameType> otherIterator = stack.descendingIterator();
+ while (iterator.hasNext() && otherIterator.hasNext()) {
+ FrameType frameType = iterator.next();
+ FrameType otherFrameType = otherIterator.next();
+ if (frameType.isSingle() != otherFrameType.isSingle()) {
+ return error();
+ }
+ FrameType join =
+ frameType.isSingle()
+ ? frameType.asSingle().join(otherFrameType.asSingle()).asFrameType()
+ : frameType.asWide().join(otherFrameType.asWide()).asFrameType();
+ if (join.isTop()) {
+ return error();
+ }
+ builder.push(join);
+ }
+ return null;
}
@Override
diff --git a/src/test/java/com/android/tools/r8/cf/frames/DoubleAndLongIncompatibleTypesOnStackTest.java b/src/test/java/com/android/tools/r8/cf/frames/DoubleAndLongIncompatibleTypesOnStackTest.java
new file mode 100644
index 0000000..fa8d660
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/cf/frames/DoubleAndLongIncompatibleTypesOnStackTest.java
@@ -0,0 +1,159 @@
+// 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.cf.frames;
+
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestRuntime.CfVm;
+import com.android.tools.r8.cf.CfVersion;
+import com.android.tools.r8.transformers.ClassFileTransformer.MethodPredicate;
+import com.android.tools.r8.utils.BooleanUtils;
+import com.android.tools.r8.utils.codeinspector.AssertUtils;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.objectweb.asm.Label;
+import org.objectweb.asm.Opcodes;
+
+@RunWith(Parameterized.class)
+public class DoubleAndLongIncompatibleTypesOnStackTest extends TestBase implements Opcodes {
+
+ @Parameter(0)
+ public boolean insertFrame;
+
+ @Parameter(1)
+ public TestParameters parameters;
+
+ @Parameterized.Parameters(name = "{1}, insert frame: {0}")
+ public static List<Object[]> data() {
+ return buildParameters(
+ BooleanUtils.values(), getTestParameters().withAllRuntimesAndApiLevels().build());
+ }
+
+ @Test
+ public void testJvm() throws Exception {
+ assumeTrue(parameters.isCfRuntime());
+ testForJvm()
+ .addProgramClassFileData(getTransformedMain())
+ .run(parameters.getRuntime(), Main.class)
+ .applyIf(
+ insertFrame,
+ runResult ->
+ runResult.applyIf(
+ parameters.getRuntime().asCf().isOlderThan(CfVm.JDK9),
+ ignore -> runResult.assertSuccessWithEmptyOutput(),
+ ignore ->
+ runResult
+ .assertFailureWithErrorThatThrows(VerifyError.class)
+ .assertFailureWithErrorThatMatches(
+ containsString(
+ "Type top (current frame, stack[1]) is not assignable to"
+ + " category1 type"))),
+ runResult ->
+ runResult
+ .assertFailureWithErrorThatThrows(VerifyError.class)
+ .assertFailureWithErrorThatMatches(containsString("Mismatched stack types")));
+ }
+
+ @Test
+ public void testD8() throws Exception {
+ assumeTrue(parameters.isDexRuntime());
+ AssertUtils.assertFailsCompilation(
+ () ->
+ testForD8()
+ .addProgramClassFileData(getTransformedMain())
+ .setMinApi(parameters.getApiLevel())
+ .compile());
+ }
+
+ @Test
+ public void testR8() throws Exception {
+ AssertUtils.assertFailsCompilation(
+ () ->
+ testForR8(parameters.getBackend())
+ .addProgramClassFileData(getTransformedMain())
+ .addKeepMainRule(Main.class)
+ .setMinApi(parameters.getApiLevel())
+ .compile());
+ }
+
+ static class Main {
+
+ public static void main(String[] args) {
+ test(args.length);
+ }
+
+ public static void test(int i) {
+ stub();
+ // Code added by transformer:
+ // if (i != 0) {
+ // push 0.0d
+ // } else {
+ // push 0L
+ // }
+ // pop
+ // pop
+ }
+
+ private static void stub() {}
+ }
+
+ private byte[] getTransformedMain() throws Exception {
+ return transformer(Main.class)
+ .applyIf(!insertFrame, transformer -> transformer.setVersion(CfVersion.V1_5))
+ .setMaxs(MethodPredicate.onName("test"), 2, 1)
+ .transformMethodInsnInMethod(
+ "test",
+ (opcode, owner, name, descriptor, isInterface, visitor) -> {
+ Label elseLabel = new Label();
+ Label exitLabel = new Label();
+ visitor.visitVarInsn(ILOAD, 0);
+ visitor.visitJumpInsn(IFEQ, elseLabel);
+ visitor.visitInsn(DCONST_0);
+ visitor.visitJumpInsn(GOTO, exitLabel);
+ visitor.visitLabel(elseLabel);
+ if (insertFrame) {
+ visitor.visitFrame(
+ Opcodes.F_FULL,
+ // Locals
+ 1,
+ new Object[] {Opcodes.INTEGER},
+ // Stack
+ 0,
+ new Object[0]);
+ }
+ visitor.visitInsn(LCONST_0);
+ visitor.visitLabel(exitLabel);
+ if (insertFrame) {
+ visitor.visitFrame(
+ Opcodes.F_FULL,
+ // Locals
+ 1,
+ new Object[] {Opcodes.INTEGER},
+ // Stack
+ 2,
+ new Object[] {Opcodes.TOP, Opcodes.TOP});
+ }
+ visitor.visitInsn(POP);
+ visitor.visitInsn(POP);
+ visitor.visitInsn(RETURN);
+ if (insertFrame) {
+ visitor.visitFrame(
+ Opcodes.F_FULL,
+ // Locals
+ 1,
+ new Object[] {Opcodes.INTEGER},
+ // Stack
+ 0,
+ new Object[0]);
+ }
+ })
+ .transform();
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/cf/frames/OverlappingWidesOnStackTest.java b/src/test/java/com/android/tools/r8/cf/frames/OverlappingWidesOnStackTest.java
new file mode 100644
index 0000000..24b76fb
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/cf/frames/OverlappingWidesOnStackTest.java
@@ -0,0 +1,165 @@
+// 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.cf.frames;
+
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestRuntime.CfVm;
+import com.android.tools.r8.cf.CfVersion;
+import com.android.tools.r8.transformers.ClassFileTransformer.MethodPredicate;
+import com.android.tools.r8.utils.BooleanUtils;
+import com.android.tools.r8.utils.codeinspector.AssertUtils;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.objectweb.asm.Label;
+import org.objectweb.asm.Opcodes;
+
+@RunWith(Parameterized.class)
+public class OverlappingWidesOnStackTest extends TestBase implements Opcodes {
+
+ @Parameter(0)
+ public boolean insertFrame;
+
+ @Parameter(1)
+ public TestParameters parameters;
+
+ @Parameterized.Parameters(name = "{1}, insert frame: {0}")
+ public static List<Object[]> data() {
+ return buildParameters(
+ BooleanUtils.values(), getTestParameters().withAllRuntimesAndApiLevels().build());
+ }
+
+ @Test
+ public void testJvm() throws Exception {
+ assumeTrue(parameters.isCfRuntime());
+ testForJvm()
+ .addProgramClassFileData(getTransformedMain())
+ .run(parameters.getRuntime(), Main.class)
+ .applyIf(
+ insertFrame,
+ runResult ->
+ runResult.applyIf(
+ parameters.getRuntime().asCf().isOlderThan(CfVm.JDK9),
+ ignore -> runResult.assertSuccessWithEmptyOutput(),
+ ignore ->
+ runResult
+ .assertFailureWithErrorThatThrows(VerifyError.class)
+ .assertFailureWithErrorThatMatches(
+ containsString(
+ "Type top (current frame, stack[2]) is not assignable to"
+ + " category1 type"))),
+ runResult ->
+ runResult
+ .assertFailureWithErrorThatThrows(VerifyError.class)
+ .assertFailureWithErrorThatMatches(containsString("Mismatched stack types")));
+ }
+
+ @Test
+ public void testD8() throws Exception {
+ assumeTrue(parameters.isDexRuntime());
+ AssertUtils.assertFailsCompilation(
+ () ->
+ testForD8()
+ .addProgramClassFileData(getTransformedMain())
+ .setMinApi(parameters.getApiLevel())
+ .compile());
+ }
+
+ @Test
+ public void testR8() throws Exception {
+ AssertUtils.assertFailsCompilation(
+ () ->
+ testForR8(parameters.getBackend())
+ .addProgramClassFileData(getTransformedMain())
+ .addKeepMainRule(Main.class)
+ .setMinApi(parameters.getApiLevel())
+ .compile());
+ }
+
+ static class Main {
+
+ public static void main(String[] args) {
+ test(args.length);
+ }
+
+ public static void test(int i) {
+ stub();
+ // Code added by transformer:
+ // if (i != 0) {
+ // push 0
+ // push 0L
+ // } else {
+ // push 0L
+ // push 0
+ // }
+ // pop
+ // pop
+ // pop
+ }
+
+ private static void stub() {}
+ }
+
+ private byte[] getTransformedMain() throws Exception {
+ return transformer(Main.class)
+ .applyIf(!insertFrame, transformer -> transformer.setVersion(CfVersion.V1_5))
+ .setMaxs(MethodPredicate.onName("test"), 3, 1)
+ .transformMethodInsnInMethod(
+ "test",
+ (opcode, owner, name, descriptor, isInterface, visitor) -> {
+ Label elseLabel = new Label();
+ Label exitLabel = new Label();
+ visitor.visitVarInsn(ILOAD, 0);
+ visitor.visitJumpInsn(IFEQ, elseLabel);
+ visitor.visitInsn(ICONST_0);
+ visitor.visitInsn(LCONST_0);
+ visitor.visitJumpInsn(GOTO, exitLabel);
+ visitor.visitLabel(elseLabel);
+ if (insertFrame) {
+ visitor.visitFrame(
+ Opcodes.F_FULL,
+ // Locals
+ 1,
+ new Object[] {Opcodes.INTEGER},
+ // Stack
+ 0,
+ new Object[0]);
+ }
+ visitor.visitInsn(LCONST_0);
+ visitor.visitInsn(ICONST_0);
+ visitor.visitLabel(exitLabel);
+ if (insertFrame) {
+ visitor.visitFrame(
+ Opcodes.F_FULL,
+ // Locals
+ 1,
+ new Object[] {Opcodes.INTEGER},
+ // Stack
+ 3,
+ new Object[] {Opcodes.TOP, Opcodes.TOP, Opcodes.TOP});
+ }
+ visitor.visitInsn(POP);
+ visitor.visitInsn(POP);
+ visitor.visitInsn(POP);
+ visitor.visitInsn(RETURN);
+ if (insertFrame) {
+ visitor.visitFrame(
+ Opcodes.F_FULL,
+ // Locals
+ 1,
+ new Object[] {Opcodes.INTEGER},
+ // Stack
+ 0,
+ new Object[0]);
+ }
+ })
+ .transform();
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/cf/frames/PrimitiveAndObjectIncompatibleTypesOnStackTest.java b/src/test/java/com/android/tools/r8/cf/frames/PrimitiveAndObjectIncompatibleTypesOnStackTest.java
new file mode 100644
index 0000000..1b368c1
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/cf/frames/PrimitiveAndObjectIncompatibleTypesOnStackTest.java
@@ -0,0 +1,157 @@
+// 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.cf.frames;
+
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestRuntime.CfVm;
+import com.android.tools.r8.cf.CfVersion;
+import com.android.tools.r8.transformers.ClassFileTransformer.MethodPredicate;
+import com.android.tools.r8.utils.BooleanUtils;
+import com.android.tools.r8.utils.codeinspector.AssertUtils;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.objectweb.asm.Label;
+import org.objectweb.asm.Opcodes;
+
+@RunWith(Parameterized.class)
+public class PrimitiveAndObjectIncompatibleTypesOnStackTest extends TestBase implements Opcodes {
+
+ @Parameter(0)
+ public boolean insertFrame;
+
+ @Parameter(1)
+ public TestParameters parameters;
+
+ @Parameterized.Parameters(name = "{1}, insert frame: {0}")
+ public static List<Object[]> data() {
+ return buildParameters(
+ BooleanUtils.values(), getTestParameters().withAllRuntimesAndApiLevels().build());
+ }
+
+ @Test
+ public void testJvm() throws Exception {
+ assumeTrue(parameters.isCfRuntime());
+ testForJvm()
+ .addProgramClassFileData(getTransformedMain())
+ .run(parameters.getRuntime(), Main.class)
+ .applyIf(
+ insertFrame,
+ runResult ->
+ runResult.applyIf(
+ parameters.getRuntime().asCf().isOlderThan(CfVm.JDK9),
+ ignore -> runResult.assertSuccessWithEmptyOutput(),
+ ignore ->
+ runResult
+ .assertFailureWithErrorThatThrows(VerifyError.class)
+ .assertFailureWithErrorThatMatches(
+ containsString(
+ "Type top (current frame, stack[0]) is not assignable to"
+ + " category1 type"))),
+ runResult ->
+ runResult
+ .assertFailureWithErrorThatThrows(VerifyError.class)
+ .assertFailureWithErrorThatMatches(containsString("Mismatched stack types")));
+ }
+
+ @Test
+ public void testD8() throws Exception {
+ assumeTrue(parameters.isDexRuntime());
+ AssertUtils.assertFailsCompilation(
+ () ->
+ testForD8()
+ .addProgramClassFileData(getTransformedMain())
+ .setMinApi(parameters.getApiLevel())
+ .compile());
+ }
+
+ @Test
+ public void testR8() throws Exception {
+ AssertUtils.assertFailsCompilation(
+ () ->
+ testForR8(parameters.getBackend())
+ .addProgramClassFileData(getTransformedMain())
+ .addKeepMainRule(Main.class)
+ .setMinApi(parameters.getApiLevel())
+ .compile());
+ }
+
+ static class Main {
+
+ public static void main(String[] args) {
+ test(args.length, new Object());
+ }
+
+ public static void test(int i, Object obj) {
+ stub();
+ // Code added by transformer:
+ // if (i != 0) {
+ // push 0
+ // } else {
+ // push obj
+ // }
+ // pop
+ }
+
+ private static void stub() {}
+ }
+
+ private byte[] getTransformedMain() throws Exception {
+ return transformer(Main.class)
+ .applyIf(!insertFrame, transformer -> transformer.setVersion(CfVersion.V1_5))
+ .setMaxs(MethodPredicate.onName("test"), 1, 2)
+ .transformMethodInsnInMethod(
+ "test",
+ (opcode, owner, name, descriptor, isInterface, visitor) -> {
+ Label elseLabel = new Label();
+ Label exitLabel = new Label();
+ visitor.visitVarInsn(ILOAD, 0);
+ visitor.visitJumpInsn(IFEQ, elseLabel);
+ visitor.visitInsn(ICONST_0);
+ visitor.visitJumpInsn(GOTO, exitLabel);
+ visitor.visitLabel(elseLabel);
+ if (insertFrame) {
+ visitor.visitFrame(
+ Opcodes.F_FULL,
+ // Locals
+ 2,
+ new Object[] {Opcodes.INTEGER, binaryName(Object.class)},
+ // Stack
+ 0,
+ new Object[0]);
+ }
+ visitor.visitVarInsn(ALOAD, 1);
+ visitor.visitLabel(exitLabel);
+ if (insertFrame) {
+ visitor.visitFrame(
+ Opcodes.F_FULL,
+ // Locals
+ 2,
+ new Object[] {Opcodes.INTEGER, binaryName(Object.class)},
+ // Stack
+ 1,
+ new Object[] {Opcodes.TOP});
+ }
+ visitor.visitInsn(POP);
+ visitor.visitInsn(RETURN);
+ if (insertFrame) {
+ visitor.visitFrame(
+ Opcodes.F_FULL,
+ // Locals
+ 2,
+ new Object[] {Opcodes.INTEGER, binaryName(Object.class)},
+ // Stack
+ 0,
+ new Object[] {});
+ }
+ })
+ .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 9b4d076..52485b2 100644
--- a/src/test/java/com/android/tools/r8/transformers/ClassFileTransformer.java
+++ b/src/test/java/com/android/tools/r8/transformers/ClassFileTransformer.java
@@ -25,6 +25,7 @@
import com.android.tools.r8.references.Reference;
import com.android.tools.r8.transformers.MethodTransformer.MethodContext;
import com.android.tools.r8.utils.DescriptorUtils;
+import com.android.tools.r8.utils.ThrowingConsumer;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
@@ -149,6 +150,14 @@
return create(ToolHelper.getClassAsBytes(clazz), classFromTypeName(clazz.getTypeName()));
}
+ public <E extends Exception> ClassFileTransformer applyIf(
+ boolean condition, ThrowingConsumer<ClassFileTransformer, E> consumer) throws E {
+ if (condition) {
+ consumer.accept(this);
+ }
+ return this;
+ }
+
public byte[] transform() {
return transform(0);
}