Add tests and TODOs for rebinding of fields
Bug: 123857022
Change-Id: If0935cf3204c0dec77afa824eacefef99b1bcf41
diff --git a/src/main/java/com/android/tools/r8/graph/DexEncodedField.java b/src/main/java/com/android/tools/r8/graph/DexEncodedField.java
index 42cc0ce..0efc380 100644
--- a/src/main/java/com/android/tools/r8/graph/DexEncodedField.java
+++ b/src/main/java/com/android/tools/r8/graph/DexEncodedField.java
@@ -7,6 +7,7 @@
import com.android.tools.r8.dex.MixedSectionCollection;
import com.android.tools.r8.ir.code.Instruction;
import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.shaking.Enqueuer.AppInfoWithLiveness;
public class DexEncodedField extends KeyedDexItem<DexField> {
@@ -109,22 +110,23 @@
}
// Returns a const instructions if this field is a compile time final const.
- public Instruction valueAsConstInstruction(AppInfo appInfo, Value dest) {
+ public Instruction valueAsConstInstruction(AppInfoWithLiveness appInfo, Value dest) {
// The only way to figure out whether the DexValue contains the final value
// is ensure the value is not the default or check <clinit> is not present.
- if (accessFlags.isStatic() && accessFlags.isPublic() && accessFlags.isFinal()) {
- DexClass clazz = appInfo.definitionFor(field.getHolder());
+ boolean isEffectivelyFinal =
+ (accessFlags.isFinal() || !appInfo.fieldsWritten.contains(field))
+ && !appInfo.isPinned(field);
+ if (!isEffectivelyFinal) {
+ return null;
+ }
+ if (accessFlags.isStatic()) {
+ DexClass clazz = appInfo.definitionFor(field.clazz);
assert clazz != null : "Class for the field must be present";
return getStaticValue().asConstInstruction(clazz.hasClassInitializer(), dest);
}
return null;
}
- public DexEncodedField toRenamedField(DexString name, DexItemFactory dexItemFactory) {
- return new DexEncodedField(dexItemFactory.createField(field.clazz, field.type, name),
- accessFlags, annotations, staticValue);
- }
-
public DexEncodedField toTypeSubstitutedField(DexField field) {
if (this.field == field) {
return this;
diff --git a/src/main/java/com/android/tools/r8/graph/DexType.java b/src/main/java/com/android/tools/r8/graph/DexType.java
index 0c48ea9..3e07a4f 100644
--- a/src/main/java/com/android/tools/r8/graph/DexType.java
+++ b/src/main/java/com/android/tools/r8/graph/DexType.java
@@ -13,6 +13,7 @@
import com.android.tools.r8.naming.NamingLens;
import com.android.tools.r8.utils.DescriptorUtils;
import com.android.tools.r8.utils.InternalOptions.OutlineOptions;
+import com.google.common.base.Predicates;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Sets;
@@ -120,6 +121,10 @@
return implementedInterfaces(appInfo).contains(appInfo.dexItemFactory.serializableType);
}
+ public boolean classInitializationMayHaveSideEffects(AppInfo appInfo) {
+ return classInitializationMayHaveSideEffects(appInfo, Predicates.alwaysFalse());
+ }
+
public boolean classInitializationMayHaveSideEffects(AppInfo appInfo, Predicate<DexType> ignore) {
DexClass clazz = appInfo.definitionFor(this);
return clazz == null || clazz.classInitializationMayHaveSideEffects(appInfo, ignore);
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/MemberValuePropagation.java b/src/main/java/com/android/tools/r8/ir/optimize/MemberValuePropagation.java
index 3deeb63..95d2275 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/MemberValuePropagation.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/MemberValuePropagation.java
@@ -221,6 +221,8 @@
InstructionListIterator iterator,
StaticGet current) {
DexField field = current.getField();
+
+ // TODO(b/123857022): Should be able to use definitionFor().
DexEncodedField target = appInfo.lookupStaticTarget(field.getHolder(), field);
if (target == null) {
return;
@@ -254,7 +256,7 @@
DexClass holderDefinition = appInfo.definitionFor(field.getHolder());
if (holderDefinition != null
&& holderDefinition.accessFlags.isFinal()
- && !appInfo.canTriggerStaticInitializer(field.getHolder(), true)) {
+ && !field.getHolder().classInitializationMayHaveSideEffects(appInfo)) {
Value outValue = current.dest();
DexEncodedMethod classInitializer = holderDefinition.getClassInitializer();
if (classInitializer != null && !isProcessedConcurrently.test(classInitializer)) {
@@ -277,12 +279,14 @@
private void rewritePutWithConstantValues(
InstructionIterator iterator, FieldInstruction current) {
- DexField field = current.asFieldInstruction().getField();
+ DexField field = current.getField();
+ // TODO(b/123857022): Should be possible to use definitionFor().
DexEncodedField target =
current.isInstancePut()
? appInfo.lookupInstanceTarget(field.getHolder(), field)
: appInfo.lookupStaticTarget(field.getHolder(), field);
- if (target != null && !isFieldRead(target)) {
+ // TODO(b/123857022): Should be possible to use `!isFieldRead(field)`.
+ if (target != null && !isFieldRead(target.field)) {
// Remove writes to dead (i.e. never read) fields.
iterator.removeOrReplaceByDebugLocalRead();
}
@@ -290,8 +294,8 @@
/**
* Replace invoke targets and field accesses with constant values where possible.
- * <p>
- * Also assigns value ranges to values where possible.
+ *
+ * <p>Also assigns value ranges to values where possible.
*/
public void rewriteWithConstantValues(
IRCode code, DexType callingContext, Predicate<DexEncodedMethod> isProcessedConcurrently) {
@@ -324,12 +328,16 @@
assert code.isConsistentSSA();
}
- private boolean isFieldRead(DexEncodedField field) {
- if (appInfo.fieldsRead.contains(field.field) || appInfo.isPinned(field.field)) {
- return true;
- }
- // For library classes we don't know whether a field is read.
- DexClass holder = appInfo.definitionFor(field.field.clazz);
+ private boolean isFieldRead(DexField field) {
+ return appInfo.fieldsRead.contains(field)
+ // TODO(b/121354886): Pinned fields should be in `fieldsRead`.
+ || appInfo.isPinned(field)
+ // For library classes we don't know whether a field is read.
+ || isLibraryField(field);
+ }
+
+ private boolean isLibraryField(DexField field) {
+ DexClass holder = appInfo.definitionFor(field.clazz);
return holder == null || holder.isLibraryClass();
}
}
diff --git a/src/test/java/com/android/tools/r8/memberrebinding/IllegalFieldRebindingTest.java b/src/test/java/com/android/tools/r8/memberrebinding/IllegalFieldRebindingTest.java
new file mode 100644
index 0000000..cc35cbd
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/memberrebinding/IllegalFieldRebindingTest.java
@@ -0,0 +1,125 @@
+// Copyright (c) 2019, 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.memberrebinding;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.memberrebinding.testclasses.IllegalFieldRebindingTestClasses;
+import com.android.tools.r8.memberrebinding.testclasses.IllegalFieldRebindingTestClasses.B;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.InstructionSubject;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+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 IllegalFieldRebindingTest extends TestBase {
+
+ private final Backend backend;
+
+ @Parameters(name = "Backend: {0}")
+ public static Backend[] data() {
+ return Backend.values();
+ }
+
+ public IllegalFieldRebindingTest(Backend backend) {
+ this.backend = backend;
+ }
+
+ @Test
+ public void test() throws Exception {
+ String expectedOutput = StringUtils.lines("42");
+
+ if (backend == Backend.CF) {
+ testForJvm().addTestClasspath().run(TestClass.class).assertSuccessWithOutput(expectedOutput);
+ }
+
+ CodeInspector inspector =
+ testForR8(Backend.DEX)
+ .addInnerClasses(IllegalFieldRebindingTest.class)
+ .addInnerClasses(IllegalFieldRebindingTestClasses.class)
+ .addKeepMainRule(TestClass.class)
+ .enableMergeAnnotations()
+ .run(TestClass.class)
+ .assertSuccessWithOutput(expectedOutput)
+ .inspector();
+
+ ClassSubject classSubject = inspector.clazz(TestClass.class);
+ assertThat(classSubject, isPresent());
+
+ MethodSubject methodSubject = classSubject.mainMethod();
+ assertThat(methodSubject, isPresent());
+
+ // Verify that the static-put instruction has not been removed.
+ assertEquals(
+ 1, methodSubject.streamInstructions().filter(InstructionSubject::isStaticPut).count());
+ }
+
+ @Test
+ public void otherTest() throws Exception {
+ String expectedOutput = StringUtils.lines("0");
+
+ if (backend == Backend.CF) {
+ testForJvm()
+ .addTestClasspath()
+ .run(OtherTestClass.class)
+ .assertSuccessWithOutput(expectedOutput);
+ }
+
+ CodeInspector inspector =
+ testForR8(Backend.DEX)
+ .addInnerClasses(IllegalFieldRebindingTest.class)
+ .addInnerClasses(IllegalFieldRebindingTestClasses.class)
+ .addKeepMainRule(OtherTestClass.class)
+ .enableMergeAnnotations()
+ .run(OtherTestClass.class)
+ .assertSuccessWithOutput(expectedOutput)
+ .inspector();
+
+ // The static-get instruction in OtherTestClass.main() should have been replaced by the
+ // constant 0, which makes the remaining classes unreachable.
+ assertEquals(1, inspector.allClasses().size());
+
+ ClassSubject classSubject = inspector.clazz(OtherTestClass.class);
+ assertThat(classSubject, isPresent());
+
+ MethodSubject methodSubject = classSubject.mainMethod();
+ assertThat(methodSubject, isPresent());
+
+ // Verify that a constant 0 is being loaded in main().
+ assertEquals(
+ 1,
+ methodSubject
+ .streamInstructions()
+ .filter(instruction -> instruction.isConstNumber(0))
+ .count());
+ }
+
+ static class TestClass {
+
+ public static void main(String[] args) {
+ // Cannot be member rebound because A is inaccessible to this package. However, the
+ // instruction cannot be removed as the field `A.f` is actually read.
+ B.f = 42;
+ B.print();
+ }
+ }
+
+ static class OtherTestClass {
+
+ public static void main(String[] args) {
+ // Cannot be member rebound because A is inaccessible to this package.
+ // However, the instruction should still be replaced by the constant 0.
+ System.out.println(B.f);
+ }
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/memberrebinding/testclasses/IllegalFieldRebindingTestClasses.java b/src/test/java/com/android/tools/r8/memberrebinding/testclasses/IllegalFieldRebindingTestClasses.java
new file mode 100644
index 0000000..64b4d1f
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/memberrebinding/testclasses/IllegalFieldRebindingTestClasses.java
@@ -0,0 +1,22 @@
+// Copyright (c) 2019, 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.memberrebinding.testclasses;
+
+import com.android.tools.r8.NeverMerge;
+
+public class IllegalFieldRebindingTestClasses {
+
+ @NeverMerge
+ static class A {
+ public static int f;
+ }
+
+ public static class B extends A {
+
+ public static void print() {
+ System.out.println(A.f);
+ }
+ }
+}