Reimplement access modification as top-down class hierarchy traversal

Bug: b/131130038
Bug: b/279124123
Bug: b/132677331
Bug: b/266345581
Change-Id: I0622ec061d19ec47232200c73edf757128089d87
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index 7bd8767..1684062 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -62,10 +62,11 @@
 import com.android.tools.r8.naming.ProguardMapMinifier;
 import com.android.tools.r8.naming.RecordRewritingNamingLens;
 import com.android.tools.r8.naming.signature.GenericSignatureRewriter;
-import com.android.tools.r8.optimize.AccessModifier;
+import com.android.tools.r8.optimize.LegacyAccessModifier;
 import com.android.tools.r8.optimize.MemberRebindingAnalysis;
 import com.android.tools.r8.optimize.MemberRebindingIdentityLens;
 import com.android.tools.r8.optimize.MemberRebindingIdentityLensFactory;
+import com.android.tools.r8.optimize.accessmodification.AccessModifier;
 import com.android.tools.r8.optimize.bridgehoisting.BridgeHoisting;
 import com.android.tools.r8.optimize.fields.FieldFinalizer;
 import com.android.tools.r8.optimize.interfaces.analysis.CfOpenClosedInterfacesAnalysis;
@@ -448,7 +449,8 @@
       // to clear the cache, so that we will recompute the type lattice elements.
       appView.dexItemFactory().clearTypeElementsCache();
 
