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);
   }