Handle final fields that are assigned more than once in constructors
Change-Id: I85486a04dedd4765ba8aa136930eb37b37cdbec3
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/fieldvalueanalysis/FieldValueAnalysis.java b/src/main/java/com/android/tools/r8/ir/analysis/fieldvalueanalysis/FieldValueAnalysis.java
index e4bf2a9..fd8dbf7 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/fieldvalueanalysis/FieldValueAnalysis.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/fieldvalueanalysis/FieldValueAnalysis.java
@@ -15,20 +15,36 @@
import com.android.tools.r8.ir.code.IRCode;
import com.android.tools.r8.ir.code.Instruction;
import com.android.tools.r8.ir.code.InstructionIterator;
+import com.android.tools.r8.ir.code.InvokeDirect;
import com.android.tools.r8.ir.code.Value;
import com.android.tools.r8.ir.optimize.ClassInitializerDefaultsOptimization.ClassInitializerDefaultsResult;
import com.android.tools.r8.ir.optimize.info.OptimizationFeedback;
+import com.android.tools.r8.ir.optimize.info.field.InstanceFieldInitializationInfo;
+import com.android.tools.r8.ir.optimize.info.field.UnknownInstanceFieldInitializationInfo;
import com.android.tools.r8.shaking.AppInfoWithLiveness;
import com.android.tools.r8.utils.DequeUtils;
+import com.android.tools.r8.utils.ListUtils;
+import java.util.ArrayList;
import java.util.Deque;
import java.util.IdentityHashMap;
-import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
public abstract class FieldValueAnalysis {
+ static class FieldInitializationInfo {
+
+ private final Instruction instruction;
+ private final InstanceFieldInitializationInfo instanceFieldInitializationInfo;
+
+ FieldInitializationInfo(
+ Instruction instruction, InstanceFieldInitializationInfo instanceFieldInitializationInfo) {
+ this.instruction = instruction;
+ this.instanceFieldInitializationInfo = instanceFieldInitializationInfo;
+ }
+ }
+
final AppView<AppInfoWithLiveness> appView;
final IRCode code;
final ProgramMethod context;
@@ -37,7 +53,7 @@
private DominatorTree dominatorTree;
private Map<BasicBlock, AbstractFieldSet> fieldsMaybeReadBeforeBlockInclusiveCache;
- final Map<DexEncodedField, LinkedList<FieldInstruction>> putsPerField = new IdentityHashMap<>();
+ final Map<DexEncodedField, List<FieldInitializationInfo>> putsPerField = new IdentityHashMap<>();
FieldValueAnalysis(
AppView<AppInfoWithLiveness> appView, IRCode code, OptimizationFeedback feedback) {
@@ -61,8 +77,27 @@
return fieldsMaybeReadBeforeBlockInclusiveCache;
}
+ boolean isInstanceFieldValueAnalysis() {
+ return false;
+ }
+
+ InstanceFieldValueAnalysis asInstanceFieldValueAnalysis() {
+ return null;
+ }
+
abstract boolean isSubjectToOptimization(DexEncodedField field);
+ void recordFieldPut(DexEncodedField field, Instruction instruction) {
+ recordFieldPut(field, instruction, UnknownInstanceFieldInitializationInfo.getInstance());
+ }
+
+ void recordFieldPut(
+ DexEncodedField field, Instruction instruction, InstanceFieldInitializationInfo info) {
+ putsPerField
+ .computeIfAbsent(field, ignore -> new ArrayList<>())
+ .add(new FieldInitializationInfo(instruction, info));
+ }
+
/** This method analyzes initializers with the purpose of computing field optimization info. */
void computeFieldOptimizationInfo(ClassInitializerDefaultsResult classInitializerDefaultsResult) {
AppInfoWithLiveness appInfo = appView.appInfo();
@@ -80,31 +115,42 @@
DexField field = fieldPut.getField();
DexEncodedField encodedField = appInfo.resolveField(field).getResolvedField();
if (encodedField != null && isSubjectToOptimization(encodedField)) {
- putsPerField.computeIfAbsent(encodedField, ignore -> new LinkedList<>()).add(fieldPut);
+ recordFieldPut(encodedField, fieldPut);
}
+ } else if (isInstanceFieldValueAnalysis()
+ && instruction.isInvokeConstructor(appView.dexItemFactory())) {
+ InvokeDirect invoke = instruction.asInvokeDirect();
+ asInstanceFieldValueAnalysis().analyzeForwardingConstructorCall(invoke, code.getThis());
}
}
}
List<BasicBlock> normalExitBlocks = code.computeNormalExitBlocks();
- for (Entry<DexEncodedField, LinkedList<FieldInstruction>> entry : putsPerField.entrySet()) {
- DexEncodedField encodedField = entry.getKey();
- LinkedList<FieldInstruction> fieldPuts = entry.getValue();
+ for (Entry<DexEncodedField, List<FieldInitializationInfo>> entry : putsPerField.entrySet()) {
+ DexEncodedField field = entry.getKey();
+ List<FieldInitializationInfo> fieldPuts = entry.getValue();
if (fieldPuts.size() > 1) {
continue;
}
- FieldInstruction fieldPut = fieldPuts.getFirst();
+ FieldInitializationInfo info = ListUtils.first(fieldPuts);
+ Instruction instruction = info.instruction;
+ if (instruction.isInvokeDirect()) {
+ asInstanceFieldValueAnalysis()
+ .recordInstanceFieldIsInitializedWithInfo(field, info.instanceFieldInitializationInfo);
+ continue;
+ }
+ FieldInstruction fieldPut = instruction.asFieldInstruction();
if (!isStraightLineCode) {
if (!getOrCreateDominatorTree().dominatesAllOf(fieldPut.getBlock(), normalExitBlocks)) {
continue;
}
}
boolean priorReadsWillReadSameValue =
- !classInitializerDefaultsResult.hasStaticValue(encodedField) && fieldPut.value().isZero();
- if (!priorReadsWillReadSameValue && fieldMaybeReadBeforeInstruction(encodedField, fieldPut)) {
+ !classInitializerDefaultsResult.hasStaticValue(field) && fieldPut.value().isZero();
+ if (!priorReadsWillReadSameValue && fieldMaybeReadBeforeInstruction(field, fieldPut)) {
continue;
}
- updateFieldOptimizationInfo(encodedField, fieldPut, fieldPut.value());
+ updateFieldOptimizationInfo(field, fieldPut, fieldPut.value());
}
}
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/fieldvalueanalysis/InstanceFieldValueAnalysis.java b/src/main/java/com/android/tools/r8/ir/analysis/fieldvalueanalysis/InstanceFieldValueAnalysis.java
index b818cc2..f508479 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/fieldvalueanalysis/InstanceFieldValueAnalysis.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/fieldvalueanalysis/InstanceFieldValueAnalysis.java
@@ -11,6 +11,7 @@
import com.android.tools.r8.graph.DexEncodedField;
import com.android.tools.r8.graph.DexEncodedMethod;
import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.ProgramMethod;
import com.android.tools.r8.ir.analysis.type.ClassTypeElement;
import com.android.tools.r8.ir.analysis.type.TypeElement;
import com.android.tools.r8.ir.analysis.value.AbstractValue;
@@ -26,8 +27,10 @@
import com.android.tools.r8.ir.optimize.ClassInitializerDefaultsOptimization.ClassInitializerDefaultsResult;
import com.android.tools.r8.ir.optimize.info.OptimizationFeedback;
import com.android.tools.r8.ir.optimize.info.field.EmptyInstanceFieldInitializationInfoCollection;
+import com.android.tools.r8.ir.optimize.info.field.InstanceFieldInitializationInfo;
import com.android.tools.r8.ir.optimize.info.field.InstanceFieldInitializationInfoCollection;
import com.android.tools.r8.ir.optimize.info.field.InstanceFieldInitializationInfoFactory;
+import com.android.tools.r8.ir.optimize.info.field.UnknownInstanceFieldInitializationInfo;
import com.android.tools.r8.shaking.AppInfoWithLiveness;
import com.android.tools.r8.utils.Timing;
@@ -106,6 +109,16 @@
}
@Override
+ boolean isInstanceFieldValueAnalysis() {
+ return true;
+ }
+
+ @Override
+ InstanceFieldValueAnalysis asInstanceFieldValueAnalysis() {
+ return this;
+ }
+
+ @Override
boolean isSubjectToOptimization(DexEncodedField field) {
return !field.isStatic() && field.holder() == context.getHolderType();
}
@@ -113,11 +126,45 @@
@Override
void updateFieldOptimizationInfo(DexEncodedField field, FieldInstruction fieldPut, Value value) {
if (fieldNeverWrittenBetweenInstancePutAndMethodExit(field, fieldPut.asInstancePut())) {
- recordFieldIsInitializedWithValue(field, value);
+ recordInstanceFieldIsInitializedWithValue(field, value);
+ }
+ }
+
+ void analyzeForwardingConstructorCall(InvokeDirect invoke, Value thisValue) {
+ if (invoke.getReceiver() != thisValue
+ || invoke.getInvokedMethod().getHolderType() != context.getHolderType()) {
+ // Not a forwarding constructor call.
+ return;
+ }
+
+ ProgramMethod singleTarget = invoke.lookupSingleProgramTarget(appView, context);
+ if (singleTarget == null) {
+ // Failure, should generally not happen.
+ return;
+ }
+
+ InstanceFieldInitializationInfoCollection infos =
+ singleTarget
+ .getDefinition()
+ .getOptimizationInfo()
+ .getInstanceInitializerInfo()
+ .fieldInitializationInfos();
+ for (DexEncodedField field : singleTarget.getHolder().instanceFields()) {
+ assert isSubjectToOptimization(field);
+ InstanceFieldInitializationInfo info = infos.get(field);
+ if (info.isArgumentInitializationInfo()) {
+ int argumentIndex = info.asArgumentInitializationInfo().getArgumentIndex();
+ info = getInstanceFieldInitializationInfo(field, invoke.getArgument(argumentIndex));
+ }
+ recordFieldPut(field, invoke, info);
}
}
private void analyzeParentConstructorCall() {
+ if (parentConstructor.getHolderType() == context.getHolderType()) {
+ // Forwarding constructor calls are handled similar to instance-put instructions.
+ return;
+ }
InstanceFieldInitializationInfoCollection infos =
parentConstructor
.getOptimizationInfo()
@@ -129,8 +176,8 @@
if (fieldNeverWrittenBetweenParentConstructorCallAndMethodExit(field)) {
if (info.isArgumentInitializationInfo()) {
int argumentIndex = info.asArgumentInitializationInfo().getArgumentIndex();
- recordFieldIsInitializedWithValue(
- field, parentConstructorCall.arguments().get(argumentIndex));
+ recordInstanceFieldIsInitializedWithValue(
+ field, parentConstructorCall.getArgument(argumentIndex));
} else {
assert info.isSingleValue() || info.isTypeInitializationInfo();
builder.recordInitializationInfo(field, info);
@@ -139,32 +186,39 @@
});
}
- private void recordFieldIsInitializedWithValue(DexEncodedField field, Value value) {
+ private InstanceFieldInitializationInfo getInstanceFieldInitializationInfo(
+ DexEncodedField field, Value value) {
Value root = value.getAliasedValue();
if (root.isDefinedByInstructionSatisfying(Instruction::isArgument)) {
Argument argument = root.definition.asArgument();
- builder.recordInitializationInfo(
- field, factory.createArgumentInitializationInfo(argument.getIndex()));
- return;
+ return factory.createArgumentInitializationInfo(argument.getIndex());
}
-
AbstractValue abstractValue = value.getAbstractValue(appView, context);
if (abstractValue.isSingleValue()) {
- builder.recordInitializationInfo(field, abstractValue.asSingleValue());
- return;
+ return abstractValue.asSingleValue();
}
-
- DexType fieldType = field.field.type;
+ DexType fieldType = field.type();
if (fieldType.isClassType()) {
ClassTypeElement dynamicLowerBoundType = value.getDynamicLowerBoundType(appView);
TypeElement dynamicUpperBoundType = value.getDynamicUpperBoundType(appView);
TypeElement staticFieldType = TypeElement.fromDexType(fieldType, maybeNull(), appView);
if (dynamicLowerBoundType != null || !dynamicUpperBoundType.equals(staticFieldType)) {
- builder.recordInitializationInfo(
- field,
- factory.createTypeInitializationInfo(dynamicLowerBoundType, dynamicUpperBoundType));
+ return factory.createTypeInitializationInfo(dynamicLowerBoundType, dynamicUpperBoundType);
}
}
+ return UnknownInstanceFieldInitializationInfo.getInstance();
+ }
+
+ void recordInstanceFieldIsInitializedWithInfo(
+ DexEncodedField field, InstanceFieldInitializationInfo info) {
+ if (!info.isUnknown()) {
+ builder.recordInitializationInfo(field, info);
+ }
+ }
+
+ void recordInstanceFieldIsInitializedWithValue(DexEncodedField field, Value value) {
+ recordInstanceFieldIsInitializedWithInfo(
+ field, getInstanceFieldInitializationInfo(field, value));
}
private boolean fieldNeverWrittenBetweenInstancePutAndMethodExit(
diff --git a/src/main/java/com/android/tools/r8/ir/code/Instruction.java b/src/main/java/com/android/tools/r8/ir/code/Instruction.java
index 6a42c80..62c94b8 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Instruction.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Instruction.java
@@ -9,6 +9,7 @@
import com.android.tools.r8.errors.Unreachable;
import com.android.tools.r8.graph.AppView;
import com.android.tools.r8.graph.DebugLocalInfo;
+import com.android.tools.r8.graph.DexItemFactory;
import com.android.tools.r8.graph.DexType;
import com.android.tools.r8.graph.ProgramMethod;
import com.android.tools.r8.ir.analysis.ClassInitializationAnalysis.AnalysisAssumption;
@@ -1240,6 +1241,10 @@
return null;
}
+ public boolean isInvokeConstructor(DexItemFactory dexItemFactory) {
+ return false;
+ }
+
public boolean isInvokeDirect() {
return false;
}
diff --git a/src/main/java/com/android/tools/r8/ir/code/InvokeDirect.java b/src/main/java/com/android/tools/r8/ir/code/InvokeDirect.java
index 6d452e1..7d488a0 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InvokeDirect.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InvokeDirect.java
@@ -8,6 +8,7 @@
import com.android.tools.r8.dex.Constants;
import com.android.tools.r8.graph.AppView;
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.ProgramMethod;
@@ -109,6 +110,11 @@
}
@Override
+ public boolean isInvokeConstructor(DexItemFactory dexItemFactory) {
+ return getInvokedMethod().isInstanceInitializer(dexItemFactory);
+ }
+
+ @Override
public boolean isInvokeDirect() {
return true;
}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/fields/NonFinalFinalFieldTest.java b/src/test/java/com/android/tools/r8/ir/optimize/fields/NonFinalFinalFieldTest.java
new file mode 100644
index 0000000..292f74e
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/fields/NonFinalFinalFieldTest.java
@@ -0,0 +1,170 @@
+// 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.ir.optimize.fields;
+
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.graph.AccessFlags;
+import com.android.tools.r8.transformers.MethodTransformer;
+import com.google.common.collect.ImmutableList;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class NonFinalFinalFieldTest extends TestBase {
+
+ private final TestParameters parameters;
+
+ @Parameters(name = "{0}")
+ public static TestParametersCollection data() {
+ return getTestParameters().withAllRuntimesAndApiLevels().build();
+ }
+
+ public NonFinalFinalFieldTest(TestParameters parameters) {
+ this.parameters = parameters;
+ }
+
+ @Test
+ public void testJvm() throws Exception {
+ assumeTrue(parameters.isCfRuntime());
+ testForJvm()
+ .addProgramClasses(TestClass.class)
+ .addProgramClassFileData(getProgramClassFileData())
+ .run(parameters.getRuntime(), TestClass.class)
+ .assertSuccessWithOutputLines("2", "2", "2");
+ }
+
+ @Test
+ public void testR8() throws Exception {
+ testForR8(parameters.getBackend())
+ .addProgramClasses(TestClass.class)
+ .addProgramClassFileData(getProgramClassFileData())
+ .addKeepMainRule(TestClass.class)
+ .setMinApi(parameters.getApiLevel())
+ .compile()
+ .run(parameters.getRuntime(), TestClass.class)
+ .assertSuccessWithOutputLines("2", "2", "2");
+ }
+
+ private List<byte[]> getProgramClassFileData() throws Exception {
+ return ImmutableList.of(
+ transformer(A.class)
+ .setAccessFlags(A.class.getDeclaredField("f"), AccessFlags::setFinal)
+ .transform(),
+ transformer(B.class)
+ .setAccessFlags(B.class.getDeclaredField("f"), AccessFlags::setFinal)
+ .addMethodTransformer(createRemoveObjectInitMethodTransformer())
+ .transform(),
+ transformer(C.class)
+ .setAccessFlags(C.class.getDeclaredField("f"), AccessFlags::setFinal)
+ .addMethodTransformer(createRemoveObjectInitMethodTransformer())
+ .transform());
+ }
+
+ private MethodTransformer createRemoveObjectInitMethodTransformer() {
+ return new MethodTransformer() {
+
+ private boolean seenInit = false;
+
+ @Override
+ public void visitMethodInsn(
+ int opcode, String owner, String name, String descriptor, boolean isInterface) {
+ if (isDefaultConstructor()) {
+ if (name.equals("<init>")) {
+ seenInit = true;
+ return;
+ }
+ super.visitMethodInsn(
+ opcode, owner, name.equals("init") ? "<init>" : name, descriptor, isInterface);
+ } else {
+ super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
+ }
+ }
+
+ @Override
+ public void visitVarInsn(int opcode, int var) {
+ if (isDefaultConstructor()) {
+ if (seenInit) {
+ super.visitVarInsn(opcode, var);
+ }
+ } else {
+ super.visitVarInsn(opcode, var);
+ }
+ }
+
+ private boolean isDefaultConstructor() {
+ return getContext().getReference().getMethodName().equals("<init>")
+ && getContext().getReference().getFormalTypes().isEmpty();
+ }
+ };
+ }
+
+ static class TestClass {
+
+ public static void main(String[] args) {
+ System.out.println(new A().f);
+ System.out.println(new B().f);
+ System.out.println(new C().f);
+ }
+ }
+
+ @NeverClassInline
+ static class A {
+
+ /*final*/ int f;
+
+ A() {
+ this(1);
+ this.f = 2;
+ }
+
+ A(int f) {
+ this.f = f;
+ }
+ }
+
+ @NeverClassInline
+ static class B {
+
+ /*final*/ int f;
+
+ B() {
+ this.f = 1;
+ init(2); // Rewritten to B(2)
+ }
+
+ B(int f) {
+ this.f = f;
+ }
+
+ private void init(int f) {}
+ }
+
+ @NeverClassInline
+ static class C {
+
+ /*final*/ int f;
+
+ C() {
+ this.f = 1;
+ init(2, true); // Rewritten to B(2, true)
+ }
+
+ C(int f, boolean b) {
+ if (b) {
+ this.f = f;
+ }
+ }
+
+ private void init(int f, boolean b) {}
+ }
+}