Add message to ErroneousCfFrameState

Change-Id: Id6a38aa1739d9c8499271ca9bc8da0b1c3887346
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfInstanceFieldWrite.java b/src/main/java/com/android/tools/r8/cf/code/CfInstanceFieldWrite.java
index ea0d0f8..5695d7e 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfInstanceFieldWrite.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfInstanceFieldWrite.java
@@ -4,8 +4,10 @@
 
 package com.android.tools.r8.cf.code;
 
+import static com.android.tools.r8.optimize.interfaces.analysis.ErroneousCfFrameState.formatActual;
 import static com.android.tools.r8.utils.BiPredicateUtils.or;
 
+import com.android.tools.r8.cf.code.CfFrame.FrameType;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.CfCode;
 import com.android.tools.r8.graph.DexClassAndMethod;
@@ -21,6 +23,7 @@
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.InliningConstraints;
 import com.android.tools.r8.optimize.interfaces.analysis.CfFrameState;
+import com.android.tools.r8.optimize.interfaces.analysis.ErroneousCfFrameState;
 import java.util.ListIterator;
 import org.objectweb.asm.Opcodes;
 
@@ -94,6 +97,14 @@
             appView,
             getField().getHolderType(),
             context,
-            (state, head) -> head.isUninitializedNew() ? CfFrameState.error() : state);
+            (state, head) -> head.isUninitializedNew() ? error(head) : state);
+  }
+
+  private ErroneousCfFrameState error(FrameType objectType) {
+    return CfFrameState.error(
+        "Frame type "
+            + formatActual(objectType)
+            + " is not assignable to "
+            + getField().getHolderType().getTypeName());
   }
 }
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfJsrRet.java b/src/main/java/com/android/tools/r8/cf/code/CfJsrRet.java
index ab730fb..a338a14 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfJsrRet.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfJsrRet.java
@@ -100,7 +100,7 @@
       ProgramMethod context,
       AppView<?> appView,
       DexItemFactory dexItemFactory) {
-    return CfFrameState.error();
+    return CfFrameState.error("Unexpected JSR/RET instruction");
   }
 
   public int getLocal() {
diff --git a/src/main/java/com/android/tools/r8/graph/DexMethod.java b/src/main/java/com/android/tools/r8/graph/DexMethod.java
index 466e1cf..921d0a6 100644
--- a/src/main/java/com/android/tools/r8/graph/DexMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/DexMethod.java
@@ -269,6 +269,10 @@
     return toSourceString(false, false);
   }
 
