Implement join of cf locals

Bug: 231521474
Change-Id: If06a5c6cd054f6fa062eddc5fd1090107ddf7ae9
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfFrame.java b/src/main/java/com/android/tools/r8/cf/code/CfFrame.java
index 4bbd2bc..c0e229d 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfFrame.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfFrame.java
@@ -6,6 +6,9 @@
 import static org.objectweb.asm.Opcodes.F_NEW;
 
 import com.android.tools.r8.cf.CfPrinter;
+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.errors.Unreachable;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.CfCode;
@@ -50,7 +53,7 @@
   public abstract static class FrameType {
 
     public static FrameType initialized(DexType type) {
-      return new InitializedType(type);
+      return type.isWideType() ? new WideInitializedType(type) : new SingleInitializedType(type);
     }
 
     public static FrameType uninitializedNew(CfLabel label, DexType typeToInitialize) {
@@ -73,6 +76,10 @@
       return TwoWord.SINGLETON;
     }
 
+    public FrameType asFrameType() {
+      return this;
+    }
+
     abstract Object getTypeOpcode(GraphLens graphLens, NamingLens namingLens);
 
     public boolean isObject() {
@@ -88,10 +95,18 @@
       return !isWide();
     }
 
+    public SingleFrameType asSingle() {
+      return null;
+    }
+
     public boolean isWide() {
       return false;
     }
 
+    public WideFrameType asWide() {
+      return null;
+    }
+
     public boolean isUninitializedNew() {
       return false;
     }
@@ -226,16 +241,22 @@
     return CfCompareHelper.compareIdUniquelyDeterminesEquality(this, other);
   }
 
