Throw null if an instruction will do.

Based on SSA value's nullability as well as uninstantiated type info,
if an instruction requires non-null receiver, e.g., instance-{get|put},
we can replace it with `throw null` when the receiver is known to be
null.

One caveat is, for field instructions, resolution-related errors come
first. This CL refactors dead condition of {instance|static}-get and
rather simulates field resolutions. If it will raise errors other
than NPE, we should honor that.

Bug: 72443802, 129530569
Change-Id: Iecddc7feafd07da8496634835ddc0cacc08feea8
diff --git a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
index 90dfef0..dfda737 100644
--- a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
+++ b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
@@ -181,23 +181,29 @@
   public final DexString methodDescriptor = createString("Ljava/lang/reflect/Method;");
   public final DexString enumDescriptor = createString("Ljava/lang/Enum;");
   public final DexString annotationDescriptor = createString("Ljava/lang/annotation/Annotation;");
-  public final DexString throwableDescriptor = createString(throwableDescriptorString);
-  public final DexString exceptionInInitializerErrorDescriptor =
-      createString("Ljava/lang/ExceptionInInitializerError;");
   public final DexString objectsDescriptor = createString("Ljava/util/Objects;");
+
   public final DexString stringBuilderDescriptor = createString("Ljava/lang/StringBuilder;");
   public final DexString stringBufferDescriptor = createString("Ljava/lang/StringBuffer;");
+
   public final DexString varHandleDescriptor = createString("Ljava/lang/invoke/VarHandle;");
   public final DexString methodHandleDescriptor = createString("Ljava/lang/invoke/MethodHandle;");
   public final DexString methodTypeDescriptor = createString("Ljava/lang/invoke/MethodType;");
-
   public final DexString invocationHandlerDescriptor =
       createString("Ljava/lang/reflect/InvocationHandler;");
-  public final DexString npeDescriptor = createString("Ljava/lang/NullPointerException;");
   public final DexString proxyDescriptor = createString("Ljava/lang/reflect/Proxy;");
   public final DexString serviceLoaderDescriptor = createString("Ljava/util/ServiceLoader;");
   public final DexString listDescriptor = createString("Ljava/util/List;");
 
+  public final DexString throwableDescriptor = createString(throwableDescriptorString);
+  public final DexString illegalAccessErrorDescriptor =
+      createString("Ljava/lang/IllegalAccessError;");
+  public final DexString icceDescriptor = createString("Ljava/lang/IncompatibleClassChangeError;");
+  public final DexString exceptionInInitializerErrorDescriptor =
+      createString("Ljava/lang/ExceptionInInitializerError;");
+  public final DexString noSuchFieldErrorDescriptor = createString("Ljava/lang/NoSuchFieldError");
+  public final DexString npeDescriptor = createString("Ljava/lang/NullPointerException;");
+
   public final DexString intFieldUpdaterDescriptor =
       createString("Ljava/util/concurrent/atomic/AtomicIntegerFieldUpdater;");
   public final DexString longFieldUpdaterDescriptor =
@@ -243,9 +249,7 @@
   public final DexType classArrayType = createType(classArrayDescriptor);
   public final DexType enumType = createType(enumDescriptor);
   public final DexType annotationType = createType(annotationDescriptor);
-  public final DexType throwableType = createType(throwableDescriptor);
-  public final DexType exceptionInInitializerErrorType =
-      createType(exceptionInInitializerErrorDescriptor);
+
   public final DexType classType = createType(classDescriptor);
   public final DexType classLoaderType = createType(classLoaderDescriptor);
   public final DexType autoCloseableType = createType(autoCloseableDescriptor);
@@ -256,13 +260,19 @@
   public final DexType varHandleType = createType(varHandleDescriptor);
   public final DexType methodHandleType = createType(methodHandleDescriptor);
   public final DexType methodTypeType = createType(methodTypeDescriptor);
-
   public final DexType invocationHandlerType = createType(invocationHandlerDescriptor);
-  public final DexType npeType = createType(npeDescriptor);
   public final DexType proxyType = createType(proxyDescriptor);
   public final DexType serviceLoaderType = createType(serviceLoaderDescriptor);
   public final DexType listType = createType(listDescriptor);
 
+  public final DexType throwableType = createType(throwableDescriptor);
+  public final DexType illegalAccessErrorType = createType(illegalAccessErrorDescriptor);
+  public final DexType icceType = createType(icceDescriptor);
+  public final DexType exceptionInInitializerErrorType =
+      createType(exceptionInInitializerErrorDescriptor);
+  public final DexType noSuchFieldErrorType = createType(noSuchFieldErrorDescriptor);
+  public final DexType npeType = createType(npeDescriptor);
+
   public final StringBuildingMethods stringBuilderMethods =
       new StringBuildingMethods(stringBuilderType);
   public final StringBuildingMethods stringBufferMethods =
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/AbstractError.java b/src/main/java/com/android/tools/r8/ir/analysis/AbstractError.java
new file mode 100644
index 0000000..dcfd5c5
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/analysis/AbstractError.java
@@ -0,0 +1,56 @@
+// Copyright (c) 2019, 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.ir.analysis;
+
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexType;
+
+// Note that this is not what D8/R8 can throw.
+//
+// Finer-grained abstraction of Throwable that an instruction can throw if any.
+//
+//       Top        // throwing, but not quite sure what it would be.
+//    /   |    \
+//  NPE  ICCE  ...  // specific exceptions.
+//    \   |    /
+//      Bottom      // not throwing.
+public class AbstractError {
+
+  private static final AbstractError TOP = new AbstractError();
+  private static final AbstractError BOTTOM = new AbstractError();
+
+  private DexType simulatedError;
+
+  private AbstractError() {}
+
+  private AbstractError(DexType throwable) {
+    simulatedError = throwable;
+  }
+
+  public static AbstractError top() {
+    return TOP;
+  }
+
+  public static AbstractError bottom() {
+    return BOTTOM;
+  }
+
+  public static AbstractError specific(DexType throwable) {
+    return new AbstractError(throwable);
+  }
+
+  public boolean isThrowing() {
+    return this != BOTTOM;
+  }
+
+  public DexType getSpecificError(DexItemFactory factory) {
+    assert isThrowing();
+    if (simulatedError != null) {
+      return simulatedError;
+    }
+    assert this == TOP;
+    return factory.throwableType;
+  }
+
+}
diff --git a/src/main/java/com/android/tools/r8/ir/code/ArrayGet.java b/src/main/java/com/android/tools/r8/ir/code/ArrayGet.java
index 3e2af3d..331a9ac 100644
--- a/src/main/java/com/android/tools/r8/ir/code/ArrayGet.java
+++ b/src/main/java/com/android/tools/r8/ir/code/ArrayGet.java
@@ -240,6 +240,16 @@
   }
 
   @Override
+  public boolean throwsOnNullInput() {
+    return true;
+  }
+
+  @Override
+  public Value getNonNullInput() {
+    return array();
+  }
+
+  @Override
   public void constrainType(TypeConstraintResolver constraintResolver) {
     constraintResolver.constrainArrayMemberType(type, dest(), array(), t -> type = t);
   }
diff --git a/src/main/java/com/android/tools/r8/ir/code/ArrayLength.java b/src/main/java/com/android/tools/r8/ir/code/ArrayLength.java
index 061aa53..6e6a9f8 100644
--- a/src/main/java/com/android/tools/r8/ir/code/ArrayLength.java
+++ b/src/main/java/com/android/tools/r8/ir/code/ArrayLength.java
@@ -119,4 +119,15 @@
   public boolean throwsNpeIfValueIsNull(Value value, DexItemFactory dexItemFactory) {
     return array() == value;
   }
+
+  @Override
+  public boolean throwsOnNullInput() {
+    return true;
+  }
+
+  @Override
+  public Value getNonNullInput() {
+    return array();
+  }
+
 }
diff --git a/src/main/java/com/android/tools/r8/ir/code/ArrayPut.java b/src/main/java/com/android/tools/r8/ir/code/ArrayPut.java
index a76bf42..fa3a846 100644
--- a/src/main/java/com/android/tools/r8/ir/code/ArrayPut.java
+++ b/src/main/java/com/android/tools/r8/ir/code/ArrayPut.java
@@ -189,6 +189,16 @@
   }
 
   @Override
+  public boolean throwsOnNullInput() {
+    return true;
+  }
+
+  @Override
+  public Value getNonNullInput() {
+    return array();
+  }
+
+  @Override
   public void constrainType(TypeConstraintResolver constraintResolver) {
     constraintResolver.constrainArrayMemberType(type, value(), array(), t -> type = t);
   }
