Optimize unused return values

Change-Id: Ib3a5c3bb109df60cf530df2e05aa838c06110878
diff --git a/src/main/java/com/android/tools/r8/graph/AppView.java b/src/main/java/com/android/tools/r8/graph/AppView.java
index 14b0ded..24c47b4 100644
--- a/src/main/java/com/android/tools/r8/graph/AppView.java
+++ b/src/main/java/com/android/tools/r8/graph/AppView.java
@@ -515,7 +515,6 @@
   }
 
   public void setRootSet(RootSet rootSet) {
-    assert this.rootSet == null : "Root set should never be recomputed";
     this.rootSet = rootSet;
   }
 
@@ -806,9 +805,12 @@
             appView.setProguardCompatibilityActions(
                 appView.getProguardCompatibilityActions().rewrittenWithLens(lens));
           }
-          if (appView.getMainDexRootSet() != null) {
+          if (appView.hasMainDexRootSet()) {
             appView.setMainDexRootSet(appView.getMainDexRootSet().rewrittenWithLens(lens));
           }
+          if (appView.hasRootSet()) {
+            appView.setRootSet(appView.rootSet().rewrittenWithLens(lens));
+          }
         });
   }
 
diff --git a/src/main/java/com/android/tools/r8/graph/RewrittenPrototypeDescription.java b/src/main/java/com/android/tools/r8/graph/RewrittenPrototypeDescription.java
index e6a6cba..a382689 100644
--- a/src/main/java/com/android/tools/r8/graph/RewrittenPrototypeDescription.java
+++ b/src/main/java/com/android/tools/r8/graph/RewrittenPrototypeDescription.java
@@ -240,7 +240,6 @@
       this.oldType = oldType;
       this.newType = newType;
       this.singleValue = singleValue;
-      assert !newType.isVoidType() || singleValue != null;
     }
 
     public RewrittenTypeInfo combine(RewrittenPrototypeDescription other) {
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriter.java b/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriter.java
index 8bcdea8..cd0e433 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriter.java
@@ -386,38 +386,42 @@
                 }
 
                 Instruction constantReturnMaterializingInstruction = null;
