Record field names minification
Bug: 201277582
Change-Id: I1d09fccc1dada55c39f4fe389e483acf5fd5932e
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/records/RecordCfToCfRewriter.java b/src/main/java/com/android/tools/r8/ir/desugar/records/RecordCfToCfRewriter.java
new file mode 100644
index 0000000..76c468a6
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/desugar/records/RecordCfToCfRewriter.java
@@ -0,0 +1,106 @@
+// 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.ir.desugar.records;
+
+import static com.android.tools.r8.ir.desugar.records.RecordRewriterHelper.isInvokeDynamicOnRecord;
+import static com.android.tools.r8.ir.desugar.records.RecordRewriterHelper.parseInvokeDynamicOnRecord;
+
+import com.android.tools.r8.cf.code.CfInvokeDynamic;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexEncodedField;
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMethodHandle;
+import com.android.tools.r8.graph.DexMethodHandle.MethodHandleType;
+import com.android.tools.r8.graph.DexString;
+import com.android.tools.r8.graph.DexValue;
+import com.android.tools.r8.graph.DexValue.DexValueMethodHandle;
+import com.android.tools.r8.graph.DexValue.DexValueString;
+import com.android.tools.r8.graph.DexValue.DexValueType;
+import com.android.tools.r8.graph.GraphLens;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.ir.desugar.records.RecordRewriterHelper.RecordInvokeDynamic;
+import com.android.tools.r8.naming.NamingLens;
+import java.util.ArrayList;
+
+/** Used to shrink records in Cf to Cf compilations */
+public class RecordCfToCfRewriter {
+
+ private final AppView<?> appView;
+
+ public static RecordCfToCfRewriter create(AppView<?> appView) {
+ if (appView.enableWholeProgramOptimizations()
+ && appView.options().isGeneratingClassFiles()
+ && appView.options().testing.enableRecordModeling) {
+ return new RecordCfToCfRewriter(appView);
+ }
+ return null;
+ }
+
+ private RecordCfToCfRewriter(AppView<?> appView) {
+ this.appView = appView;
+ }
+
+ // Called after final tree shaking, prune and minify field names and field values.
+ public CfInvokeDynamic rewriteRecordInvokeDynamic(
+ CfInvokeDynamic invokeDynamic, ProgramMethod context, NamingLens namingLens) {
+ if (!isInvokeDynamicOnRecord(invokeDynamic, appView, context)) {
+ return invokeDynamic;
+ }
+ RecordInvokeDynamic recordInvokeDynamic =
+ parseInvokeDynamicOnRecord(invokeDynamic, appView, context);
+ DexString newFieldNames =
+ recordInvokeDynamic
+ .computeRecordFieldNamesComputationInfo()
+ .internalComputeNameFor(
+ recordInvokeDynamic.getRecordClass().type,
+ appView,
+ appView.graphLens(),
+ namingLens);
+ DexField[] newFields = computePresentFields(appView.graphLens(), recordInvokeDynamic);
+ return writeRecordInvokeDynamic(
+ recordInvokeDynamic.withFieldNamesAndFields(newFieldNames, newFields));
+ }
+
+ private DexField[] computePresentFields(
+ GraphLens graphLens, RecordInvokeDynamic recordInvokeDynamic) {
+ ArrayList<DexField> finalFields = new ArrayList<>();
+ for (DexField field : recordInvokeDynamic.getFields()) {
+ DexEncodedField dexEncodedField =
+ recordInvokeDynamic
+ .getRecordClass()
+ .lookupInstanceField(graphLens.getRenamedFieldSignature(field));
+ if (dexEncodedField != null) {
+ finalFields.add(field);
+ }
+ }
+ DexField[] newFields = new DexField[finalFields.size()];
+ for (int i = 0; i < finalFields.size(); i++) {
+ newFields[i] = finalFields.get(i);
+ }
+ return newFields;
+ }
+
+ private CfInvokeDynamic writeRecordInvokeDynamic(RecordInvokeDynamic recordInvokeDynamic) {
+ DexItemFactory factory = appView.dexItemFactory();
+ DexMethodHandle bootstrapMethod =
+ new DexMethodHandle(
+ MethodHandleType.INVOKE_STATIC, factory.objectMethodsMembers.bootstrap, false, null);
+ ArrayList<DexValue> bootstrapArgs = new ArrayList<>();
+ bootstrapArgs.add(new DexValueType(recordInvokeDynamic.getRecordClass().type));
+ bootstrapArgs.add(new DexValueString(recordInvokeDynamic.getFieldNames()));
+ for (DexField field : recordInvokeDynamic.getFields()) {
+ bootstrapArgs.add(
+ new DexValueMethodHandle(
+ new DexMethodHandle(MethodHandleType.INSTANCE_GET, field, false, null)));
+ }
+ return new CfInvokeDynamic(
+ factory.createCallSite(
+ recordInvokeDynamic.getMethodName(),
+ recordInvokeDynamic.getMethodProto(),
+ bootstrapMethod,
+ bootstrapArgs));
+ }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/records/RecordDesugaring.java b/src/main/java/com/android/tools/r8/ir/desugar/records/RecordDesugaring.java
index 0c1ee6e..3829aa0 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/records/RecordDesugaring.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/records/RecordDesugaring.java
@@ -6,9 +6,12 @@
import static com.android.tools.r8.cf.code.CfStackInstruction.Opcode.Dup;
import static com.android.tools.r8.cf.code.CfStackInstruction.Opcode.Swap;
+import static com.android.tools.r8.ir.desugar.records.RecordRewriterHelper.isInvokeDynamicOnRecord;
+import static com.android.tools.r8.ir.desugar.records.RecordRewriterHelper.parseInvokeDynamicOnRecord;
import com.android.tools.r8.cf.code.CfConstClass;
import com.android.tools.r8.cf.code.CfConstString;
+import com.android.tools.r8.cf.code.CfDexItemBasedConstString;
import com.android.tools.r8.cf.code.CfFieldInstruction;
import com.android.tools.r8.cf.code.CfInstruction;
import com.android.tools.r8.cf.code.CfInvoke;
@@ -21,20 +24,14 @@
import com.android.tools.r8.errors.Unreachable;
import com.android.tools.r8.graph.AppView;
import com.android.tools.r8.graph.CfCode;
-import com.android.tools.r8.graph.DexCallSite;
import com.android.tools.r8.graph.DexClass;
import com.android.tools.r8.graph.DexEncodedMethod;
import com.android.tools.r8.graph.DexField;
import com.android.tools.r8.graph.DexItemFactory;
import com.android.tools.r8.graph.DexMethod;
-import com.android.tools.r8.graph.DexMethodHandle;
import com.android.tools.r8.graph.DexProgramClass;
import com.android.tools.r8.graph.DexProto;
-import com.android.tools.r8.graph.DexString;
import com.android.tools.r8.graph.DexType;
-import com.android.tools.r8.graph.DexValue.DexValueMethodHandle;
-import com.android.tools.r8.graph.DexValue.DexValueString;
-import com.android.tools.r8.graph.DexValue.DexValueType;
import com.android.tools.r8.graph.MethodAccessFlags;
import com.android.tools.r8.graph.ProgramMethod;
import com.android.tools.r8.ir.desugar.CfClassSynthesizerDesugaring;
@@ -48,6 +45,7 @@
import com.android.tools.r8.ir.desugar.LocalStackAllocator;
import com.android.tools.r8.ir.desugar.ProgramAdditions;
import com.android.tools.r8.ir.desugar.records.RecordDesugaringEventConsumer.RecordInstructionDesugaringEventConsumer;
+import com.android.tools.r8.ir.desugar.records.RecordRewriterHelper.RecordInvokeDynamic;
import com.android.tools.r8.ir.synthetic.CallObjectInitCfCodeProvider;
import com.android.tools.r8.ir.synthetic.RecordCfCodeProvider.RecordEqualsCfCodeProvider;
import com.android.tools.r8.ir.synthetic.RecordCfCodeProvider.RecordGetFieldsAsObjectsCfCodeProvider;
@@ -100,14 +98,15 @@
CfCode cfCode = method.getDefinition().getCode().asCfCode();
for (CfInstruction instruction : cfCode.getInstructions()) {
if (instruction.isInvokeDynamic() && needsDesugaring(instruction, method)) {
- prepareInvokeDynamicOnRecord(instruction.asInvokeDynamic(), method, programAdditions);
+ prepareInvokeDynamicOnRecord(instruction.asInvokeDynamic(), programAdditions, method);
}
}
}
private void prepareInvokeDynamicOnRecord(
- CfInvokeDynamic invokeDynamic, ProgramMethod context, ProgramAdditions programAdditions) {
- RecordInvokeDynamic recordInvokeDynamic = parseInvokeDynamicOnRecord(invokeDynamic, context);
+ CfInvokeDynamic invokeDynamic, ProgramAdditions programAdditions, ProgramMethod context) {
+ RecordInvokeDynamic recordInvokeDynamic =
+ parseInvokeDynamicOnRecord(invokeDynamic, appView, context);
if (recordInvokeDynamic.getMethodName() == factory.toStringMethodName
|| recordInvokeDynamic.getMethodName() == factory.hashCodeMethodName) {
ensureGetFieldsAsObjects(recordInvokeDynamic, programAdditions);
@@ -178,8 +177,8 @@
return desugarInvokeDynamicOnRecord(
instruction.asInvokeDynamic(),
localStackAllocator,
- context,
eventConsumer,
+ context,
methodProcessingContext);
}
assert instruction.isInvoke();
@@ -191,71 +190,17 @@
new CfInvoke(cfInvoke.getOpcode(), newMethod, cfInvoke.isInterface()));
}
- static class RecordInvokeDynamic {
-
- private final DexString methodName;
- private final DexString fieldNames;
- private final DexField[] fields;
- private final DexProgramClass recordClass;
-
- private RecordInvokeDynamic(
- DexString methodName,
- DexString fieldNames,
- DexField[] fields,
- DexProgramClass recordClass) {
- this.methodName = methodName;
- this.fieldNames = fieldNames;
- this.fields = fields;
- this.recordClass = recordClass;
- }
-
- DexField[] getFields() {
- return fields;
- }
-
- DexProgramClass getRecordClass() {
- return recordClass;
- }
-
- DexString getFieldNames() {
- return fieldNames;
- }
-
- DexString getMethodName() {
- return methodName;
- }
- }
-
- private RecordInvokeDynamic parseInvokeDynamicOnRecord(
- CfInvokeDynamic invokeDynamic, ProgramMethod context) {
- assert needsDesugaring(invokeDynamic, context);
- DexCallSite callSite = invokeDynamic.getCallSite();
- DexValueType recordValueType = callSite.bootstrapArgs.get(0).asDexValueType();
- DexValueString valueString = callSite.bootstrapArgs.get(1).asDexValueString();
- DexString fieldNames = valueString.getValue();
- DexField[] fields = new DexField[callSite.bootstrapArgs.size() - 2];
- for (int i = 2; i < callSite.bootstrapArgs.size(); i++) {
- DexValueMethodHandle handle = callSite.bootstrapArgs.get(i).asDexValueMethodHandle();
- fields[i - 2] = handle.value.member.asDexField();
- }
- DexProgramClass recordClass =
- appView.definitionFor(recordValueType.getValue()).asProgramClass();
- return new RecordInvokeDynamic(callSite.methodName, fieldNames, fields, recordClass);
- }
-
private List<CfInstruction> desugarInvokeDynamicOnRecord(
CfInvokeDynamic invokeDynamic,
LocalStackAllocator localStackAllocator,
- ProgramMethod context,
CfInstructionDesugaringEventConsumer eventConsumer,
+ ProgramMethod context,
MethodProcessingContext methodProcessingContext) {
- RecordInvokeDynamic recordInvokeDynamic = parseInvokeDynamicOnRecord(invokeDynamic, context);
+ RecordInvokeDynamic recordInvokeDynamic =
+ parseInvokeDynamicOnRecord(invokeDynamic, appView, context);
if (recordInvokeDynamic.getMethodName() == factory.toStringMethodName) {
return desugarInvokeRecordToString(
- recordInvokeDynamic,
- localStackAllocator,
- eventConsumer,
- methodProcessingContext);
+ recordInvokeDynamic, localStackAllocator, eventConsumer, methodProcessingContext);
}
if (recordInvokeDynamic.getMethodName() == factory.hashCodeMethodName) {
return desugarInvokeRecordHashCode(
@@ -392,7 +337,15 @@
ArrayList<CfInstruction> instructions = new ArrayList<>();
instructions.add(new CfInvoke(Opcodes.INVOKESPECIAL, getFieldsAsObjects, false));
instructions.add(new CfConstClass(recordInvokeDynamic.getRecordClass().type, true));
- instructions.add(new CfConstString(recordInvokeDynamic.getFieldNames()));
+ if (appView.options().testing.enableRecordModeling
+ && appView.enableWholeProgramOptimizations()) {
+ instructions.add(
+ new CfDexItemBasedConstString(
+ recordInvokeDynamic.getRecordClass().type,
+ recordInvokeDynamic.computeRecordFieldNamesComputationInfo()));
+ } else {
+ instructions.add(new CfConstString(recordInvokeDynamic.getFieldNames()));
+ }
ProgramMethod programMethod =
synthesizeRecordHelper(
recordToStringHelperProto,
@@ -426,9 +379,7 @@
appView,
builder -> {
DexEncodedMethod init = synthesizeRecordInitMethod();
- builder
- .setAbstract()
- .setDirectMethods(ImmutableList.of(init));
+ builder.setAbstract().setDirectMethods(ImmutableList.of(init));
},
eventConsumer::acceptRecordClass);
}
@@ -484,75 +435,7 @@
}
private boolean needsDesugaring(CfInvokeDynamic invokeDynamic, ProgramMethod context) {
- DexCallSite callSite = invokeDynamic.getCallSite();
- // 1. Validates this is an invoke-static to ObjectMethods#bootstrap.
- DexMethodHandle bootstrapMethod = callSite.bootstrapMethod;
- if (!bootstrapMethod.type.isInvokeStatic()) {
- return false;
- }
- if (bootstrapMethod.member != factory.objectMethodsMembers.bootstrap) {
- return false;
- }
- // From there on we assume in the assertions that the invoke to the library method is
- // well-formed. If the invoke is not well formed assertions will fail but the execution is
- // correct.
- if (bootstrapMethod.isInterface) {
- assert false
- : "Invoke-dynamic invoking non interface method ObjectMethods#bootstrap as an interface"
- + " method.";
- return false;
- }
- // 2. Validate the bootstrapArgs include the record type, the instance field names and
- // the corresponding instance getters.
- if (callSite.bootstrapArgs.size() < 2) {
- assert false
- : "Invoke-dynamic invoking method ObjectMethods#bootstrap with less than 2 parameters.";
- return false;
- }
- DexValueType recordType = callSite.bootstrapArgs.get(0).asDexValueType();
- if (recordType == null) {
- assert false : "Invoke-dynamic invoking method ObjectMethods#bootstrap with an invalid type.";
- return false;
- }
- DexClass recordClass = appView.definitionFor(recordType.getValue());
- if (recordClass == null || recordClass.isNotProgramClass()) {
- return false;
- }
- DexValueString valueString = callSite.bootstrapArgs.get(1).asDexValueString();
- if (valueString == null) {
- assert false
- : "Invoke-dynamic invoking method ObjectMethods#bootstrap with invalid field names.";
- return false;
- }
- DexString fieldNames = valueString.getValue();
- assert fieldNames.toString().isEmpty()
- || (fieldNames.toString().split(";").length == callSite.bootstrapArgs.size() - 2);
- assert recordClass.instanceFields().size() == callSite.bootstrapArgs.size() - 2;
- for (int i = 2; i < callSite.bootstrapArgs.size(); i++) {
- DexValueMethodHandle handle = callSite.bootstrapArgs.get(i).asDexValueMethodHandle();
- if (handle == null
- || !handle.value.type.isInstanceGet()
- || !handle.value.member.isDexField()) {
- assert false
- : "Invoke-dynamic invoking method ObjectMethods#bootstrap with invalid getters.";
- return false;
- }
- }
- // 3. Create the invoke-record instruction.
- if (callSite.methodName == factory.toStringMethodName) {
- assert callSite.methodProto == factory.createProto(factory.stringType, recordClass.getType());
- return true;
- }
- if (callSite.methodName == factory.hashCodeMethodName) {
- assert callSite.methodProto == factory.createProto(factory.intType, recordClass.getType());
- return true;
- }
- if (callSite.methodName == factory.equalsMethodName) {
- assert callSite.methodProto
- == factory.createProto(factory.booleanType, recordClass.getType(), factory.objectType);
- return true;
- }
- return false;
+ return isInvokeDynamicOnRecord(invokeDynamic, appView, context);
}
@SuppressWarnings("ConstantConditions")
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/records/RecordRewriterHelper.java b/src/main/java/com/android/tools/r8/ir/desugar/records/RecordRewriterHelper.java
new file mode 100644
index 0000000..dc589e2
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/desugar/records/RecordRewriterHelper.java
@@ -0,0 +1,166 @@
+// 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.ir.desugar.records;
+
+import com.android.tools.r8.cf.code.CfInvokeDynamic;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexCallSite;
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMethodHandle;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexProto;
+import com.android.tools.r8.graph.DexString;
+import com.android.tools.r8.graph.DexValue.DexValueMethodHandle;
+import com.android.tools.r8.graph.DexValue.DexValueString;
+import com.android.tools.r8.graph.DexValue.DexValueType;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.naming.dexitembasedstring.RecordFieldNamesComputationInfo;
+
+public class RecordRewriterHelper {
+
+ public static boolean isInvokeDynamicOnRecord(
+ CfInvokeDynamic invokeDynamic, AppView<?> appView, ProgramMethod context) {
+ DexItemFactory factory = appView.dexItemFactory();
+ DexCallSite callSite = invokeDynamic.getCallSite();
+ // 1. Validates this is an invoke-static to ObjectMethods#bootstrap.
+ DexMethodHandle bootstrapMethod = callSite.bootstrapMethod;
+ if (!bootstrapMethod.type.isInvokeStatic()) {
+ return false;
+ }
+ if (bootstrapMethod.member != factory.objectMethodsMembers.bootstrap) {
+ return false;
+ }
+ // From there on we assume in the assertions that the invoke to the library method is
+ // well-formed. If the invoke is not well formed assertions will fail but the execution is
+ // correct.
+ if (bootstrapMethod.isInterface) {
+ assert false
+ : "Invoke-dynamic invoking non interface method ObjectMethods#bootstrap as an interface"
+ + " method.";
+ return false;
+ }
+ // 2. Validate the bootstrapArgs include the record type, the instance field names and
+ // the corresponding instance getters.
+ if (callSite.bootstrapArgs.size() < 2) {
+ assert false
+ : "Invoke-dynamic invoking method ObjectMethods#bootstrap with less than 2 parameters.";
+ return false;
+ }
+ DexValueType recordType = callSite.bootstrapArgs.get(0).asDexValueType();
+ if (recordType == null) {
+ assert false : "Invoke-dynamic invoking method ObjectMethods#bootstrap with an invalid type.";
+ return false;
+ }
+ DexClass recordClass = appView.definitionFor(recordType.getValue(), context);
+ if (recordClass == null || recordClass.isNotProgramClass()) {
+ return false;
+ }
+ DexValueString valueString = callSite.bootstrapArgs.get(1).asDexValueString();
+ if (valueString == null) {
+ assert false
+ : "Invoke-dynamic invoking method ObjectMethods#bootstrap with invalid field names.";
+ return false;
+ }
+ DexString fieldNames = valueString.getValue();
+ assert fieldNames.toString().isEmpty()
+ || (fieldNames.toString().split(";").length == callSite.bootstrapArgs.size() - 2);
+ assert recordClass.instanceFields().size() == callSite.bootstrapArgs.size() - 2;
+ for (int i = 2; i < callSite.bootstrapArgs.size(); i++) {
+ DexValueMethodHandle handle = callSite.bootstrapArgs.get(i).asDexValueMethodHandle();
+ if (handle == null
+ || !handle.value.type.isInstanceGet()
+ || !handle.value.member.isDexField()) {
+ assert false
+ : "Invoke-dynamic invoking method ObjectMethods#bootstrap with invalid getters.";
+ return false;
+ }
+ }
+ // 3. Check it matches one of the 3 invokeDynamicOnRecord instruction.
+ if (callSite.methodName == factory.toStringMethodName) {
+ assert callSite.methodProto == factory.createProto(factory.stringType, recordClass.getType());
+ return true;
+ }
+ if (callSite.methodName == factory.hashCodeMethodName) {
+ assert callSite.methodProto == factory.createProto(factory.intType, recordClass.getType());
+ return true;
+ }
+ if (callSite.methodName == factory.equalsMethodName) {
+ assert callSite.methodProto
+ == factory.createProto(factory.booleanType, recordClass.getType(), factory.objectType);
+ return true;
+ }
+ return false;
+ }
+
+ public static RecordInvokeDynamic parseInvokeDynamicOnRecord(
+ CfInvokeDynamic invokeDynamic, AppView<?> appView, ProgramMethod context) {
+ assert isInvokeDynamicOnRecord(invokeDynamic, appView, context);
+ DexCallSite callSite = invokeDynamic.getCallSite();
+ DexValueType recordValueType = callSite.bootstrapArgs.get(0).asDexValueType();
+ DexValueString valueString = callSite.bootstrapArgs.get(1).asDexValueString();
+ DexString fieldNames = valueString.getValue();
+ DexField[] fields = new DexField[callSite.bootstrapArgs.size() - 2];
+ for (int i = 2; i < callSite.bootstrapArgs.size(); i++) {
+ DexValueMethodHandle handle = callSite.bootstrapArgs.get(i).asDexValueMethodHandle();
+ fields[i - 2] = handle.value.member.asDexField();
+ }
+ DexProgramClass recordClass =
+ appView.definitionFor(recordValueType.getValue()).asProgramClass();
+ return new RecordInvokeDynamic(
+ callSite.methodName, callSite.methodProto, fieldNames, fields, recordClass);
+ }
+
+ static class RecordInvokeDynamic {
+
+ private final DexString methodName;
+ private final DexProto methodProto;
+ private final DexString fieldNames;
+ private final DexField[] fields;
+ private final DexProgramClass recordClass;
+
+ private RecordInvokeDynamic(
+ DexString methodName,
+ DexProto methodProto,
+ DexString fieldNames,
+ DexField[] fields,
+ DexProgramClass recordClass) {
+ this.methodName = methodName;
+ this.methodProto = methodProto;
+ this.fieldNames = fieldNames;
+ this.fields = fields;
+ this.recordClass = recordClass;
+ }
+
+ RecordInvokeDynamic withFieldNamesAndFields(DexString fieldNames, DexField[] fields) {
+ return new RecordInvokeDynamic(methodName, methodProto, fieldNames, fields, recordClass);
+ }
+
+ DexField[] getFields() {
+ return fields;
+ }
+
+ DexProgramClass getRecordClass() {
+ return recordClass;
+ }
+
+ DexString getFieldNames() {
+ return fieldNames;
+ }
+
+ DexString getMethodName() {
+ return methodName;
+ }
+
+ DexProto getMethodProto() {
+ return methodProto;
+ }
+
+ RecordFieldNamesComputationInfo computeRecordFieldNamesComputationInfo() {
+ return RecordFieldNamesComputationInfo.forFieldNamesAndFields(getFieldNames(), getFields());
+ }
+ }
+}
diff --git a/src/main/java/com/android/tools/r8/naming/IdentifierMinifier.java b/src/main/java/com/android/tools/r8/naming/IdentifierMinifier.java
index a775a72..a22a766 100644
--- a/src/main/java/com/android/tools/r8/naming/IdentifierMinifier.java
+++ b/src/main/java/com/android/tools/r8/naming/IdentifierMinifier.java
@@ -19,10 +19,12 @@
import com.android.tools.r8.graph.DexValue;
import com.android.tools.r8.graph.DexValue.DexItemBasedValueString;
import com.android.tools.r8.graph.DexValue.DexValueString;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.ir.desugar.records.RecordCfToCfRewriter;
import com.android.tools.r8.shaking.AppInfoWithLiveness;
import com.android.tools.r8.shaking.ProguardClassFilter;
+import com.android.tools.r8.utils.ListUtils;
import com.android.tools.r8.utils.ThreadUtils;
-import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
@@ -35,11 +37,13 @@
private final AppView<AppInfoWithLiveness> appView;
private final ProguardClassFilter adaptClassStrings;
+ private final RecordCfToCfRewriter recordCfToCfRewriter;
private final NamingLens lens;
IdentifierMinifier(AppView<AppInfoWithLiveness> appView, NamingLens lens) {
this.appView = appView;
this.adaptClassStrings = appView.options().getProguardConfiguration().getAdaptClassStrings();
+ this.recordCfToCfRewriter = RecordCfToCfRewriter.create(appView);
this.lens = lens;
}
@@ -117,12 +121,10 @@
for (DexEncodedField field : clazz.staticFields()) {
replaceDexItemBasedConstStringInStaticField(field);
}
- clazz
- .methods(DexEncodedMethod::hasCode)
- .forEach(this::replaceDexItemBasedConstStringInMethod);
+ clazz.forEachProgramMethodMatching(
+ DexEncodedMethod::hasCode, this::replaceDexItemBasedConstStringInMethod);
},
- executorService
- );
+ executorService);
}
private void replaceDexItemBasedConstStringInStaticField(DexEncodedField encodedField) {
@@ -137,8 +139,8 @@
}
}
- private void replaceDexItemBasedConstStringInMethod(DexEncodedMethod encodedMethod) {
- Code code = encodedMethod.getCode();
+ private void replaceDexItemBasedConstStringInMethod(ProgramMethod programMethod) {
+ Code code = programMethod.getDefinition().getCode();
assert code != null;
if (code.isDexCode()) {
Instruction[] instructions = code.asDexCode().instructions;
@@ -157,23 +159,23 @@
} else {
assert code.isCfCode();
List<CfInstruction> instructions = code.asCfCode().getInstructions();
- List<CfInstruction> newInstructions = null;
- for (int i = 0; i < instructions.size(); ++i) {
- CfInstruction instruction = instructions.get(i);
- if (instruction.isDexItemBasedConstString()) {
- CfDexItemBasedConstString cnst = instruction.asDexItemBasedConstString();
- DexString replacement =
- cnst.getNameComputationInfo()
- .computeNameFor(cnst.getItem(), appView, appView.graphLens(), lens);
- if (newInstructions == null) {
- newInstructions = new ArrayList<>(instructions);
- }
- newInstructions.set(i, new CfConstString(replacement));
- }
- }
- if (newInstructions != null) {
- code.asCfCode().setInstructions(newInstructions);
- }
+ List<CfInstruction> newInstructions =
+ ListUtils.mapOrElse(
+ instructions,
+ (int i, CfInstruction instruction) -> {
+ if (instruction.isDexItemBasedConstString()) {
+ CfDexItemBasedConstString cnst = instruction.asDexItemBasedConstString();
+ return new CfConstString(
+ cnst.getNameComputationInfo()
+ .computeNameFor(cnst.getItem(), appView, appView.graphLens(), lens));
+ } else if (recordCfToCfRewriter != null && instruction.isInvokeDynamic()) {
+ return recordCfToCfRewriter.rewriteRecordInvokeDynamic(
+ instruction.asInvokeDynamic(), programMethod, lens);
+ }
+ return instruction;
+ },
+ instructions);
+ code.asCfCode().setInstructions(newInstructions);
}
}
}
diff --git a/src/main/java/com/android/tools/r8/naming/dexitembasedstring/NameComputationInfo.java b/src/main/java/com/android/tools/r8/naming/dexitembasedstring/NameComputationInfo.java
index 67e574c..8a16fb0 100644
--- a/src/main/java/com/android/tools/r8/naming/dexitembasedstring/NameComputationInfo.java
+++ b/src/main/java/com/android/tools/r8/naming/dexitembasedstring/NameComputationInfo.java
@@ -27,6 +27,10 @@
return asClassNameComputationInfo()
.internalComputeNameFor(rewritten.asDexType(), definitions, namingLens);
}
+ if (isRecordFieldNamesComputationInfo()) {
+ return asRecordFieldNamesComputationInfo()
+ .internalComputeNameFor(rewritten.asDexType(), definitions, graphLens, namingLens);
+ }
}
return namingLens.lookupName(rewritten, definitions.dexItemFactory());
}
@@ -53,4 +57,12 @@
public ClassNameComputationInfo asClassNameComputationInfo() {
return null;
}
+
+ public boolean isRecordFieldNamesComputationInfo() {
+ return false;
+ }
+
+ public RecordFieldNamesComputationInfo asRecordFieldNamesComputationInfo() {
+ return null;
+ }
}
diff --git a/src/main/java/com/android/tools/r8/naming/dexitembasedstring/RecordFieldNamesComputationInfo.java b/src/main/java/com/android/tools/r8/naming/dexitembasedstring/RecordFieldNamesComputationInfo.java
new file mode 100644
index 0000000..17ac83a
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/naming/dexitembasedstring/RecordFieldNamesComputationInfo.java
@@ -0,0 +1,168 @@
+// 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.naming.dexitembasedstring;
+
+import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexDefinitionSupplier;
+import com.android.tools.r8.graph.DexEncodedField;
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexString;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.GraphLens;
+import com.android.tools.r8.naming.NamingLens;
+import com.android.tools.r8.utils.StringUtils;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.IntFunction;
+
+/**
+ * Specific subclass for Record field names. Record field names are specific DexString instances
+ * used at the Java class file level for Records which encode the record field names in a single
+ * String separated by semi-colon. The computation is able to minify and prune such names.
+ *
+ * <p>Example: record Person(String name, int age) {} The string is: name;age
+ *
+ * <p>The JVM normally generates the Record field names string such as it includes each field name
+ * in the string. However, bytecode manipulation tools may rewrite the field name and not the
+ * string. For this reason we have two subclasses. - MatchingRecordFieldNamesComputationInfo is used
+ * when the name in the string matches the field names, and the R8 compilation is able to minify and
+ * prune the string based on the fields. - MissMatchingRecordFieldNamesComputationInfo is used when
+ * the name in the string does not match the field names, and the R8 compilation is then only able
+ * to prune the fields while maintaining the non-matching field names.
+ */
+public abstract class RecordFieldNamesComputationInfo extends NameComputationInfo<DexType> {
+
+ final DexField[] fields;
+
+ protected RecordFieldNamesComputationInfo(DexField[] fields) {
+ this.fields = fields;
+ }
+
+ private static class MissMatchingRecordFieldNamesComputationInfo
+ extends RecordFieldNamesComputationInfo {
+
+ private final String[] fieldNames;
+
+ private MissMatchingRecordFieldNamesComputationInfo(String[] fieldNames, DexField[] fields) {
+ super(fields);
+ this.fieldNames = fieldNames;
+ }
+
+ @Override
+ public DexString internalComputeNameFor(
+ DexType type,
+ DexDefinitionSupplier definitions,
+ GraphLens graphLens,
+ NamingLens namingLens) {
+ return internalComputeNameFor(type, definitions, graphLens, i -> fieldNames[i]);
+ }
+ }
+
+ private static class MatchingRecordFieldNamesComputationInfo
+ extends RecordFieldNamesComputationInfo {
+
+ public MatchingRecordFieldNamesComputationInfo(DexField[] fields) {
+ super(fields);
+ }
+
+ @Override
+ public DexString internalComputeNameFor(
+ DexType type,
+ DexDefinitionSupplier definitions,
+ GraphLens graphLens,
+ NamingLens namingLens) {
+ return internalComputeNameFor(
+ type,
+ definitions,
+ graphLens,
+ i ->
+ namingLens
+ .lookupField(
+ graphLens.getRenamedFieldSignature(fields[i]), definitions.dexItemFactory())
+ .name
+ .toString());
+ }
+ }
+
+ static DexString dexStringFromFieldNames(List<String> fieldNames, DexItemFactory factory) {
+ return factory.createString(StringUtils.join(";", fieldNames));
+ }
+
+ public static RecordFieldNamesComputationInfo forFieldNamesAndFields(
+ DexString fieldNames, DexField[] fields) {
+ String fieldNamesString = fieldNames.toString();
+ String[] fieldNamesSplit =
+ fieldNamesString.isEmpty() ? new String[0] : fieldNamesString.split(";");
+ assert fieldNamesSplit.length == fields.length;
+ if (fieldsMatchNames(fieldNamesSplit, fields)) {
+ return new MatchingRecordFieldNamesComputationInfo(fields);
+ }
+ return new MissMatchingRecordFieldNamesComputationInfo(fieldNamesSplit, fields);
+ }
+
+ private static boolean fieldsMatchNames(String[] fieldNames, DexField[] fields) {
+ for (int i = 0; i < fieldNames.length; i++) {
+ if (!(fields[i].name.toString().equals(fieldNames[i]))) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ public DexString internalComputeNameFor(
+ DexType type,
+ DexDefinitionSupplier definitions,
+ GraphLens graphLens,
+ IntFunction<String> nameSupplier) {
+ assert Arrays.stream(fields).allMatch(f -> f.holder == type);
+ DexClass recordClass = definitions.contextIndependentDefinitionFor(type);
+ assert recordClass != null;
+ List<String> names = new ArrayList<>(fields.length);
+ for (int i = 0; i < fields.length; i++) {
+ DexEncodedField recordField =
+ recordClass.lookupInstanceField(graphLens.getRenamedFieldSignature(fields[i]));
+ if (recordField != null) {
+ names.add(nameSupplier.apply(i));
+ } else {
+ // TODO(b/201277582): Temporarily work-around as long as the field values are not also
+ // shrunk in dex.
+ names.add("<pruned>");
+ }
+ }
+ return dexStringFromFieldNames(names, definitions.dexItemFactory());
+ }
+
+ @Override
+ DexString internalComputeNameFor(
+ DexType reference, DexDefinitionSupplier definitions, NamingLens namingLens) {
+ throw new Unreachable();
+ }
+
+ public abstract DexString internalComputeNameFor(
+ DexType type, DexDefinitionSupplier definitions, GraphLens graphLens, NamingLens namingLens);
+
+ @Override
+ public boolean needsToComputeName() {
+ return true;
+ }
+
+ @Override
+ public boolean needsToRegisterReference() {
+ return false;
+ }
+
+ @Override
+ public boolean isRecordFieldNamesComputationInfo() {
+ return true;
+ }
+
+ @Override
+ public RecordFieldNamesComputationInfo asRecordFieldNamesComputationInfo() {
+ return this;
+ }
+}
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 ff3878e..24e9259 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -1590,6 +1590,10 @@
public boolean testEnableTestAssertions = false;
public boolean keepMetadataInR8IfNotRewritten = true;
+ // If set, pruned record fields are not used in hashCode/equals/toString and toString prints
+ // minified field names instead of original field names.
+ public boolean enableRecordModeling = false;
+
public boolean allowConflictingSyntheticTypes = false;
// Flag to allow processing of resources in D8. A data resource consumer still needs to be
diff --git a/src/test/java/com/android/tools/r8/desugar/records/RecordShrinkFieldTest.java b/src/test/java/com/android/tools/r8/desugar/records/RecordShrinkFieldTest.java
index 7b653ed..df6be44 100644
--- a/src/test/java/com/android/tools/r8/desugar/records/RecordShrinkFieldTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/records/RecordShrinkFieldTest.java
@@ -29,6 +29,13 @@
private static final String EXPECTED_RESULT_D8 =
String.format(EXPECTED_RESULT, "Person", "Person");
private static final String EXPECTED_RESULT_R8 = String.format(EXPECTED_RESULT, "a", "a");
+ // TODO(b/201277582): These results are temporary while we transition into pruned minified record
+ // fields.
+ private static final String EXPECTED_RESULT_R8_ADVANCED_DEX =
+ StringUtils.lines(
+ "a[a=Jane Doe, <pruned>=42, <pruned>=-1]", "a[a=Bob, <pruned>=42, <pruned>=-1]");
+ private static final String EXPECTED_RESULT_R8_ADVANCED_CF =
+ StringUtils.lines("a[a=Jane Doe, b=42, c=-1]", "a[a=Bob, b=42, c=-1]");
private final TestParameters parameters;
@@ -83,6 +90,43 @@
}
@Test
+ public void testR8AdvancedShrinking() throws Exception {
+ testForR8(parameters.getBackend())
+ .addProgramClassFileData(PROGRAM_DATA)
+ .setMinApi(parameters.getApiLevel())
+ .addKeepMainRule(MAIN_TYPE)
+ .addOptionsModification(opt -> opt.testing.enableRecordModeling = true)
+ .addOptionsModification(TestingOptions::allowExperimentClassFileVersion)
+ .compile()
+ .inspect(this::assertSingleField)
+ .run(parameters.getRuntime(), MAIN_TYPE)
+ .assertSuccessWithOutput(EXPECTED_RESULT_R8_ADVANCED_DEX);
+ }
+
+ @Test
+ public void testR8CfThenDexAdvancedShrinking() throws Exception {
+ Path desugared =
+ testForR8(Backend.CF)
+ .addProgramClassFileData(PROGRAM_DATA)
+ .setMinApi(parameters.getApiLevel())
+ .addKeepMainRule(MAIN_TYPE)
+ .addLibraryFiles(RecordTestUtils.getJdk15LibraryFiles(temp))
+ .addOptionsModification(opt -> opt.testing.enableRecordModeling = true)
+ .addOptionsModification(TestingOptions::allowExperimentClassFileVersion)
+ .compile()
+ .writeToZip();
+ testForR8(parameters.getBackend())
+ .addProgramFiles(desugared)
+ .setMinApi(parameters.getApiLevel())
+ .addKeepMainRule(MAIN_TYPE)
+ .addOptionsModification(TestingOptions::allowExperimentClassFileVersion)
+ .compile()
+ .inspect(this::assertSingleField)
+ .run(parameters.getRuntime(), MAIN_TYPE)
+ .assertSuccessWithOutput(EXPECTED_RESULT_R8_ADVANCED_CF);
+ }
+
+ @Test
public void testR8CfThenDex() throws Exception {
Path desugared =
testForR8(Backend.CF)