-      AccessModifier.run(appViewWithLiveness, executorService, timing);
+      // TODO(b/132677331): Remove legacy access modifier.
+      LegacyAccessModifier.run(appViewWithLiveness, executorService, timing);
       if (appView.graphLens().isPublicizerLens()) {
         // We can now remove redundant bridges. Note that we do not need to update the
         // invoke-targets here, as the existing invokes will simply dispatch to the now
@@ -463,6 +465,10 @@
       new MemberRebindingAnalysis(appViewWithLiveness).run(executorService);
       appViewWithLiveness.appInfo().notifyMemberRebindingFinished(appViewWithLiveness);
 
+      assert ArtProfileCompletenessChecker.verify(appView);
+
+      AccessModifier.run(appViewWithLiveness, executorService, timing);
+
       boolean isKotlinLibraryCompilationWithInlinePassThrough =
           options.enableCfByteCodePassThrough && appView.hasCfByteCodePassThroughMethods();
 
diff --git a/src/main/java/com/android/tools/r8/optimize/AccessModifier.java b/src/main/java/com/android/tools/r8/optimize/LegacyAccessModifier.java
similarity index 95%
rename from src/main/java/com/android/tools/r8/optimize/AccessModifier.java
rename to src/main/java/com/android/tools/r8/optimize/LegacyAccessModifier.java
index dea6346..2c82527 100644
--- a/src/main/java/com/android/tools/r8/optimize/AccessModifier.java
+++ b/src/main/java/com/android/tools/r8/optimize/LegacyAccessModifier.java
@@ -24,6 +24,7 @@
 import com.android.tools.r8.ir.optimize.MethodPoolCollection;
 import com.android.tools.r8.optimize.PublicizerLens.PublicizedLensBuilder;
 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.OptionalBool;
 import com.android.tools.r8.utils.Timing;
@@ -33,7 +34,7 @@
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
 
-public final class AccessModifier {
+public final class LegacyAccessModifier {
 
   private final AppView<AppInfoWithLiveness> appView;
   private final SubtypingInfo subtypingInfo;
@@ -41,7 +42,7 @@
 
   private final PublicizedLensBuilder lensBuilder = PublicizerLens.createBuilder();
 
-  private AccessModifier(AppView<AppInfoWithLiveness> appView) {
+  private LegacyAccessModifier(AppView<AppInfoWithLiveness> appView) {
     this.appView = appView;
     this.subtypingInfo = appView.appInfo().computeSubtypingInfo();
     this.methodPoolCollection =
@@ -59,9 +60,11 @@
   public static void run(
       AppView<AppInfoWithLiveness> appView, ExecutorService executorService, Timing timing)
       throws ExecutionException {
-    if (appView.options().getProguardConfiguration().isAccessModificationAllowed()) {
+    InternalOptions options = appView.options();
+    if (options.isAccessModificationEnabled()
+        && !options.getAccessModifierOptions().isExperimentalAccessModificationEnabled()) {
       timing.begin("Access modification");
-      new AccessModifier(appView).internalRun(executorService, timing);
+      new LegacyAccessModifier(appView).internalRun(executorService, timing);
       timing.end();
     }
   }
diff --git a/src/main/java/com/android/tools/r8/optimize/accessmodification/AccessModifier.java b/src/main/java/com/android/tools/r8/optimize/accessmodification/AccessModifier.java
new file mode 100644
index 0000000..b7342dc
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/accessmodification/AccessModifier.java
@@ -0,0 +1,314 @@
+// Copyright (c) 2023, 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.accessmodification;
+
+import static com.android.tools.r8.dex.Constants.ACC_PRIVATE;
+import static com.android.tools.r8.dex.Constants.ACC_PROTECTED;
+import static com.android.tools.r8.dex.Constants.ACC_PUBLIC;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexMethodSignature;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.FieldAccessFlags;
+import com.android.tools.r8.graph.FieldAccessInfo;
+import com.android.tools.r8.graph.ImmediateProgramSubtypingInfo;
+import com.android.tools.r8.graph.InnerClassAttribute;
+import com.android.tools.r8.graph.MethodAccessFlags;
+import com.android.tools.r8.graph.ProgramDefinition;
+import com.android.tools.r8.graph.ProgramField;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.optimize.accessmodification.AccessModifierTraversal.BottomUpTraversalState;
+import com.android.tools.r8.optimize.argumentpropagation.utils.ProgramClassesBidirectedGraph;
+import com.android.tools.r8.optimize.utils.ConcurrentNonProgramMethodsCollection;
+import com.android.tools.r8.optimize.utils.NonProgramMethodsCollection;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.shaking.KeepMethodInfo;
+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 com.google.common.collect.BiMap;
+import com.google.common.collect.HashBiMap;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+
+public class AccessModifier {
+
+  private final AppView<AppInfoWithLiveness> appView;
+  private final ImmediateProgramSubtypingInfo immediateSubtypingInfo;
+  private final AccessModifierLens.Builder lensBuilder = AccessModifierLens.builder();
+  private final NonProgramMethodsCollection nonProgramMethodsCollection;
+  private final InternalOptions options;
+
+  private AccessModifier(AppView<AppInfoWithLiveness> appView) {
+    this.appView = appView;
+    this.immediateSubtypingInfo =
+        ImmediateProgramSubtypingInfo.createWithDeterministicOrder(appView);
+    this.nonProgramMethodsCollection =
+        ConcurrentNonProgramMethodsCollection.createVirtualMethodsCollection(appView);
+    this.options = appView.options();
+  }
+
+  public static void run(
+      AppView<AppInfoWithLiveness> appView, ExecutorService executorService, Timing timing)
+      throws ExecutionException {
+    timing.begin("Access modification");
+    if (appView.options().getAccessModifierOptions().isExperimentalAccessModificationEnabled()) {
+      new AccessModifier(appView)
+          .processStronglyConnectedComponents(executorService)
+          .installLens(executorService, timing);
+    }
+    timing.end();
+  }
+
+  private AccessModifier processStronglyConnectedComponents(ExecutorService executorService)
+      throws ExecutionException {
+    // Compute the connected program classes and process the components in parallel.
+    List<Set<DexProgramClass>> stronglyConnectedComponents =
+        new ProgramClassesBidirectedGraph(appView, immediateSubtypingInfo)
+            .computeStronglyConnectedComponents();
+    ThreadUtils.processItems(
+        stronglyConnectedComponents, this::processStronglyConnectedComponent, executorService);
+    return this;
+  }
+
+  private void processStronglyConnectedComponent(Set<DexProgramClass> stronglyConnectedComponent) {
+    // Perform a top-down traversal over the class hierarchy.
+    new AccessModifierTraversal(
+            appView,
+            immediateSubtypingInfo,
+            this,
+            AccessModifierNamingState.createInitialNamingState(
+                appView, stronglyConnectedComponent, nonProgramMethodsCollection))
+        .run(ListUtils.sort(stronglyConnectedComponent, Comparator.comparing(DexClass::getType)));
+  }
+
+  private void installLens(ExecutorService executorService, Timing timing)
+      throws ExecutionException {
+    if (!lensBuilder.isEmpty()) {
+      appView.rewriteWithLens(lensBuilder.build(appView), executorService, timing);
+    }
+  }
+
+  // Publicizing of classes and members.
+
+  void processClass(
+      DexProgramClass clazz,
+      AccessModifierNamingState namingState,
+      BottomUpTraversalState traversalState) {
+    publicizeClass(clazz);
+    publicizeFields(clazz);
+    publicizeMethods(clazz, namingState, traversalState);
+    // TODO(b/278736230): Also finalize classes and methods here.
+    finalizeFields(clazz);
+  }
+
+  private void publicizeClass(DexProgramClass clazz) {
+    if (isAccessModificationAllowed(clazz) && !clazz.getAccessFlags().isPublic()) {
+      clazz.getAccessFlags().promoteToPublic();
+    }
+
+    // Update inner class attribute.
+    // TODO(b/285494837): Carry-over from the legacy access modifier. We should never publicize
+    //  items unconditionally, but account for keep info.
+    InnerClassAttribute attr = clazz.getInnerClassAttributeForThisClass();
+    if (attr != null) {
+      int accessFlags = ((attr.getAccess() | ACC_PUBLIC) & ~ACC_PRIVATE) & ~ACC_PROTECTED;
+      clazz.replaceInnerClassAttributeForThisClass(
+          new InnerClassAttribute(
+              accessFlags, attr.getInner(), attr.getOuter(), attr.getInnerName()));
+    }
+  }
+
+  private void publicizeFields(DexProgramClass clazz) {
+    clazz.forEachProgramField(this::publicizeField);
+  }
+
+  private void publicizeField(ProgramField field) {
+    if (isAccessModificationAllowed(field) && !field.getAccessFlags().isPublic()) {
+      field.getAccessFlags().promoteToPublic();
+    }
+  }
+
+  private void publicizeMethods(
+      DexProgramClass clazz,
+      AccessModifierNamingState namingState,
+      BottomUpTraversalState traversalState) {
+    // Create a local naming state to keep track of the methods present on the current class.
+    // Start by reserving the pinned method signatures on the current class.
+    BiMap<DexMethod, DexMethod> localNamingState = HashBiMap.create();
+    clazz.forEachProgramMethod(
+        method -> {
+          if (!method.getDefinition().isInitializer() && !isRenamingAllowed(method)) {
+            localNamingState.put(method.getReference(), method.getReference());
+          }
+        });
+    clazz
+        .getMethodCollection()
+        .<ProgramMethod>replaceClassAndMethods(
+            method -> publicizeMethod(method, localNamingState, namingState, traversalState));
+  }
+
+  private DexEncodedMethod publicizeMethod(
+      ProgramMethod method,
+      BiMap<DexMethod, DexMethod> localNamingState,
+      AccessModifierNamingState namingState,
+      BottomUpTraversalState traversalState) {
+    MethodAccessFlags accessFlags = method.getAccessFlags();
+    if (accessFlags.isPublic() || !isAccessModificationAllowed(method)) {
+      return commitMethod(method, localNamingState, namingState);
+    }
+
+    if (method.getDefinition().isInstanceInitializer()
+        || (accessFlags.isPackagePrivate()
+            && !traversalState.hasIllegalOverrideOfPackagePrivateMethod(method))
+        || accessFlags.isProtected()) {
+      method.getAccessFlags().promoteToPublic();
+      return commitMethod(method, localNamingState, namingState);
+    }
+
+    if (accessFlags.isPrivate()) {
+      if (isRenamingAllowed(method)) {
+        method.getAccessFlags().promoteToPublic();
+        return commitMethod(method, localNamingState, namingState);
+      }
+      assert localNamingState.containsKey(method.getReference());
+      assert localNamingState.get(method.getReference()) == method.getReference();
+      if (namingState.isFree(method.getMethodSignature())) {
+        method.getAccessFlags().promoteToPublic();
+        namingState.addBlockedMethodSignature(method.getMethodSignature());
+      }
+      return commitMethod(method, method.getReference());
+    }
+
+    // TODO(b/279126633): Add support for publicizing package-private methods by renaming.
+    assert accessFlags.isPackagePrivate();
+    assert traversalState.hasIllegalOverrideOfPackagePrivateMethod(method);
+    return commitMethod(method, localNamingState, namingState);
+  }
+
+  private DexMethod getAndReserveNewMethodReference(
+      ProgramMethod method,
+      BiMap<DexMethod, DexMethod> localNamingState,
+      AccessModifierNamingState namingState) {
+    if (method.getDefinition().isInitializer()) {
+      return method.getReference();
+    }
+    if (!isRenamingAllowed(method)) {
+      assert localNamingState.containsKey(method.getReference());
+      assert localNamingState.get(method.getReference()) == method.getReference();
+      assert method.getAccessFlags().isPrivate()
+          || method
+              .getMethodSignature()
+              .equals(namingState.getReservedSignature(method.getMethodSignature()));
+      return method.getReference();
+    }
+    DexItemFactory dexItemFactory = appView.dexItemFactory();
+    if (method.getAccessFlags().isPrivate()) {
+      // Find a fresh method name and reserve it for the current class.
+      DexMethod newMethodReference =
+          dexItemFactory.createFreshMethodNameWithoutHolder(
+              method.getName().toString(),
+              method.getProto(),
+              method.getHolderType(),
+              candidate ->
+                  !localNamingState.containsValue(candidate)
+                      && namingState.isFree(candidate.getSignature()));
+      localNamingState.put(method.getReference(), newMethodReference);
+      return newMethodReference;
+    }
+    // Check if a mapping already exists for this method signature.
+    if (!method.getAccessFlags().isPromotedFromPrivateToPublic()) {
+      DexMethodSignature reservedSignature =
+          namingState.getReservedSignature(method.getMethodSignature());
+      if (reservedSignature != null) {
+        return reservedSignature.withHolder(method, appView.dexItemFactory());
+      }
+    }
+    // Find a fresh method name and block/reserve it globally.
+    DexMethod newMethodReference =
+        dexItemFactory.createFreshMethodNameWithoutHolder(
+            method.getName().toString(),
+            method.getProto(),
+            method.getHolderType(),
+            candidate ->
+                !localNamingState.containsValue(candidate)
+                    && namingState.isFree(candidate.getSignature()));
+    if (method.getAccessFlags().belongsToVirtualPool()) {
+      if (method.getAccessFlags().isPromotedFromPrivateToPublic()) {
+        namingState.addBlockedMethodSignature(newMethodReference.getSignature());
+      } else {
+        namingState.addRenaming(method.getMethodSignature(), newMethodReference.getSignature());
+      }
+    }
+    return newMethodReference;
+  }
+
+  private boolean isAccessModificationAllowed(ProgramDefinition definition) {
+    // TODO(b/278687711): Also check that the definition does not have any illegal accesses to it.
+    return appView.getKeepInfo(definition).isAccessModificationAllowed(options);
+  }
+
+  private boolean isRenamingAllowed(ProgramMethod method) {
+    KeepMethodInfo keepInfo = appView.getKeepInfo(method);
+    return keepInfo.isOptimizationAllowed(options) && keepInfo.isShrinkingAllowed(options);
+  }
+
+  private DexEncodedMethod commitMethod(
+      ProgramMethod method,
+      BiMap<DexMethod, DexMethod> localNamingState,
+      AccessModifierNamingState namingState) {
+    return commitMethod(
+        method, getAndReserveNewMethodReference(method, localNamingState, namingState));
+  }
+
+  private DexEncodedMethod commitMethod(ProgramMethod method, DexMethod newMethodReference) {
+    DexProgramClass holder = method.getHolder();
+    if (newMethodReference != method.getReference()) {
+      lensBuilder.recordMove(method.getReference(), newMethodReference);
+      method =
+          new ProgramMethod(
+              holder, method.getDefinition().toTypeSubstitutedMethod(newMethodReference));
+    }
+    if (method.getAccessFlags().isPromotedFromPrivateToPublic()
+        && method.getAccessFlags().belongsToVirtualPool()) {
+      lensBuilder.addPublicizedPrivateVirtualMethod(method.getHolder(), newMethodReference);
+      method.getDefinition().setLibraryMethodOverride(OptionalBool.FALSE);
+    }
+    return method.getDefinition();
+  }
+
+  // Finalization of classes and members.
+
+  private void finalizeFields(DexProgramClass clazz) {
+    clazz.forEachProgramField(this::finalizeField);
+  }
+
+  private void finalizeField(ProgramField field) {
+    FieldAccessFlags flags = field.getAccessFlags();
+    FieldAccessInfo accessInfo =
+        appView.appInfo().getFieldAccessInfoCollection().get(field.getReference());
+    if (!appView.getKeepInfo(field).isPinned(options)
+        && !accessInfo.hasReflectiveWrite()
+        && !accessInfo.isWrittenFromMethodHandle()
+        && accessInfo.isWrittenOnlyInMethodSatisfying(
+            method ->
+                method.getDefinition().isInitializer()
+                    && method.getAccessFlags().isStatic() == flags.isStatic()
+                    && method.getHolder() == field.getHolder())
+        && !flags.isFinal()
+        && !flags.isVolatile()) {
+      flags.promoteToFinal();
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/optimize/accessmodification/AccessModifierLens.java b/src/main/java/com/android/tools/r8/optimize/accessmodification/AccessModifierLens.java
new file mode 100644
index 0000000..76da486
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/accessmodification/AccessModifierLens.java
@@ -0,0 +1,117 @@
+// Copyright (c) 2023, 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.accessmodification;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.lens.DefaultNonIdentityGraphLens;
+import com.android.tools.r8.graph.lens.GraphLens;
+import com.android.tools.r8.graph.lens.MethodLookupResult;
+import com.android.tools.r8.ir.code.InvokeType;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.collections.BidirectionalOneToOneHashMap;
+import com.android.tools.r8.utils.collections.BidirectionalOneToOneMap;
+import com.android.tools.r8.utils.collections.MutableBidirectionalOneToOneMap;
+import com.google.common.collect.Sets;
+import java.util.Set;
+
+public class AccessModifierLens extends DefaultNonIdentityGraphLens {
+
+  private final BidirectionalOneToOneMap<DexMethod, DexMethod> methodMap;
+
+  // Private interface methods that have been publicized. Invokes targeting these methods must be
+  // rewritten from invoke-direct to invoke-interface.
+  private final Set<DexMethod> publicizedPrivateInterfaceMethods;
+
+  // Private class methods that have been publicized. Invokes targeting these methods must be
+  // rewritten from invoke-direct to invoke-virtual.
+  private final Set<DexMethod> publicizedPrivateVirtualMethods;
+
+  AccessModifierLens(
+      AppView<AppInfoWithLiveness> appView,
+      BidirectionalOneToOneMap<DexMethod, DexMethod> methodMap,
+      Set<DexMethod> publicizedPrivateInterfaceMethods,
+      Set<DexMethod> publicizedPrivateVirtualMethods) {
+    super(appView);
+    this.methodMap = methodMap;
+    this.publicizedPrivateInterfaceMethods = publicizedPrivateInterfaceMethods;
+    this.publicizedPrivateVirtualMethods = publicizedPrivateVirtualMethods;
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  @Override
+  public DexMethod getNextMethodSignature(DexMethod method) {
+    return methodMap.getOrDefault(method, method);
+  }
+
+  @Override
+  public DexMethod getPreviousMethodSignature(DexMethod method) {
+    return methodMap.getRepresentativeKeyOrDefault(method, method);
+  }
+
+  @Override
+  public MethodLookupResult internalDescribeLookupMethod(
+      MethodLookupResult previous, DexMethod context, GraphLens codeLens) {
+    assert !previous.hasReboundReference();
+    DexMethod newMethod = getNextMethodSignature(previous.getReference());
+    InvokeType newInvokeType = previous.getType();
+    if (previous.getType() == InvokeType.DIRECT) {
+      if (publicizedPrivateInterfaceMethods.contains(newMethod)) {
+        newInvokeType = InvokeType.INTERFACE;
+      } else if (publicizedPrivateVirtualMethods.contains(newMethod)) {
+        newInvokeType = InvokeType.VIRTUAL;
+      }
+    }
+    if (newInvokeType != previous.getType() || newMethod != previous.getReference()) {
+      return MethodLookupResult.builder(this)
+          .setReference(newMethod)
+          .setPrototypeChanges(previous.getPrototypeChanges())
+          .setType(newInvokeType)
+          .build();
+    }
+    return previous;
+  }
+
+  public static class Builder {
+
+    private final MutableBidirectionalOneToOneMap<DexMethod, DexMethod> methodMap =
+        new BidirectionalOneToOneHashMap<>();
+    private final Set<DexMethod> publicizedPrivateInterfaceMethods = Sets.newConcurrentHashSet();
+    private final Set<DexMethod> publicizedPrivateVirtualMethods = Sets.newConcurrentHashSet();
+
+    public Builder addPublicizedPrivateVirtualMethod(DexProgramClass holder, DexMethod method) {
+      if (holder.isInterface()) {
+        publicizedPrivateInterfaceMethods.add(method);
+      } else {
+        publicizedPrivateVirtualMethods.add(method);
+      }
+      return this;
+    }
+
+    public Builder recordMove(DexMethod from, DexMethod to) {
+      assert from != to;
+      synchronized (methodMap) {
+        methodMap.put(from, to);
+      }
+      return this;
+    }
+
+    public boolean isEmpty() {
+      return methodMap.isEmpty()
+          && publicizedPrivateInterfaceMethods.isEmpty()
+          && publicizedPrivateVirtualMethods.isEmpty();
+    }
+
+    public AccessModifierLens build(AppView<AppInfoWithLiveness> appView) {
+      assert !isEmpty();
+      return new AccessModifierLens(
+          appView, methodMap, publicizedPrivateInterfaceMethods, publicizedPrivateVirtualMethods);
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/optimize/accessmodification/AccessModifierNamingState.java b/src/main/java/com/android/tools/r8/optimize/accessmodification/AccessModifierNamingState.java
new file mode 100644
index 0000000..cae79be
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/accessmodification/AccessModifierNamingState.java
@@ -0,0 +1,86 @@
+// Copyright (c) 2023, 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.accessmodification;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.ClasspathOrLibraryClass;
+import com.android.tools.r8.graph.DexMethodSignature;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.optimize.utils.NonProgramMethodsCollection;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.shaking.KeepMethodInfo;
+import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.collections.DexMethodSignatureBiMap;
+import com.android.tools.r8.utils.collections.DexMethodSignatureSet;
+import com.google.common.collect.Sets;
+import java.util.Set;
+
+public class AccessModifierNamingState {
+
+  // The set of private method signatures that have been publicized. These method signatures are
+  // "blocked" to ensure that virtual methods with the same method signature are given a different
+  // name.
+  private final DexMethodSignatureSet blockedMethodSignatures = DexMethodSignatureSet.create();
+
+  // Records which method signatures in the component have been mapped to. This uses a bidirectional
+  // map to allow efficiently finding a fresh method signature in the component.
+  private final DexMethodSignatureBiMap<DexMethodSignature> reservedMethodSignatures;
+
+  private AccessModifierNamingState(
+      DexMethodSignatureBiMap<DexMethodSignature> reservedMethodSignatures) {
+    this.reservedMethodSignatures = reservedMethodSignatures;
+  }
+
+  static AccessModifierNamingState createInitialNamingState(
+      AppView<AppInfoWithLiveness> appView,
+      Set<DexProgramClass> stronglyConnectedComponent,
+      NonProgramMethodsCollection nonProgramMethodsCollection) {
+    DexMethodSignatureBiMap<DexMethodSignature> reservedSignatures =
+        new DexMethodSignatureBiMap<>();
+    Set<ClasspathOrLibraryClass> seenNonProgramClasses = Sets.newIdentityHashSet();
+    for (DexProgramClass clazz : stronglyConnectedComponent) {
+      // Reserve the signatures that are pinned in this class.
+      clazz.forEachProgramMethodMatching(
+          method -> !method.isInstanceInitializer() && !method.getAccessFlags().isPrivate(),
+          method -> {
+            KeepMethodInfo keepInfo = appView.getKeepInfo(method);
+            InternalOptions options = appView.options();
+            if (!keepInfo.isOptimizationAllowed(options) || !keepInfo.isShrinkingAllowed(options)) {
+              DexMethodSignature methodSignature = method.getMethodSignature();
+              reservedSignatures.put(methodSignature, methodSignature);
+            }
+          });
+      // Reserve the signatures in the library.
+      clazz.forEachImmediateSuperClassMatching(
+          appView,
+          (supertype, superclass) ->
+              superclass != null
+                  && !superclass.isProgramClass()
+                  && seenNonProgramClasses.add(superclass.asClasspathOrLibraryClass()),
+          (supertype, superclass) ->
+              reservedSignatures.putAllToIdentity(
+                  nonProgramMethodsCollection.getOrComputeNonProgramMethods(
+                      superclass.asClasspathOrLibraryClass())));
+    }
+    return new AccessModifierNamingState(reservedSignatures);
+  }
+
+  void addBlockedMethodSignature(DexMethodSignature signature) {
+    blockedMethodSignatures.add(signature);
+  }
+
+  void addRenaming(DexMethodSignature signature, DexMethodSignature newSignature) {
+    reservedMethodSignatures.put(signature, newSignature);
+  }
+
+  DexMethodSignature getReservedSignature(DexMethodSignature signature) {
+    return reservedMethodSignatures.get(signature);
+  }
+
+  boolean isFree(DexMethodSignature signature) {
+    return !blockedMethodSignatures.contains(signature)
+        && !reservedMethodSignatures.containsValue(signature);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/optimize/accessmodification/AccessModifierOptions.java b/src/main/java/com/android/tools/r8/optimize/accessmodification/AccessModifierOptions.java
new file mode 100644
index 0000000..7ee64c7
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/accessmodification/AccessModifierOptions.java
@@ -0,0 +1,29 @@
+// Copyright (c) 2023, 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.accessmodification;
+
+import com.android.tools.r8.utils.InternalOptions;
+
+public class AccessModifierOptions {
+
+  // TODO(b/132677331): Enable new access modifier by default.
+  private boolean enableExperimentalAccessModification = false;
+
+  private InternalOptions options;
+
+  public AccessModifierOptions(InternalOptions options) {
+    this.options = options;
+  }
+
+  public boolean isAccessModificationEnabled() {
+    return options.hasProguardConfiguration()
+        && options.getProguardConfiguration().isAccessModificationAllowed();
+  }
+
+  public boolean isExperimentalAccessModificationEnabled() {
+    // TODO(b/132677331): Do not require -allowaccessmodification in R8 full mode.
+    return isAccessModificationEnabled() && enableExperimentalAccessModification;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/optimize/accessmodification/AccessModifierTraversal.java b/src/main/java/com/android/tools/r8/optimize/accessmodification/AccessModifierTraversal.java
new file mode 100644
index 0000000..1fef67f
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/accessmodification/AccessModifierTraversal.java
@@ -0,0 +1,177 @@
+// Copyright (c) 2023, 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.accessmodification;
+
+import static com.android.tools.r8.graph.DexProgramClass.asProgramClassOrNull;
+import static com.android.tools.r8.utils.MapUtils.ignoreKey;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.ImmediateProgramSubtypingInfo;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.optimize.argumentpropagation.utils.DepthFirstTopDownClassHierarchyTraversal;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.MapUtils;
+import com.android.tools.r8.utils.collections.DexMethodSignatureMap;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Sets;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.IdentityHashMap;
+import java.util.Map;
+import java.util.Set;
+
+class AccessModifierTraversal extends DepthFirstTopDownClassHierarchyTraversal {
+
+  private final AccessModifier accessModifier;
+  private final AccessModifierNamingState namingState;
+
+  private final Map<DexType, TraversalState> states = new IdentityHashMap<>();
+
+  AccessModifierTraversal(
+      AppView<AppInfoWithLiveness> appView,
+      ImmediateProgramSubtypingInfo immediateSubtypingInfo,
+      AccessModifier accessModifier,
+      AccessModifierNamingState namingState) {
+    super(appView, immediateSubtypingInfo);
+    this.accessModifier = accessModifier;
+    this.namingState = namingState;
+  }
+
+  /** Predicate that specifies which program classes the depth-first traversal should start from. */
+  @Override
+  public boolean isRoot(DexProgramClass clazz) {
+    return Iterables.all(
+        clazz.allImmediateSupertypes(),
+        supertype -> asProgramClassOrNull(appView.definitionFor(supertype)) == null);
+  }
+
+  /** Called when {@param clazz} is visited for the first time during the downwards traversal. */
+  @Override
+  public void visit(DexProgramClass clazz) {
+    // TODO(b/279126633): Store a top down traversal state for the current class, which contains the
+    //  protected and public method signatures when traversing downwards to enable publicizing of
+    //  package private methods with illegal overrides.
+    states.put(clazz.getType(), TopDownTraversalState.empty());
+  }
+
+  /** Called during backtracking when all subclasses of {@param clazz} have been processed. */
+  @Override
+  public void prune(DexProgramClass clazz) {
+    // Remove the traversal state since all subclasses have now been processed.
+    states.remove(clazz.getType());
+
+    // Remove and join the bottom up traversal states of the subclasses.
+    BottomUpTraversalState state = new BottomUpTraversalState();
+    forEachSubClass(
+        clazz,
+        subclass -> {
+          BottomUpTraversalState subState =
+              MapUtils.removeOrDefault(states, subclass.getType(), BottomUpTraversalState.empty())
+                  .asBottomUpTraversalState();
+          state.add(subState);
+        });
+
+    // Apply access modification to the class and its members.
+    accessModifier.processClass(clazz, namingState, state);
+
+    // Add the methods of the current class.
+    clazz.forEachProgramVirtualMethod(state::addMethod);
+
+    // Store the bottom up traversal state for the current class.
+    if (state.isEmpty()) {
+      states.remove(clazz.getType());
+    } else {
+      states.put(clazz.getType(), state);
+    }
+  }
+
+  abstract static class TraversalState {
+
+    BottomUpTraversalState asBottomUpTraversalState() {
+      return null;
+    }
+
+    TopDownTraversalState asTopDownTraversalState() {
+      return null;
+    }
+  }
+
+  // TODO(b/279126633): Collect the protected and public method signatures when traversing downwards
+  //  to enable publicizing of package private methods with illegal overrides.
+  static class TopDownTraversalState extends TraversalState {
+
+    private static final TopDownTraversalState EMPTY = new TopDownTraversalState();
+
+    static TopDownTraversalState empty() {
+      return EMPTY;
+    }
+
+    @Override
+    TopDownTraversalState asTopDownTraversalState() {
+      return this;
+    }
+
+    boolean isEmpty() {
+      return true;
+    }
+  }
+
+  static class BottomUpTraversalState extends TraversalState {
+
+    private static final BottomUpTraversalState EMPTY =
+        new BottomUpTraversalState(DexMethodSignatureMap.empty());
+
+    // The set of non-private virtual methods below the current class.
+    DexMethodSignatureMap<Set<String>> nonPrivateVirtualMethods;
+
+    BottomUpTraversalState() {
+      this(DexMethodSignatureMap.create());
+    }
+
+    BottomUpTraversalState(DexMethodSignatureMap<Set<String>> packagePrivateMethods) {
+      this.nonPrivateVirtualMethods = packagePrivateMethods;
+    }
+
+    static BottomUpTraversalState empty() {
+      return EMPTY;
+    }
+
+    @Override
+    BottomUpTraversalState asBottomUpTraversalState() {
+      return this;
+    }
+
+    void add(BottomUpTraversalState backtrackingState) {
+      backtrackingState.nonPrivateVirtualMethods.forEach(
+          (methodSignature, packageDescriptors) ->
+              this.nonPrivateVirtualMethods
+                  .computeIfAbsent(methodSignature, ignoreKey(HashSet::new))
+                  .addAll(packageDescriptors));
+    }
+
+    void addMethod(ProgramMethod method) {
+      assert method.getDefinition().belongsToVirtualPool();
+      nonPrivateVirtualMethods
+          .computeIfAbsent(method.getMethodSignature(), ignoreKey(Sets::newIdentityHashSet))
+          .add(method.getHolderType().getPackageDescriptor());
+    }
+
+    boolean hasIllegalOverrideOfPackagePrivateMethod(ProgramMethod method) {
+      assert method.getAccessFlags().isPackagePrivate();
+      String methodPackageDescriptor = method.getHolderType().getPackageDescriptor();
+      return Iterables.any(
+          nonPrivateVirtualMethods.getOrDefault(
+              method.getMethodSignature(), Collections.emptySet()),
+          methodOverridePackageDescriptor ->
+              !methodOverridePackageDescriptor.equals(methodPackageDescriptor));
+    }
+
+    boolean isEmpty() {
+      return nonPrivateVirtualMethods.isEmpty();
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/InternalOptions.java b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
index 538b9ee..6259f3a 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -74,6 +74,7 @@
 import com.android.tools.r8.naming.ClassNameMapper;
 import com.android.tools.r8.naming.MapConsumer;
 import com.android.tools.r8.naming.MapVersion;
+import com.android.tools.r8.optimize.accessmodification.AccessModifierOptions;
 import com.android.tools.r8.optimize.argumentpropagation.ArgumentPropagatorEventConsumer;
 import com.android.tools.r8.optimize.redundantbridgeremoval.RedundantBridgeRemovalOptions;
 import com.android.tools.r8.origin.Origin;
@@ -843,8 +844,7 @@
 
   @Override
   public boolean isAccessModificationEnabled() {
-    return getProguardConfiguration() != null
-        && getProguardConfiguration().isAccessModificationAllowed();
+    return accessModifierOptions.isAccessModificationEnabled();
   }
 
   @Override
@@ -879,6 +879,7 @@
 
   public boolean debug = false;
 
+  private final AccessModifierOptions accessModifierOptions = new AccessModifierOptions(this);
   private final RewriteArrayOptions rewriteArrayOptions = new RewriteArrayOptions();
   private final CallSiteOptimizationOptions callSiteOptimizationOptions =
       new CallSiteOptimizationOptions();
@@ -956,6 +957,10 @@
     return desugarSpecificOptions;
   }
 
+  public AccessModifierOptions getAccessModifierOptions() {
+    return accessModifierOptions;
+  }
+
   public CfCodeAnalysisOptions getCfCodeAnalysisOptions() {
     return cfCodeAnalysisOptions;
   }