-                if (prototypeChanges.hasBeenChangedToReturnVoid() && invoke.hasOutValue()) {
-                  TypeAndLocalInfoSupplier typeAndLocalInfo =
-                      new TypeAndLocalInfoSupplier() {
-                        @Override
-                        public DebugLocalInfo getLocalInfo() {
-                          return invoke.getLocalInfo();
-                        }
+                if (invoke.hasOutValue()) {
+                  if (invoke.hasUnusedOutValue()) {
+                    invoke.clearOutValue();
+                  } else if (prototypeChanges.hasBeenChangedToReturnVoid()) {
+                    TypeAndLocalInfoSupplier typeAndLocalInfo =
+                        new TypeAndLocalInfoSupplier() {
+                          @Override
+                          public DebugLocalInfo getLocalInfo() {
+                            return invoke.getLocalInfo();
+                          }
 
-                        @Override
-                        public TypeElement getOutType() {
-                          return graphLens
-                              .lookupType(invokedMethod.getReturnType(), codeLens)
-                              .toTypeElement(appView);
-                        }
-                      };
-                  prototypeChanges.verifyConstantReturnAccessibleInContext(
-                      appView.withLiveness(), method, graphLens);
-                  constantReturnMaterializingInstruction =
-                      prototypeChanges.getConstantReturn(
-                          appView.withLiveness(),
-                          code,
-                          invoke.getPosition(),
-                          typeAndLocalInfo);
-                  if (invoke.outValue().hasLocalInfo()) {
-                    constantReturnMaterializingInstruction
+                          @Override
+                          public TypeElement getOutType() {
+                            return graphLens
+                                .lookupType(invokedMethod.getReturnType(), codeLens)
+                                .toTypeElement(appView);
+                          }
+                        };
+                    assert prototypeChanges.verifyConstantReturnAccessibleInContext(
+                        appView.withLiveness(), method, graphLens);
+                    constantReturnMaterializingInstruction =
+                        prototypeChanges.getConstantReturn(
+                            appView.withLiveness(), code, invoke.getPosition(), typeAndLocalInfo);
+                    if (invoke.outValue().hasLocalInfo()) {
+                      constantReturnMaterializingInstruction
+                          .outValue()
+                          .setLocalInfo(invoke.outValue().getLocalInfo());
+                    }
+                    invoke
                         .outValue()
-                        .setLocalInfo(invoke.outValue().getLocalInfo());
-                  }
-                  invoke.outValue().replaceUsers(constantReturnMaterializingInstruction.outValue());
-                  if (invoke.getOutType() != constantReturnMaterializingInstruction.getOutType()) {
-                    affectedPhis.addAll(
-                        constantReturnMaterializingInstruction.outValue().uniquePhiUsers());
+                        .replaceUsers(constantReturnMaterializingInstruction.outValue());
+                    if (invoke.getOutType()
+                        != constantReturnMaterializingInstruction.getOutType()) {
+                      affectedPhis.addAll(
+                          constantReturnMaterializingInstruction.outValue().uniquePhiUsers());
+                    }
                   }
                 }
 
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/DefaultMethodOptimizationInfo.java b/src/main/java/com/android/tools/r8/ir/optimize/info/DefaultMethodOptimizationInfo.java
index 95483ce..f6aa6ad 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/DefaultMethodOptimizationInfo.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/DefaultMethodOptimizationInfo.java
@@ -18,6 +18,7 @@
 import com.android.tools.r8.ir.optimize.info.initializer.DefaultInstanceInitializerInfo;
 import com.android.tools.r8.ir.optimize.info.initializer.InstanceInitializerInfo;
 import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.OptionalBool;
 import com.google.common.collect.ImmutableSet;
 import java.util.BitSet;
 import java.util.Set;
@@ -161,6 +162,11 @@
   }
 
   @Override
+  public OptionalBool isReturnValueUsed() {
+    return OptionalBool.unknown();
+  }
+
+  @Override
   public boolean forceInline() {
     return false;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfo.java b/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfo.java
index 17cffe5..473f718 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfo.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfo.java
@@ -15,6 +15,7 @@
 import com.android.tools.r8.ir.optimize.info.bridge.BridgeInfo;
 import com.android.tools.r8.ir.optimize.info.initializer.InstanceInitializerInfo;
 import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.OptionalBool;
 import java.util.BitSet;
 import java.util.Set;
 
@@ -86,6 +87,8 @@
 
   public abstract boolean isMultiCallerMethod();
 
+  public abstract OptionalBool isReturnValueUsed();
+
   public abstract boolean forceInline();
 
   public abstract boolean triggersClassInitBeforeAnySideEffect();
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/MutableMethodOptimizationInfo.java b/src/main/java/com/android/tools/r8/ir/optimize/info/MutableMethodOptimizationInfo.java
index 69f0497..2cffcc0 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/MutableMethodOptimizationInfo.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/MutableMethodOptimizationInfo.java
@@ -28,6 +28,7 @@
 import com.android.tools.r8.utils.BitSetUtils;
 import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.OptionalBool;
 import java.util.BitSet;
 import java.util.Collections;
 import java.util.Set;
@@ -47,6 +48,7 @@
       EnumUnboxerMethodClassification.unknown();
   private DynamicType dynamicType = DynamicType.unknown();
   private InlinePreference inlining = InlinePreference.Default;
+  private OptionalBool isReturnValueUsed = OptionalBool.unknown();
   // Stores information about instance methods and constructors for
   // class inliner, null value indicates that the method is not eligible.
   private BridgeInfo bridgeInfo = null;
@@ -481,6 +483,15 @@
   }
 
   @Override
+  public OptionalBool isReturnValueUsed() {
+    return isReturnValueUsed;
+  }
+
+  void setIsReturnValueUsed(OptionalBool isReturnValueUsed) {
+    this.isReturnValueUsed = isReturnValueUsed;
+  }
+
+  @Override
   public boolean forceInline() {
     return inlining == InlinePreference.ForceInline;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackSimple.java b/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackSimple.java
index 5be58f0..1b54d1f 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackSimple.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackSimple.java
@@ -18,6 +18,7 @@
 import com.android.tools.r8.ir.optimize.info.bridge.BridgeInfo;
 import com.android.tools.r8.ir.optimize.info.initializer.InstanceInitializerInfoCollection;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.OptionalBool;
 import java.util.BitSet;
 import java.util.Set;
 import java.util.function.Consumer;
@@ -179,6 +180,10 @@
     method.getMutableOptimizationInfo().setInitializerEnablingJavaAssertions();
   }
 
+  public void setIsReturnValueUsed(OptionalBool isReturnValueUsed, ProgramMethod method) {
+    method.getDefinition().getMutableOptimizationInfo().setIsReturnValueUsed(isReturnValueUsed);
+  }
+
   @Override
   public void setNonNullParamOrThrow(DexEncodedMethod method, BitSet facts) {
     method.getMutableOptimizationInfo().setNonNullParamOrThrow(facts);
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorCodeScanner.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorCodeScanner.java
index 2839aeb..ba4b05a 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorCodeScanner.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorCodeScanner.java
@@ -47,7 +47,6 @@
 import com.android.tools.r8.optimize.argumentpropagation.utils.WideningUtils;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.Timing;
-import com.google.common.collect.Iterables;
 import com.google.common.collect.Sets;
 import java.util.ArrayList;
 import java.util.IdentityHashMap;
@@ -152,12 +151,6 @@
   }
 
   private void scan(InvokeMethod invoke, ProgramMethod context, Timing timing) {
-    List<Value> arguments = invoke.arguments();
-    if (arguments.isEmpty()) {
-      // Nothing to propagate.
-      return;
-    }
-
     DexMethod invokedMethod = invoke.getInvokedMethod();
     if (invokedMethod.getHolderType().isArrayType()) {
       // Nothing to propagate; the targeted method is not a program method.
@@ -191,7 +184,7 @@
       return;
     }
 
-    if (arguments.size() != resolvedMethod.getDefinition().getNumberOfArguments()
+    if (invoke.arguments().size() != resolvedMethod.getDefinition().getNumberOfArguments()
         || invoke.isInvokeStatic() != resolvedMethod.getAccessFlags().isStatic()) {
       // Nothing to propagate; the invoke instruction fails.
       return;
@@ -410,13 +403,10 @@
               methodReprocessingCriteria.getParameterReprocessingCriteria(argumentIndex)));
     }
 
-    // If all parameter states are unknown, then return a canonicalized unknown method state that
-    // has this property.
-    if (Iterables.all(parameterStates, ParameterState::isUnknown)) {
-      return MethodState.unknown();
-    }
-
-    return new ConcreteMonomorphicMethodState(parameterStates);
+    // We simulate that the return value is used for methods with void return type. This ensures
+    // that we will widen the method state to unknown if/when all parameter states become unknown.
+    boolean isReturnValueUsed = invoke.getReturnType().isVoidType() || invoke.hasUsedOutValue();
+    return ConcreteMonomorphicMethodState.create(isReturnValueUsed, parameterStates);
   }
 
   // For receivers there is not much point in trying to track an abstract value. Therefore we only
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorOptimizationInfoPopulator.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorOptimizationInfoPopulator.java
index 470e442..32a3036 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorOptimizationInfoPopulator.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorOptimizationInfoPopulator.java
@@ -34,6 +34,7 @@
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.ListUtils;
+import com.android.tools.r8.utils.OptionalBool;
 import com.android.tools.r8.utils.ThreadUtils;
 import com.android.tools.r8.utils.Timing;
 import java.util.List;
@@ -197,11 +198,17 @@
         .map(ParameterState::asConcrete)
         .noneMatch(ConcreteParameterState::hasInParameters);
 