diff --git a/src/main/java/com/android/tools/r8/ir/code/FieldInstruction.java b/src/main/java/com/android/tools/r8/ir/code/FieldInstruction.java
index 2900491..ac7d337 100644
--- a/src/main/java/com/android/tools/r8/ir/code/FieldInstruction.java
+++ b/src/main/java/com/android/tools/r8/ir/code/FieldInstruction.java
@@ -3,7 +3,14 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.ir.code;
 
+import static com.android.tools.r8.optimize.MemberRebindingAnalysis.isMemberVisibleFromOriginalContext;
+
+import com.android.tools.r8.graph.AppInfo;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexEncodedField;
 import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.analysis.AbstractError;
 import java.util.Collections;
 import java.util.List;
 
@@ -42,6 +49,53 @@
   }
 
   @Override
+  public AbstractError instructionInstanceCanThrow(
+      AppView<? extends AppInfo> appView, DexType context) {
+    // Not applicable for D8.
+    if (!appView.enableWholeProgramOptimizations()) {
+      return AbstractError.top();
+    }
+
+    // TODO(b/123857022): Should be possible to use definitionFor().
+    DexEncodedField resolvedField = appView.appInfo().resolveField(getField());
+    // * NoSuchFieldError (resolution failure).
+    if (resolvedField == null) {
+      return AbstractError.specific(appView.dexItemFactory().noSuchFieldErrorType);
+    }
+    // * IncompatibleClassChangeError (instance-* for static field and vice versa).
+    if (resolvedField.isStaticMember()) {
+      if (isInstanceGet() || isInstancePut()) {
+        return AbstractError.specific(appView.dexItemFactory().icceType);
+      }
+    } else {
+      if (isStaticGet() || isStaticPut()) {
+        return AbstractError.specific(appView.dexItemFactory().icceType);
+      }
+    }
+    // * IllegalAccessError (not visible from the access context).
+    if (!isMemberVisibleFromOriginalContext(
+        appView, context, resolvedField.field.holder, resolvedField.accessFlags)) {
+      return AbstractError.specific(appView.dexItemFactory().illegalAccessErrorType);
+    }
+    // * NullPointerException (null receiver).
+    if (isInstanceGet() || isInstancePut()) {
+      Value receiver = inValues.get(0);
+      if (receiver.isAlwaysNull(appView) || receiver.typeLattice.isNullable()) {
+        return AbstractError.specific(appView.dexItemFactory().npeType);
+      }
+    }
+    // May trigger <clinit> that may have side effects.
+    if (field.holder.classInitializationMayHaveSideEffects(
+        appView,
+        // Types that are a super type of `context` are guaranteed to be initialized already.
+        type -> appView.isSubtype(context, type).isTrue())) {
+      return AbstractError.top();
+    }
+
+    return AbstractError.bottom();
+  }
+
+  @Override
   public boolean hasInvariantOutType() {
     // TODO(jsjeon): what if the target field is known to be non-null?
     return true;
diff --git a/src/main/java/com/android/tools/r8/ir/code/InstanceGet.java b/src/main/java/com/android/tools/r8/ir/code/InstanceGet.java
index 86d549d..6bb754f 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InstanceGet.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InstanceGet.java
@@ -4,8 +4,6 @@
 
 package com.android.tools.r8.ir.code;
 
-import static com.android.tools.r8.optimize.MemberRebindingAnalysis.isMemberVisibleFromOriginalContext;
-
 import com.android.tools.r8.cf.LoadStoreHelper;
 import com.android.tools.r8.cf.TypeVerificationHelper;
 import com.android.tools.r8.cf.code.CfFieldInstruction;
@@ -20,7 +18,6 @@
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.AppInfo;
 import com.android.tools.r8.graph.AppView;
-import com.android.tools.r8.graph.DexEncodedField;
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexType;
@@ -105,30 +102,22 @@
   }
 
   @Override
-  public boolean canBeDeadCode(AppView<? extends AppInfo> appView, IRCode code) {
-    // Not applicable for D8.
-    if (!appView.enableWholeProgramOptimizations()) {
-      return false;
-    }
+  public boolean instructionMayHaveSideEffects(
+      AppView<? extends AppInfo> appView, DexType context) {
+    return instructionInstanceCanThrow(appView, context).isThrowing();
+  }
 
+  @Override
+  public boolean canBeDeadCode(AppView<? extends AppInfo> appView, IRCode code) {
     // instance-get can be dead code as long as it cannot have any of the following:
     // * NoSuchFieldError (resolution failure)
+    // * IncompatibleClassChangeError (instance-* instruction for static fields)
     // * IllegalAccessError (not visible from the access context)
-    // * NullPointerException (null receiver).
-    // TODO(b/123857022): Should be possible to use definitionFor().
-    AppInfo appInfo = appView.appInfo();
-    DexEncodedField resolvedField = appInfo.resolveField(getField());
-    if (resolvedField == null) {
-      return false;
-    }
-    if (!isMemberVisibleFromOriginalContext(
-        appView,
-        code.method.method.holder,
-        resolvedField.field.holder,
-        resolvedField.accessFlags)) {
-      return false;
-    }
-    return object().getTypeLattice().nullability().isDefinitelyNotNull();
+    // * NullPointerException (null receiver)
+    boolean canBeDeadCode = !instructionMayHaveSideEffects(appView, code.method.method.holder);
+    assert appView.enableWholeProgramOptimizations() || !canBeDeadCode
+        : "Expected instance-get instruction to have side effects in D8";
+    return canBeDeadCode;
   }
 
   @Override
@@ -200,6 +189,16 @@
   }
 
   @Override
+  public boolean throwsOnNullInput() {
+    return true;
+  }
+
+  @Override
+  public Value getNonNullInput() {
+    return object();
+  }
+
+  @Override
   public boolean definitelyTriggersClassInitialization(
       DexType clazz,
       DexType context,
diff --git a/src/main/java/com/android/tools/r8/ir/code/InstancePut.java b/src/main/java/com/android/tools/r8/ir/code/InstancePut.java
index e4b062f..20c4733 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InstancePut.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InstancePut.java
@@ -174,6 +174,16 @@
   }
 
   @Override
+  public boolean throwsOnNullInput() {
+    return true;
+  }
+
+  @Override
+  public Value getNonNullInput() {
+    return object();
+  }
+
+  @Override
   public boolean definitelyTriggersClassInitialization(
       DexType clazz,
       DexType context,
diff --git a/src/main/java/com/android/tools/r8/ir/code/Instruction.java b/src/main/java/com/android/tools/r8/ir/code/Instruction.java
index dcbef34..bc9bc6b 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Instruction.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Instruction.java
@@ -12,6 +12,7 @@
 import com.android.tools.r8.graph.DebugLocalInfo;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.analysis.AbstractError;
 import com.android.tools.r8.ir.analysis.ClassInitializationAnalysis.AnalysisAssumption;
 import com.android.tools.r8.ir.analysis.ClassInitializationAnalysis.Query;
 import com.android.tools.r8.ir.analysis.constant.Bottom;
@@ -514,8 +515,15 @@
     return instructionInstanceCanThrow();
   }
 
+  public AbstractError instructionInstanceCanThrow(
+      AppView<? extends AppInfo> appView, DexType context) {
+    return instructionInstanceCanThrow() ? AbstractError.top() : AbstractError.bottom();
+  }
+
   /** Returns true is this instruction can be treated as dead code if its outputs are not used. */
   public boolean canBeDeadCode(AppView<? extends AppInfo> appView, IRCode code) {
+    // TODO(b/129530569): instructions with fine-grained side effect analysis may use:
+    // return !instructionMayHaveSideEffects(appView, code.method.method.holder);
     return !instructionInstanceCanThrow();
   }
 
@@ -1264,6 +1272,21 @@
     return false;
   }
 
