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;
}