Replace Class.forName() with const-class in presence of clinits

Change-Id: I1a0b7727dde7e59c9384b489a55f409c91d2f739
diff --git a/src/main/java/com/android/tools/r8/features/ClassToFeatureSplitMap.java b/src/main/java/com/android/tools/r8/features/ClassToFeatureSplitMap.java
index 6ba885d..519bd27 100644
--- a/src/main/java/com/android/tools/r8/features/ClassToFeatureSplitMap.java
+++ b/src/main/java/com/android/tools/r8/features/ClassToFeatureSplitMap.java
@@ -14,6 +14,7 @@
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.GraphLens;
+import com.android.tools.r8.graph.ProgramDefinition;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.utils.InternalOptions;
 import com.google.common.collect.Sets;
@@ -76,8 +77,8 @@
     return result;
   }
 
-  public FeatureSplit getFeatureSplit(DexProgramClass clazz) {
-    return getFeatureSplit(clazz.getType());
+  public FeatureSplit getFeatureSplit(ProgramDefinition clazz) {
+    return getFeatureSplit(clazz.getContextType());
   }
 
   public FeatureSplit getFeatureSplit(DexType type) {
@@ -92,7 +93,7 @@
     return getFeatureSplit(clazz).isBase();
   }
 
-  public boolean isInBaseOrSameFeatureAs(DexProgramClass clazz, DexProgramClass context) {
+  public boolean isInBaseOrSameFeatureAs(DexProgramClass clazz, ProgramDefinition context) {
     FeatureSplit split = getFeatureSplit(clazz);
     return split.isBase() || split == getFeatureSplit(context);
   }
diff --git a/src/main/java/com/android/tools/r8/graph/AccessControl.java b/src/main/java/com/android/tools/r8/graph/AccessControl.java
index ed19598..f872a56 100644
--- a/src/main/java/com/android/tools/r8/graph/AccessControl.java
+++ b/src/main/java/com/android/tools/r8/graph/AccessControl.java
@@ -27,8 +27,7 @@
       return OptionalBool.FALSE;
     }
     if (clazz.isProgramClass()
-        && !classToFeatureSplitMap.isInBaseOrSameFeatureAs(
-            clazz.asProgramClass(), context.getContextClass())) {
+        && !classToFeatureSplitMap.isInBaseOrSameFeatureAs(clazz.asProgramClass(), context)) {
       return OptionalBool.UNKNOWN;
     }
     return OptionalBool.TRUE;
diff --git a/src/main/java/com/android/tools/r8/graph/DexClass.java b/src/main/java/com/android/tools/r8/graph/DexClass.java
index e827a25..c49b888 100644
--- a/src/main/java/com/android/tools/r8/graph/DexClass.java
+++ b/src/main/java/com/android/tools/r8/graph/DexClass.java
@@ -9,6 +9,7 @@
 import com.android.tools.r8.graph.GenericSignature.ClassSignature;
 import com.android.tools.r8.kotlin.KotlinClassLevelInfo;
 import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.OptionalBool;
 import com.android.tools.r8.utils.TraversalContinuation;
@@ -659,6 +660,16 @@
         appView, Predicates.alwaysFalse(), Sets.newIdentityHashSet());
   }
 