+  // Any instructions that override `throwsNpeIfValueIsNull` should override this too.
+  // `throw` instruction is also throwing if the input is null. However, this util and the below
+  // one are used to insert Assume instruction if input is known to be non-null or replace the
+  // instruction with `throw null`. In either case, nullability of `throw` does not add anything
+  // new: `throw X` with null reference X has the same effect as `throw null`; `throw X` with
+  // non-null reference X ends, hence having non-null X after that instruction is non-sense.
+  public boolean throwsOnNullInput() {
+    return false;
+  }
+
+  // Any instructions that override `throwsOnNullInput` should override this too.
+  public Value getNonNullInput() {
+    throw new Unreachable("Should conform to throwsOnNullInput.");
+  }
+
   /**
    * Indicates whether the instruction triggers the class initialization (i.e. the <clinit> method)
    * of the given class at runtime execution.
diff --git a/src/main/java/com/android/tools/r8/ir/code/InvokeMethodWithReceiver.java b/src/main/java/com/android/tools/r8/ir/code/InvokeMethodWithReceiver.java
index 1ca8df7..2e15003 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InvokeMethodWithReceiver.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InvokeMethodWithReceiver.java
@@ -48,6 +48,16 @@
   }
 
   @Override
+  public boolean throwsOnNullInput() {
+    return true;
+  }
+
+  @Override
+  public Value getNonNullInput() {
+    return getReceiver();
+  }
+
+  @Override
   public boolean verifyTypes(AppView<? extends AppInfo> appView) {
     assert super.verifyTypes(appView);
 
diff --git a/src/main/java/com/android/tools/r8/ir/code/Monitor.java b/src/main/java/com/android/tools/r8/ir/code/Monitor.java
index 60b99f8..8d691db 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Monitor.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Monitor.java
@@ -134,4 +134,14 @@
   public boolean throwsNpeIfValueIsNull(Value value, DexItemFactory dexItemFactory) {
     return object() == value;
   }
+
+  @Override
+  public boolean throwsOnNullInput() {
+    return true;
+  }
+
+  @Override
+  public Value getNonNullInput() {
+    return object();
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/code/StaticGet.java b/src/main/java/com/android/tools/r8/ir/code/StaticGet.java
index 328b2f2..6c48db2 100644
--- a/src/main/java/com/android/tools/r8/ir/code/StaticGet.java
+++ b/src/main/java/com/android/tools/r8/ir/code/StaticGet.java
@@ -3,8 +3,6 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.ir.code;
 
-import static com.android.tools.r8.optimize.MemberRebindingAnalysis.isMemberVisibleFromOriginalContext;
-
 import com.android.tools.r8.cf.LoadStoreHelper;
 import com.android.tools.r8.cf.TypeVerificationHelper;
 import com.android.tools.r8.cf.code.CfFieldInstruction;
@@ -19,7 +17,6 @@
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.AppInfo;
 import com.android.tools.r8.graph.AppView;
-import com.android.tools.r8.graph.DexEncodedField;
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.analysis.ClassInitializationAnalysis;
@@ -98,36 +95,22 @@
   }
 
   @Override
-  public boolean canBeDeadCode(AppView<? extends AppInfo> appView, IRCode code) {
-    // Not applicable for D8.
-    if (!appView.enableWholeProgramOptimizations()) {
-      return false;
-    }
+  public boolean instructionMayHaveSideEffects(
+      AppView<? extends AppInfo> appView, DexType context) {
+    return instructionInstanceCanThrow(appView, context).isThrowing();
+  }
 
+  @Override
+  public boolean canBeDeadCode(AppView<? extends AppInfo> appView, IRCode code) {
     // static-get can be dead as long as it cannot have any of the following:
     // * NoSuchFieldError (resolution failure)
+    // * IncompatibleClassChangeError (static-* instruction for instance fields)
     // * IllegalAccessError (not visible from the access context)
     // * side-effects in <clinit>
-    // TODO(b/123857022): Should be possible to use definitionFor().
-    AppInfo appInfo = appView.appInfo();
-    DexEncodedField resolvedField = appInfo.resolveField(getField());
-    if (resolvedField == null) {
-      return false;
-    }
-    if (!isMemberVisibleFromOriginalContext(
-        appView,
-        code.method.method.holder,
-        resolvedField.field.holder,
-        resolvedField.accessFlags)) {
-      return false;
-    }
-    DexType context = code.method.method.holder;
-    return !getField()
-        .holder
-        .classInitializationMayHaveSideEffects(
-            appInfo,
-            // Types that are a super type of `context` are guaranteed to be initialized already.
-            type -> appView.isSubtype(context, type).isTrue());
+    boolean canBeDeadCode = !instructionMayHaveSideEffects(appView, code.method.method.holder);
+    assert appView.enableWholeProgramOptimizations() || !canBeDeadCode
+        : "Expected static-get instruction to have side effects in D8";
+    return canBeDeadCode;
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
index 2e72732..bfdbd88 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
@@ -935,7 +935,7 @@
       devirtualizer.devirtualizeInvokeInterface(code, method.method.holder);
     }
     if (uninstantiatedTypeOptimization != null) {
-      uninstantiatedTypeOptimization.rewrite(method, code);
+      uninstantiatedTypeOptimization.rewrite(code);
     }
 
     assert code.verifyTypes(appView);
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/NonNullTracker.java b/src/main/java/com/android/tools/r8/ir/optimize/NonNullTracker.java
index fffd3da..9d493dc 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/NonNullTracker.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/NonNullTracker.java
@@ -5,7 +5,6 @@
 
 import static com.android.tools.r8.ir.code.DominatorTree.Assumption.MAY_HAVE_UNREACHABLE_BLOCKS;
 
-import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.AppInfo;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexEncodedMethod;
@@ -25,7 +24,6 @@
 import com.android.tools.r8.ir.code.Phi;
 import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.ir.conversion.OptimizationFeedback;
-import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Predicates;
 import com.google.common.collect.Sets;
 import it.unimi.dsi.fastutil.ints.IntArrayList;
@@ -49,36 +47,6 @@
     this.appView = appView;
   }
 
-  @VisibleForTesting
-  static boolean throwsOnNullInput(Instruction instruction) {
-    return (instruction.isInvokeMethodWithReceiver() && !instruction.isInvokeDirect())
-        || instruction.isInstanceGet()
-        || instruction.isInstancePut()
-        || instruction.isArrayGet()
-        || instruction.isArrayPut()
-        || instruction.isArrayLength()
-        || instruction.isMonitor();
-  }
-
-  private Value getNonNullInput(Instruction instruction) {
-    if (instruction.isInvokeMethodWithReceiver()) {
-      return instruction.asInvokeMethodWithReceiver().getReceiver();
-    } else if (instruction.isInstanceGet()) {
-      return instruction.asInstanceGet().object();
-    } else if (instruction.isInstancePut()) {
-      return instruction.asInstancePut().object();
-    } else if (instruction.isArrayGet()) {
-      return instruction.asArrayGet().array();
-    } else if (instruction.isArrayPut()) {
-      return instruction.asArrayPut().array();
-    } else if (instruction.isArrayLength()) {
-      return instruction.asArrayLength().array();
-    } else if (instruction.isMonitor()) {
-      return instruction.asMonitor().object();
-    }
-    throw new Unreachable("Should conform to throwsOnNullInput.");
-  }
-
   public void addNonNull(IRCode code) {
     addNonNullInPart(code, code.listIterator(), Predicates.alwaysTrue());
   }
@@ -114,10 +82,10 @@
             knownToBeNonNullValues.add(knownToBeNonNullValue);
           }
         }
-        if (throwsOnNullInput(current)) {
-          Value knownToBeNonNullValue = getNonNullInput(current);
-          if (isNonNullCandidate(knownToBeNonNullValue)) {
-            knownToBeNonNullValues.add(knownToBeNonNullValue);
+        if (current.throwsOnNullInput()) {
+          Value couldBeNonNull = current.getNonNullInput();
+          if (isNonNullCandidate(couldBeNonNull)) {
+            knownToBeNonNullValues.add(couldBeNonNull);
           }
         }
         if (current.isInvokeMethod() && !current.isInvokePolymorphic()) {
@@ -365,8 +333,8 @@
     return predecessorIndexes;
   }
 
-  private boolean isNonNullCandidate(Value knownToBeNonNullValue) {
-    TypeLatticeElement typeLattice = knownToBeNonNullValue.getTypeLattice();
+  private static boolean isNonNullCandidate(Value couldBeNonNullValue) {
+    TypeLatticeElement typeLattice = couldBeNonNullValue.getTypeLattice();
     return typeLattice.isReference() && !typeLattice.isNullType() && typeLattice.isNullable();
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/UninstantiatedTypeOptimization.java b/src/main/java/com/android/tools/r8/ir/optimize/UninstantiatedTypeOptimization.java
index 31ae148..40f20ed 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/UninstantiatedTypeOptimization.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/UninstantiatedTypeOptimization.java
@@ -8,6 +8,7 @@
 import static com.android.tools.r8.ir.optimize.UninstantiatedTypeOptimization.Strategy.DISALLOW_ARGUMENT_REMOVAL;
 
 import com.android.tools.r8.graph.AppInfo;
+import com.android.tools.r8.graph.AppInfoWithSubtyping;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexEncodedField;
@@ -23,6 +24,7 @@
 import com.android.tools.r8.graph.GraphLense.RewrittenPrototypeDescription.RemovedArgumentInfo;
 import com.android.tools.r8.graph.GraphLense.RewrittenPrototypeDescription.RemovedArgumentsInfo;
 import com.android.tools.r8.graph.TopDownClassHierarchyTraversal;
+import com.android.tools.r8.ir.analysis.AbstractError;
 import com.android.tools.r8.ir.analysis.type.Nullability;
 import com.android.tools.r8.ir.analysis.type.TypeLatticeElement;
 import com.android.tools.r8.ir.code.BasicBlock;
@@ -104,8 +106,10 @@
   private final AppView<AppInfoWithLiveness> appView;
 
   private int numberOfInstanceGetOrInstancePutWithNullReceiver = 0;
+  private int numberOfArrayInstructionsWithNullArray = 0;
   private int numberOfInvokesWithNullArgument = 0;
   private int numberOfInvokesWithNullReceiver = 0;
+  private int numberOfMonitorWithNullReceiver = 0;
 
   public UninstantiatedTypeOptimization(AppView<AppInfoWithLiveness> appView) {
     this.appView = appView;
@@ -349,7 +353,7 @@
         method.holder, dexItemFactory.createProto(newReturnType, newParameters), method.name);
   }
 
-  public void rewrite(DexEncodedMethod method, IRCode code) {
+  public void rewrite(IRCode code) {
     Set<BasicBlock> blocksToBeRemoved = Sets.newIdentityHashSet();
     ListIterator<BasicBlock> blockIterator = code.listIterator();
     while (blockIterator.hasNext()) {
@@ -360,22 +364,35 @@
       InstructionListIterator instructionIterator = block.listIterator();
       while (instructionIterator.hasNext()) {
         Instruction instruction = instructionIterator.next();
-        if (instruction.isFieldInstruction()) {
-          if (instruction.isInstanceGet() || instruction.isInstancePut()) {
-            rewriteInstanceFieldInstruction(
-                instruction.asFieldInstruction(),
-                blockIterator,
-                instructionIterator,
-                code,
-                blocksToBeRemoved);
-          } else {
-            rewriteStaticFieldInstruction(
-                instruction.asFieldInstruction(),
-                blockIterator,
-                instructionIterator,
-                code,
-                blocksToBeRemoved);
+        if (instruction.throwsOnNullInput()) {
+          Value couldBeNullValue = instruction.getNonNullInput();
+          if (isThrowNullCandidate(
+              couldBeNullValue, instruction, appView, code.method.method.holder)) {
+            if (instruction.isInstanceGet() || instruction.isInstancePut()) {
+              ++numberOfInstanceGetOrInstancePutWithNullReceiver;
+            } else if (instruction.isInvokeMethodWithReceiver()) {
+              ++numberOfInvokesWithNullReceiver;
+            } else if (instruction.isArrayGet()
+                || instruction.isArrayPut()
+                || instruction.isArrayLength()) {
+              ++numberOfArrayInstructionsWithNullArray;
+            } else if (instruction.isMonitor()) {
+              ++numberOfMonitorWithNullReceiver;
+            } else {
+              assert false;
+            }
+            instructionIterator.replaceCurrentInstructionWithThrowNull(
+                appView, code, blockIterator, blocksToBeRemoved);
+            continue;
           }
+        }
+        if (instruction.isFieldInstruction()) {
+          rewriteFieldInstruction(
+              instruction.asFieldInstruction(),
+              blockIterator,
+              instructionIterator,
+              code,
+              blocksToBeRemoved);
         } else if (instruction.isInvokeMethod()) {
           rewriteInvoke(
               instruction.asInvokeMethod(),
@@ -392,6 +409,29 @@
     assert code.isConsistentSSA();
   }
 
+  private static boolean isThrowNullCandidate(
+      Value couldBeNullValue,
+      Instruction current,
+      AppView<? extends AppInfoWithSubtyping> appView,
+      DexType context) {
+    if (!couldBeNullValue.isAlwaysNull(appView)) {
+      return false;
+    }
+    if (current.isFieldInstruction()) {
+      // Other resolution-related errors come first.
+      AbstractError abstractError =
+          current.asFieldInstruction().instructionInstanceCanThrow(appView, context);
+      if (abstractError.isThrowing()
+          && abstractError.getSpecificError(appView.dexItemFactory())
+              != appView.dexItemFactory().npeType) {
+        // We can't replace the current instruction with `throw null` if it may throw another
+        // Error/Exception than NullPointerException.
+        return false;
+      }
+    }
+    return true;
+  }
+
   public void logResults() {
     assert Log.ENABLED;
     Log.info(
@@ -399,50 +439,19 @@
         "Number of instance-get/instance-put with null receiver: %s",
         numberOfInstanceGetOrInstancePutWithNullReceiver);
     Log.info(
+        getClass(),
+        "Number of array instructions with null reference: %s",
+        numberOfArrayInstructionsWithNullArray);
+    Log.info(
         getClass(), "Number of invokes with null argument: %s", numberOfInvokesWithNullArgument);
     Log.info(
         getClass(), "Number of invokes with null receiver: %s", numberOfInvokesWithNullReceiver);
+    Log.info(
+        getClass(), "Number of monitor with null receiver: %s", numberOfMonitorWithNullReceiver);
   }
 
-  private void rewriteInstanceFieldInstruction(
-      FieldInstruction instruction,
-      ListIterator<BasicBlock> blockIterator,
-      InstructionListIterator instructionIterator,
-      IRCode code,
-      Set<BasicBlock> blocksToBeRemoved) {
-    assert instruction.isInstanceGet() || instruction.isInstancePut();
-    boolean replacedByThrowNull = false;
-
-    Value receiver = instruction.inValues().get(0);
-    if (receiver.isAlwaysNull(appView)) {
-      // Unable to rewrite instruction if the receiver is defined from "const-number 0", since this
-      // would lead to an IncompatibleClassChangeError (see MemberResolutionTest#lookupStaticField-
-      // WithFieldGetFromNullReferenceDirectly).
-      if (!receiver.getTypeLattice().isDefinitelyNull()) {
-        instructionIterator.replaceCurrentInstructionWithThrowNull(
-            appView, code, blockIterator, blocksToBeRemoved);
-        ++numberOfInstanceGetOrInstancePutWithNullReceiver;
-        replacedByThrowNull = true;
-      }
-    }
-
-    if (!replacedByThrowNull) {
-      rewriteFieldInstruction(
-          instruction, blockIterator, instructionIterator, code, blocksToBeRemoved);
-    }
-  }
-
-  private void rewriteStaticFieldInstruction(
-      FieldInstruction instruction,
-      ListIterator<BasicBlock> blockIterator,
-      InstructionListIterator instructionIterator,
-      IRCode code,
-      Set<BasicBlock> blocksToBeRemoved) {
-    assert instruction.isStaticGet() || instruction.isStaticPut();
-    rewriteFieldInstruction(
-        instruction, blockIterator, instructionIterator, code, blocksToBeRemoved);
-  }
-
+  // instance-{get|put} with a null receiver has already been rewritten to `throw null`.
+  // At this point, field-instruction whose target field type is uninstantiated will be handled.
   private void rewriteFieldInstruction(
       FieldInstruction instruction,
       ListIterator<BasicBlock> blockIterator,
@@ -451,13 +460,18 @@
       Set<BasicBlock> blocksToBeRemoved) {
     DexType fieldType = instruction.getField().type;
     if (fieldType.isAlwaysNull(appView)) {
-      // Before trying to remove this instruction, we need to be sure that the field actually
-      // exists. Otherwise this instruction would throw a NoSuchFieldError exception.
-      DexEncodedField field = appView.definitionFor(instruction.getField());
-      if (field == null) {
+      AbstractError abstractError =
+          instruction.instructionInstanceCanThrow(appView, code.method.method.holder);
+      if (abstractError.isThrowing()) {
         return;
       }
 
+      // TODO(b/123857022): Should be possible to use definitionFor().
+      DexEncodedField field = appView.appInfo().resolveField(instruction.getField());
+      if (field == null) {
+        assert false : "Expected field-instruction with non-existent field to throw";
+        return;
+      }
       // We also need to be sure that this field instruction cannot trigger static class
       // initialization.
       if (field.field.holder != code.method.method.holder) {
@@ -497,22 +511,16 @@
     }
   }
 
+  // invoke instructions with a null receiver has already been rewritten to `throw null`.
+  // At this point, we attempt to explore non-null-param-or-throw optimization info and replace
+  // the invocation with `throw null` if an argument is known to be null and the method is going to
+  // throw for that null argument.
   private void rewriteInvoke(
       InvokeMethod invoke,
       ListIterator<BasicBlock> blockIterator,
       InstructionListIterator instructionIterator,
       IRCode code,
       Set<BasicBlock> blocksToBeRemoved) {
-    if (invoke.isInvokeMethodWithReceiver()) {
-      Value receiver = invoke.asInvokeMethodWithReceiver().getReceiver();
-      if (receiver.isAlwaysNull(appView)) {
-        instructionIterator.replaceCurrentInstructionWithThrowNull(
-            appView, code, blockIterator, blocksToBeRemoved);
-        ++numberOfInvokesWithNullReceiver;
-        return;
-      }
-    }
-
     DexEncodedMethod target =
         invoke.lookupSingleTarget(appView.appInfo(), code.method.method.holder);
     if (target == null) {
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/AlwaysThrowNullTest.java b/src/test/java/com/android/tools/r8/ir/optimize/AlwaysThrowNullTest.java
new file mode 100644
index 0000000..cc5893c
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/AlwaysThrowNullTest.java
@@ -0,0 +1,254 @@
+// Copyright (c) 2019, 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.ir.optimize;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.D8TestRunResult;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NeverMerge;
+import com.android.tools.r8.NeverPropagateValue;
+import com.android.tools.r8.R8TestRunResult;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.TestRunResult;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.InstructionSubject;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Streams;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+class AlwaysThrowNullTestClass {
+  @NeverMerge
+  static class Base {
+    Object dead;
+
+    void foo() {
+      System.out.println("Base::foo");
+    }
+  }
+
+  static class Uninitialized extends Base {
+  }
+
+  @NeverPropagateValue
+  @NeverInline
+  static void uninstantiatedInstancePutWithNPEGuard(Uninitialized arg) {
+    try {
+      arg.dead = new Object();
+    } catch (NullPointerException npe) {
+      System.out.println("Expected NPE");
+    }
+  }
+
+  @NeverPropagateValue
+  @NeverInline
+  static void uninstantiatedInstancePutWithOtherGuard(Uninitialized arg) {
+    try {
+      arg.dead = new Object();
+    } catch (IllegalArgumentException e) {
+      fail("Unexpected exception kind");
+    }
+  }
+
+  @NeverPropagateValue
+  @NeverInline
+  static void uninstantiatedInstanceInvoke(Uninitialized arg) {
+    arg.foo();
+    // Dead code.
+    System.out.println("invoke-virtual with null receiver: " + arg.dead.toString());
+  }
+
+  static Object nullObject() {
+    return null;
+  }
+
+  @NeverPropagateValue
+  @NeverInline
+  static void nullReferenceInvoke() {
+    try {
+      Object o = nullObject();
+      o.hashCode();
+      // Dead code.
+      System.out.println("invoke-virtual with null receiver: " + o.toString());
+    } catch (NullPointerException npe) {
+      System.out.println("Expected NPE");
+    }
+  }
+
+  @NeverPropagateValue
+  @NeverInline
+  static void uninstantiatedInstancePut(Uninitialized arg) {
+    arg.dead = new Object();
+    // Dead code.
+    System.out.println("instance-put with null receiver: " + arg.dead.toString());
+  }
+
+  @NeverPropagateValue
+  @NeverInline
+  static void uninstantiatedInstanceGet(Uninitialized arg) {
+    Object dead = arg.dead;
+    // Dead code.
+    System.out.println("instance-get with null receiver: " + dead.toString());
+  }
+
+  public static void main(String[] args) {
+    Base b = new Base();
+    try {
+      uninstantiatedInstancePutWithNPEGuard(null);
+    } catch (NullPointerException npe) {
+      fail("Unexpected NPE");
+    }
+    try {
+      uninstantiatedInstancePutWithOtherGuard(null);
+      fail("Expected NullPointerException");
+    } catch (NullPointerException npe) {
+      System.out.println("Expected NPE");
+    }
+    try {
+      uninstantiatedInstanceInvoke(null);
+      fail("Expected NullPointerException");
+    } catch (NullPointerException npe) {
+      System.out.println("Expected NPE");
+    }
+    try {
+      nullReferenceInvoke();
+    } catch (NullPointerException npe) {
+      fail("Unexpected NPE");
+    }
+    try {
+      uninstantiatedInstancePut(null);
+      fail("Expected NullPointerException");
+    } catch (NullPointerException npe) {
+      System.out.println("Expected NPE");
+    }
+    try {
+      uninstantiatedInstanceGet(null);
+      fail("Expected NullPointerException");
+    } catch (NullPointerException npe) {
+      System.out.println("Expected NPE");
+    }
+  }
+}
+
+@RunWith(Parameterized.class)
+public class AlwaysThrowNullTest extends TestBase {
+  private static final String JAVA_OUTPUT = StringUtils.lines(
+      "Expected NPE",
+      "Expected NPE",
+      "Expected NPE",
+      "Expected NPE",
+      "Expected NPE",
+      "Expected NPE"
+  );
+  private static final Class<?> MAIN = AlwaysThrowNullTestClass.class;
+  private static final List<String> METHODS_WITH_NPE_GUARD = ImmutableList.of(
+      "uninstantiatedInstancePutWithNPEGuard",
+      "nullReferenceInvoke"
+  );
+  private static final List<String> METHODS_WITHOUT_GUARD = ImmutableList.of(
+      "uninstantiatedInstanceInvoke",
+      "uninstantiatedInstancePut",
+      "uninstantiatedInstanceGet"
+  );
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimes().build();
+  }
+
+  private final TestParameters parameters;
+
+  public AlwaysThrowNullTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testJvmOutput() throws Exception {
+    assumeTrue("Only run JVM reference once (for CF backend)", parameters.isCfRuntime());
+    testForJvm()
+        .addTestClasspath()
+        .run(parameters.getRuntime(), MAIN)
+        .assertSuccessWithOutput(JAVA_OUTPUT);
+  }
+
+  private void test(TestRunResult result, boolean hasLiveness) throws Exception {
+    CodeInspector codeInspector = result.inspector();
+    ClassSubject mainClass = codeInspector.clazz(MAIN);
+
+    int expectedThrow = hasLiveness ? 1 : 0;
+    for (String methodName : METHODS_WITH_NPE_GUARD) {
+      MethodSubject withNPEGuard = mainClass.uniqueMethodWithName(methodName);
+      assertThat(withNPEGuard, isPresent());
+      // catch handlers could be split, and thus not always 1, but some small positive numbers.
+      assertTrue(
+        Streams.stream(
+            withNPEGuard.iterateTryCatches(
+                t -> t.isCatching(NullPointerException.class.getName()))
+        ).count() > 0);
+      assertEquals(
+          expectedThrow,
+          Streams.stream(withNPEGuard.iterateInstructions(InstructionSubject::isThrow)).count());
+    }
+
+    int expectedHandler = hasLiveness ? 0 : 1;
+    MethodSubject withOtherGuard =
+        mainClass.uniqueMethodWithName("uninstantiatedInstancePutWithOtherGuard");
+    assertThat(withOtherGuard, isPresent());
+    assertEquals(expectedHandler, Streams.stream(withOtherGuard.iterateTryCatches()).count());
+
+    for (String methodName : METHODS_WITHOUT_GUARD) {
+      MethodSubject mtd = mainClass.uniqueMethodWithName(methodName);
+      assertThat(mtd, isPresent());
+      assertEquals(
+          hasLiveness,
+          Streams.stream(mtd.iterateInstructions(InstructionSubject::isInvoke)).count() == 0);
+      assertEquals(
+          expectedThrow,
+          Streams.stream(mtd.iterateInstructions(InstructionSubject::isThrow)).count());
+    }
+  }
+
+  @Test
+  public void testD8() throws Exception {
+    assumeTrue("Only run D8 for Dex backend", parameters.isDexRuntime());
+    D8TestRunResult result =
+        testForD8()
+            .release()
+            .addProgramClassesAndInnerClasses(MAIN)
+            .setMinApi(parameters.getRuntime())
+            .run(parameters.getRuntime(), MAIN)
+            .assertSuccessWithOutput(JAVA_OUTPUT);
+    test(result, false);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    R8TestRunResult result =
+        testForR8(parameters.getBackend())
+            .addProgramClassesAndInnerClasses(MAIN)
+            .enableMergeAnnotations()
+            .enableInliningAnnotations()
+            .enableMemberValuePropagationAnnotations()
+            .addKeepMainRule(MAIN)
+            .noMinification()
+            .setMinApi(parameters.getRuntime())
+            .run(parameters.getRuntime(), MAIN)
+            .assertSuccessWithOutput(JAVA_OUTPUT);
+    test(result, true);
+  }
+
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/NonNullTrackerTest.java b/src/test/java/com/android/tools/r8/ir/optimize/NonNullTrackerTest.java
index 3f3ccd2..26a3f4e 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/NonNullTrackerTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/NonNullTrackerTest.java
@@ -64,7 +64,7 @@
       if (curr.isAssumeNonNull()) {
         // Make sure non-null is added to the right place.
         assertTrue(prev == null
-            || NonNullTracker.throwsOnNullInput(prev)
+            || prev.throwsOnNullInput()
             || (prev.isIf() && prev.asIf().isZeroTest())
             || !curr.getBlock().getPredecessors().contains(prev.getBlock()));
         // Make sure non-null is used or inserted for arguments.
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/ObjectsRequireNonNullTest.java b/src/test/java/com/android/tools/r8/ir/optimize/ObjectsRequireNonNullTest.java
index 9e22ade..df4598f 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/ObjectsRequireNonNullTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/ObjectsRequireNonNullTest.java
@@ -6,6 +6,7 @@
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
 import static org.junit.Assume.assumeTrue;
 
 import com.android.tools.r8.D8TestRunResult;
@@ -77,12 +78,14 @@
     unknownArg(instance);
     try {
       unknownArg(null);
+      fail("Expected NullPointerException");
     } catch (NullPointerException npe) {
       System.out.println("Expected NPE");
     }
 
     try {
       consumeUninitialized(null);
+      fail("Expected NullPointerException");
     } catch (NullPointerException npe) {
       System.out.println("Expected NPE");
     }
@@ -167,35 +170,38 @@
   @Test
   public void testD8() throws Exception {
     assumeTrue("Only run D8 for Dex backend", parameters.isDexRuntime());
-    D8TestRunResult result = testForD8()
-        .debug()
-        .addProgramClassesAndInnerClasses(MAIN)
-        .setMinApi(parameters.getRuntime())
-        .run(parameters.getRuntime(), MAIN)
-        .assertSuccessWithOutput(JAVA_OUTPUT);
+    D8TestRunResult result =
+        testForD8()
+            .debug()
+            .addProgramClassesAndInnerClasses(MAIN)
+            .setMinApi(parameters.getRuntime())
+            .run(parameters.getRuntime(), MAIN)
+            .assertSuccessWithOutput(JAVA_OUTPUT);
     test(result, 2, 1);
 
-    result = testForD8()
-        .release()
-        .addProgramClassesAndInnerClasses(MAIN)
-        .setMinApi(parameters.getRuntime())
-        .run(parameters.getRuntime(), MAIN)
-        .assertSuccessWithOutput(JAVA_OUTPUT);
+    result =
+        testForD8()
+            .release()
+            .addProgramClassesAndInnerClasses(MAIN)
+            .setMinApi(parameters.getRuntime())
+            .run(parameters.getRuntime(), MAIN)
+            .assertSuccessWithOutput(JAVA_OUTPUT);
     test(result, 0, 1);
   }
 
   @Test
   public void testR8() throws Exception {
     assumeTrue("CF disables move result optimization", parameters.isDexRuntime());
-    R8TestRunResult result = testForR8(parameters.getBackend())
-        .addProgramClassesAndInnerClasses(MAIN)
-        .enableInliningAnnotations()
-        .enableMemberValuePropagationAnnotations()
-        .addKeepMainRule(MAIN)
-        .noMinification()
-        .setMinApi(parameters.getRuntime())
-        .run(parameters.getRuntime(), MAIN)
-        .assertSuccessWithOutput(JAVA_OUTPUT);
+    R8TestRunResult result =
+        testForR8(parameters.getBackend())
+            .addProgramClassesAndInnerClasses(MAIN)
+            .enableInliningAnnotations()
+            .enableMemberValuePropagationAnnotations()
+            .addKeepMainRule(MAIN)
+            .noMinification()
+            .setMinApi(parameters.getRuntime())
+            .run(parameters.getRuntime(), MAIN)
+            .assertSuccessWithOutput(JAVA_OUTPUT);
     test(result, 0, 0);
   }
 }
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/instanceofremoval/UninstantiatedAnnotationTypeTest.java b/src/test/java/com/android/tools/r8/ir/optimize/instanceofremoval/UninstantiatedAnnotationTypeTest.java
index 7f61c30..98f28ba 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/instanceofremoval/UninstantiatedAnnotationTypeTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/instanceofremoval/UninstantiatedAnnotationTypeTest.java
@@ -5,12 +5,13 @@
 package com.android.tools.r8.ir.optimize.instanceofremoval;
 
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertThat;
 
 import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.TestBase;
-import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import com.android.tools.r8.utils.codeinspector.InstructionSubject;
@@ -58,15 +59,15 @@
     }
   }
 
-  @Parameters(name = "Backend: {0}")
-  public static Backend[] data() {
-    return ToolHelper.getBackends();
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimes().build();
   }
 
-  private final Backend backend;
+  private final TestParameters parameters;
 
-  public UninstantiatedAnnotationTypeTest(Backend backend) {
-    this.backend = backend;
+  public UninstantiatedAnnotationTypeTest(TestParameters parameters) {
+    this.parameters = parameters;
   }
 
   @Test
@@ -76,12 +77,13 @@
     testForJvm().addTestClasspath().run(TestClass.class).assertSuccessWithOutput(expected);
 
     CodeInspector inspector =
-        testForR8(backend)
+        testForR8(parameters.getBackend())
             .addInnerClasses(UninstantiatedAnnotationTypeTest.class)
             .addKeepMainRule(TestClass.class)
             .addKeepRules("-keepattributes RuntimeVisibleAnnotations")
             .enableInliningAnnotations()
-            .run(TestClass.class)
+            .setMinApi(parameters.getRuntime())
+            .run(parameters.getRuntime(), TestClass.class)
             .assertSuccessWithOutput(expected)
             .inspector();
 
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/instanceofremoval/UninstantiatedLibraryTypeTest.java b/src/test/java/com/android/tools/r8/ir/optimize/instanceofremoval/UninstantiatedLibraryTypeTest.java
index cff27b4..62db9fc 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/instanceofremoval/UninstantiatedLibraryTypeTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/instanceofremoval/UninstantiatedLibraryTypeTest.java
@@ -5,12 +5,13 @@
 package com.android.tools.r8.ir.optimize.instanceofremoval;
 
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertThat;
 
 import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.TestBase;
-import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import com.android.tools.r8.utils.codeinspector.InstructionSubject;
@@ -59,15 +60,15 @@
     }
   }
 
-  @Parameters(name = "Backend: {0}")
-  public static Backend[] data() {
-    return ToolHelper.getBackends();
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimes().build();
   }
 
-  private final Backend backend;
+  private final TestParameters parameters;
 
-  public UninstantiatedLibraryTypeTest(Backend backend) {
-    this.backend = backend;
+  public UninstantiatedLibraryTypeTest(TestParameters parameters) {
+    this.parameters = parameters;
   }
 
   @Test
@@ -77,11 +78,12 @@
     testForJvm().addTestClasspath().run(TestClass.class).assertSuccessWithOutput(expected);
 
     CodeInspector inspector =
-        testForR8(backend)
+        testForR8(parameters.getBackend())
             .addInnerClasses(UninstantiatedLibraryTypeTest.class)
             .addKeepMainRule(TestClass.class)
             .enableInliningAnnotations()
-            .run(TestClass.class)
+            .setMinApi(parameters.getRuntime())
+            .run(parameters.getRuntime(), TestClass.class)
             .assertSuccessWithOutput(expected)
             .inspector();
 
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/instanceofremoval/UninstantiatedProgramTypeTest.java b/src/test/java/com/android/tools/r8/ir/optimize/instanceofremoval/UninstantiatedProgramTypeTest.java
index df2739a..471405d 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/instanceofremoval/UninstantiatedProgramTypeTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/instanceofremoval/UninstantiatedProgramTypeTest.java
@@ -5,13 +5,14 @@
 package com.android.tools.r8.ir.optimize.instanceofremoval;
 
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.core.IsNot.not;
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertThat;
 
 import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.TestBase;
-import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import com.android.tools.r8.utils.codeinspector.InstructionSubject;
@@ -58,15 +59,15 @@
     }
   }
 
-  @Parameters(name = "Backend: {0}")
-  public static Backend[] data() {
-    return ToolHelper.getBackends();
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimes().build();
   }
 
-  private final Backend backend;
+  private final TestParameters parameters;
 
-  public UninstantiatedProgramTypeTest(Backend backend) {
-    this.backend = backend;
+  public UninstantiatedProgramTypeTest(TestParameters parameters) {
+    this.parameters = parameters;
   }
 
   @Test
@@ -76,11 +77,12 @@
     testForJvm().addTestClasspath().run(TestClass.class).assertSuccessWithOutput(expected);
 
     CodeInspector inspector =
-        testForR8(backend)
+        testForR8(parameters.getBackend())
             .addInnerClasses(UninstantiatedProgramTypeTest.class)
             .addKeepMainRule(TestClass.class)
             .enableInliningAnnotations()
-            .run(TestClass.class)
+            .setMinApi(parameters.getRuntime())
+            .run(parameters.getRuntime(), TestClass.class)
             .assertSuccessWithOutput(expected)
             .inspector();
 
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizerTest.java b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizerTest.java
index b5071f4..a676b5d 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizerTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizerTest.java
@@ -6,17 +6,17 @@
 
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertThat;
 import static org.junit.Assert.assertTrue;
-import static org.junit.Assume.assumeTrue;
 
 import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.TestRunResult;
-import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.code.Instruction;
 import com.android.tools.r8.code.InvokeDirect;
 import com.android.tools.r8.code.InvokeStatic;
@@ -60,20 +60,20 @@
 
 @RunWith(Parameterized.class)
 public class ClassStaticizerTest extends TestBase {
-  private Backend backend;
+  private final TestParameters parameters;
 
-  @Parameterized.Parameters(name = "Backend: {0}")
-  public static Backend[] data() {
-    return ToolHelper.getBackends();
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    // TODO(b/112831361): support for class staticizer in CF backend.
+    return getTestParameters().withDexRuntimes().build();
   }
 
-  public ClassStaticizerTest(Backend backend) {
-    this.backend = backend;
+  public ClassStaticizerTest(TestParameters parameters) {
+    this.parameters = parameters;
   }
 
   @Test
   public void testTrivial() throws Exception {
-    assumeTrue("b/112831361", backend == Backend.DEX);
     Class<?> main = TrivialTestClass.class;
     Class<?>[] classes = {
         NeverInline.class,
@@ -86,17 +86,18 @@
         SimpleWithPhi.Companion.class
     };
     String javaOutput = runOnJava(main);
-    TestRunResult result = testForR8(backend)
-        .addProgramClasses(classes)
-        .enableProguardTestOptions()
-        .enableInliningAnnotations()
-        .addKeepMainRule(main)
-        .noMinification()
-        .addKeepRules("-allowaccessmodification")
-        .addKeepRules("-keepattributes InnerClasses,EnclosingMethod")
-        .addOptionsModification(this::configure)
-        .run(main)
-        .assertSuccessWithOutput(javaOutput);
+    TestRunResult result =
+        testForR8(parameters.getBackend())
+            .addProgramClasses(classes)
+            .enableInliningAnnotations()
+            .addKeepMainRule(main)
+            .noMinification()
+            .addKeepRules("-allowaccessmodification")
+            .addKeepRules("-keepattributes InnerClasses,EnclosingMethod")
+            .addOptionsModification(this::configure)
+            .setMinApi(parameters.getRuntime())
+            .run(parameters.getRuntime(), main)
+            .assertSuccessWithOutput(javaOutput);
 
     CodeInspector inspector = result.inspector();
     ClassSubject clazz = inspector.clazz(main);
@@ -155,7 +156,6 @@
 
   @Test
   public void testMoveToHost_fieldOnly() throws Exception {
-    assumeTrue("b/112831361", backend == Backend.DEX);
     Class<?> main = MoveToHostFieldOnlyTestClass.class;
     Class<?>[] classes = {
         NeverInline.class,
@@ -163,15 +163,16 @@
         HostOkFieldOnly.class,
         CandidateOkFieldOnly.class
     };
-    TestRunResult result = testForR8(backend)
-        .addProgramClasses(classes)
-        .enableProguardTestOptions()
-        .enableInliningAnnotations()
-        .addKeepMainRule(main)
-        .noMinification()
-        .addKeepRules("-allowaccessmodification")
-        .addOptionsModification(this::configure)
-        .run(main);
+    TestRunResult result =
+        testForR8(parameters.getBackend())
+            .addProgramClasses(classes)
+            .enableInliningAnnotations()
+            .addKeepMainRule(main)
+            .noMinification()
+            .addKeepRules("-allowaccessmodification")
+            .addOptionsModification(this::configure)
+            .setMinApi(parameters.getRuntime())
+            .run(parameters.getRuntime(), main);
 
     CodeInspector inspector = result.inspector();
     ClassSubject clazz = inspector.clazz(main);
@@ -185,7 +186,6 @@
 
   @Test
   public void testMoveToHost() throws Exception {
-    assumeTrue("b/112831361", backend == Backend.DEX);
     Class<?> main = MoveToHostTestClass.class;
     Class<?>[] classes = {
         NeverInline.class,
@@ -200,16 +200,17 @@
         CandidateConflictField.class
     };
     String javaOutput = runOnJava(main);
-    TestRunResult result = testForR8(backend)
-        .addProgramClasses(classes)
-        .enableProguardTestOptions()
-        .enableInliningAnnotations()
-        .addKeepMainRule(main)
-        .noMinification()
-        .addKeepRules("-allowaccessmodification")
-        .addOptionsModification(this::configure)
-        .run(main)
-        .assertSuccessWithOutput(javaOutput);
+    TestRunResult result =
+        testForR8(parameters.getBackend())
+            .addProgramClasses(classes)
+            .enableInliningAnnotations()
+            .addKeepMainRule(main)
+            .noMinification()
+            .addKeepRules("-allowaccessmodification")
+            .addOptionsModification(this::configure)
+            .setMinApi(parameters.getRuntime())
+            .run(parameters.getRuntime(), main)
+            .assertSuccessWithOutput(javaOutput);
 
     CodeInspector inspector = result.inspector();
     ClassSubject clazz = inspector.clazz(main);
@@ -313,28 +314,28 @@
 
   private void configure(InternalOptions options) {
     options.enableClassInlining = false;
+    options.enableUninstantiatedTypeOptimization = false;
   }
 
-
   @Test
   public void dualInlinedMethodRewritten() throws Exception {
-    assumeTrue("b/112831361", backend == Backend.DEX);
     Class<?> main = DualCallTest.class;
     Class<?>[] classes = {
         DualCallTest.class,
         Candidate.class
     };
     String javaOutput = runOnJava(main);
-    TestRunResult result = testForR8(backend)
-        .addProgramClasses(classes)
-        .enableProguardTestOptions()
-        .enableInliningAnnotations()
-        .addKeepMainRule(main)
-        .noMinification()
-        .addKeepRules("-allowaccessmodification")
-        .addOptionsModification(this::configure)
-        .run(main)
-        .assertSuccessWithOutput(javaOutput);
+    TestRunResult result =
+        testForR8(parameters.getBackend())
+            .addProgramClasses(classes)
+            .enableInliningAnnotations()
+            .addKeepMainRule(main)
+            .noMinification()
+            .addKeepRules("-allowaccessmodification")
+            .addOptionsModification(this::configure)
+            .setMinApi(parameters.getRuntime())
+            .run(parameters.getRuntime(), main)
+            .assertSuccessWithOutput(javaOutput);
 
     CodeInspector inspector = result.inspector();
     ClassSubject clazz = inspector.clazz(main);
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/string/StringValueOfTest.java b/src/test/java/com/android/tools/r8/ir/optimize/string/StringValueOfTest.java
index 4d8dddb..91ffa3d 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/string/StringValueOfTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/string/StringValueOfTest.java
@@ -99,7 +99,11 @@
     } catch (NullPointerException npe) {
       fail("Not expected: " + npe);
     }
-    System.out.println(consumeUninitialized(null));
+    try {
+      System.out.println(consumeUninitialized(null));
+    } catch (NullPointerException npe) {
+      fail("Not expected: " + npe);
+    }
 
     // No matter what we pass, that function will return null.
     // But, we're not sure about it, hence not optimizing String#valueOf.
@@ -129,7 +133,6 @@
   private static final Class<?> MAIN = StringValueOfTestMain.class;
 
   private static final String STRING_DESCRIPTOR = "Ljava/lang/String;";
-  private static final String STRING_TYPE = "java.lang.String";
 
   @Parameterized.Parameters(name = "{0}")
   public static TestParametersCollection data() {
diff --git a/src/test/java/com/android/tools/r8/naming/EnumMinificationKotlinTest.java b/src/test/java/com/android/tools/r8/naming/EnumMinificationKotlinTest.java
index cd1520e..d56ae8e 100644
--- a/src/test/java/com/android/tools/r8/naming/EnumMinificationKotlinTest.java
+++ b/src/test/java/com/android/tools/r8/naming/EnumMinificationKotlinTest.java
@@ -4,11 +4,11 @@
 package com.android.tools.r8.naming;
 
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertThat;
 
 import com.android.tools.r8.KotlinTestBase;
-import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
 import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
@@ -27,30 +27,35 @@
   private static final String MAIN_CLASS_NAME = "minify_enum.MainKt";
   private static final String ENUM_CLASS_NAME = "minify_enum.MinifyEnum";
 
-  private final Backend backend;
+  private final TestParameters parameters;
   private final boolean minify;
 
-  @Parameterized.Parameters(name = "Backend: {0} target: {1} minify: {2}")
+  @Parameterized.Parameters(name = "{0} target: {1} minify: {2}")
   public static Collection<Object[]> data() {
-    return buildParameters(ToolHelper.getBackends(), KotlinTargetVersion.values(), BooleanUtils.values());
+    return buildParameters(
+        getTestParameters().withAllRuntimes().build(),
+        KotlinTargetVersion.values(),
+        BooleanUtils.values());
   }
 
   public EnumMinificationKotlinTest(
-      Backend backend, KotlinTargetVersion targetVersion, boolean minify) {
+      TestParameters parameters, KotlinTargetVersion targetVersion, boolean minify) {
     super(targetVersion);
-    this.backend = backend;
+    this.parameters = parameters;
     this.minify = minify;
   }
 
   @Test
   public void b121221542() throws Exception {
-    CodeInspector inspector = testForR8(backend)
-        .addProgramFiles(getKotlinJarFile(FOLDER))
-        .addProgramFiles(getJavaJarFile(FOLDER))
-        .addKeepMainRule(MAIN_CLASS_NAME)
-        .minification(minify)
-        .run(MAIN_CLASS_NAME)
-        .inspector();
+    CodeInspector inspector =
+        testForR8(parameters.getBackend())
+            .addProgramFiles(getKotlinJarFile(FOLDER))
+            .addProgramFiles(getJavaJarFile(FOLDER))
+            .addKeepMainRule(MAIN_CLASS_NAME)
+            .minification(minify)
+            .setMinApi(parameters.getRuntime())
+            .run(parameters.getRuntime(), MAIN_CLASS_NAME)
+            .inspector();
     ClassSubject enumClass = inspector.clazz(ENUM_CLASS_NAME);
     assertThat(enumClass, isPresent());
     assertEquals(minify, enumClass.isRenamed());
diff --git a/src/test/java/com/android/tools/r8/shaking/FieldReadsJasminTest.java b/src/test/java/com/android/tools/r8/shaking/FieldReadsJasminTest.java
index 20b2850..f22cc41 100644
--- a/src/test/java/com/android/tools/r8/shaking/FieldReadsJasminTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/FieldReadsJasminTest.java
@@ -5,13 +5,14 @@
 
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isRenamed;
+import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertThat;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
-import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.jasmin.JasminBuilder;
 import com.android.tools.r8.jasmin.JasminBuilder.ClassBuilder;
 import com.android.tools.r8.jasmin.JasminTestBase;
@@ -30,15 +31,15 @@
 public class FieldReadsJasminTest extends JasminTestBase {
   private static final String CLS = "Empty";
   private static final String MAIN = "Main";
-  private final Backend backend;
+  private final TestParameters parameters;
 
-  @Parameterized.Parameters(name = "Backend: {0}")
-  public static Object[] data() {
-    return ToolHelper.getBackends();
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimes().build();
   }
 
-  public FieldReadsJasminTest(Backend backend) {
-    this.backend = backend;
+  public FieldReadsJasminTest(TestParameters parameters) {
+    this.parameters = parameters;
   }
 
   @Test
@@ -79,9 +80,10 @@
   }
 
   private void ensureNoFields(JasminBuilder app, ClassBuilder clazz) throws Exception {
-    testForR8(backend)
+    testForR8(parameters.getBackend())
         .addProgramClassFileData(app.buildClasses())
         .addKeepRules("-keep class * { <methods>; }")
+        .setMinApi(parameters.getRuntime())
         .compile()
         .inspect(inspector -> {
           ClassSubject classSubject = inspector.clazz(clazz.name);
@@ -129,9 +131,10 @@
       ClassBuilder fieldHolder,
       String fieldName)
       throws Exception {
-    testForR8(backend)
+    testForR8(parameters.getBackend())
         .addProgramClassFileData(app.buildClasses())
         .addKeepRules("-keep class * { <methods>; }")
+        .setMinApi(parameters.getRuntime())
         .compile()
         .inspect(inspector -> {
           FieldSubject fld = inspector.clazz(fieldHolder.name).uniqueFieldWithName(fieldName);
@@ -264,9 +267,10 @@
       ClassBuilder fieldHolder,
       String fieldName)
       throws Exception {
-    testForR8(backend)
+    testForR8(parameters.getBackend())
         .addProgramClassFileData(app.buildClasses())
         .addKeepRules("-keep class * { <methods>; }")
+        .setMinApi(parameters.getRuntime())
         .compile()
         .inspect(inspector -> {
           FieldSubject fld = inspector.clazz(fieldHolder.name).uniqueFieldWithName(fieldName);
diff --git a/src/test/java/com/android/tools/r8/shaking/InvalidTypesTest.java b/src/test/java/com/android/tools/r8/shaking/InvalidTypesTest.java
index 032bd05..64dbadf 100644
--- a/src/test/java/com/android/tools/r8/shaking/InvalidTypesTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/InvalidTypesTest.java
@@ -13,6 +13,7 @@
 import com.android.tools.r8.DXTestRunResult;
 import com.android.tools.r8.ProguardTestRunResult;
 import com.android.tools.r8.R8TestRunResult;
+import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestRunResult;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.DexVm.Version;
@@ -64,19 +65,20 @@
     }
   }
 
-  private final Backend backend;
+  private final TestParameters parameters;
   private final Mode mode;
   private final boolean useInterface;
 
-  public InvalidTypesTest(Backend backend, Mode mode, boolean useInterface) {
-    this.backend = backend;
+  public InvalidTypesTest(TestParameters parameters, Mode mode, boolean useInterface) {
+    this.parameters = parameters;
     this.mode = mode;
     this.useInterface = useInterface;
   }
 
-  @Parameters(name = "Backend: {0}, mode: {1}, use interface: {2}")
+  @Parameters(name = "{0}, mode: {1}, use interface: {2}")
   public static Collection<Object[]> parameters() {
-    return buildParameters(ToolHelper.getBackends(), Mode.values(), BooleanUtils.values());
+    return buildParameters(
+        getTestParameters().withAllRuntimes().build(), Mode.values(), BooleanUtils.values());
   }
 
   @Test
@@ -167,7 +169,7 @@
     Path inputJar = temp.getRoot().toPath().resolve("input.jar");
     jasminBuilder.writeJar(inputJar);
 
-    if (backend == Backend.CF) {
+    if (parameters.isCfRuntime()) {
       TestRunResult<?> jvmResult = testForJvm().addClasspath(inputJar).run(mainClass.name);
       checkTestRunResult(jvmResult, Compiler.JAVAC);
 
@@ -179,7 +181,7 @@
               .run(mainClass.name);
       checkTestRunResult(proguardResult, Compiler.PROGUARD);
     } else {
-      assert backend == Backend.DEX;
+      assert parameters.isDexRuntime();
 
       DXTestRunResult dxResult = testForDX().addProgramFiles(inputJar).run(mainClass.name);
       checkTestRunResult(dxResult, Compiler.DX);
@@ -189,7 +191,7 @@
     }
 
     R8TestRunResult r8Result =
-        testForR8(backend)
+        testForR8(parameters.getBackend())
             .addProgramFiles(inputJar)
             .addKeepMainRule(mainClass.name)
             .addKeepRules(
@@ -202,7 +204,8 @@
                   }
                 })
             .enableInliningAnnotations()
-            .run(mainClass.name);
+            .setMinApi(parameters.getRuntime())
+            .run(parameters.getRuntime(), mainClass.name);
     checkTestRunResult(r8Result, Compiler.R8);
   }
 
@@ -304,13 +307,13 @@
   }
 
   private Matcher<String> getMatcherForExpectedError() {
-    if (backend == Backend.CF) {
+    if (parameters.isCfRuntime()) {
       return allOf(
           containsString("java.lang.VerifyError"),
           containsString("Bad type in putfield/putstatic"));
     }
 
-    assert backend == Backend.DEX;
+    assert parameters.isDexRuntime();
     return allOf(
         containsString("java.lang.VerifyError"),
         anyOf(