-    getSimpleFeedback()
-        .setArgumentInfos(
-            method,
-            ConcreteCallSiteOptimizationInfo.fromMethodState(
-                appView, method, monomorphicMethodState));
+    if (monomorphicMethodState.size() > 0) {
+      getSimpleFeedback()
+          .setArgumentInfos(
+              method,
+              ConcreteCallSiteOptimizationInfo.fromMethodState(
+                  appView, method, monomorphicMethodState));
+    }
+
+    if (!monomorphicMethodState.isReturnValueUsed()) {
+      getSimpleFeedback().setIsReturnValueUsed(OptionalBool.FALSE, method);
+    }
 
     // Strengthen the return value of the method if the method is known to return one of the
     // arguments.
@@ -223,10 +230,18 @@
     }
 
     int numberOfArguments = method.getDefinition().getNumberOfArguments();
-    List<ParameterState> parameterStates =
-        methodState.isMonomorphic()
-            ? methodState.asMonomorphic().getParameterStates()
-            : ListUtils.newInitializedArrayList(numberOfArguments, ParameterState.unknown());
+    boolean isReturnValueUsed;
+    List<ParameterState> parameterStates;
+    if (methodState.isMonomorphic()) {
+      ConcreteMonomorphicMethodState monomorphicMethodState = methodState.asMonomorphic();
+      isReturnValueUsed = monomorphicMethodState.isReturnValueUsed();
+      parameterStates = monomorphicMethodState.getParameterStates();
+    } else {
+      assert methodState.isUnknown();
+      isReturnValueUsed = true;
+      parameterStates =
+          ListUtils.newInitializedArrayList(numberOfArguments, ParameterState.unknown());
+    }
     List<ParameterState> narrowedParameterStates =
         ListUtils.mapOrElse(
             parameterStates,
@@ -243,7 +258,7 @@
             },
             null);
     return narrowedParameterStates != null
-        ? new ConcreteMonomorphicMethodState(narrowedParameterStates)
+        ? new ConcreteMonomorphicMethodState(isReturnValueUsed, narrowedParameterStates)
         : methodState;
   }
 
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorProgramOptimizer.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorProgramOptimizer.java
index c2912c0..202b249 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorProgramOptimizer.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorProgramOptimizer.java
@@ -44,6 +44,7 @@
 import com.android.tools.r8.utils.IntBox;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.InternalOptions.CallSiteOptimizationOptions;
+import com.android.tools.r8.utils.OptionalBool;
 import com.android.tools.r8.utils.Pair;
 import com.android.tools.r8.utils.ThreadUtils;
 import com.android.tools.r8.utils.Timing;
@@ -389,14 +390,10 @@
             }
 
             // Also record the found return value for abstract virtual methods.