+  public String toSourceStringWithoutReturnType() {
+    return toSourceString(true, false);
+  }
+
   private String toSourceString(boolean includeHolder, boolean includeReturnType) {
     StringBuilder builder = new StringBuilder();
     if (includeReturnType) {
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/type/BooleanTypeElement.java b/src/main/java/com/android/tools/r8/ir/analysis/type/BooleanTypeElement.java
index eea19ef..e00ecf9 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/type/BooleanTypeElement.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/type/BooleanTypeElement.java
@@ -12,6 +12,11 @@
   }
 
   @Override
+  public String getTypeName() {
+    return "boolean";
+  }
+
+  @Override
   boolean isBoolean() {
     return true;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/type/ByteTypeElement.java b/src/main/java/com/android/tools/r8/ir/analysis/type/ByteTypeElement.java
index c9e5e9f..849f508 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/type/ByteTypeElement.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/type/ByteTypeElement.java
@@ -12,6 +12,11 @@
   }
 
   @Override
+  public String getTypeName() {
+    return "byte";
+  }
+
+  @Override
   boolean isByte() {
     return true;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/type/CharTypeElement.java b/src/main/java/com/android/tools/r8/ir/analysis/type/CharTypeElement.java
index 0d33bfd..5a47cda 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/type/CharTypeElement.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/type/CharTypeElement.java
@@ -12,6 +12,11 @@
   }
 
   @Override
+  public String getTypeName() {
+    return "char";
+  }
+
+  @Override
   boolean isChar() {
     return true;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/type/DoubleTypeElement.java b/src/main/java/com/android/tools/r8/ir/analysis/type/DoubleTypeElement.java
index e9632a8..85a501c 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/type/DoubleTypeElement.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/type/DoubleTypeElement.java
@@ -12,6 +12,11 @@
   }
 
   @Override
+  public String getTypeName() {
+    return "double";
+  }
+
+  @Override
   public boolean isDouble() {
     return true;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/type/FloatTypeElement.java b/src/main/java/com/android/tools/r8/ir/analysis/type/FloatTypeElement.java
index 386981d..9efadf8 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/type/FloatTypeElement.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/type/FloatTypeElement.java
@@ -11,6 +11,11 @@
   }
 
   @Override
+  public String getTypeName() {
+    return "float";
+  }
+
+  @Override
   public boolean isFloat() {
     return true;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/type/IntTypeElement.java b/src/main/java/com/android/tools/r8/ir/analysis/type/IntTypeElement.java
index 5ecb417..881cb19 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/type/IntTypeElement.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/type/IntTypeElement.java
@@ -11,6 +11,11 @@
   }
 
   @Override
+  public String getTypeName() {
+    return "int";
+  }
+
+  @Override
   public boolean isInt() {
     return true;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/type/LongTypeElement.java b/src/main/java/com/android/tools/r8/ir/analysis/type/LongTypeElement.java
index caf2ed3..7f71468 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/type/LongTypeElement.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/type/LongTypeElement.java
@@ -12,6 +12,11 @@
   }
 
   @Override
+  public String getTypeName() {
+    return "long";
+  }
+
+  @Override
   public boolean isLong() {
     return true;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/type/PrimitiveTypeElement.java b/src/main/java/com/android/tools/r8/ir/analysis/type/PrimitiveTypeElement.java
index 4e9b5f1a..9797274 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/type/PrimitiveTypeElement.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/type/PrimitiveTypeElement.java
@@ -12,6 +12,8 @@
 /** A {@link TypeElement} that abstracts primitive types. */
 public abstract class PrimitiveTypeElement extends TypeElement {
 
+  public abstract String getTypeName();
+
   @Override
   public Nullability nullability() {
     return Nullability.definitelyNotNull();
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/type/ShortTypeElement.java b/src/main/java/com/android/tools/r8/ir/analysis/type/ShortTypeElement.java
index 61b8514..fd7d65a 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/type/ShortTypeElement.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/type/ShortTypeElement.java
@@ -12,6 +12,11 @@
   }
 
   @Override
+  public String getTypeName() {
+    return "short";
+  }
+
+  @Override
   boolean isShort() {
     return true;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/type/SinglePrimitiveTypeElement.java b/src/main/java/com/android/tools/r8/ir/analysis/type/SinglePrimitiveTypeElement.java
index 40436d1..5d3d16b 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/type/SinglePrimitiveTypeElement.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/type/SinglePrimitiveTypeElement.java
@@ -3,6 +3,8 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.ir.analysis.type;
 
+import com.android.tools.r8.errors.Unreachable;
+
 /** A {@link TypeElement} that abstracts primitive types, which fit in 32 bits. */
 public class SinglePrimitiveTypeElement extends PrimitiveTypeElement {
 
@@ -17,6 +19,11 @@
   }
 
   @Override
+  public String getTypeName() {
+    throw new Unreachable("Unexpected attempt to get type name of " + this);
+  }
+
+  @Override
   public boolean isSinglePrimitive() {
     return true;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/type/WidePrimitiveTypeElement.java b/src/main/java/com/android/tools/r8/ir/analysis/type/WidePrimitiveTypeElement.java
index 0ae7426..f13800d 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/type/WidePrimitiveTypeElement.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/type/WidePrimitiveTypeElement.java
@@ -3,6 +3,8 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.ir.analysis.type;
 
+import com.android.tools.r8.errors.Unreachable;
+
 /** A {@link TypeElement} that abstracts primitive types, which fit in 64 bits. */
 public class WidePrimitiveTypeElement extends PrimitiveTypeElement {
 
@@ -17,6 +19,11 @@
   }
 
   @Override
+  public String getTypeName() {
+    throw new Unreachable("Unexpected attempt to get type name of " + this);
+  }
+
+  @Override
   public boolean isWidePrimitive() {
     return true;
   }
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 a536102..61ba967 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
@@ -4,7 +4,6 @@
 
 package com.android.tools.r8.optimize.interfaces.analysis;
 
-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.graph.AppView;
@@ -31,11 +30,7 @@
 
   @Override
   public CfFrameState check(AppView<?> appView, CfFrame frame) {
-    if (CfAssignability.isFrameAssignable(new CfFrame(), frame, appView).isFailed()) {
-      return error();
-    }
-    CfFrame frameCopy = frame.mutableCopy();
-    return new ConcreteCfFrameState(frameCopy.getMutableLocals(), frameCopy.getMutableStack());
+    return new ConcreteCfFrameState().check(appView, frame);
   }
 
   @Override
@@ -45,36 +40,37 @@
 
   @Override
   public CfFrameState markInitialized(FrameType uninitializedType, DexType initializedType) {
-    return error();
+    // Initializing an uninitialized type is a no-op when the frame is empty.
+    return this;
   }
 
   @Override
-  public CfFrameState pop() {
-    return error();
+  public ErroneousCfFrameState pop() {
+    return error("Unexpected pop from empty stack");
   }
 
   @Override
-  public CfFrameState pop(BiFunction<CfFrameState, FrameType, CfFrameState> fn) {
-    return error();
+  public ErroneousCfFrameState pop(BiFunction<CfFrameState, FrameType, CfFrameState> fn) {
+    return pop();
   }
 
   @Override
-  public CfFrameState popAndInitialize(
+  public ErroneousCfFrameState popAndInitialize(
       AppView<?> appView, DexMethod constructor, ProgramMethod context) {
-    return error();
+    return pop();
   }
 
   @Override
-  public CfFrameState popInitialized(
+  public ErroneousCfFrameState popInitialized(
       AppView<?> appView,
       DexType expectedType,
       BiFunction<CfFrameState, FrameType, CfFrameState> fn) {
-    return error();
+    return pop();
   }
 
   @Override
   public CfFrameState popInitialized(AppView<?> appView, DexType... expectedTypes) {
-    return expectedTypes.length == 0 ? this : error();
+    return expectedTypes.length == 0 ? this : pop();
   }
 
   @Override
@@ -88,12 +84,12 @@
   }
 
   @Override
-  public CfFrameState readLocal(
+  public ErroneousCfFrameState readLocal(
       AppView<?> appView,
       int localIndex,
       ValueType expectedType,
       BiFunction<CfFrameState, FrameType, CfFrameState> fn) {
-    return error();
+    return error("Unexpected local read from empty frame");
   }
 
   @Override
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 0b297a9..91923f3 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
@@ -4,6 +4,9 @@
 
 package com.android.tools.r8.optimize.interfaces.analysis;
 
+import static com.android.tools.r8.optimize.interfaces.analysis.ErroneousCfFrameState.formatActual;
+import static com.android.tools.r8.optimize.interfaces.analysis.ErroneousCfFrameState.formatExpected;
+
 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;
@@ -28,8 +31,39 @@
     return BottomCfFrameState.getInstance();
   }
 
-  public static ErroneousCfFrameState error() {
-    return ErroneousCfFrameState.getInstance();
+  public static ErroneousCfFrameState error(String message) {
+    return new ErroneousCfFrameState(message);
+  }
+
+  public static ErroneousCfFrameState errorUnexpectedLocal(
+      FrameType frameType, ValueType expectedType, int localIndex) {
+    return internalError(
+        formatActual(frameType), formatExpected(expectedType), "at local index " + localIndex);
+  }
+
+  public static ErroneousCfFrameState errorUnexpectedStack(
+      FrameType frameType, DexType expectedType) {
+    return internalErrorUnexpectedStack(formatActual(frameType), formatExpected(expectedType));
+  }
+
+  public static ErroneousCfFrameState errorUnexpectedStack(
+      FrameType frameType, FrameType expectedType) {
+    return internalErrorUnexpectedStack(formatActual(frameType), formatExpected(expectedType));
+  }
+
+  public static ErroneousCfFrameState errorUnexpectedStack(
+      FrameType frameType, ValueType expectedType) {
+    return internalErrorUnexpectedStack(formatActual(frameType), formatExpected(expectedType));
+  }
+
+  private static ErroneousCfFrameState internalErrorUnexpectedStack(
+      String actual, String expected) {
+    return internalError(actual, expected, "on stack");
+  }
+
+  private static ErroneousCfFrameState internalError(
+      String actual, String expected, String location) {
+    return error("Expected " + expected + " " + location + ", but was " + actual);
   }
 
   @Override
@@ -62,6 +96,10 @@
     return false;
   }
 
+  public ErroneousCfFrameState asError() {
+    return null;
+  }
+
   public abstract CfFrameState check(AppView<?> appView, CfFrame frame);
 
   public abstract CfFrameState clear();
@@ -111,7 +149,9 @@
   }
 
   public final CfFrameState popObject(BiFunction<CfFrameState, FrameType, CfFrameState> fn) {
-    return pop((state, head) -> head.isObject() ? fn.apply(state, head) : error());
+    return pop(
+        (state, head) ->
+            head.isObject() ? fn.apply(state, head) : errorUnexpectedStack(head, ValueType.OBJECT));
   }
 
   @SuppressWarnings("InconsistentOverloads")
@@ -126,7 +166,7 @@
                     && CfAssignability.isAssignable(
                         head.getObjectType(context), expectedType, appView)
                 ? fn.apply(state, head)
-                : error());
+                : errorUnexpectedStack(head, expectedType));
   }
 
   public final CfFrameState popSingle() {
@@ -134,7 +174,11 @@
   }
 
   public final CfFrameState popSingle(BiFunction<CfFrameState, FrameType, CfFrameState> fn) {
-    return pop((state, single) -> single.isSingle() ? fn.apply(state, single) : error());
+    return pop(
+        (state, single) ->
+            single.isSingle()
+                ? fn.apply(state, single)
+                : errorUnexpectedStack(single, FrameType.oneWord()));
   }
 
   public final CfFrameState popSingles(
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 1e958b8..55469be 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
@@ -5,8 +5,10 @@
 package com.android.tools.r8.optimize.interfaces.analysis;
 
 import static com.android.tools.r8.cf.code.CfFrame.getInitializedFrameType;
+import static com.android.tools.r8.optimize.interfaces.analysis.ErroneousCfFrameState.formatActual;
 
 import com.android.tools.r8.cf.code.CfAssignability;
+import com.android.tools.r8.cf.code.CfAssignability.AssignabilityResult;
 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;
@@ -16,6 +18,7 @@
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.code.ValueType;
+import com.android.tools.r8.utils.FunctionUtils;
 import com.google.common.collect.Iterables;
 import it.unimi.dsi.fastutil.ints.Int2ObjectAVLTreeMap;
 import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
@@ -61,8 +64,10 @@
   @Override
   public CfFrameState check(AppView<?> appView, CfFrame frame) {
     CfFrame currentFrame = CfFrame.builder().setLocals(locals).setStack(stack).build();
-    if (CfAssignability.isFrameAssignable(currentFrame, frame, appView).isFailed()) {
-      return error();
+    AssignabilityResult assignabilityResult =
+        CfAssignability.isFrameAssignable(currentFrame, frame, appView);
+    if (assignabilityResult.isFailed()) {
+      return error(assignabilityResult.asFailed().getMessage());
     }
     CfFrame frameCopy = frame.mutableCopy();
     return new ConcreteCfFrameState(frameCopy.getMutableLocals(), frameCopy.getMutableStack());
@@ -76,7 +81,7 @@
   @Override
   public CfFrameState markInitialized(FrameType uninitializedType, DexType initializedType) {
     if (uninitializedType.isInitialized()) {
-      return error();
+      return error("Unexpected attempt to initialize already initialized type");
     }
     for (Int2ObjectMap.Entry<FrameType> entry : locals.int2ObjectEntrySet()) {
       FrameType frameType = entry.getValue();
@@ -97,17 +102,14 @@
 
   @Override
   public CfFrameState pop() {
-    if (stack.isEmpty()) {
-      return error();
-    }
-    stack.removeLast();
-    return this;
+    return pop(FunctionUtils::getFirst);
   }
 
   @Override
   public CfFrameState pop(BiFunction<CfFrameState, FrameType, CfFrameState> fn) {
     if (stack.isEmpty()) {
-      return error();
+      // Return the same error as when popping from the bottom state.
+      return bottom().pop();
     }
     FrameType frameType = stack.removeLast();
     return fn.apply(this, frameType);
@@ -118,35 +120,60 @@
       AppView<?> appView, DexMethod constructor, ProgramMethod context) {
     return pop(
         (state, frameType) -> {
-          if (frameType.isUninitializedThis()) {
-            if (constructor.getHolderType() == context.getHolderType()
-                || constructor.getHolderType() == context.getHolder().getSuperType()) {
-              return state.markInitialized(frameType, context.getHolderType());
+          if (frameType.isUninitializedObject()) {
+            if (frameType.isUninitializedThis()) {
+              if (constructor.getHolderType() == context.getHolderType()
+                  || constructor.getHolderType() == context.getHolder().getSuperType()) {
+                return state.markInitialized(frameType, context.getHolderType());
+              }
+            } else if (frameType.isUninitializedNew()) {
+              DexType uninitializedNewType = frameType.getUninitializedNewType();
+              if (constructor.getHolderType() == uninitializedNewType) {
+                return state.markInitialized(frameType, uninitializedNewType);
+              }
             }
-          } else if (frameType.isUninitializedNew()) {
-            DexType uninitializedNewType = frameType.getUninitializedNewType();
-            if (constructor.getHolderType() == uninitializedNewType) {
-              return state.markInitialized(frameType, uninitializedNewType);
-            }
+            return popAndInitializeConstructorMismatchError(frameType, constructor, context);
           }
-          return error();
+          return popAndInitializeInitializedObjectError(frameType);
         });
   }
 
+  private ErroneousCfFrameState popAndInitializeConstructorMismatchError(
+      FrameType frameType, DexMethod constructor, ProgramMethod context) {
+    assert frameType.isUninitializedObject();
+    StringBuilder message = new StringBuilder("Constructor mismatch, expected ");
+    if (frameType.isUninitializedNew()) {
+      message.append(frameType.getUninitializedNewType().getTypeName());
+    } else {
+      assert frameType.isUninitializedThis();
+      message
+          .append(context.getHolderType().getTypeName())
+          .append(" or ")
+          .append(context.getHolder().getSuperType().getTypeName());
+    }
+    message.append(" constructor, but was ").append(constructor.toSourceStringWithoutReturnType());
+    return error(message.toString());
+  }
+
+  private ErroneousCfFrameState popAndInitializeInitializedObjectError(FrameType frameType) {
+    return error("Unexpected attempt to initialize " + formatActual(frameType));
+  }
+
   @Override
   public CfFrameState popInitialized(
       AppView<?> appView,
       DexType expectedType,
       BiFunction<CfFrameState, FrameType, CfFrameState> fn) {
     return pop(
-        (state, frameType) ->
-            frameType.isInitialized()
-                    && CfAssignability.isAssignable(
-                        frameType.getInitializedType(appView.dexItemFactory()),
-                        expectedType,
-                        appView)
-                ? fn.apply(state, frameType)
-                : error());
+        (state, frameType) -> {
+          if (frameType.isInitialized()) {
+            DexType initializedType = frameType.getInitializedType(appView.dexItemFactory());
+            if (CfAssignability.isAssignable(initializedType, expectedType, appView)) {
+              return fn.apply(state, frameType);
+            }
+          }
+          return errorUnexpectedStack(frameType, FrameType.initialized(expectedType));
+        });
   }
 
   @Override
@@ -177,17 +204,17 @@
       BiFunction<CfFrameState, FrameType, CfFrameState> fn) {
     FrameType frameType = locals.get(localIndex);
     if (frameType == null) {
-      return error();
+      return error("Unexpected read of missing local at index " + localIndex);
     }
-    if (frameType.isInitialized()
-        && CfAssignability.isAssignable(
-            frameType.getInitializedType(appView.dexItemFactory()), expectedType, appView)) {
+    if (frameType.isInitialized()) {
+      if (CfAssignability.isAssignable(
+          frameType.getInitializedType(appView.dexItemFactory()), expectedType, appView)) {
+        return fn.apply(this, frameType);
+      }
+    } else if (frameType.isUninitializedObject() && expectedType.isObject()) {
       return fn.apply(this, frameType);
     }
-    if (frameType.isUninitializedObject() && expectedType.isObject()) {
-      return fn.apply(this, frameType);
-    }
-    return error();
+    return errorUnexpectedLocal(frameType, expectedType, localIndex);
   }
 
   @Override
@@ -431,29 +458,52 @@
   private ErroneousCfFrameState joinStack(Deque<FrameType> stack, CfFrame.Builder builder) {
     Iterator<FrameType> iterator = this.stack.iterator();
     Iterator<FrameType> otherIterator = stack.iterator();
+    int stackIndex = 0;
     while (iterator.hasNext() && otherIterator.hasNext()) {
       FrameType frameType = iterator.next();
       FrameType otherFrameType = otherIterator.next();
       if (frameType.isSingle() != otherFrameType.isSingle()) {
-        return error();
+        return error(
+            "Cannot join stacks, expected frame types at stack index "
+                + stackIndex
+                + " to have the same width, but was: "
+                + formatActual(frameType)
+                + " and "
+                + formatActual(otherFrameType));
       }
       if (frameType.isSingle()) {
         SingleFrameType join = frameType.asSingle().join(otherFrameType.asSingle());
         if (join.isOneWord()) {
-          return error();
+          return joinStackImpreciseJoinError(stackIndex, frameType, otherFrameType);
         }
         builder.push(join.asFrameType());
       } else {
         WideFrameType join = frameType.asWide().join(otherFrameType.asWide());
         if (join.isTwoWord()) {
-          return error();
+          return joinStackImpreciseJoinError(stackIndex, frameType, otherFrameType);
         }
         builder.push(join.asFrameType());
       }
+      stackIndex++;
+    }
+    if (iterator.hasNext() || otherIterator.hasNext()) {
+      return error("Cannot join stacks of different size");
     }
     return null;
   }
 
+  private ErroneousCfFrameState joinStackImpreciseJoinError(
+      int stackIndex, FrameType first, FrameType second) {
+    return error(
+        "Cannot join stacks, expected frame types at stack index "
+            + stackIndex
+            + " to join to a precise (non-top) type, but types "
+            + formatActual(first)
+            + " and "
+            + formatActual(second)
+            + " do not");
+  }
+
   @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 23a6351..f08458f 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
@@ -16,12 +16,90 @@
 /** An analysis state representing that the code does not type check. */
 public class ErroneousCfFrameState extends CfFrameState {
 
-  private static final ErroneousCfFrameState INSTANCE = new ErroneousCfFrameState();
+  private enum FormatKind {
+    ACTUAL,
+    EXPECTED
+  }
 
-  private ErroneousCfFrameState() {}
+  private final String message;
 
-  static ErroneousCfFrameState getInstance() {
-    return INSTANCE;
+  ErroneousCfFrameState(String message) {
+    this.message = message;
+  }
+
+  public static String formatExpected(DexType type) {
+    return format(type);
+  }
+
+  private static String format(DexType type) {
+    if (type.isArrayType() || type.isClassType()) {
+      return type.getTypeName();
+    } else if (type.isNullValueType()) {
+      return "null";
+    } else {
+      assert type.isPrimitiveType();
+      return "primitive " + type.getTypeName();
+    }
+  }
+
+  public static String formatActual(FrameType frameType) {
+    return format(frameType, FormatKind.ACTUAL);
+  }
+
+  public static String formatExpected(FrameType frameType) {
+    return format(frameType, FormatKind.EXPECTED);
+  }
+
+  private static String format(FrameType frameType, FormatKind formatKind) {
+    if (frameType.isInitialized()) {
+      if (frameType.isObject()) {
+        DexType initializedType = frameType.asSingleInitializedType().getInitializedType();
+        if (initializedType.isArrayType()) {
+          return initializedType.getTypeName();
+        } else if (initializedType.isClassType()) {
+          return "initialized " + initializedType.getTypeName();
+        } else {
+          assert initializedType.isNullValueType();
+          return "null";
+        }
+      } else {
+        assert frameType.isPrimitive();
+        return "primitive " + frameType.asPrimitive().getTypeName();
+      }
+    } else if (frameType.isUninitializedObject()) {
+      if (frameType.isUninitializedNew()) {
+        DexType uninitializedNewType = frameType.getUninitializedNewType();
+        if (uninitializedNewType != null) {
+          return "uninitialized " + uninitializedNewType.getTypeName();
+        }
+        return "uninitialized-new";
+      } else {
+        return "uninitialized-this";
+      }
+    } else {
+      assert frameType.isOneWord() || frameType.isTwoWord();
+      if (formatKind == FormatKind.ACTUAL) {
+        return "top";
+      } else {
+        return frameType.isOneWord() ? "a single width value" : "a double width value";
+      }
+    }
+  }
+
+  public static String formatExpected(ValueType valueType) {
+    return format(valueType);
+  }
+
+  private static String format(ValueType valueType) {
+    if (valueType.isObject()) {
+      return "object";
+    } else {
+      return "primitive " + valueType.toPrimitiveType().getTypeName();
+    }
+  }
+
+  public String getMessage() {
+    return message;
   }
 
   @Override
@@ -30,6 +108,11 @@
   }
 
   @Override
+  public ErroneousCfFrameState asError() {
+    return this;
+  }
+
+  @Override
   public CfFrameState check(AppView<?> appView, CfFrame frame) {
     return this;
   }