|  | // 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.horizontalclassmerging; | 
|  |  | 
|  | import static com.android.tools.r8.dex.Constants.TEMPORARY_INSTANCE_INITIALIZER_PREFIX; | 
|  |  | 
|  | import com.android.tools.r8.cf.CfVersion; | 
|  | import com.android.tools.r8.dex.Constants; | 
|  | import com.android.tools.r8.graph.AppInfoWithClassHierarchy; | 
|  | import com.android.tools.r8.graph.AppView; | 
|  | import com.android.tools.r8.graph.Code; | 
|  | 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.DexType; | 
|  | import com.android.tools.r8.graph.DexTypeUtils; | 
|  | import com.android.tools.r8.graph.MethodAccessFlags; | 
|  | import com.android.tools.r8.graph.ProgramMethod; | 
|  | import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger.Mode; | 
|  | import com.android.tools.r8.horizontalclassmerging.code.ConstructorEntryPointSynthesizedCode; | 
|  | import com.android.tools.r8.horizontalclassmerging.code.SyntheticInitializerConverter; | 
|  | import com.android.tools.r8.ir.conversion.ExtraConstantIntParameter; | 
|  | import com.android.tools.r8.ir.conversion.ExtraParameter; | 
|  | import com.android.tools.r8.ir.conversion.ExtraUnusedNullParameter; | 
|  | import com.android.tools.r8.utils.ArrayUtils; | 
|  | import com.android.tools.r8.utils.BooleanUtils; | 
|  | import com.android.tools.r8.utils.ListUtils; | 
|  | import com.android.tools.r8.utils.structural.Ordered; | 
|  | import com.google.common.collect.ImmutableList; | 
|  | import com.google.common.collect.Iterables; | 
|  | import it.unimi.dsi.fastutil.ints.Int2ReferenceAVLTreeMap; | 
|  | import it.unimi.dsi.fastutil.ints.Int2ReferenceSortedMap; | 
|  | import it.unimi.dsi.fastutil.objects.Reference2IntMap; | 
|  | import java.util.ArrayList; | 
|  | import java.util.Collections; | 
|  | import java.util.List; | 
|  |  | 
|  | public class InstanceInitializerMerger { | 
|  |  | 
|  | private final AppView<? extends AppInfoWithClassHierarchy> appView; | 
|  | private final Reference2IntMap<DexType> classIdentifiers; | 
|  | private final DexItemFactory dexItemFactory; | 
|  | private final MergeGroup group; | 
|  | private final List<ProgramMethod> instanceInitializers; | 
|  | private final InstanceInitializerDescription instanceInitializerDescription; | 
|  | private final HorizontalClassMergerGraphLens.Builder lensBuilder; | 
|  | private final Mode mode; | 
|  |  | 
|  | InstanceInitializerMerger( | 
|  | AppView<? extends AppInfoWithClassHierarchy> appView, | 
|  | Reference2IntMap<DexType> classIdentifiers, | 
|  | MergeGroup group, | 
|  | List<ProgramMethod> instanceInitializers, | 
|  | HorizontalClassMergerGraphLens.Builder lensBuilder, | 
|  | Mode mode) { | 
|  | this(appView, classIdentifiers, group, instanceInitializers, lensBuilder, mode, null); | 
|  | } | 
|  |  | 
|  | InstanceInitializerMerger( | 
|  | AppView<? extends AppInfoWithClassHierarchy> appView, | 
|  | Reference2IntMap<DexType> classIdentifiers, | 
|  | MergeGroup group, | 
|  | List<ProgramMethod> instanceInitializers, | 
|  | HorizontalClassMergerGraphLens.Builder lensBuilder, | 
|  | Mode mode, | 
|  | InstanceInitializerDescription instanceInitializerDescription) { | 
|  | this.appView = appView; | 
|  | this.classIdentifiers = classIdentifiers; | 
|  | this.dexItemFactory = appView.dexItemFactory(); | 
|  | this.group = group; | 
|  | this.instanceInitializers = instanceInitializers; | 
|  | this.instanceInitializerDescription = instanceInitializerDescription; | 
|  | this.lensBuilder = lensBuilder; | 
|  | this.mode = mode; | 
|  |  | 
|  | // Constructors should not be empty and all constructors should have the same prototype unless | 
|  | // equivalent. | 
|  | assert !instanceInitializers.isEmpty(); | 
|  | assert instanceInitializers.stream().map(ProgramMethod::getProto).distinct().count() == 1 | 
|  | || instanceInitializerDescription != null; | 
|  | } | 
|  |  | 
|  | public int getArity() { | 
|  | return instanceInitializers.iterator().next().getReference().getArity(); | 
|  | } | 
|  |  | 
|  | public List<ProgramMethod> getInstanceInitializers() { | 
|  | return instanceInitializers; | 
|  | } | 
|  |  | 
|  | private CfVersion getNewClassFileVersion() { | 
|  | CfVersion classFileVersion = null; | 
|  | for (ProgramMethod instanceInitializer : instanceInitializers) { | 
|  | if (instanceInitializer.getDefinition().hasClassFileVersion()) { | 
|  | classFileVersion = | 
|  | Ordered.maxIgnoreNull( | 
|  | classFileVersion, instanceInitializer.getDefinition().getClassFileVersion()); | 
|  | } | 
|  | } | 
|  | return classFileVersion; | 
|  | } | 
|  |  | 
|  | private DexMethod getNewMethodReference(ProgramMethod representative) { | 
|  | return getNewMethodReference(representative, false); | 
|  | } | 
|  |  | 
|  | private DexMethod getNewMethodReference(ProgramMethod representative, boolean needsClassId) { | 
|  | DexType[] oldParameters = representative.getParameters().values; | 
|  | DexType[] newParameters = | 
|  | new DexType[representative.getParameters().size() + BooleanUtils.intValue(needsClassId)]; | 
|  | System.arraycopy(oldParameters, 0, newParameters, 0, oldParameters.length); | 
|  | for (int i = 0; i < oldParameters.length; i++) { | 
|  | final int parameterIndex = i; | 
|  | newParameters[i] = | 
|  | DexTypeUtils.computeLeastUpperBound( | 
|  | appView, | 
|  | Iterables.transform( | 
|  | instanceInitializers, | 
|  | instanceInitializer -> instanceInitializer.getParameter(parameterIndex))); | 
|  | } | 
|  | if (needsClassId) { | 
|  | assert ArrayUtils.last(newParameters) == null; | 
|  | newParameters[newParameters.length - 1] = dexItemFactory.intType; | 
|  | } | 
|  | return dexItemFactory.createInstanceInitializer(group.getTarget().getType(), newParameters); | 
|  | } | 
|  |  | 
|  | private DexMethod getOriginalMethodReference() { | 
|  | return appView.graphLens().getOriginalMethodSignature(getRepresentative().getReference()); | 
|  | } | 
|  |  | 
|  | private ProgramMethod getRepresentative() { | 
|  | return ListUtils.first(instanceInitializers); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Returns a special original method signature for the synthesized constructor that did not exist | 
|  | * prior to horizontal class merging. Otherwise we might accidentally think that the synthesized | 
|  | * constructor corresponds to the previous <init>() method on the target class, which could have | 
|  | * unintended side-effects such as leading to unused argument removal being applied to the | 
|  | * synthesized constructor all-though it by construction doesn't have any unused arguments. | 
|  | */ | 
|  | private DexMethod getSyntheticMethodReference( | 
|  | ClassMethodsBuilder classMethodsBuilder, ProgramMethod representative) { | 
|  | return dexItemFactory.createFreshMethodNameWithoutHolder( | 
|  | "$r8$init$synthetic", | 
|  | representative.getProto(), | 
|  | representative.getHolderType(), | 
|  | classMethodsBuilder::isFresh); | 
|  | } | 
|  |  | 
|  | private Int2ReferenceSortedMap<DexMethod> createClassIdToInstanceInitializerMap() { | 
|  | assert !hasInstanceInitializerDescription(); | 
|  | Int2ReferenceSortedMap<DexMethod> typeConstructorClassMap = new Int2ReferenceAVLTreeMap<>(); | 
|  | for (ProgramMethod instanceInitializer : instanceInitializers) { | 
|  | typeConstructorClassMap.put( | 
|  | classIdentifiers.getInt(instanceInitializer.getHolderType()), | 
|  | lensBuilder.getRenamedMethodSignature(instanceInitializer.getReference())); | 
|  | } | 
|  | return typeConstructorClassMap; | 
|  | } | 
|  |  | 
|  | public int size() { | 
|  | return instanceInitializers.size(); | 
|  | } | 
|  |  | 
|  | public static class Builder { | 
|  |  | 
|  | private final AppView<? extends AppInfoWithClassHierarchy> appView; | 
|  | private final Reference2IntMap<DexType> classIdentifiers; | 
|  | private int estimatedDexCodeSize; | 
|  | private final List<List<ProgramMethod>> instanceInitializerGroups = new ArrayList<>(); | 
|  | private final HorizontalClassMergerGraphLens.Builder lensBuilder; | 
|  | private final Mode mode; | 
|  |  | 
|  | public Builder( | 
|  | AppView<? extends AppInfoWithClassHierarchy> appView, | 
|  | Reference2IntMap<DexType> classIdentifiers, | 
|  | HorizontalClassMergerGraphLens.Builder lensBuilder, | 
|  | Mode mode) { | 
|  | this.appView = appView; | 
|  | this.classIdentifiers = classIdentifiers; | 
|  | this.lensBuilder = lensBuilder; | 
|  | this.mode = mode; | 
|  | createNewGroup(); | 
|  | } | 
|  |  | 
|  | private void createNewGroup() { | 
|  | estimatedDexCodeSize = 0; | 
|  | instanceInitializerGroups.add(new ArrayList<>()); | 
|  | } | 
|  |  | 
|  | public Builder add(ProgramMethod instanceInitializer) { | 
|  | int estimatedMaxSizeInBytes = | 
|  | instanceInitializer.getDefinition().getCode().estimatedDexCodeSizeUpperBoundInBytes(); | 
|  | // If the constructor gets too large, then the constructor should be merged into a new group. | 
|  | if (estimatedDexCodeSize + estimatedMaxSizeInBytes | 
|  | > appView.options().minimumVerificationSizeLimitInBytes() / 2 | 
|  | && estimatedDexCodeSize > 0) { | 
|  | createNewGroup(); | 
|  | } | 
|  |  | 
|  | ListUtils.last(instanceInitializerGroups).add(instanceInitializer); | 
|  | estimatedDexCodeSize += estimatedMaxSizeInBytes; | 
|  | return this; | 
|  | } | 
|  |  | 
|  | public Builder addEquivalent(ProgramMethod instanceInitializer) { | 
|  | ListUtils.last(instanceInitializerGroups).add(instanceInitializer); | 
|  | return this; | 
|  | } | 
|  |  | 
|  | public List<InstanceInitializerMerger> build(MergeGroup group) { | 
|  | assert instanceInitializerGroups.stream().noneMatch(List::isEmpty); | 
|  | return ListUtils.map( | 
|  | instanceInitializerGroups, | 
|  | instanceInitializers -> | 
|  | new InstanceInitializerMerger( | 
|  | appView, classIdentifiers, group, instanceInitializers, lensBuilder, mode)); | 
|  | } | 
|  |  | 
|  | public InstanceInitializerMerger buildSingle( | 
|  | MergeGroup group, InstanceInitializerDescription instanceInitializerDescription) { | 
|  | assert instanceInitializerGroups.stream().noneMatch(List::isEmpty); | 
|  | assert instanceInitializerGroups.size() == 1; | 
|  | List<ProgramMethod> instanceInitializers = ListUtils.first(instanceInitializerGroups); | 
|  | return new InstanceInitializerMerger( | 
|  | appView, | 
|  | classIdentifiers, | 
|  | group, | 
|  | instanceInitializers, | 
|  | lensBuilder, | 
|  | mode, | 
|  | instanceInitializerDescription); | 
|  | } | 
|  | } | 
|  |  | 
|  | private boolean hasInstanceInitializerDescription() { | 
|  | return instanceInitializerDescription != null; | 
|  | } | 
|  |  | 
|  | private DexMethod moveInstanceInitializer( | 
|  | ClassMethodsBuilder classMethodsBuilder, ProgramMethod instanceInitializer) { | 
|  | DexMethod method = | 
|  | dexItemFactory.createFreshMethodNameWithHolder( | 
|  | TEMPORARY_INSTANCE_INITIALIZER_PREFIX, | 
|  | instanceInitializer.getHolderType(), | 
|  | instanceInitializer.getProto(), | 
|  | group.getTarget().getType(), | 
|  | classMethodsBuilder::isFresh); | 
|  |  | 
|  | DexEncodedMethod encodedMethod = | 
|  | instanceInitializer.getDefinition().toTypeSubstitutedMethod(method); | 
|  | encodedMethod.getMutableOptimizationInfo().markForceInline(); | 
|  | encodedMethod.getAccessFlags().unsetConstructor(); | 
|  | encodedMethod.getAccessFlags().unsetPublic(); | 
|  | encodedMethod.getAccessFlags().unsetProtected(); | 
|  | encodedMethod.getAccessFlags().setPrivate(); | 
|  | classMethodsBuilder.addDirectMethod(encodedMethod); | 
|  |  | 
|  | return method; | 
|  | } | 
|  |  | 
|  | private MethodAccessFlags getNewAccessFlags() { | 
|  | // TODO(b/164998929): ensure this behaviour is correct, should probably calculate upper bound | 
|  | return MethodAccessFlags.fromSharedAccessFlags( | 
|  | Constants.ACC_PUBLIC | Constants.ACC_SYNTHETIC, true); | 
|  | } | 
|  |  | 
|  | private Code getNewCode( | 
|  | DexMethod newMethodReference, | 
|  | DexMethod syntheticMethodReference, | 
|  | boolean needsClassId, | 
|  | int extraNulls) { | 
|  | if (hasInstanceInitializerDescription()) { | 
|  | return instanceInitializerDescription.createCfCode( | 
|  | newMethodReference, | 
|  | getOriginalMethodReference(), | 
|  | syntheticMethodReference, | 
|  | group, | 
|  | needsClassId, | 
|  | extraNulls); | 
|  | } | 
|  | if (isSingleton() && !group.hasClassIdField()) { | 
|  | return getRepresentative().getDefinition().getCode(); | 
|  | } | 
|  | return new ConstructorEntryPointSynthesizedCode( | 
|  | createClassIdToInstanceInitializerMap(), | 
|  | newMethodReference, | 
|  | group.hasClassIdField() ? group.getClassIdField() : null, | 
|  | syntheticMethodReference); | 
|  | } | 
|  |  | 
|  | private boolean isSingleton() { | 
|  | return instanceInitializers.size() == 1; | 
|  | } | 
|  |  | 
|  | /** Synthesize a new method which selects the constructor based on a parameter type. */ | 
|  | void merge( | 
|  | ClassMethodsBuilder classMethodsBuilder, | 
|  | SyntheticArgumentClass syntheticArgumentClass, | 
|  | SyntheticInitializerConverter.Builder syntheticInitializerConverterBuilder) { | 
|  | ProgramMethod representative = ListUtils.first(instanceInitializers); | 
|  |  | 
|  | // Create merged instance initializer reference. | 
|  | boolean needsClassId = | 
|  | instanceInitializers.size() > 1 | 
|  | && (!hasInstanceInitializerDescription() || group.hasClassIdField()); | 
|  | assert mode.isInitial() || !needsClassId; | 
|  |  | 
|  | DexMethod newMethodReferenceTemplate = getNewMethodReference(representative, needsClassId); | 
|  | assert mode.isInitial() || classMethodsBuilder.isFresh(newMethodReferenceTemplate); | 
|  |  | 
|  | DexMethod newMethodReference = | 
|  | dexItemFactory.createInstanceInitializerWithFreshProto( | 
|  | newMethodReferenceTemplate, | 
|  | mode.isInitial() ? syntheticArgumentClass.getArgumentClasses() : ImmutableList.of(), | 
|  | classMethodsBuilder::isFresh); | 
|  | int extraNulls = newMethodReference.getArity() - newMethodReferenceTemplate.getArity(); | 
|  |  | 
|  | // Verify that the merge is a simple renaming in the final round of merging. | 
|  | assert mode.isInitial() || newMethodReference == newMethodReferenceTemplate; | 
|  |  | 
|  | // Move instance initializers to target class. | 
|  | if (hasInstanceInitializerDescription()) { | 
|  | lensBuilder.moveMethods(instanceInitializers, newMethodReference); | 
|  | } else if (isSingleton() && !group.hasClassIdField()) { | 
|  | lensBuilder.moveMethod(representative.getReference(), newMethodReference, true); | 
|  | } else { | 
|  | for (ProgramMethod instanceInitializer : instanceInitializers) { | 
|  | DexMethod movedInstanceInitializer = | 
|  | moveInstanceInitializer(classMethodsBuilder, instanceInitializer); | 
|  | lensBuilder.mapMethod(movedInstanceInitializer, movedInstanceInitializer); | 
|  | lensBuilder.recordNewMethodSignature( | 
|  | instanceInitializer.getReference(), movedInstanceInitializer); | 
|  | } | 
|  | } | 
|  |  | 
|  | // Add a mapping from a synthetic name to the synthetic constructor. | 
|  | DexMethod syntheticMethodReference = | 
|  | getSyntheticMethodReference(classMethodsBuilder, representative); | 
|  | if (!isSingleton() || group.hasClassIdField()) { | 
|  | lensBuilder.recordNewMethodSignature(syntheticMethodReference, newMethodReference, true); | 
|  | } | 
|  |  | 
|  | // Map each of the instance initializers to the new instance initializer in the graph lens. | 
|  | for (ProgramMethod instanceInitializer : instanceInitializers) { | 
|  | List<ExtraParameter> extraParameters = new ArrayList<>(); | 
|  | if (needsClassId) { | 
|  | int classIdentifier = classIdentifiers.getInt(instanceInitializer.getHolderType()); | 
|  | extraParameters.add(new ExtraConstantIntParameter(classIdentifier)); | 
|  | } | 
|  | extraParameters.addAll(Collections.nCopies(extraNulls, new ExtraUnusedNullParameter())); | 
|  | lensBuilder.mapMergedConstructor( | 
|  | instanceInitializer.getReference(), newMethodReference, extraParameters); | 
|  | } | 
|  |  | 
|  | DexEncodedMethod representativeMethod = representative.getDefinition(); | 
|  | DexEncodedMethod newInstanceInitializer = | 
|  | DexEncodedMethod.syntheticBuilder() | 
|  | .setMethod(newMethodReference) | 
|  | .setAccessFlags(getNewAccessFlags()) | 
|  | .setCode( | 
|  | getNewCode(newMethodReference, syntheticMethodReference, needsClassId, extraNulls)) | 
|  | .setClassFileVersion(getNewClassFileVersion()) | 
|  | .setApiLevelForDefinition(representativeMethod.getApiLevelForDefinition()) | 
|  | .setApiLevelForCode(representativeMethod.getApiLevelForCode()) | 
|  | .build(); | 
|  | classMethodsBuilder.addDirectMethod(newInstanceInitializer); | 
|  |  | 
|  | if (mode.isFinal()) { | 
|  | if (appView.options().isGeneratingDex() && !newInstanceInitializer.getCode().isDexCode()) { | 
|  | syntheticInitializerConverterBuilder.add( | 
|  | new ProgramMethod(group.getTarget(), newInstanceInitializer)); | 
|  | } else { | 
|  | assert appView.options().isGeneratingDex() | 
|  | || newInstanceInitializer.getCode().isCfWritableCode(); | 
|  | } | 
|  | } | 
|  | } | 
|  | } |