-            if (newReturnType == dexItemFactory.voidType) {
+            if (newReturnType == dexItemFactory.voidType && returnValueForVirtualMethods != null) {
               for (ProgramMethod method : methods) {
                 if (method.getAccessFlags().isAbstract()) {
                   returnValuesForVirtualMethods.put(method, returnValueForVirtualMethods);
-                } else {
-                  AbstractValue returnValueForVirtualMethod =
-                      method.getOptimizationInfo().getAbstractReturnValue();
-                  assert returnValueForVirtualMethod.equals(returnValueForVirtualMethods);
                 }
               }
             }
@@ -441,7 +438,10 @@
         if (!appView.appInfo().mayPropagateValueFor(method)) {
           return null;
         }
-        AbstractValue returnValueForMethod = method.getOptimizationInfo().getAbstractReturnValue();
+        AbstractValue returnValueForMethod =
+            method.getReturnType().isAlwaysNull(appView)
+                ? appView.abstractValueFactory().createNullValue()
+                : method.getOptimizationInfo().getAbstractReturnValue();
         if (!returnValueForMethod.isSingleValue()
             || !returnValueForMethod.asSingleValue().isMaterializableInAllContexts(appView)
             || (returnValue != null && !returnValueForMethod.equals(returnValue))) {
@@ -510,16 +510,17 @@
 
     private DexType getNewReturnTypeForVirtualMethods(
         ProgramMethodSet methods, SingleValue returnValue) {
-      if (returnValue != null) {
+      if (returnValue != null || isReturnValueUnusedForVirtualMethods(methods)) {
         return dexItemFactory.voidType;
       }
+
       DexType newReturnType = null;
       for (ProgramMethod method : methods) {
         if (method.getDefinition().isAbstract()) {
           // OK, this method can have any return type.
           continue;
         }
-        DexType newReturnTypeForMethod = getNewReturnType(method, null);
+        DexType newReturnTypeForMethod = getNewReturnType(method, OptionalBool.UNKNOWN, null);
         if (newReturnTypeForMethod == null
             || (newReturnType != null && newReturnType != newReturnTypeForMethod)) {
           return null;
@@ -530,6 +531,16 @@
       return newReturnType;
     }
 
+    private boolean isReturnValueUnusedForVirtualMethods(ProgramMethodSet methods) {
+      ProgramMethod representative = methods.getFirst();
+      return !representative.getReturnType().isVoidType()
+          && Iterables.all(
+              methods,
+              method ->
+                  appView.getKeepInfo(method).isUnusedReturnValueOptimizationAllowed(options)
+                      && method.getOptimizationInfo().isReturnValueUsed().isFalse());
+    }
+
     private DexType getNewParameterTypeForVirtualMethods(
         ProgramMethodSet methods, int parameterIndex) {
       if (parameterIndex == 0) {
@@ -885,18 +896,26 @@
     }
 
     private DexType getNewReturnType(ProgramMethod method) {
-      return getNewReturnType(method, getReturnValue(method));
+      return getNewReturnType(
+          method, method.getOptimizationInfo().isReturnValueUsed(), getReturnValue(method));
     }
 
-    private DexType getNewReturnType(ProgramMethod method, SingleValue returnValue) {
+    private DexType getNewReturnType(
+        ProgramMethod method, OptionalBool isReturnValueUsed, SingleValue returnValue) {
       DexType staticType = method.getReturnType();
-      if (staticType.isVoidType()
-          || !appView.getKeepInfo(method).isReturnTypeStrengtheningAllowed(options)) {
+      if (staticType.isVoidType()) {
         return null;
       }
       if (returnValue != null) {
         return dexItemFactory.voidType;
       }
+      KeepMethodInfo keepInfo = appView.getKeepInfo(method);
+      if (keepInfo.isUnusedReturnValueOptimizationAllowed(options) && isReturnValueUsed.isFalse()) {
+        return dexItemFactory.voidType;
+      }
+      if (!keepInfo.isReturnTypeStrengtheningAllowed(options)) {
+        return null;
+      }
       TypeElement newReturnTypeElement =
           method
               .getOptimizationInfo()
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorUnoptimizableMethods.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorUnoptimizableMethods.java
index 6319e90..c277bb7 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorUnoptimizableMethods.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorUnoptimizableMethods.java
@@ -12,15 +12,12 @@
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.UnknownMethodState;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.InternalOptions;
-import com.android.tools.r8.utils.MethodSignatureEquivalence;
 import com.android.tools.r8.utils.classhierarchy.MethodOverridesCollector;
 import com.android.tools.r8.utils.collections.ProgramMethodSet;
 import java.util.Collection;
 
 public class ArgumentPropagatorUnoptimizableMethods {
 
-  private static final MethodSignatureEquivalence equivalence = MethodSignatureEquivalence.get();
-
   private final AppView<AppInfoWithLiveness> appView;
   private final ImmediateProgramSubtypingInfo immediateSubtypingInfo;
   private final MethodStateCollectionByReference methodStates;
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteMonomorphicMethodState.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteMonomorphicMethodState.java
index d32df00..e730d74 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteMonomorphicMethodState.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteMonomorphicMethodState.java
@@ -16,15 +16,25 @@
 public class ConcreteMonomorphicMethodState extends ConcreteMethodState
     implements ConcreteMonomorphicMethodStateOrBottom, ConcreteMonomorphicMethodStateOrUnknown {
 
+  boolean isReturnValueUsed;
   List<ParameterState> parameterStates;
 
-  public ConcreteMonomorphicMethodState(List<ParameterState> parameterStates) {
+  public ConcreteMonomorphicMethodState(
+      boolean isReturnValueUsed, List<ParameterState> parameterStates) {
     assert Streams.stream(Iterables.skip(parameterStates, 1))
         .noneMatch(x -> x.isConcrete() && x.asConcrete().isReceiverParameter());
+    this.isReturnValueUsed = isReturnValueUsed;
     this.parameterStates = parameterStates;
     assert !isEffectivelyUnknown() : "Must use UnknownMethodState instead";
   }
 
+  public static ConcreteMonomorphicMethodStateOrUnknown create(
+      boolean isReturnValueUsed, List<ParameterState> parameterStates) {
+    return isEffectivelyUnknown(isReturnValueUsed, parameterStates)
+        ? unknown()
+        : new ConcreteMonomorphicMethodState(isReturnValueUsed, parameterStates);
+  }
+
   public ParameterState getParameterState(int index) {
     return parameterStates.get(index);
   }
@@ -33,8 +43,21 @@
     return parameterStates;
   }
 
+  public boolean isReturnValueUsed() {
+    return isReturnValueUsed;
+  }
+
+  public boolean isEffectivelyBottom() {
+    return Iterables.any(parameterStates, ParameterState::isBottom);
+  }
+
   public boolean isEffectivelyUnknown() {
-    return Iterables.all(parameterStates, ParameterState::isUnknown);
+    return isEffectivelyUnknown(isReturnValueUsed, parameterStates);
+  }
+
+  private static boolean isEffectivelyUnknown(
+      boolean isReturnValueUsed, List<ParameterState> parameterStates) {
+    return isReturnValueUsed && Iterables.all(parameterStates, ParameterState::isUnknown);
   }
 
   @Override
@@ -43,7 +66,7 @@
     for (ParameterState parameterState : getParameterStates()) {
       copiedParametersStates.add(parameterState.mutableCopy());
     }
-    return new ConcreteMonomorphicMethodState(copiedParametersStates);
+    return new ConcreteMonomorphicMethodState(isReturnValueUsed, copiedParametersStates);
   }
 
   public ConcreteMonomorphicMethodStateOrUnknown mutableJoin(
@@ -56,6 +79,10 @@
       return unknown();
     }
 
+    if (methodState.isReturnValueUsed()) {
+      isReturnValueUsed = true;
+    }
+
     int argumentIndex = 0;
     if (size() > methodSignature.getArity()) {
       assert size() == methodSignature.getArity() + 1;
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/InParameterFlowPropagator.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/InParameterFlowPropagator.java
index 9b10015..fae616e 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/InParameterFlowPropagator.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/InParameterFlowPropagator.java
@@ -133,18 +133,10 @@
       return;
     }
     assert methodState.isMonomorphic();
-    boolean allUnknown = true;
-    for (ParameterState parameterState : methodState.asMonomorphic().getParameterStates()) {
-      if (parameterState.isBottom()) {
-        methodStates.set(method, MethodState.bottom());
-        return;
-      }
-      if (!parameterState.isUnknown()) {
-        assert parameterState.isConcrete();
-        allUnknown = false;
-      }
-    }
-    if (allUnknown) {
+    ConcreteMonomorphicMethodState monomorphicMethodState = methodState.asMonomorphic();
+    if (monomorphicMethodState.isEffectivelyBottom()) {
+      methodStates.set(method, MethodState.bottom());
+    } else if (monomorphicMethodState.isEffectivelyUnknown()) {
       methodStates.set(method, MethodState.unknown());
     }
   }
diff --git a/src/main/java/com/android/tools/r8/shaking/DependentMinimumKeepInfoCollection.java b/src/main/java/com/android/tools/r8/shaking/DependentMinimumKeepInfoCollection.java
index 8e48dfe..4c36623 100644
--- a/src/main/java/com/android/tools/r8/shaking/DependentMinimumKeepInfoCollection.java
+++ b/src/main/java/com/android/tools/r8/shaking/DependentMinimumKeepInfoCollection.java
@@ -152,10 +152,14 @@
     DependentMinimumKeepInfoCollection rewrittenDependentMinimumKeepInfo =
         new DependentMinimumKeepInfoCollection();
     forEach(
-        (preconditionEvent, minimumKeepInfo) ->
+        (preconditionEvent, minimumKeepInfo) -> {
+          EnqueuerEvent rewrittenPreconditionEvent = preconditionEvent.rewrittenWithLens(graphLens);
+          if (!rewrittenPreconditionEvent.isNoSuchEvent()) {
             rewrittenDependentMinimumKeepInfo
-                .getOrCreateMinimumKeepInfoFor(preconditionEvent.rewrittenWithLens(graphLens))
-                .merge(minimumKeepInfo.rewrittenWithLens(graphLens)));
+                .getOrCreateMinimumKeepInfoFor(rewrittenPreconditionEvent)
+                .merge(minimumKeepInfo.rewrittenWithLens(graphLens));
+          }
+        });
     return rewrittenDependentMinimumKeepInfo;
   }
 }
diff --git a/src/main/java/com/android/tools/r8/shaking/EnqueuerEvent.java b/src/main/java/com/android/tools/r8/shaking/EnqueuerEvent.java
index ee2b7d0..8a3439d 100644
--- a/src/main/java/com/android/tools/r8/shaking/EnqueuerEvent.java
+++ b/src/main/java/com/android/tools/r8/shaking/EnqueuerEvent.java
@@ -16,6 +16,10 @@
     return null;
   }
 
+  public boolean isNoSuchEvent() {
+    return false;
+  }
+
   public boolean isClassEvent() {
     return false;
   }
@@ -46,6 +50,27 @@
 
   public abstract EnqueuerEvent rewrittenWithLens(GraphLens lens);
 
+  public static class NoSuchEnqueuerEvent extends EnqueuerEvent {
+
+    private static final NoSuchEnqueuerEvent INSTANCE = new NoSuchEnqueuerEvent();
+
+    private NoSuchEnqueuerEvent() {}
+
+    public static NoSuchEnqueuerEvent get() {
+      return INSTANCE;
+    }
+
+    @Override
+    public boolean isNoSuchEvent() {
+      return true;
+    }
+
+    @Override
+    public EnqueuerEvent rewrittenWithLens(GraphLens lens) {
+      return this;
+    }
+  }
+
   public abstract static class ClassEnqueuerEvent extends EnqueuerEvent {
 
     private final DexType clazz;
@@ -96,7 +121,11 @@
 
     @Override
     public EnqueuerEvent rewrittenWithLens(GraphLens lens) {
-      return new LiveClassEnqueuerEvent(lens.lookupType(getType()));
+      DexType rewrittenType = lens.lookupType(getType());
+      if (rewrittenType.isIntType()) {
+        return NoSuchEnqueuerEvent.get();
+      }
+      return new LiveClassEnqueuerEvent(rewrittenType);
     }
 
     @Override
@@ -139,7 +168,11 @@
 
     @Override
     public EnqueuerEvent rewrittenWithLens(GraphLens lens) {
-      return new InstantiatedClassEnqueuerEvent(lens.lookupType(getType()));
+      DexType rewrittenType = lens.lookupType(getType());
+      if (rewrittenType.isIntType()) {
+        return NoSuchEnqueuerEvent.get();
+      }
+      return new InstantiatedClassEnqueuerEvent(rewrittenType);
     }
 
     @Override
diff --git a/src/main/java/com/android/tools/r8/shaking/RootSetUtils.java b/src/main/java/com/android/tools/r8/shaking/RootSetUtils.java
index 2832262..4555f23 100644
--- a/src/main/java/com/android/tools/r8/shaking/RootSetUtils.java
+++ b/src/main/java/com/android/tools/r8/shaking/RootSetUtils.java
@@ -1823,6 +1823,35 @@
       }
     }
 
+    public RootSet rewrittenWithLens(GraphLens graphLens) {
+      if (graphLens.isIdentityLens()) {
+        return this;
+      }
+      return new RootSet(
+          getDependentMinimumKeepInfo().rewrittenWithLens(graphLens),
+          reasonAsked,
+          alwaysInline,
+          neverInlineDueToSingleCaller,
+          bypassClinitForInlining,
+          whyAreYouNotInlining,
+          reprocess,
+          neverReprocess,
+          alwaysClassInline,
+          neverClassInline,
+          noUnusedInterfaceRemoval,
+          noVerticalClassMerging,
+          noHorizontalClassMerging,
+          neverPropagateValue,
+          mayHaveSideEffects,
+          noSideEffects,
+          assumedValues,
+          dependentKeepClassCompatRule,
+          identifierNameStrings,
+          ifRules,
+          delayedRootSetActionItems,
+          pendingMethodMoveInverse);
+    }
+
     void shouldNotBeMinified(ProgramDefinition definition) {
       getDependentMinimumKeepInfo()
           .getOrCreateUnconditionalMinimumKeepInfoFor(definition.getReference())
@@ -2133,6 +2162,7 @@
       // Do nothing.
     }
 
+    @Override
     public MainDexRootSet rewrittenWithLens(GraphLens graphLens) {
       if (graphLens.isIdentityLens()) {
         return this;
diff --git a/src/test/java/com/android/tools/r8/cf/InlineCmpDoubleTest.java b/src/test/java/com/android/tools/r8/cf/InlineCmpDoubleTest.java
index 1db3ee4..36e6189 100644
--- a/src/test/java/com/android/tools/r8/cf/InlineCmpDoubleTest.java
+++ b/src/test/java/com/android/tools/r8/cf/InlineCmpDoubleTest.java
@@ -5,6 +5,7 @@
 
 import static org.junit.Assert.assertEquals;
 
+import com.android.tools.r8.KeepUnusedReturnValue;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.naming.MemberNaming.MethodSignature;
@@ -42,6 +43,7 @@
         .addProgramClasses(TestClass.class)
         .addKeepMainRule(TestClass.class)
         .addOptionsModification(options -> options.inlinerOptions().enableInlining = enableInlining)
+        .enableKeepUnusedReturnValueAnnotations()
         .setMinApi(parameters.getApiLevel())
         .compile()
         .inspect(this::inspect)
@@ -66,6 +68,7 @@
       inlineMe(x + 41);
     }
 
+    @KeepUnusedReturnValue
     public static int inlineMe(int x) {
       // Side effect to ensure that the invocation is not removed simply because the method does not
       // have any side effects.
diff --git a/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerTest.java b/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerTest.java
index b056931..bfd9f97 100644
--- a/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerTest.java
@@ -15,6 +15,7 @@
 import static org.junit.Assume.assumeTrue;
 
 import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.KeepUnusedReturnValue;
 import com.android.tools.r8.ProgramResourceProvider;
 import com.android.tools.r8.R8FullTestBuilder;
 import com.android.tools.r8.R8TestCompileResult;
@@ -184,9 +185,12 @@
         "return");
 
     // Add two methods with the same name that have return types A[] and B[], respectively.
+    classBuilder.addRuntimeInvisibleAnnotation(KeepUnusedReturnValue.class.getTypeName());
     classBuilder.addStaticMethod(
         "method", ImmutableList.of(), "[Lclassmerging/A;",
         ".limit stack 1", ".limit locals 1", "iconst_0", "anewarray classmerging/A", "areturn");
+
+    classBuilder.addRuntimeInvisibleAnnotation(KeepUnusedReturnValue.class.getTypeName());
     classBuilder.addStaticMethod(
         "method", ImmutableList.of(), "[Lclassmerging/B;",
         ".limit stack 1", ".limit locals 1", "iconst_0", "anewarray classmerging/B", "areturn");
@@ -209,7 +213,8 @@
                 "-neverinline class " + main + " {",
                 "  static classmerging.A[] method(...);",
                 "  static classmerging.B[] method(...);",
-                "}"),
+                "}")
+            .enableKeepUnusedReturnValueAnnotations(),
         main,
         jasminBuilder.build(),
         preservedClassNames::contains);
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/R8InliningTest.java b/src/test/java/com/android/tools/r8/ir/optimize/R8InliningTest.java
index bded17f..b7a3474 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/R8InliningTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/R8InliningTest.java
@@ -328,7 +328,7 @@
 
     m =
         clazz.method(
-            "int",
+            allowAccessModification ? "void" : "int",
             "notInlinable",
             ImmutableList.of("inlining." + (allowAccessModification ? "B" : "A")));
     assertCounters(INLINABLE, NEVER_INLINABLE, countInvokes(inspector, m));
@@ -344,12 +344,12 @@
 
     m =
         clazz.method(
-            "int", "notInlinableOnThrow", ImmutableList.of("java.lang.IllegalArgumentException"));
+            "void", "notInlinableOnThrow", ImmutableList.of("java.lang.IllegalArgumentException"));
     assertCounters(ALWAYS_INLINABLE, NEVER_INLINABLE, countInvokes(inspector, m));
 
     m =
         clazz.method(
-            "int",
+            "void",
             "notInlinableDueToMissingNpeBeforeThrow",
             ImmutableList.of("java.lang.IllegalArgumentException"));
     assertCounters(ALWAYS_INLINABLE, NEVER_INLINABLE * 2, countInvokes(inspector, m));
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/InvokeStaticWithNullOutvalueTest.java b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/InvokeStaticWithNullOutvalueTest.java
index 6750562..9dfcb42 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/InvokeStaticWithNullOutvalueTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/InvokeStaticWithNullOutvalueTest.java
@@ -8,6 +8,7 @@
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertTrue;
 
+import com.android.tools.r8.KeepUnusedReturnValue;
 import com.android.tools.r8.NeverClassInline;
 import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.NeverPropagateValue;
@@ -46,6 +47,7 @@
         .addInnerClasses(InvokeStaticWithNullOutvalueTest.class)
         .addKeepMainRule(MAIN)
         .enableInliningAnnotations()
+        .enableKeepUnusedReturnValueAnnotations()
         .enableMemberValuePropagationAnnotations()
         .enableNeverClassInliningAnnotations()
         .enableNoHorizontalClassMergingAnnotations()
@@ -85,6 +87,7 @@
 
     @NoHorizontalClassMerging
     static class Companion {
+      @KeepUnusedReturnValue
       @NeverInline
       @NeverPropagateValue
       private static Object boo() {
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/uninstantiatedtypes/VoidReturnTypeRewritingTest.java b/src/test/java/com/android/tools/r8/ir/optimize/uninstantiatedtypes/VoidReturnTypeRewritingTest.java
index f8dd8d9..16c8f61 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/uninstantiatedtypes/VoidReturnTypeRewritingTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/uninstantiatedtypes/VoidReturnTypeRewritingTest.java
@@ -45,7 +45,12 @@
             "SubFactory.createVirtual() -> null",
             "SubSubFactory.createVirtual() -> null");
 
-    testForJvm().addTestClasspath().run(TestClass.class).assertSuccessWithOutput(expected);
+    if (parameters.isCfRuntime()) {
+      testForJvm()
+          .addTestClasspath()
+          .run(parameters.getRuntime(), TestClass.class)
+          .assertSuccessWithOutput(expected);
+    }
 
     CodeInspector inspector =
         testForR8(parameters.getBackend())
@@ -55,8 +60,8 @@
             .enableNoMethodStaticizingAnnotations()
             .enableNoVerticalClassMergingAnnotations()
             .enableNoHorizontalClassMergingAnnotations()
-            .addKeepRules("-dontobfuscate")
             .addOptionsModification(options -> options.enableClassInlining = false)
+            .noMinification()
             .setMinApi(parameters.getApiLevel())
             .run(parameters.getRuntime(), TestClass.class)
             .assertSuccessWithOutput(expected)
@@ -66,16 +71,16 @@
     MethodSubject createStaticMethodSubject =
         factoryClassSubject.uniqueMethodWithName("createStatic");
     assertThat(createStaticMethodSubject, isPresent());
-    assertTrue(createStaticMethodSubject.getMethod().getReference().proto.returnType.isVoidType());
+    assertTrue(createStaticMethodSubject.getMethod().getReturnType().isVoidType());
     MethodSubject createVirtualMethodSubject =
         factoryClassSubject.uniqueMethodWithName("createVirtual");
     assertThat(createVirtualMethodSubject, isPresent());
-    assertTrue(createVirtualMethodSubject.getMethod().getReference().proto.returnType.isVoidType());
+    assertTrue(createVirtualMethodSubject.getMethod().getReturnType().isVoidType());
 
     createVirtualMethodSubject =
         inspector.clazz(SubFactory.class).uniqueMethodWithName("createVirtual");
     assertThat(createVirtualMethodSubject, isPresent());
-    assertTrue(createVirtualMethodSubject.getMethod().getReference().proto.returnType.isVoidType());
+    assertTrue(createVirtualMethodSubject.getMethod().getReturnType().isVoidType());
 
     ClassSubject subSubFactoryClassSubject = inspector.clazz(SubSubFactory.class);
     assertThat(subSubFactoryClassSubject.method("void", "createVirtual"), isPresent());
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/unusedarguments/UnusedArgumentRemovalWithOverridingTest.java b/src/test/java/com/android/tools/r8/ir/optimize/unusedarguments/UnusedArgumentRemovalWithOverridingTest.java
index 58b052c..44c15e6 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/unusedarguments/UnusedArgumentRemovalWithOverridingTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/unusedarguments/UnusedArgumentRemovalWithOverridingTest.java
@@ -83,7 +83,7 @@
 
     @NeverInline
     public String greeting(String used) {
-      return used;
+      return System.currentTimeMillis() >= 0 ? used : null;
     }
   }
 
diff --git a/src/test/java/com/android/tools/r8/naming/retrace/RetraceInlineeWithNoSuchMethodErrorTest.java b/src/test/java/com/android/tools/r8/naming/retrace/RetraceInlineeWithNoSuchMethodErrorTest.java
index 1c2f06d..f3718cd 100644
--- a/src/test/java/com/android/tools/r8/naming/retrace/RetraceInlineeWithNoSuchMethodErrorTest.java
+++ b/src/test/java/com/android/tools/r8/naming/retrace/RetraceInlineeWithNoSuchMethodErrorTest.java
@@ -7,6 +7,7 @@
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static org.hamcrest.MatcherAssert.assertThat;
 
+import com.android.tools.r8.KeepUnusedReturnValue;
 import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.R8FullTestBuilder;
 import com.android.tools.r8.R8TestRunResult;
@@ -78,6 +79,7 @@
             .addKeepAttributeSourceFile()
             .setMinApi(parameters.getApiLevel())
             .enableInliningAnnotations()
+            .enableKeepUnusedReturnValueAnnotations()
             .enableExperimentalMapFileVersion();
     R8TestRunResult runResult;
     if (throwReceiverNpe) {
@@ -112,6 +114,7 @@
       throw new RuntimeException("Will be removed");
     }
 
+    @KeepUnusedReturnValue
     Object inlinable() {
       return foo();
     }
diff --git a/src/test/java/com/android/tools/r8/naming/retraceproguard/VerticalClassMergingRetraceTest.java b/src/test/java/com/android/tools/r8/naming/retraceproguard/VerticalClassMergingRetraceTest.java
index bb37a8a..5403673 100644
--- a/src/test/java/com/android/tools/r8/naming/retraceproguard/VerticalClassMergingRetraceTest.java
+++ b/src/test/java/com/android/tools/r8/naming/retraceproguard/VerticalClassMergingRetraceTest.java
@@ -25,8 +25,6 @@
 import com.google.common.collect.ImmutableList;
 import java.io.IOException;
 import java.util.Collection;
-import java.util.HashSet;
-import java.util.Set;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -34,7 +32,6 @@
 
 @RunWith(Parameterized.class)
 public class VerticalClassMergingRetraceTest extends RetraceTestBase {
-  private Set<StackTraceLine> haveSeenLines = new HashSet<>();
 
   @Parameters(name = "{0}, mode: {1}, compat: {2}")
   public static Collection<Object[]> data() {
@@ -136,7 +133,6 @@
   public void testNoLineNumberTable() throws Exception {
     assumeTrue(compat);
     assumeTrue(parameters.isDexRuntime());
-    haveSeenLines.clear();
     Box<MethodSubject> syntheticMethod = new Box<>();
     runTest(
         ImmutableList.of(),
@@ -171,8 +167,11 @@
   // Will be merged down, and represented as:
   //     java.lang.String ...ResourceWrapper.foo() -> a
   @NeverInline
-  String foo() {
-    throw null;
+  String foo(boolean doThrow) {
+    if (doThrow) {
+      throw null;
+    }
+    return System.currentTimeMillis() > 0 ? "arg" : null;
   }
 }
 
@@ -181,6 +180,7 @@
 class MainApp {
   public static void main(String[] args) {
     TintResources t = new TintResources();
-    System.out.println(t.foo());
+    boolean doThrow = System.currentTimeMillis() > 0;
+    System.out.println(t.foo(doThrow));
   }
 }
diff --git a/src/test/java/com/android/tools/r8/neverreturnsnormally/NeverReturnsNormallyTest.java b/src/test/java/com/android/tools/r8/neverreturnsnormally/NeverReturnsNormallyTest.java
index aedd6a4..173990f 100644
--- a/src/test/java/com/android/tools/r8/neverreturnsnormally/NeverReturnsNormallyTest.java
+++ b/src/test/java/com/android/tools/r8/neverreturnsnormally/NeverReturnsNormallyTest.java
@@ -13,7 +13,6 @@
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.ToolHelper;
-import com.android.tools.r8.cf.code.CfStackInstruction.Opcode;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.BooleanUtils;
@@ -201,11 +200,7 @@
           insn instanceof DexInstructionSubject && ((DexInstructionSubject) insn).isConst4());
     } else {
       assertTrue(insn instanceof CfInstructionSubject);
-      assertTrue(((CfInstructionSubject) insn).isStackInstruction(Opcode.Pop));
-      assertTrue(instructions.hasNext());
-      insn = instructions.next();
-      assertTrue(insn instanceof CfInstructionSubject);
-      assertTrue(((CfInstructionSubject) insn).isConstNull());
+      assertTrue(insn.isConstNull());
     }
     assertTrue(nextInstruction(instructions).isThrow());
     assertFalse(instructions.hasNext());
diff --git a/src/test/java/com/android/tools/r8/optimize/argumentpropagation/MonomorphicMethodWithUnusedReturnValueTest.java b/src/test/java/com/android/tools/r8/optimize/argumentpropagation/MonomorphicMethodWithUnusedReturnValueTest.java
new file mode 100644
index 0000000..d7130ef
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/optimize/argumentpropagation/MonomorphicMethodWithUnusedReturnValueTest.java
@@ -0,0 +1,73 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.optimize.argumentpropagation;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+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.utils.codeinspector.MethodSubject;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class MonomorphicMethodWithUnusedReturnValueTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .enableInliningAnnotations()
+        // TODO(b/173398086): uniqueMethodWithName() does not work with proto changes.
+        .noMinification()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspect(
+            inspector -> {
+              // The test() method has been changed to have return type void.
+              MethodSubject testMethodSubject =
+                  inspector.clazz(Main.class).uniqueMethodWithName("test");
+              assertThat(testMethodSubject, isPresent());
+              assertTrue(testMethodSubject.getProgramMethod().getReturnType().isVoidType());
+
+              // The class ReturnType has been removed by tree shaking.
+              assertThat(inspector.clazz(ReturnType.class), isAbsent());
+            })
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("create()");
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      test();
+    }
+
+    @NeverInline
+    static ReturnType test() {
+      System.out.println("create()");
+      return new ReturnType();
+    }
+  }
+
+  static class ReturnType {}
+}
diff --git a/src/test/java/com/android/tools/r8/optimize/argumentpropagation/PolymorphicMethodWithUnusedReturnValueTest.java b/src/test/java/com/android/tools/r8/optimize/argumentpropagation/PolymorphicMethodWithUnusedReturnValueTest.java
new file mode 100644
index 0000000..e8007c3
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/optimize/argumentpropagation/PolymorphicMethodWithUnusedReturnValueTest.java
@@ -0,0 +1,91 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.optimize.argumentpropagation;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.NoHorizontalClassMerging;
+import com.android.tools.r8.NoVerticalClassMerging;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class PolymorphicMethodWithUnusedReturnValueTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .enableNoHorizontalClassMergingAnnotations()
+        .enableNoVerticalClassMergingAnnotations()
+        // TODO(b/173398086): uniqueMethodWithName() does not work with proto changes.
+        .noMinification()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspect(
+            inspector -> {
+              // The test() methods have been changed to have return type void.
+              for (Class<?> clazz : new Class<?>[] {A.class, B.class}) {
+                MethodSubject testMethodSubject = inspector.clazz(clazz).uniqueMethodWithName("m");
+                assertThat(testMethodSubject, isPresent());
+                assertTrue(testMethodSubject.getProgramMethod().getReturnType().isVoidType());
+              }
+
+              // The class ReturnType has been removed by tree shaking.
+              assertThat(inspector.clazz(ReturnType.class), isAbsent());
+            })
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("A.m()");
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      A a = System.currentTimeMillis() >= 0 ? new A() : new B();
+      a.m();
+    }
+  }
+
+  @NoHorizontalClassMerging
+  @NoVerticalClassMerging
+  static class A {
+
+    ReturnType m() {
+      System.out.println("A.m()");
+      return new ReturnType();
+    }
+  }
+
+  static class B extends A {
+
+    @Override
+    ReturnType m() {
+      System.out.println("B.m()");
+      return new ReturnType();
+    }
+  }
+
+  @NoHorizontalClassMerging
+  static class ReturnType {}
+}
diff --git a/src/test/java/com/android/tools/r8/shaking/annotations/KeepDisallowAnnotationRemovalAllowOptimizationTest.java b/src/test/java/com/android/tools/r8/shaking/annotations/KeepDisallowAnnotationRemovalAllowOptimizationTest.java
index 8fa7bc1..62bcf76 100644
--- a/src/test/java/com/android/tools/r8/shaking/annotations/KeepDisallowAnnotationRemovalAllowOptimizationTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/annotations/KeepDisallowAnnotationRemovalAllowOptimizationTest.java
@@ -11,6 +11,7 @@
 import static org.hamcrest.CoreMatchers.not;
 import static org.hamcrest.MatcherAssert.assertThat;
 
+import com.android.tools.r8.KeepUnusedReturnValue;
 import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
@@ -53,6 +54,7 @@
         // In compatibility mode the rule above is a no-op.
         .allowUnusedProguardConfigurationRules(enableCompatibilityMode)
         .enableInliningAnnotations()
+        .enableKeepUnusedReturnValueAnnotations()
         .setMinApi(parameters.getApiLevel())
         .compile()
         .inspect(
@@ -83,6 +85,7 @@
       }
     }
 
+    @KeepUnusedReturnValue
     @NeverInline
     static Object getNonNull() {
       System.out.println("getNonNull()");