-  private static class InitializedType extends FrameType {
+  private static class SingleInitializedType extends FrameType implements SingleFrameType {
 
     private final DexType type;
 
-    private InitializedType(DexType type) {
+    private SingleInitializedType(DexType type) {
       assert type != null;
       this.type = type;
     }
 
     @Override
+    public SingleFrameType join(SingleFrameType frameType) {
+      // TODO(b/214496607): Implement this.
+      throw new Unimplemented();
+    }
+
+    @Override
     public boolean equals(Object obj) {
       if (this == obj) {
         return true;
@@ -243,7 +264,7 @@
       if (obj == null || getClass() != obj.getClass()) {
         return false;
       }
-      InitializedType initializedType = (InitializedType) obj;
+      SingleInitializedType initializedType = (SingleInitializedType) obj;
       return type == initializedType.type;
     }
 
@@ -280,8 +301,13 @@
     }
 
     @Override
+    public SingleFrameType asSingle() {
+      return this;
+    }
+
+    @Override
     public boolean isWide() {
-      return type.isPrimitiveType() && (type.toShorty() == 'J' || type.toShorty() == 'D');
+      return false;
     }
 
     @Override
@@ -306,13 +332,105 @@
     }
   }
 
-  private static class Top extends SingletonFrameType {
+  private static class WideInitializedType extends FrameType implements WideFrameType {
+
+    private final DexType type;
+
+    private WideInitializedType(DexType type) {
+      assert type != null;
+      this.type = type;
+    }
+
+    @Override
+    public boolean isWide() {
+      return true;
+    }
+
+    @Override
+    public WideFrameType asWide() {
+      return this;
+    }
+
+    @Override
+    public WideFrameType join(WideFrameType frameType) {
+      // TODO(b/214496607): Implement this.
+      throw new Unimplemented();
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+      if (this == obj) {
+        return true;
+      }
+      if (obj == null || getClass() != obj.getClass()) {
+        return false;
+      }
+      WideInitializedType initializedType = (WideInitializedType) obj;
+      return type == initializedType.type;
+    }
+
+    @Override
+    public int hashCode() {
+      return type.hashCode();
+    }
+
+    @Override
+    public String toString() {
+      return "Initialized(" + type.toString() + ")";
+    }
+
+    @Override
+    Object getTypeOpcode(GraphLens graphLens, NamingLens namingLens) {
+      switch (type.toShorty()) {
+        case 'J':
+          return Opcodes.LONG;
+        case 'D':
+          return Opcodes.DOUBLE;
+        default:
+          throw new Unreachable("Unexpected value type: " + type);
+      }
+    }
+
+    @Override
+    public boolean isInitialized() {
+      return true;
+    }
+
+    @Override
+    public DexType getInitializedType() {
+      return type;
+    }
+
+    @Override
+    public boolean isObject() {
+      return type.isNullValueType() || type.isReferenceType();
+    }
+
+    @Override
+    public DexType getObjectType(ProgramMethod context) {
+      assert isObject() : "Unexpected use of getObjectType() for non-object FrameType";
+      return type;
+    }
+  }
+
+  private static class Top extends SingletonFrameType implements SingleFrameType {
 
     private static final Top SINGLETON = new Top();
 
     private Top() {}
 
     @Override
+    public SingleFrameType asSingle() {
+      return this;
+    }
+
+    @Override
+    public SingleFrameType join(SingleFrameType frameType) {
+      // TODO(b/214496607): Implement this.
+      throw new Unimplemented();
+    }
+
+    @Override
     public String toString() {
       return "top";
     }
@@ -328,7 +446,7 @@
     }
   }
 
-  private static class UninitializedNew extends FrameType {
+  private static class UninitializedNew extends FrameType implements SingleFrameType {
 
     private final CfLabel label;
     private final DexType type;
@@ -339,6 +457,17 @@
     }
 
     @Override
+    public SingleFrameType asSingle() {
+      return this;
+    }
+
+    @Override
+    public SingleFrameType join(SingleFrameType frameType) {
+      // TODO(b/214496607): Implement this.
+      throw new Unimplemented();
+    }
+
+    @Override
     public boolean equals(Object o) {
       if (this == o) {
         return true;
@@ -396,13 +525,24 @@
     }
   }
 
-  private static class UninitializedThis extends SingletonFrameType {
+  private static class UninitializedThis extends SingletonFrameType implements SingleFrameType {
 
     private static final UninitializedThis SINGLETON = new UninitializedThis();
 
     private UninitializedThis() {}
 
     @Override
+    public SingleFrameType asSingle() {
+      return this;
+    }
+
+    @Override
+    public SingleFrameType join(SingleFrameType frameType) {
+      // TODO(b/214496607): Implement this.
+      throw new Unimplemented();
+    }
+
+    @Override
     Object getTypeOpcode(GraphLens graphLens, NamingLens namingLens) {
       return Opcodes.UNINITIALIZED_THIS;
     }
@@ -433,13 +573,24 @@
     }
   }
 
-  private static class OneWord extends SingletonFrameType {
+  private static class OneWord extends SingletonFrameType implements SingleFrameType {
 
     private static final OneWord SINGLETON = new OneWord();
 
     private OneWord() {}
 
     @Override
+    public SingleFrameType asSingle() {
+      return this;
+    }
+
+    @Override
+    public SingleFrameType join(SingleFrameType frameType) {
+      // TODO(b/214496607): Implement this.
+      throw new Unimplemented();
+    }
+
+    @Override
     Object getTypeOpcode(GraphLens graphLens, NamingLens namingLens) {
       throw new Unreachable("Should only be used for verification");
     }
@@ -455,20 +606,31 @@
     }
   }
 
-  private static class TwoWord extends SingletonFrameType {
+  private static class TwoWord extends SingletonFrameType implements WideFrameType {
 
     private static final TwoWord SINGLETON = new TwoWord();
 
     private TwoWord() {}
 
     @Override
-    Object getTypeOpcode(GraphLens graphLens, NamingLens namingLens) {
-      throw new Unreachable("Should only be used for verification");
+    public boolean isWide() {
+      return true;
     }
 
     @Override
-    public boolean isWide() {
-      return true;
+    public WideFrameType asWide() {
+      return this;
+    }
+
+    @Override
+    public WideFrameType join(WideFrameType frameType) {
+      // TODO(b/214496607): Implement this.
+      throw new Unimplemented();
+    }
+
+    @Override
+    Object getTypeOpcode(GraphLens graphLens, NamingLens namingLens) {
+      throw new Unreachable("Should only be used for verification");
     }
 
     @Override
@@ -786,10 +948,17 @@
       return this;
     }
 
+    public boolean hasLocal(int localIndex) {
+      return locals.containsKey(localIndex);
+    }
+
+    public FrameType getLocal(int localIndex) {
+      assert hasLocal(localIndex);
+      return locals.get(localIndex);
+    }
+
     public Builder push(FrameType frameType) {
-      if (stack == EMPTY_STACK) {
-        stack = new ArrayDeque<>();
-      }
+      ensureMutableStack();
       stack.addLast(frameType);
       return this;
     }
@@ -800,9 +969,7 @@
     }
 
     private Builder internalStore(int localIndex, FrameType frameType) {
-      if (locals == EMPTY_LOCALS) {
-        locals = new Int2ObjectAVLTreeMap<>();
-      }
+      ensureMutableLocals();
       locals.put(localIndex, frameType);
       if (frameType.isWide()) {
         locals.put(localIndex + 1, frameType);
@@ -813,5 +980,23 @@
     public CfFrame build() {
       return new CfFrame(locals, stack);
     }
+
+    public CfFrame buildMutable() {
+      ensureMutableLocals();
+      ensureMutableStack();
+      return build();
+    }
+
+    private void ensureMutableLocals() {
+      if (locals == EMPTY_LOCALS) {
+        locals = new Int2ObjectAVLTreeMap<>();
+      }
+    }
+
+    private void ensureMutableStack() {
+      if (stack == EMPTY_STACK) {
+        stack = new ArrayDeque<>();
+      }
+    }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/cf/code/frame/SingleFrameType.java b/src/main/java/com/android/tools/r8/cf/code/frame/SingleFrameType.java
new file mode 100644
index 0000000..2f1ead8
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/cf/code/frame/SingleFrameType.java
@@ -0,0 +1,14 @@
+// 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.code.frame;
+
+import com.android.tools.r8.cf.code.CfFrame.FrameType;
+
+public interface SingleFrameType {
+
+  FrameType asFrameType();
+
+  SingleFrameType join(SingleFrameType frameType);
+}
diff --git a/src/main/java/com/android/tools/r8/cf/code/frame/WideFrameType.java b/src/main/java/com/android/tools/r8/cf/code/frame/WideFrameType.java
new file mode 100644
index 0000000..52efbcc
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/cf/code/frame/WideFrameType.java
@@ -0,0 +1,14 @@
+// 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.code.frame;
+
+import com.android.tools.r8.cf.code.CfFrame.FrameType;
+
+public interface WideFrameType {
+
+  FrameType asFrameType();
+
+  WideFrameType join(WideFrameType frameType);
+}
diff --git a/src/main/java/com/android/tools/r8/optimize/interfaces/analysis/BottomCfFrameState.java b/src/main/java/com/android/tools/r8/optimize/interfaces/analysis/BottomCfFrameState.java
index 0c527a1..46c1930 100644
--- a/src/main/java/com/android/tools/r8/optimize/interfaces/analysis/BottomCfFrameState.java
+++ b/src/main/java/com/android/tools/r8/optimize/interfaces/analysis/BottomCfFrameState.java
@@ -25,6 +25,11 @@
   }
 
   @Override
+  public boolean isBottom() {
+    return true;
+  }
+
+  @Override
   public CfFrameState check(AppView<?> appView, CfFrame frame) {
     if (CfAssignability.isFrameAssignable(new CfFrame(), frame, appView).isFailed()) {
       return error();
diff --git a/src/main/java/com/android/tools/r8/optimize/interfaces/analysis/CfFrameState.java b/src/main/java/com/android/tools/r8/optimize/interfaces/analysis/CfFrameState.java
index ead8fb6..4e63781 100644
--- a/src/main/java/com/android/tools/r8/optimize/interfaces/analysis/CfFrameState.java
+++ b/src/main/java/com/android/tools/r8/optimize/interfaces/analysis/CfFrameState.java
@@ -7,7 +7,6 @@
 import com.android.tools.r8.cf.code.CfAssignability;
 import com.android.tools.r8.cf.code.CfFrame;
 import com.android.tools.r8.cf.code.CfFrame.FrameType;
-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;
@@ -23,7 +22,7 @@
 
 public abstract class CfFrameState extends AbstractState<CfFrameState> {
 
-  public static CfFrameState bottom() {
+  public static BottomCfFrameState bottom() {
     return BottomCfFrameState.getInstance();
   }
 
@@ -36,6 +35,22 @@
     return this;
   }
 
+  public boolean isBottom() {
+    return false;
+  }
+
+  public boolean isConcrete() {
+    return false;
+  }
+
+  public ConcreteCfFrameState asConcrete() {
+    return null;
+  }
+
+  public boolean isError() {
+    return false;
+  }
+
   public abstract CfFrameState check(AppView<?> appView, CfFrame frame);
 
   public abstract CfFrameState clear();
@@ -216,8 +231,15 @@
 
   @Override
   public final CfFrameState join(CfFrameState state) {
-    // TODO(b/214496607): Implement join.
-    throw new Unimplemented();
+    if (state.isBottom() || isError()) {
+      return this;
+    }
+    if (isBottom() || state.isError()) {
+      return state;
+    }
+    assert isConcrete();
+    assert state.isConcrete();
+    return asConcrete().join(state.asConcrete());
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/optimize/interfaces/analysis/CfOpenClosedInterfacesAnalysis.java b/src/main/java/com/android/tools/r8/optimize/interfaces/analysis/CfOpenClosedInterfacesAnalysis.java
index feed0d2..288f281 100644
--- a/src/main/java/com/android/tools/r8/optimize/interfaces/analysis/CfOpenClosedInterfacesAnalysis.java
+++ b/src/main/java/com/android/tools/r8/optimize/interfaces/analysis/CfOpenClosedInterfacesAnalysis.java
@@ -5,7 +5,6 @@
 package com.android.tools.r8.optimize.interfaces.analysis;
 
 import com.android.tools.r8.cf.code.CfInstruction;
-import com.android.tools.r8.errors.Unimplemented;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.CfCode;
 import com.android.tools.r8.graph.Code;
@@ -68,9 +67,8 @@
 
     @Override
     public CfFrameState computeBlockEntryState(
-        CfBlock cfBlock, CfBlock predecessor, CfFrameState predecessorExitState) {
-      // TODO(b/214496607): Implement this.
-      throw new Unimplemented();
+        CfBlock block, CfBlock predecessor, CfFrameState predecessorExitState) {
+      return predecessorExitState;
     }
   }
 }
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 31484f5..b3aa677 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
@@ -9,6 +9,9 @@
 import com.android.tools.r8.cf.code.CfAssignability;
 import com.android.tools.r8.cf.code.CfFrame;
 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;
@@ -16,7 +19,9 @@
 import com.android.tools.r8.ir.code.ValueType;
 import it.unimi.dsi.fastutil.ints.Int2ObjectAVLTreeMap;
 import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
+import it.unimi.dsi.fastutil.ints.Int2ObjectMap.Entry;
 import it.unimi.dsi.fastutil.ints.Int2ObjectSortedMap;
+import it.unimi.dsi.fastutil.objects.ObjectBidirectionalIterator;
 import java.util.ArrayDeque;
 import java.util.Deque;
 import java.util.Objects;
@@ -37,6 +42,16 @@
   }
 
   @Override
+  public boolean isConcrete() {
+    return true;
+  }
+
+  @Override
+  public ConcreteCfFrameState asConcrete() {
+    return this;
+  }
+
+  @Override
   public CfFrameState check(AppView<?> appView, CfFrame frame) {
     if (CfAssignability.isFrameAssignable(new CfFrame(), frame, appView).isFailed()) {
       return error();
@@ -199,6 +214,196 @@
     return this;
   }
 
+  public CfFrameState join(ConcreteCfFrameState state) {
+    CfFrame.Builder builder = CfFrame.builder();
+    joinLocals(state.locals, builder);
+    joinStack(state.stack, builder);
+    CfFrame frame = builder.buildMutable();
+    return new ConcreteCfFrameState(frame.getLocals(), frame.getStack());
+  }
+
+  private void joinLocals(Int2ObjectSortedMap<FrameType> locals, CfFrame.Builder builder) {
+    ObjectBidirectionalIterator<Entry<FrameType>> iterator =
+        this.locals.int2ObjectEntrySet().iterator();
+    ObjectBidirectionalIterator<Entry<FrameType>> otherIterator =
+        locals.int2ObjectEntrySet().iterator();
+    while (iterator.hasNext() && otherIterator.hasNext()) {
+      Entry<FrameType> entry = iterator.next();
+      int localIndex = entry.getIntKey();
+      FrameType frameType = entry.getValue();
+
+      Entry<FrameType> otherEntry = otherIterator.next();
+      int otherLocalIndex = otherEntry.getIntKey();
+      FrameType otherFrameType = otherEntry.getValue();
+
+      if (localIndex < otherLocalIndex) {
+        joinLocalsWithDifferentIndices(
+            localIndex,
+            frameType,
+            otherLocalIndex,
+            otherFrameType,
+            iterator,
+            otherIterator,
+            builder);
+      } else if (otherLocalIndex < localIndex) {
+        joinLocalsWithDifferentIndices(
+            otherLocalIndex,
+            otherFrameType,
+            localIndex,
+            frameType,
+            otherIterator,
+            iterator,
+            builder);
+      } else {
+        joinLocalsWithSameIndex(
+            localIndex, frameType, otherFrameType, iterator, otherIterator, builder);
+      }
+    }
+    iterator.forEachRemaining(entry -> joinLocalOnlyPresentInOne(entry, builder));
+    otherIterator.forEachRemaining(entry -> joinLocalOnlyPresentInOne(entry, builder));
+  }
+
+  private void joinLocalsWithDifferentIndices(
+      int localIndex,
+      FrameType frameType,
+      int otherLocalIndex,
+      FrameType otherFrameType,
+      ObjectBidirectionalIterator<Entry<FrameType>> iterator,
+      ObjectBidirectionalIterator<Entry<FrameType>> otherIterator,
+      CfFrame.Builder builder) {
+    assert localIndex < otherLocalIndex;
+
+    // Check if the smaller local does not overlap with the larger local.
+    if (frameType.isSingle() || localIndex + 1 < otherLocalIndex) {
+      setLocalToTop(localIndex, frameType, builder);
+      otherIterator.previous();
+      return;
+    }
+
+    // The smaller local is a wide that overlaps with the larger local.
+    setLocalToTop(localIndex, frameType, builder);
+
+    // If the larger local is a wide, then its high part is no longer usable. We also need to handle
+    // overlapping of the other local and the next local in the iterator.
+    if (otherFrameType.isWide()) {
+      int lastLocalIndexMarkedTop = otherLocalIndex + 1;
+      setSingleLocalToTop(lastLocalIndexMarkedTop, builder);
+      handleOverlappingLocals(lastLocalIndexMarkedTop, iterator, otherIterator, builder);
+    }
+  }
+
+  private void joinLocalsWithSameIndex(
+      int localIndex,
+      FrameType frameType,
+      FrameType otherFrameType,
+      ObjectBidirectionalIterator<Entry<FrameType>> iterator,
+      ObjectBidirectionalIterator<Entry<FrameType>> otherIterator,
+      CfFrame.Builder builder) {
+    if (frameType.isSingle()) {
+      if (otherFrameType.isSingle()) {
+        joinSingleLocalsWithSameIndex(
+            localIndex, frameType.asSingle(), otherFrameType.asSingle(), builder);
+      } else {
+        setWideLocalToTop(localIndex, builder);
+        handleOverlappingLocals(localIndex + 1, iterator, otherIterator, builder);
+      }
+    } else {
+      if (otherFrameType.isWide()) {
+        joinWideLocalsWithSameIndex(
+            localIndex, frameType.asWide(), otherFrameType.asWide(), builder);
+      } else {
+        setWideLocalToTop(localIndex, builder);
+        handleOverlappingLocals(localIndex + 1, otherIterator, iterator, builder);
+      }
+    }
+  }
+
+  private void joinSingleLocalsWithSameIndex(
+      int localIndex,
+      SingleFrameType frameType,
+      SingleFrameType otherFrameType,
+      CfFrame.Builder builder) {
+    builder.store(localIndex, frameType.join(otherFrameType).asFrameType());
+  }
+
+  private void joinWideLocalsWithSameIndex(
+      int localIndex,
+      WideFrameType frameType,
+      WideFrameType otherFrameType,
+      CfFrame.Builder builder) {
+    builder.store(localIndex, frameType.join(otherFrameType).asFrameType());
+  }
+
+  // TODO(b/231521474): By splitting each wide type into single left/right types, the join of each
+  //  (single) local index can be determined by looking at only locals[i] and otherLocals[i] (i.e.,
+  //  there is no carry-over). Thus this entire method could be avoided.
+  private void handleOverlappingLocals(
+      int lastLocalIndexMarkedTop,
+      ObjectBidirectionalIterator<Entry<FrameType>> iterator,
+      ObjectBidirectionalIterator<Entry<FrameType>> otherIterator,
+      CfFrame.Builder builder) {
+    ObjectBidirectionalIterator<Entry<FrameType>> currentIterator = iterator;
+    while (currentIterator.hasNext()) {
+      Entry<FrameType> entry = currentIterator.next();
+      int currentLocalIndex = entry.getIntKey();
+      FrameType currentFrameType = entry.getValue();
+
+      // Check if this local overlaps with the previous wide local that was set to top. If not, then
+      // this local is not affected.
+      if (lastLocalIndexMarkedTop < currentLocalIndex) {
+        // The current local still needs to be handled, thus this rewinds the iterator.
+        currentIterator.previous();
+        break;
+      }
+
+      // Verify that the low part of the current local has been set to top.
+      assert builder.hasLocal(currentLocalIndex);
+      assert builder.getLocal(currentLocalIndex).isTop();
+
+      // If the current local is not a wide, then we're done.
+      if (currentFrameType.isSingle()) {
+        // The current local has become top due to the overlap with a wide local. Therefore, this
+        // intentionally does not rewind the iterator.
+        break;
+      }
+
+      // The current local is a wide. We mark its high local index as top due to the overlap, and
+      // check if this wide local overlaps with a wide local in the other locals.
+      lastLocalIndexMarkedTop = currentLocalIndex + 1;
+      setSingleLocalToTop(lastLocalIndexMarkedTop, builder);
+      currentIterator = currentIterator == iterator ? otherIterator : iterator;
+    }
+  }
+
+  private void joinLocalOnlyPresentInOne(Entry<FrameType> entry, CfFrame.Builder builder) {
+    setLocalToTop(entry.getIntKey(), entry.getValue(), builder);
+  }
+
+  private void setLocalToTop(int localIndex, FrameType frameType, CfFrame.Builder builder) {
+    if (frameType.isSingle()) {
+      setSingleLocalToTop(localIndex, builder);
+    } else {
+      setWideLocalToTop(localIndex, builder);
+    }
+  }
+
+  private void setSingleLocalToTop(int localIndex, CfFrame.Builder builder) {
+    assert !builder.hasLocal(localIndex);
+    builder.store(localIndex, FrameType.top());
+  }
+
+  private void setWideLocalToTop(int localIndex, CfFrame.Builder builder) {
+    assert !builder.hasLocal(localIndex);
+    assert !builder.hasLocal(localIndex + 1);
+    setSingleLocalToTop(localIndex, builder);
+    setSingleLocalToTop(localIndex + 1, builder);
+  }
+
+  private void joinStack(Deque<FrameType> stack, CfFrame.Builder builder) {
+    // TODO(b/214496607): Implement this.
+    throw new Unimplemented();
+  }
+
   @Override
   public boolean equals(Object o) {
     if (this == o) {
diff --git a/src/main/java/com/android/tools/r8/optimize/interfaces/analysis/ErroneousCfFrameState.java b/src/main/java/com/android/tools/r8/optimize/interfaces/analysis/ErroneousCfFrameState.java
index a84041c..8c846ed 100644
--- a/src/main/java/com/android/tools/r8/optimize/interfaces/analysis/ErroneousCfFrameState.java
+++ b/src/main/java/com/android/tools/r8/optimize/interfaces/analysis/ErroneousCfFrameState.java
@@ -25,6 +25,11 @@
   }
 
   @Override
+  public boolean isError() {
+    return true;
+  }
+
+  @Override
   public CfFrameState check(AppView<?> appView, CfFrame frame) {
     return this;
   }