+  public boolean classInitializationMayHaveSideEffectsInContext(
+      AppView<AppInfoWithLiveness> appView, ProgramDefinition context) {
+    return classInitializationMayHaveSideEffects(
+        appView,
+        // Types that are a super type of the current context are guaranteed to be initialized
+        // already.
+        type -> appView.appInfo().isSubtype(context.getContextType(), type),
+        Sets.newIdentityHashSet());
+  }
+
   public boolean classInitializationMayHaveSideEffects(
       AppView<?> appView, Predicate<DexType> ignore, Set<DexType> seen) {
     if (!seen.add(type)) {
diff --git a/src/main/java/com/android/tools/r8/ir/code/BasicBlockInstructionListIterator.java b/src/main/java/com/android/tools/r8/ir/code/BasicBlockInstructionListIterator.java
index e3fa34e..a3f8421 100644
--- a/src/main/java/com/android/tools/r8/ir/code/BasicBlockInstructionListIterator.java
+++ b/src/main/java/com/android/tools/r8/ir/code/BasicBlockInstructionListIterator.java
@@ -4,11 +4,13 @@
 
 package com.android.tools.r8.ir.code;
 
+import static com.android.tools.r8.ir.analysis.type.Nullability.definitelyNotNull;
 import static com.android.tools.r8.ir.analysis.type.Nullability.maybeNull;
 import static com.android.tools.r8.ir.code.DominatorTree.Assumption.MAY_HAVE_UNREACHABLE_BLOCKS;
 
 import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DebugLocalInfo;
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.graph.DexType;
@@ -247,6 +249,19 @@
   }
 
   @Override
+  public void replaceCurrentInstructionWithConstClass(
+      AppView<?> appView, IRCode code, DexType type, DebugLocalInfo localInfo) {
+    if (current == null) {
+      throw new IllegalStateException();
+    }
+
+    TypeElement typeElement = TypeElement.classClassType(appView, definitelyNotNull());
+    Value value = code.createValue(typeElement, localInfo);
+    ConstClass constClass = new ConstClass(value, type);
+    replaceCurrentInstruction(constClass);
+  }
+
+  @Override
   public void replaceCurrentInstructionWithConstInt(IRCode code, int value) {
     if (current == null) {
       throw new IllegalStateException();
diff --git a/src/main/java/com/android/tools/r8/ir/code/IRCode.java b/src/main/java/com/android/tools/r8/ir/code/IRCode.java
index 90e6097..8e7236c 100644
--- a/src/main/java/com/android/tools/r8/ir/code/IRCode.java
+++ b/src/main/java/com/android/tools/r8/ir/code/IRCode.java
@@ -55,7 +55,7 @@
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
-public class IRCode {
+public class IRCode implements ValueFactory {
 
   private static final int MAX_MARKING_COLOR = 0x40000000;
 
@@ -1097,12 +1097,9 @@
     return thisValue;
   }
 
-  public Value createValue(TypeElement typeLattice, DebugLocalInfo local) {
-    return new Value(valueNumberGenerator.next(), typeLattice, local);
-  }
-
-  public Value createValue(TypeElement typeLattice) {
-    return createValue(typeLattice, null);
+  @Override
+  public Value createValue(TypeElement type, DebugLocalInfo local) {
+    return new Value(valueNumberGenerator.next(), type, local);
   }
 
   public ConstNumber createNumberConstant(long value, TypeElement type) {
diff --git a/src/main/java/com/android/tools/r8/ir/code/IRCodeInstructionListIterator.java b/src/main/java/com/android/tools/r8/ir/code/IRCodeInstructionListIterator.java
index bec911e..cb33b18 100644
--- a/src/main/java/com/android/tools/r8/ir/code/IRCodeInstructionListIterator.java
+++ b/src/main/java/com/android/tools/r8/ir/code/IRCodeInstructionListIterator.java
@@ -7,6 +7,7 @@
 import com.android.tools.r8.errors.Unimplemented;
 import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DebugLocalInfo;
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.graph.DexType;
@@ -41,6 +42,12 @@
   }
 
   @Override
+  public void replaceCurrentInstructionWithConstClass(
+      AppView<?> appView, IRCode code, DexType type, DebugLocalInfo localInfo) {
+    instructionIterator.replaceCurrentInstructionWithConstClass(appView, code, type, localInfo);
+  }
+
+  @Override
   public void replaceCurrentInstructionWithConstInt(IRCode code, int value) {
     instructionIterator.replaceCurrentInstructionWithConstInt(code, value);
   }
diff --git a/src/main/java/com/android/tools/r8/ir/code/InitClass.java b/src/main/java/com/android/tools/r8/ir/code/InitClass.java
index 61a1882..c878d9a 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InitClass.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InitClass.java
@@ -36,6 +36,10 @@
     this.clazz = clazz;
   }
 
+  public static Builder builder() {
+    return new Builder();
+  }
+
   public DexType getClassValue() {
     return clazz;
   }
@@ -175,4 +179,26 @@
   public String toString() {
     return super.toString() + "; " + clazz.toSourceString();
   }
+
+  public static class Builder extends BuilderBase<Builder, InitClass> {
+
+    private DexType type;
+
+    private Builder() {}
+
+    public Builder setType(DexType type) {
+      this.type = type;
+      return this;
+    }
+
+    @Override
+    public InitClass build() {
+      return amend(new InitClass(outValue, type));
+    }
+
+    @Override
+    public Builder self() {
+      return this;
+    }
+  }
 }
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 9711ff2..7fc0d77 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
@@ -1546,6 +1546,7 @@
 
   public abstract static class BuilderBase<B extends BuilderBase<B, I>, I extends Instruction> {
 
+    protected Value outValue;
     protected Position position;
 
     public abstract I build();
@@ -1559,6 +1560,15 @@
       return instruction;
     }
 
+    public B setOutValue(Value outValue) {
+      this.outValue = outValue;
+      return self();
+    }
+
+    public B setFreshOutValue(ValueFactory factory, TypeElement type) {
+      return setOutValue(factory.createValue(type));
+    }
+
     public B setPosition(Position position) {
       this.position = position;
       return self();
diff --git a/src/main/java/com/android/tools/r8/ir/code/InstructionListIterator.java b/src/main/java/com/android/tools/r8/ir/code/InstructionListIterator.java
index 2790e66..792018b 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InstructionListIterator.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InstructionListIterator.java
@@ -7,6 +7,7 @@
 import com.android.tools.r8.errors.Unimplemented;
 import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DebugLocalInfo;
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.graph.DexType;
@@ -19,6 +20,12 @@
 public interface InstructionListIterator
     extends InstructionIterator, ListIterator<Instruction>, PreviousUntilIterator<Instruction> {
 
+  default void addBefore(Instruction instruction) {
+    previous();
+    add(instruction);
+    next();
+  }
+
   /** See {@link #replaceCurrentInstruction(Instruction, Set)}. */
   default void replaceCurrentInstruction(Instruction newInstruction) {
     replaceCurrentInstruction(newInstruction, null);
@@ -84,6 +91,9 @@
 
   Value insertConstStringInstruction(AppView<?> appView, IRCode code, DexString value);
 
+  void replaceCurrentInstructionWithConstClass(
+      AppView<?> appView, IRCode code, DexType type, DebugLocalInfo localInfo);
+
   void replaceCurrentInstructionWithConstInt(IRCode code, int value);
 
   void replaceCurrentInstructionWithStaticGet(
diff --git a/src/main/java/com/android/tools/r8/ir/code/InvokeStatic.java b/src/main/java/com/android/tools/r8/ir/code/InvokeStatic.java
index 47478df..9bae360 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InvokeStatic.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InvokeStatic.java
@@ -244,7 +244,6 @@
   public static class Builder extends BuilderBase<Builder, InvokeStatic> {
 
     private DexMethod method;
-    private Value outValue;
     private List<Value> arguments = Collections.emptyList();
 
     public Builder setArguments(List<Value> arguments) {
@@ -266,19 +265,13 @@
       return setMethod(method.getReference());
     }
 
-    public Builder setOutValue(Value outValue) {
-      this.outValue = outValue;
-      return this;
-    }
-
     @Override
     public InvokeStatic build() {
       assert arguments != null;
       assert method != null;
       assert method.getArity() == arguments.size();
       assert outValue == null || !method.getReturnType().isVoidType();
-      InvokeStatic result = new InvokeStatic(method, outValue, arguments);
-      return amend(result);
+      return amend(new InvokeStatic(method, outValue, arguments));
     }
 
     @Override
diff --git a/src/main/java/com/android/tools/r8/ir/code/LinearFlowInstructionListIterator.java b/src/main/java/com/android/tools/r8/ir/code/LinearFlowInstructionListIterator.java
index d851eb9..0dd2895 100644
--- a/src/main/java/com/android/tools/r8/ir/code/LinearFlowInstructionListIterator.java
+++ b/src/main/java/com/android/tools/r8/ir/code/LinearFlowInstructionListIterator.java
@@ -6,6 +6,7 @@
 
 import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DebugLocalInfo;
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.graph.DexType;
@@ -54,6 +55,12 @@
   }
 
   @Override
+  public void replaceCurrentInstructionWithConstClass(
+      AppView<?> appView, IRCode code, DexType type, DebugLocalInfo localInfo) {
+    currentBlockIterator.replaceCurrentInstructionWithConstClass(appView, code, type, localInfo);
+  }
+
+  @Override
   public void replaceCurrentInstructionWithConstInt(IRCode code, int value) {
     currentBlockIterator.replaceCurrentInstructionWithConstInt(code, value);
   }
diff --git a/src/main/java/com/android/tools/r8/ir/code/ValueFactory.java b/src/main/java/com/android/tools/r8/ir/code/ValueFactory.java
new file mode 100644
index 0000000..ef1b16c
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/code/ValueFactory.java
@@ -0,0 +1,17 @@
+// Copyright (c) 2020, 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.code;
+
+import com.android.tools.r8.graph.DebugLocalInfo;
+import com.android.tools.r8.ir.analysis.type.TypeElement;
+
+public interface ValueFactory {
+
+  default Value createValue(TypeElement type) {
+    return createValue(type, null);
+  }
+
+  Value createValue(TypeElement type, DebugLocalInfo localInfo);
+}
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 324ad93..b2c5888 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
@@ -111,7 +111,6 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
-import java.util.Set;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.atomic.AtomicBoolean;
@@ -124,7 +123,7 @@
   private static final int PEEPHOLE_OPTIMIZATION_PASSES = 2;
 
   public final AppView<?> appView;
-  public final Set<DexType> mainDexClasses;
+  public final MainDexTracingResult mainDexClasses;
 
   private final Timing timing;
   private final Outliner outliner;
@@ -196,7 +195,7 @@
     this.appView = appView;
     this.options = appView.options();
     this.printer = printer;
-    this.mainDexClasses = mainDexClasses.getClasses();
+    this.mainDexClasses = mainDexClasses;
     this.codeRewriter = new CodeRewriter(appView, this);
     this.constantCanonicalizer = new ConstantCanonicalizer(codeRewriter);
     this.classInitializerDefaultsOptimization =
@@ -1305,7 +1304,8 @@
     if (appView.appInfo().hasLiveness()) {
       // Reflection optimization 1. getClass() / forName() -> const-class
       timing.begin("Rewrite to const class");
-      ReflectionOptimizer.rewriteGetClassOrForNameToConstClass(appView.withLiveness(), code);
+      ReflectionOptimizer.rewriteGetClassOrForNameToConstClass(
+          appView.withLiveness(), code, mainDexClasses);
       timing.end();
     }
 
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/ReflectionOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/ReflectionOptimizer.java
index 4e89840..cc5e7ed 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/ReflectionOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/ReflectionOptimizer.java
@@ -4,8 +4,9 @@
 package com.android.tools.r8.ir.optimize;
 
 import static com.android.tools.r8.graph.DexProgramClass.asProgramClassOrNull;
-import static com.android.tools.r8.ir.analysis.type.Nullability.definitelyNotNull;
 
+import com.android.tools.r8.features.ClassToFeatureSplitMap;
+import com.android.tools.r8.graph.AccessControl;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexItemFactory;
@@ -13,36 +14,35 @@
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.ProgramMethod;
-import com.android.tools.r8.ir.analysis.ClassInitializationAnalysis;
 import com.android.tools.r8.ir.analysis.type.TypeAnalysis;
 import com.android.tools.r8.ir.analysis.type.TypeElement;
 import com.android.tools.r8.ir.code.BasicBlock;
-import com.android.tools.r8.ir.code.ConstClass;
 import com.android.tools.r8.ir.code.IRCode;
-import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.ir.code.InitClass;
 import com.android.tools.r8.ir.code.InstructionListIterator;
+import com.android.tools.r8.ir.code.InvokeMethod;
 import com.android.tools.r8.ir.code.InvokeStatic;
 import com.android.tools.r8.ir.code.InvokeVirtual;
 import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.shaking.MainDexTracingResult;
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.google.common.collect.Sets;
 import java.util.Set;
+import java.util.function.BiConsumer;
 
 public class ReflectionOptimizer {
 
   // Rewrite getClass() to const-class if the type of the given instance is effectively final.
   // Rewrite forName() to const-class if the type is resolvable, accessible and already initialized.
   public static void rewriteGetClassOrForNameToConstClass(
-      AppView<AppInfoWithLiveness> appView, IRCode code) {
+      AppView<AppInfoWithLiveness> appView, IRCode code, MainDexTracingResult mainDexClasses) {
     if (!appView.appInfo().canUseConstClassInstructions(appView.options())) {
       return;
     }
     Set<Value> affectedValues = Sets.newIdentityHashSet();
     ProgramMethod context = code.context();
-    ClassInitializationAnalysis classInitializationAnalysis =
-        new ClassInitializationAnalysis(appView, code);
     for (BasicBlock block : code.blocks) {
       // Conservatively bail out if the containing block has catch handlers.
       // TODO(b/118509730): unless join of all catch types is ClassNotFoundException ?
@@ -51,32 +51,28 @@
       }
       InstructionListIterator it = block.listIterator(code);
       while (it.hasNext()) {
-        Instruction current = it.next();
-        if (!current.hasOutValue() || !current.outValue().isUsed()) {
+        InvokeMethod invoke = it.nextUntil(x -> x.isInvokeStatic() || x.isInvokeVirtual());
+        if (invoke == null) {
           continue;
         }
-        DexType type = null;
-        if (current.isInvokeVirtual()) {
-          type = getTypeForGetClass(appView, context, current.asInvokeVirtual());
-        } else if (current.isInvokeStatic()) {
-          type = getTypeForClassForName(
-              appView, classInitializationAnalysis, context, current.asInvokeStatic());
-        }
-        if (type != null) {
-          affectedValues.addAll(current.outValue().affectedValues());
-          TypeElement typeLattice = TypeElement.classClassType(appView, definitelyNotNull());
-          Value value = code.createValue(typeLattice, current.getLocalInfo());
-          ConstClass constClass = new ConstClass(value, type);
-          it.replaceCurrentInstruction(constClass);
-          if (appView.options().isGeneratingClassFiles()) {
-            code.method()
-                .upgradeClassFileVersion(
-                    appView.options().requiredCfVersionForConstClassInstructions());
-          }
+
+        if (invoke.isInvokeStatic()) {
+          applyTypeForClassForNameTo(
+              appView,
+              context,
+              invoke.asInvokeStatic(),
+              rewriteSingleGetClassOrForNameToConstClass(
+                  appView, code, it, invoke, affectedValues, mainDexClasses));
+        } else {
+          applyTypeForGetClassTo(
+              appView,
+              context,
+              invoke.asInvokeVirtual(),
+              rewriteSingleGetClassOrForNameToConstClass(
+                  appView, code, it, invoke, affectedValues, mainDexClasses));
         }
       }
     }
-    classInitializationAnalysis.finish();
     // Newly introduced const-class is not null, and thus propagate that information.
     if (!affectedValues.isEmpty()) {
       new TypeAnalysis(appView).narrowing(affectedValues);
@@ -84,24 +80,79 @@
     assert code.isConsistentSSA();
   }
 
-  private static DexType getTypeForGetClass(
-      AppView<AppInfoWithLiveness> appView, ProgramMethod context, InvokeVirtual invoke) {
+  private static BiConsumer<DexType, DexClass> rewriteSingleGetClassOrForNameToConstClass(
+      AppView<AppInfoWithLiveness> appView,
+      IRCode code,
+      InstructionListIterator instructionIterator,
+      InvokeMethod invoke,
+      Set<Value> affectedValues,
+      MainDexTracingResult mainDexClasses) {
+    return (type, baseClass) -> {
+      if (invoke.getInvokedMethod().match(appView.dexItemFactory().classMethods.forName)) {
+        // Bail-out if the optimization could increase the size of the main dex.
+        if (baseClass.isProgramClass()
+            && !mainDexClasses.canReferenceItemFromContextWithoutIncreasingMainDexSize(
+                baseClass.asProgramClass(), code.context())) {
+          return;
+        }
+
+        // We need to initialize the type if it may have observable side effects.
+        if (type.isClassType()
+            && baseClass.classInitializationMayHaveSideEffectsInContext(appView, code.context())) {
+          if (!baseClass.isProgramClass() || !appView.canUseInitClass()) {
+            // No way to trigger the class initialization of the given class without
+            // Class.forName(), so skip.
+            return;
+          }
+
+          instructionIterator.addBefore(
+              InitClass.builder()
+                  .setFreshOutValue(code, TypeElement.getInt())
+                  .setType(type)
+                  .setPosition(invoke)
+                  .build());
+        }
+      }
+
+      // If there are no users of the const-class then simply remove the instruction.
+      if (!invoke.hasOutValue() || !invoke.outValue().hasAnyUsers()) {
+        instructionIterator.removeOrReplaceByDebugLocalRead();
+        return;
+      }
+
+      // Otherwise insert a const-class instruction.
+      affectedValues.addAll(invoke.outValue().affectedValues());
+      instructionIterator.replaceCurrentInstructionWithConstClass(
+          appView, code, type, invoke.getLocalInfo());
+      if (appView.options().isGeneratingClassFiles()) {
+        code.method()
+            .upgradeClassFileVersion(
+                appView.options().requiredCfVersionForConstClassInstructions());
+      }
+    };
+  }
+
+  private static void applyTypeForGetClassTo(
+      AppView<AppInfoWithLiveness> appView,
+      ProgramMethod context,
+      InvokeVirtual invoke,
+      BiConsumer<DexType, ? super DexClass> consumer) {
     DexItemFactory dexItemFactory = appView.dexItemFactory();
     DexMethod invokedMethod = invoke.getInvokedMethod();
     // Class<?> Object#getClass() is final and cannot be overridden.
     if (invokedMethod != dexItemFactory.objectMembers.getClass) {
-      return null;
+      return;
     }
     Value in = invoke.getReceiver();
     if (in.hasLocalInfo()) {
-      return null;
+      return;
     }
     TypeElement inType = in.getType();
     // Check the receiver is either class type or array type. Also make sure it is not
     // nullable.
     if (!(inType.isClassType() || inType.isArrayType())
         || inType.isNullable()) {
-      return null;
+      return;
     }
     DexType type =
         inType.isClassType()
@@ -110,49 +161,49 @@
     DexType baseType = type.toBaseType(dexItemFactory);
     // Make sure base type is a class type.
     if (!baseType.isClassType()) {
-      return null;
+      return;
     }
     // Only consider program class, e.g., platform can introduce subtypes in different
     // versions.
     DexProgramClass clazz = asProgramClassOrNull(appView.definitionFor(baseType));
     if (clazz == null) {
-      return null;
+      return;
     }
     // Only consider effectively final class. Exception: new Base().getClass().
     if (!clazz.isEffectivelyFinal(appView)
         && (in.isPhi() || !in.definition.isCreatingInstanceOrArray())) {
-      return null;
+      return;
     }
     // Make sure the target (base) type is visible.
     ConstraintWithTarget constraints =
         ConstraintWithTarget.classIsVisible(context, baseType, appView);
     if (constraints == ConstraintWithTarget.NEVER) {
-      return null;
+      return;
     }
-    return type;
+
+    consumer.accept(type, clazz);
   }
 
-  private static DexType getTypeForClassForName(
+  private static void applyTypeForClassForNameTo(
       AppView<AppInfoWithLiveness> appView,
-      ClassInitializationAnalysis classInitializationAnalysis,
       ProgramMethod context,
-      InvokeStatic invoke) {
+      InvokeStatic invoke,
+      BiConsumer<DexType, ? super DexClass> consumer) {
     DexItemFactory dexItemFactory = appView.dexItemFactory();
     DexMethod invokedMethod = invoke.getInvokedMethod();
     // Class<?> Class#forName(String) is final and cannot be overridden.
     if (invokedMethod != dexItemFactory.classMethods.forName) {
-      return null;
+      return;
     }
-    assert invoke.inValues().size() == 1;
-    Value in = invoke.inValues().get(0).getAliasedValue();
+    assert invoke.arguments().size() == 1;
+    Value in = invoke.getArgument(0).getAliasedValue();
     // Only consider const-string input without locals.
     if (in.hasLocalInfo() || in.isPhi()) {
-      return null;
+      return;
     }
     // Also, check if the result of forName() is updatable via locals.
-    Value out = invoke.outValue();
-    if (out != null && out.hasLocalInfo()) {
-      return null;
+    if (invoke.hasOutValue() && invoke.outValue().hasLocalInfo()) {
+      return;
     }
     DexType type = null;
     if (in.definition.isDexItemBasedConstString()) {
@@ -172,46 +223,39 @@
       }
       if (descriptor == null
           || descriptor.indexOf(DescriptorUtils.JAVA_PACKAGE_SEPARATOR) > 0) {
-        return null;
+        return;
       }
       type = dexItemFactory.createType(descriptor);
       // Check if the given name refers to a reference type.
       if (!type.isReferenceType()) {
-        return null;
+        return;
       }
     } else {
       // Bail out for non-deterministic input to Class<?>#forName(name).
-      return null;
+      return;
     }
     if (type == null) {
-      return null;
+      return;
     }
     // Make sure the (base) type is resolvable.
     DexType baseType = type.toBaseType(dexItemFactory);
-    DexClass baseClazz = appView.appInfo().definitionForWithoutExistenceAssert(baseType);
-    if (baseClazz == null || !baseClazz.isResolvable(appView)) {
-      return null;
+    DexClass baseClass = appView.appInfo().definitionForWithoutExistenceAssert(baseType);
+    if (baseClass == null || !baseClass.isResolvable(appView)) {
+      return;
     }
 
-    // Don't allow the instantiated class to be in a feature, if it is, we can get a
-    // NoClassDefFoundError from dalvik/art.
-    if (baseClazz.isProgramClass()
-        && appView.appInfo().getClassToFeatureSplitMap().isInFeature(baseClazz.asProgramClass())) {
-      return null;
-    }
     // Make sure the (base) type is visible.
-    ConstraintWithTarget constraints =
-        ConstraintWithTarget.classIsVisible(context, baseType, appView);
-    if (constraints == ConstraintWithTarget.NEVER) {
-      return null;
+    ClassToFeatureSplitMap classToFeatureSplitMap = appView.appInfo().getClassToFeatureSplitMap();
+    if (AccessControl.isClassAccessible(baseClass, context, classToFeatureSplitMap)
+        .isPossiblyFalse()) {
+      return;
     }
-    // Make sure the type is already initialized.
-    // Note that, if the given name refers to an array type, the corresponding Class<?> won't
-    // be initialized. So, it's okay to rewrite the instruction.
-    if (type.isClassType()
-        && !classInitializationAnalysis.isClassDefinitelyLoadedBeforeInstruction(type, invoke)) {
-      return null;
-    }
-    return type;
+
+    // If the type is guaranteed to be visible, it must be in the same feature as the current method
+    // or in the base.
+    assert !baseClass.isProgramClass()
+        || classToFeatureSplitMap.isInBaseOrSameFeatureAs(baseClass.asProgramClass(), context);
+
+    consumer.accept(type, baseClass);
   }
 }
diff --git a/src/main/java/com/android/tools/r8/naming/IdentifierNameStringUtils.java b/src/main/java/com/android/tools/r8/naming/IdentifierNameStringUtils.java
index a406a8d..01157b9 100644
--- a/src/main/java/com/android/tools/r8/naming/IdentifierNameStringUtils.java
+++ b/src/main/java/com/android/tools/r8/naming/IdentifierNameStringUtils.java
@@ -12,6 +12,7 @@
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMember;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexReference;
 import com.android.tools.r8.graph.DexString;
@@ -299,7 +300,7 @@
       AppView<AppInfoWithLiveness> appView, DexString dexString) {
     // "fully.qualified.ClassName.fieldOrMethodName"
     // "fully.qualified.ClassName#fieldOrMethodName"
-    DexReference itemBasedString = inferMemberFromNameString(appView, dexString);
+    DexMember<?, ?> itemBasedString = inferMemberFromNameString(appView, dexString);
     if (itemBasedString == null) {
       // "fully.qualified.ClassName"
       return inferTypeFromNameString(appView, dexString);
@@ -333,7 +334,7 @@
     return null;
   }
 
-  private static DexReference inferMemberFromNameString(
+  private static DexMember<?, ?> inferMemberFromNameString(
       AppView<AppInfoWithLiveness> appView, DexString dexString) {
     String identifier = dexString.toString();
     String typeIdentifier = null;
@@ -368,7 +369,7 @@
     if (holder == null) {
       return null;
     }
-    DexReference itemBasedString = inferFieldInHolder(holder, memberIdentifier, null);
+    DexMember<?, ?> itemBasedString = inferFieldInHolder(holder, memberIdentifier, null);
     if (itemBasedString == null) {
       itemBasedString = inferMethodNameInHolder(holder, memberIdentifier);
     }
diff --git a/src/main/java/com/android/tools/r8/shaking/MainDexTracingResult.java b/src/main/java/com/android/tools/r8/shaking/MainDexTracingResult.java
index 76b6d8a..6383473 100644
--- a/src/main/java/com/android/tools/r8/shaking/MainDexTracingResult.java
+++ b/src/main/java/com/android/tools/r8/shaking/MainDexTracingResult.java
@@ -6,8 +6,8 @@
 
 import com.android.tools.r8.graph.AppInfo;
 import com.android.tools.r8.graph.DexClass;
-import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.ProgramDefinition;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
 import java.util.Collection;
@@ -76,6 +76,17 @@
     this.classes = Sets.union(roots, dependencies);
   }
 
+  public boolean canReferenceItemFromContextWithoutIncreasingMainDexSize(
+      ProgramDefinition item, ProgramDefinition context) {
+    // If the context is not a root, then additional references from inside the context will not
+    // increase the size of the main dex.
+    if (!isRoot(context)) {
+      return true;
+    }
+    // Otherwise, require that the item is a root itself.
+    return isRoot(item);
+  }
+
   public boolean isEmpty() {
     assert !roots.isEmpty() || dependencies.isEmpty();
     return roots.isEmpty();
@@ -93,8 +104,8 @@
     return classes;
   }
 
-  public boolean contains(DexProgramClass clazz) {
-    return contains(clazz.type);
+  public boolean contains(ProgramDefinition clazz) {
+    return contains(clazz.getContextType());
   }
 
   public boolean contains(DexType type) {
@@ -111,6 +122,14 @@
         });
   }
 
+  public boolean isRoot(ProgramDefinition definition) {
+    return getRoots().contains(definition.getContextType());
+  }
+
+  public boolean isDependency(ProgramDefinition definition) {
+    return getDependencies().contains(definition.getContextType());
+  }
+
   public MainDexTracingResult prunedCopy(AppInfoWithLiveness appInfo) {
     Builder builder = builder(appInfo);
     Predicate<DexType> wasPruned = appInfo::wasPruned;
diff --git a/src/test/java/com/android/tools/r8/compatproguard/CompatProguardSmaliTestBase.java b/src/test/java/com/android/tools/r8/compatproguard/CompatProguardSmaliTestBase.java
index ea450e4..dbe0d64 100644
--- a/src/test/java/com/android/tools/r8/compatproguard/CompatProguardSmaliTestBase.java
+++ b/src/test/java/com/android/tools/r8/compatproguard/CompatProguardSmaliTestBase.java
@@ -3,26 +3,25 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.compatproguard;
 
-import com.android.tools.r8.CompatProguardCommandBuilder;
-import com.android.tools.r8.OutputMode;
-import com.android.tools.r8.R8Command;
-import com.android.tools.r8.ToolHelper;
-import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.R8CompatTestBuilder;
+import com.android.tools.r8.ThrowableConsumer;
 import com.android.tools.r8.smali.SmaliBuilder;
 import com.android.tools.r8.smali.SmaliTestBase;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
-import java.nio.file.Path;
 import java.util.List;
 
 class CompatProguardSmaliTestBase extends SmaliTestBase {
-  CodeInspector runCompatProguard(SmaliBuilder builder, List<String> proguardConfigurations)
-      throws Exception {
-    Path dexOutputDir = temp.newFolder().toPath();
-    R8Command.Builder commandBuilder =
-        new CompatProguardCommandBuilder(true)
-            .setOutput(dexOutputDir, OutputMode.DexIndexed)
-            .addProguardConfiguration(proguardConfigurations, Origin.unknown());
-    ToolHelper.getAppBuilder(commandBuilder).addDexProgramData(builder.compile(), Origin.unknown());
-    return new CodeInspector(ToolHelper.runR8(commandBuilder.build()));
+
+  CodeInspector runCompatProguard(SmaliBuilder builder, List<String> keepRules) throws Exception {
+    return runCompatProguard(builder, testBuilder -> testBuilder.addKeepRules(keepRules));
+  }
+
+  CodeInspector runCompatProguard(
+      SmaliBuilder builder, ThrowableConsumer<R8CompatTestBuilder> configuration) throws Exception {
+    return testForR8Compat(Backend.DEX)
+        .addProgramDexFileData(builder.compile())
+        .applyIf(configuration != null, configuration)
+        .compile()
+        .inspector();
   }
 }
diff --git a/src/test/java/com/android/tools/r8/compatproguard/ForNameTest.java b/src/test/java/com/android/tools/r8/compatproguard/ForNameTest.java
index 6aa8805..9976c33 100644
--- a/src/test/java/com/android/tools/r8/compatproguard/ForNameTest.java
+++ b/src/test/java/com/android/tools/r8/compatproguard/ForNameTest.java
@@ -12,11 +12,10 @@
 import com.android.tools.r8.code.ReturnVoid;
 import com.android.tools.r8.graph.DexCode;
 import com.android.tools.r8.smali.SmaliBuilder;
+import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import com.android.tools.r8.utils.codeinspector.MethodSubject;
-import com.google.common.collect.ImmutableList;
-import java.util.List;
 import org.junit.Test;
 
 public class ForNameTest extends CompatProguardSmaliTestBase {
@@ -35,11 +34,17 @@
         "return-void");
     builder.addClass(BOO);
 
-    List<String> pgConfigs = ImmutableList.of(
-        keepMainProguardConfiguration(CLASS_NAME),
-        "-dontshrink",
-        "-dontoptimize");
-    CodeInspector inspector = runCompatProguard(builder, pgConfigs);
+    CodeInspector inspector =
+        runCompatProguard(
+            builder,
+            testBuilder ->
+                testBuilder
+                    .addKeepMainRule(CLASS_NAME)
+                    // Add main dex rule to disable Class.forName() optimization.
+                    .addMainDexRules("-keep class " + CLASS_NAME)
+                    .noOptimization()
+                    .noTreeShaking()
+                    .setMinApi(AndroidApiLevel.B));
 
     ClassSubject clazz = inspector.clazz(CLASS_NAME);
     assertTrue(clazz.isPresent());
@@ -65,12 +70,18 @@
         "return-void");
     builder.addClass(BOO);
 
-    List<String> pgConfigs = ImmutableList.of(
-        keepMainProguardConfiguration(CLASS_NAME),
-        "-dontshrink",
-        "-dontoptimize",
-        "-dontobfuscate");
-    CodeInspector inspector = runCompatProguard(builder, pgConfigs);
+    CodeInspector inspector =
+        runCompatProguard(
+            builder,
+            testBuilder ->
+                testBuilder
+                    .addKeepMainRule(CLASS_NAME)
+                    // Add main dex rule to disable Class.forName() optimization.
+                    .addMainDexRules("-keep class " + CLASS_NAME)
+                    .noOptimization()
+                    .noMinification()
+                    .noTreeShaking()
+                    .setMinApi(AndroidApiLevel.B));
 
     ClassSubject clazz = inspector.clazz(CLASS_NAME);
     assertTrue(clazz.isPresent());
diff --git a/src/test/java/com/android/tools/r8/desugar/DefaultInterfaceWithIdentifierNameString.java b/src/test/java/com/android/tools/r8/desugar/DefaultInterfaceWithIdentifierNameString.java
index 9c006cb..5fbca11 100644
--- a/src/test/java/com/android/tools/r8/desugar/DefaultInterfaceWithIdentifierNameString.java
+++ b/src/test/java/com/android/tools/r8/desugar/DefaultInterfaceWithIdentifierNameString.java
@@ -60,7 +60,8 @@
     companionClassSubject
         .allMethods()
         .forEach(
-            method -> assertTrue(method.streamInstructions().anyMatch(this::isInvokeClassForName)));
+            method ->
+                assertTrue(method.streamInstructions().noneMatch(this::isInvokeClassForName)));
   }
 
   @Test
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/reflection/ForNameTest.java b/src/test/java/com/android/tools/r8/ir/optimize/reflection/ForNameTest.java
index f99639f..26b927e 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/reflection/ForNameTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/reflection/ForNameTest.java
@@ -161,7 +161,7 @@
             .setMinApi(parameters.getApiLevel())
             .run(parameters.getRuntime(), MAIN)
             .assertSuccessWithOutput(JAVA_OUTPUT);
-    test(result, 2, 2);
+    test(result, 1, 3);
 
     // R8 release, minification.
     result =
@@ -173,6 +173,6 @@
             .setMinApi(parameters.getApiLevel())
             // We are not checking output because it can't be matched due to minification. Just run.
             .run(parameters.getRuntime(), MAIN);
-    test(result, 2, 2);
+    test(result, 1, 3);
   }
 }
diff --git a/src/test/java/com/android/tools/r8/ir/regalloc/RegisterMoveSchedulerTest.java b/src/test/java/com/android/tools/r8/ir/regalloc/RegisterMoveSchedulerTest.java
index 3f7bb60..fa7b74e 100644
--- a/src/test/java/com/android/tools/r8/ir/regalloc/RegisterMoveSchedulerTest.java
+++ b/src/test/java/com/android/tools/r8/ir/regalloc/RegisterMoveSchedulerTest.java
@@ -9,6 +9,7 @@
 import com.android.tools.r8.graph.AppInfo;
 import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DebugLocalInfo;
 import com.android.tools.r8.graph.DexApplication;
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexString;
@@ -61,6 +62,12 @@
     }
 
     @Override
+    public void replaceCurrentInstructionWithConstClass(
+        AppView<?> appView, IRCode code, DexType type, DebugLocalInfo localInfo) {
+      throw new Unimplemented();
+    }
+
+    @Override
     public void replaceCurrentInstructionWithConstInt(IRCode code, int value) {
       throw new Unimplemented();
     }
diff --git a/src/test/java/com/android/tools/r8/naming/IdentifierMinifierTest.java b/src/test/java/com/android/tools/r8/naming/IdentifierMinifierTest.java
index ad8c526..8775bd7 100644
--- a/src/test/java/com/android/tools/r8/naming/IdentifierMinifierTest.java
+++ b/src/test/java/com/android/tools/r8/naming/IdentifierMinifierTest.java
@@ -115,19 +115,19 @@
 
   // Without -adaptclassstrings
   private static void test1_rule1(TestParameters parameters, CodeInspector inspector) {
-    int expectedRenamedIdentifierInMain = parameters.isCfRuntime() ? 2 : 1;
+    int expectedRenamedIdentifierInMain = parameters.isCfRuntime() ? 0 : 0;
     test1_rules(inspector, expectedRenamedIdentifierInMain, 0, 0);
   }
 
   // With -adaptclassstrings *.*A
   private static void test1_rule2(TestParameters parameters, CodeInspector inspector) {
-    int expectedRenamedIdentifierInMain = parameters.isCfRuntime() ? 2 : 1;
+    int expectedRenamedIdentifierInMain = parameters.isCfRuntime() ? 0 : 0;
     test1_rules(inspector, expectedRenamedIdentifierInMain, 1, 1);
   }
 
   // With -adaptclassstrings (no filter)
   private static void test1_rule3(TestParameters parameters, CodeInspector inspector) {
-    int expectedRenamedIdentifierInMain = parameters.isCfRuntime() ? 3 : 2;
+    int expectedRenamedIdentifierInMain = parameters.isCfRuntime() ? 1 : 1;
     test1_rules(inspector, expectedRenamedIdentifierInMain, 1, 1);
   }
 
diff --git a/src/test/java/com/android/tools/r8/repackage/RepackageWithDexItemBasedConstStringTest.java b/src/test/java/com/android/tools/r8/repackage/RepackageWithDexItemBasedConstStringTest.java
index 8a36090..e374f2a 100644
--- a/src/test/java/com/android/tools/r8/repackage/RepackageWithDexItemBasedConstStringTest.java
+++ b/src/test/java/com/android/tools/r8/repackage/RepackageWithDexItemBasedConstStringTest.java
@@ -4,22 +4,38 @@
 
 package com.android.tools.r8.repackage;
 
+import static com.android.tools.r8.shaking.ProguardConfigurationParser.FLATTEN_PACKAGE_HIERARCHY;
+import static com.android.tools.r8.shaking.ProguardConfigurationParser.REPACKAGE_CLASSES;
 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.TestParameters;
+import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import com.android.tools.r8.utils.codeinspector.InstructionSubject.JumboStringMode;
 import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import com.google.common.collect.ImmutableList;
+import java.util.List;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
 
 @RunWith(Parameterized.class)
 public class RepackageWithDexItemBasedConstStringTest extends RepackageTestBase {
 
+  @Parameters(name = "{1}, kind: {0}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        ImmutableList.of(FLATTEN_PACKAGE_HIERARCHY, REPACKAGE_CLASSES),
+        getTestParameters()
+            .withAllRuntimes()
+            .withApiLevelsEndingAtExcluding(AndroidApiLevel.L)
+            .build());
+  }
+
   public RepackageWithDexItemBasedConstStringTest(
       String flattenPackageHierarchyOrRepackageClasses, TestParameters parameters) {
     super(flattenPackageHierarchyOrRepackageClasses, parameters);
@@ -30,6 +46,7 @@
     testForR8(parameters.getBackend())
         .addInnerClasses(getClass())
         .addKeepMainRule(TestClass.class)
+        .addMainDexClassRules(TestClass.class)
         .apply(this::configureRepackaging)
         .setMinApi(parameters.getApiLevel())
         .compile()
diff --git a/src/test/java/com/android/tools/r8/shaking/keepparameternames/KeepParameterNamesUnsortedLocalVariablesTableTest.java b/src/test/java/com/android/tools/r8/shaking/keepparameternames/KeepParameterNamesUnsortedLocalVariablesTableTest.java
index c92d7e7..abd2407 100644
--- a/src/test/java/com/android/tools/r8/shaking/keepparameternames/KeepParameterNamesUnsortedLocalVariablesTableTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/keepparameternames/KeepParameterNamesUnsortedLocalVariablesTableTest.java
@@ -87,7 +87,7 @@
   @Test
   public void test() throws Exception {
     String expectedOutput = StringUtils.lines("In Api.api1 6", "Result 6");
-    testForR8(parameters.getBackend())
+    testForR8Compat(parameters.getBackend())
         .addInnerClasses(KeepParameterNamesUnsortedLocalVariablesTableTest.class)
         .addProgramClassFileData(dumpApi())
         .addKeepMainRule(TestClass.class)