| // Copyright (c) 2021, 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.graph; |
| |
| import com.android.tools.r8.graph.GraphLens.NonIdentityGraphLens; |
| import com.android.tools.r8.graph.proto.RewrittenPrototypeDescription; |
| import com.android.tools.r8.ir.code.InvokeType; |
| import com.android.tools.r8.utils.IterableUtils; |
| import com.android.tools.r8.utils.collections.BidirectionalManyToManyRepresentativeMap; |
| import com.android.tools.r8.utils.collections.BidirectionalManyToOneRepresentativeMap; |
| import com.android.tools.r8.utils.collections.EmptyBidirectionalOneToOneMap; |
| import java.util.Collections; |
| import java.util.Map; |
| import java.util.function.Function; |
| import java.util.stream.Collectors; |
| |
| /** |
| * GraphLens implementation with a parent lens using a simple mapping for type, method and field |
| * mapping. |
| * |
| * <p>Subclasses can override the lookup methods. |
| * |
| * <p>For method mapping where invocation type can change just override {@link |
| * #mapInvocationType(DexMethod, DexMethod, InvokeType)} if the default name mapping applies, and |
| * only invocation type might need to change. |
| */ |
| public class NestedGraphLens extends NonIdentityGraphLens { |
| |
| protected static final EmptyBidirectionalOneToOneMap<DexField, DexField> EMPTY_FIELD_MAP = |
| new EmptyBidirectionalOneToOneMap<>(); |
| protected static final EmptyBidirectionalOneToOneMap<DexMethod, DexMethod> EMPTY_METHOD_MAP = |
| new EmptyBidirectionalOneToOneMap<>(); |
| protected static final Map<DexType, DexType> EMPTY_TYPE_MAP = Collections.emptyMap(); |
| |
| protected final BidirectionalManyToOneRepresentativeMap<DexField, DexField> fieldMap; |
| protected final Function<DexMethod, DexMethod> methodMap; |
| protected final Map<DexType, DexType> typeMap; |
| |
| // Map that stores the new signature of methods that have been affected by class merging, unused |
| // argument removal, repackaging, synthetic finalization, etc. This is needed to generate a |
| // correct mapping file in the end. |
| // |
| // The difference to `methodMap` is that `methodMap` specifies how invoke-method instructions |
| // should be rewritten, whereas this map specifies where methods have been moved. In most cases |
| // the two maps are the same, but in a few cases we move a method m1 to m2 and rewrite invokes to |
| // m1 to m3. |
| // |
| // The static type of this map is a many-to-many map to facilitate both one-to-many mappings and |
| // many-to-one mappings. Currently, the concrete map is always *either* a one-to-many or a |
| // many-to-one map, and not a many-to-many map. |
| // |
| // One-to-many mappings are generally found when methods are split into two. This could be due to |
| // the code object being moved elsewhere (e.g., interface desugaring). |
| // |
| // Many-to-one mappings are generally found when methods are shared, e.g., due to horizontal class |
| // merging of synthetic finalization. |
| protected BidirectionalManyToManyRepresentativeMap<DexMethod, DexMethod> newMethodSignatures; |
| |
| // Overrides this if the sub type needs to be a nested lens while it doesn't have any mappings |
| // at all, e.g., publicizer lens that changes invocation type only. |
| protected boolean isLegitimateToHaveEmptyMappings() { |
| return false; |
| } |
| |
| public NestedGraphLens(AppView<?> appView) { |
| this( |
| appView, |
| NestedGraphLens.EMPTY_FIELD_MAP, |
| NestedGraphLens.EMPTY_METHOD_MAP, |
| NestedGraphLens.EMPTY_TYPE_MAP); |
| } |
| |
| public NestedGraphLens( |
| AppView<?> appView, |
| BidirectionalManyToOneRepresentativeMap<DexField, DexField> fieldMap, |
| BidirectionalManyToOneRepresentativeMap<DexMethod, DexMethod> methodMap, |
| Map<DexType, DexType> typeMap) { |
| this(appView, fieldMap, methodMap.getForwardMap(), typeMap, methodMap); |
| } |
| |
| public NestedGraphLens( |
| AppView<?> appView, |
| BidirectionalManyToOneRepresentativeMap<DexField, DexField> fieldMap, |
| Map<DexMethod, DexMethod> methodMap, |
| Map<DexType, DexType> typeMap, |
| BidirectionalManyToManyRepresentativeMap<DexMethod, DexMethod> newMethodSignatures) { |
| this(appView, fieldMap, methodMap::get, typeMap, newMethodSignatures); |
| assert !typeMap.isEmpty() |
| || !methodMap.isEmpty() |
| || !fieldMap.isEmpty() |
| || isLegitimateToHaveEmptyMappings(); |
| } |
| |
| public NestedGraphLens( |
| AppView<?> appView, |
| BidirectionalManyToOneRepresentativeMap<DexField, DexField> fieldMap, |
| Function<DexMethod, DexMethod> methodMap, |
| Map<DexType, DexType> typeMap, |
| BidirectionalManyToManyRepresentativeMap<DexMethod, DexMethod> newMethodSignatures) { |
| super(appView); |
| this.fieldMap = fieldMap; |
| this.methodMap = methodMap; |
| this.typeMap = typeMap; |
| this.newMethodSignatures = newMethodSignatures; |
| } |
| |
| protected DexType internalGetOriginalType(DexType previous) { |
| return previous; |
| } |
| |
| protected Iterable<DexType> internalGetOriginalTypes(DexType previous) { |
| return IterableUtils.singleton(internalGetOriginalType(previous)); |
| } |
| |
| @Override |
| public DexType getOriginalType(DexType type) { |
| return getPrevious().getOriginalType(internalGetOriginalType(type)); |
| } |
| |
| @Override |
| public Iterable<DexType> getOriginalTypes(DexType type) { |
| return IterableUtils.flatMap(internalGetOriginalTypes(type), getPrevious()::getOriginalTypes); |
| } |
| |
| @Override |
| public DexField getOriginalFieldSignature(DexField field) { |
| DexField originalField = fieldMap.getRepresentativeKeyOrDefault(field, field); |
| return getPrevious().getOriginalFieldSignature(originalField); |
| } |
| |
| @Override |
| public DexField getRenamedFieldSignature(DexField originalField, GraphLens codeLens) { |
| if (this == codeLens) { |
| return originalField; |
| } |
| DexField renamedField = getPrevious().getRenamedFieldSignature(originalField, codeLens); |
| return internalGetNextFieldSignature(renamedField); |
| } |
| |
| @Override |
| public DexMethod getRenamedMethodSignature(DexMethod originalMethod, GraphLens applied) { |
| if (this == applied) { |
| return originalMethod; |
| } |
| DexMethod renamedMethod = getPrevious().getRenamedMethodSignature(originalMethod, applied); |
| return getNextMethodSignature(renamedMethod); |
| } |
| |
| @Override |
| protected DexType internalDescribeLookupClassType(DexType previous) { |
| return typeMap.getOrDefault(previous, previous); |
| } |
| |
| @Override |
| protected FieldLookupResult internalDescribeLookupField(FieldLookupResult previous) { |
| if (previous.hasReboundReference()) { |
| // Rewrite the rebound reference and then "fixup" the non-rebound reference. |
| DexField rewrittenReboundReference = previous.getRewrittenReboundReference(fieldMap); |
| DexField rewrittenNonReboundReference = |
| previous.getReference() == previous.getReboundReference() |
| ? rewrittenReboundReference |
| : rewrittenReboundReference.withHolder( |
| internalDescribeLookupClassType(previous.getReference().getHolderType()), |
| dexItemFactory()); |
| return FieldLookupResult.builder(this) |
| .setReboundReference(rewrittenReboundReference) |
| .setReference(rewrittenNonReboundReference) |
| .setReadCastType(previous.getRewrittenReadCastType(this::internalDescribeLookupClassType)) |
| .setWriteCastType( |
| previous.getRewrittenWriteCastType(this::internalDescribeLookupClassType)) |
| .build(); |
| } else { |
| // TODO(b/168282032): We should always have the rebound reference, so this should become |
| // unreachable. |
| DexField rewrittenReference = previous.getRewrittenReference(fieldMap); |
| return FieldLookupResult.builder(this) |
| .setReference(rewrittenReference) |
| .setReadCastType(previous.getRewrittenReadCastType(this::internalDescribeLookupClassType)) |
| .setWriteCastType( |
| previous.getRewrittenWriteCastType(this::internalDescribeLookupClassType)) |
| .build(); |
| } |
| } |
| |
| @Override |
| public MethodLookupResult internalDescribeLookupMethod( |
| MethodLookupResult previous, DexMethod context) { |
| if (previous.hasReboundReference()) { |
| // TODO(sgjesse): Should we always do interface to virtual mapping? Is it a performance win |
| // that only subclasses which are known to need it actually do it? |
| DexMethod rewrittenReboundReference = previous.getRewrittenReboundReference(methodMap); |
| DexMethod rewrittenReference = |
| previous.getReference() == previous.getReboundReference() |
| ? rewrittenReboundReference |
| : // This assumes that the holder will always be moved in lock-step with the method! |
| rewrittenReboundReference.withHolder( |
| internalDescribeLookupClassType(previous.getReference().getHolderType()), |
| dexItemFactory()); |
| return MethodLookupResult.builder(this) |
| .setReference(rewrittenReference) |
| .setReboundReference(rewrittenReboundReference) |
| .setPrototypeChanges( |
| internalDescribePrototypeChanges( |
| previous.getPrototypeChanges(), rewrittenReboundReference)) |
| .setType( |
| mapInvocationType( |
| rewrittenReboundReference, previous.getReference(), previous.getType())) |
| .build(); |
| } else { |
| // TODO(b/168282032): We should always have the rebound reference, so this should become |
| // unreachable. |
| DexMethod newMethod = methodMap.apply(previous.getReference()); |
| if (newMethod == null) { |
| newMethod = previous.getReference(); |
| } |
| RewrittenPrototypeDescription newPrototypeChanges = |
| internalDescribePrototypeChanges(previous.getPrototypeChanges(), newMethod); |
| if (newMethod == previous.getReference() |
| && newPrototypeChanges == previous.getPrototypeChanges()) { |
| return previous; |
| } |
| // TODO(sgjesse): Should we always do interface to virtual mapping? Is it a performance win |
| // that only subclasses which are known to need it actually do it? |
| return MethodLookupResult.builder(this) |
| .setReference(newMethod) |
| .setPrototypeChanges(newPrototypeChanges) |
| .setType(mapInvocationType(newMethod, previous.getReference(), previous.getType())) |
| .build(); |
| } |
| } |
| |
| @Override |
| public RewrittenPrototypeDescription lookupPrototypeChangesForMethodDefinition( |
| DexMethod method, GraphLens codeLens) { |
| if (this == codeLens) { |
| return getIdentityLens().lookupPrototypeChangesForMethodDefinition(method, codeLens); |
| } |
| DexMethod previous = getPreviousMethodSignature(method); |
| RewrittenPrototypeDescription lookup = |
| getPrevious().lookupPrototypeChangesForMethodDefinition(previous, codeLens); |
| return internalDescribePrototypeChanges(lookup, method); |
| } |
| |
| protected RewrittenPrototypeDescription internalDescribePrototypeChanges( |
| RewrittenPrototypeDescription prototypeChanges, DexMethod method) { |
| return prototypeChanges; |
| } |
| |
| protected DexField internalGetNextFieldSignature(DexField field) { |
| return fieldMap.getOrDefault(field, field); |
| } |
| |
| @Override |
| public DexMethod getPreviousMethodSignature(DexMethod method) { |
| return newMethodSignatures.getRepresentativeKeyOrDefault(method, method); |
| } |
| |
| @Override |
| public DexMethod getNextMethodSignature(DexMethod method) { |
| return newMethodSignatures.getRepresentativeValueOrDefault(method, method); |
| } |
| |
| /** |
| * Default invocation type mapping. |
| * |
| * <p>This is an identity mapping. If a subclass need invocation type mapping either override this |
| * method or {@link #lookupMethod(DexMethod, DexMethod, InvokeType)} |
| */ |
| protected InvokeType mapInvocationType( |
| DexMethod newMethod, DexMethod originalMethod, InvokeType type) { |
| return type; |
| } |
| |
| /** |
| * Standard mapping between interface and virtual invoke type. |
| * |
| * <p>Handle methods moved from interface to class or class to interface. |
| */ |
| public static InvokeType mapVirtualInterfaceInvocationTypes( |
| DexDefinitionSupplier definitions, |
| DexMethod newMethod, |
| DexMethod originalMethod, |
| InvokeType type) { |
| if (type == InvokeType.VIRTUAL || type == InvokeType.INTERFACE) { |
| // Get the invoke type of the actual definition. |
| DexClass newTargetClass = definitions.definitionFor(newMethod.getHolderType()); |
| if (newTargetClass == null) { |
| return type; |
| } |
| DexClass originalTargetClass = definitions.definitionFor(originalMethod.getHolderType()); |
| if (originalTargetClass != null |
| && (originalTargetClass.isInterface() ^ (type == InvokeType.INTERFACE))) { |
| // The invoke was wrong to start with, so we keep it wrong. This is to ensure we get |
| // the IncompatibleClassChangeError the original invoke would have triggered. |
| return newTargetClass.accessFlags.isInterface() ? InvokeType.VIRTUAL : InvokeType.INTERFACE; |
| } |
| return newTargetClass.accessFlags.isInterface() ? InvokeType.INTERFACE : InvokeType.VIRTUAL; |
| } |
| return type; |
| } |
| |
| @Override |
| public boolean isContextFreeForMethods() { |
| return getPrevious().isContextFreeForMethods(); |
| } |
| |
| @Override |
| public boolean verifyIsContextFreeForMethod(DexMethod method) { |
| assert getPrevious().verifyIsContextFreeForMethod(method); |
| return true; |
| } |
| |
| @Override |
| public String toString() { |
| StringBuilder builder = new StringBuilder(); |
| typeMap.forEach( |
| (from, to) -> |
| builder |
| .append(from.getTypeName()) |
| .append(" -> ") |
| .append(to.getTypeName()) |
| .append(System.lineSeparator())); |
| fieldMap.forEachManyToOneMapping( |
| (fromSet, to) -> { |
| builder.append( |
| fromSet.stream() |
| .map(DexField::toSourceString) |
| .collect(Collectors.joining("," + System.lineSeparator()))); |
| builder.append(" -> "); |
| builder.append(to.toSourceString()).append(System.lineSeparator()); |
| }); |
| builder.append(getPrevious().toString()); |
| return builder.toString(); |
| } |
| } |