Replace instance-get instructions by requireNonNull() during value propagation
Change-Id: Ia2307f6fcb480c9b61eac944c3bd90380c66a455
Bug: 149650435
diff --git a/src/main/java/com/android/tools/r8/ir/code/FieldInstruction.java b/src/main/java/com/android/tools/r8/ir/code/FieldInstruction.java
index c8d6abc..69c6281 100644
--- a/src/main/java/com/android/tools/r8/ir/code/FieldInstruction.java
+++ b/src/main/java/com/android/tools/r8/ir/code/FieldInstruction.java
@@ -27,6 +27,15 @@
public abstract class FieldInstruction extends Instruction {
+ public enum Assumption {
+ NONE,
+ RECEIVER_NOT_NULL;
+
+ boolean canAssumeReceiverIsNotNull() {
+ return this == RECEIVER_NOT_NULL;
+ }
+ }
+
private final DexField field;
protected FieldInstruction(DexField field, Value dest, Value value) {
@@ -61,6 +70,11 @@
@Override
public AbstractError instructionInstanceCanThrow(AppView<?> appView, DexType context) {
+ return instructionInstanceCanThrow(appView, context, Assumption.NONE);
+ }
+
+ public AbstractError instructionInstanceCanThrow(
+ AppView<?> appView, DexType context, Assumption assumption) {
DexEncodedField resolvedField;
if (appView.enableWholeProgramOptimizations()) {
// TODO(b/123857022): Should be possible to use definitionFor().
@@ -106,9 +120,11 @@
// TODO(b/137168535): Without non-null tracking, only locally created receiver is allowed in D8.
// * NullPointerException (null receiver).
if (isInstanceGet() || isInstancePut()) {
- Value receiver = inValues.get(0);
- if (receiver.isAlwaysNull(appView) || receiver.typeLattice.isNullable()) {
- return AbstractError.specific(appView.dexItemFactory().npeType);
+ if (!assumption.canAssumeReceiverIsNotNull()) {
+ Value receiver = inValues.get(0);
+ if (receiver.isAlwaysNull(appView) || receiver.typeLattice.isNullable()) {
+ return AbstractError.specific(appView.dexItemFactory().npeType);
+ }
}
}
// For D8, reaching here means the field is in the same context, hence the class is guaranteed
diff --git a/src/main/java/com/android/tools/r8/ir/code/InstanceGet.java b/src/main/java/com/android/tools/r8/ir/code/InstanceGet.java
index 3155fa1..2ca1b2a 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InstanceGet.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InstanceGet.java
@@ -116,7 +116,12 @@
@Override
public boolean instructionMayHaveSideEffects(AppView<?> appView, DexType context) {
- return instructionInstanceCanThrow(appView, context).isThrowing();
+ return instructionMayHaveSideEffects(appView, context, Assumption.NONE);
+ }
+
+ public boolean instructionMayHaveSideEffects(
+ AppView<?> appView, DexType context, Assumption assumption) {
+ return instructionInstanceCanThrow(appView, context, assumption).isThrowing();
}
@Override
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 722625f..a28181f 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
@@ -20,9 +20,12 @@
import com.android.tools.r8.ir.code.FieldInstruction;
import com.android.tools.r8.ir.code.IRCode;
import com.android.tools.r8.ir.code.IRMetadata;
+import com.android.tools.r8.ir.code.InstanceGet;
import com.android.tools.r8.ir.code.Instruction;
import com.android.tools.r8.ir.code.InstructionListIterator;
import com.android.tools.r8.ir.code.InvokeMethod;
+import com.android.tools.r8.ir.code.InvokeStatic;
+import com.android.tools.r8.ir.code.InvokeVirtual;
import com.android.tools.r8.ir.code.Value;
import com.android.tools.r8.ir.optimize.info.OptimizationFeedback;
import com.android.tools.r8.ir.optimize.info.OptimizationFeedbackSimple;
@@ -31,6 +34,7 @@
import com.android.tools.r8.shaking.ProguardMemberRuleReturnValue;
import com.android.tools.r8.utils.Reporter;
import com.android.tools.r8.utils.StringDiagnostic;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.Sets;
import java.util.ListIterator;
import java.util.Set;
@@ -336,12 +340,24 @@
target.valueAsConstInstruction(code, current.outValue().getLocalInfo(), appView);
if (replacement != null) {
affectedValues.addAll(current.outValue().affectedValues());
- if (current.instructionMayHaveSideEffects(appView, code.method.method.holder)) {
- // To preserve class initialization/NPE side effects, original field-get remains as-is,
- // but its value is replaced with constant.
- replacement.setPosition(current.getPosition());
+ DexType context = code.method.method.holder;
+ if (current.instructionMayHaveSideEffects(appView, context)) {
+ // All usages are replaced by the replacement value.
current.outValue().replaceUsers(replacement.outValue());
- if (current.getBlock().hasCatchHandlers()) {
+
+ // To preserve side effects, original field-get is replaced by an explicit null-check, if
+ // the field-get instruction may only fail with an NPE, or the field-get remains as-is.
+ Instruction currentOrNullCheck;
+ if (current.isInstanceGet()) {
+ currentOrNullCheck =
+ replaceInstanceGetByNullCheckIfPossible(current.asInstanceGet(), iterator, context);
+ } else {
+ currentOrNullCheck = current;
+ }
+
+ // Insert the definition of the replacement.
+ replacement.setPosition(currentOrNullCheck.getPosition());
+ if (currentOrNullCheck.getBlock().hasCatchHandlers()) {
iterator.split(code, blocks).listIterator(code).add(replacement);
} else {
iterator.add(replacement);
@@ -353,6 +369,26 @@
}
}
+ private Instruction replaceInstanceGetByNullCheckIfPossible(
+ InstanceGet instruction, InstructionListIterator iterator, DexType context) {
+ assert !instruction.outValue().hasAnyUsers();
+ if (instruction.instructionMayHaveSideEffects(
+ appView, context, FieldInstruction.Assumption.RECEIVER_NOT_NULL)) {
+ return instruction;
+ }
+ Value receiver = instruction.object();
+ InvokeMethod replacement;
+ if (appView.options().canUseRequireNonNull()) {
+ DexMethod requireNonNullMethod = appView.dexItemFactory().objectsMethods.requireNonNull;
+ replacement = new InvokeStatic(requireNonNullMethod, null, ImmutableList.of(receiver));
+ } else {
+ DexMethod getClassMethod = appView.dexItemFactory().objectMethods.getClass;
+ replacement = new InvokeVirtual(getClassMethod, null, ImmutableList.of(receiver));
+ }
+ iterator.replaceCurrentInstruction(replacement);
+ return replacement;
+ }
+
/**
* Replace invoke targets and field accesses with constant values where possible.
*
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/membervaluepropagation/InstanceFieldValuePropagationTest.java b/src/test/java/com/android/tools/r8/ir/optimize/membervaluepropagation/InstanceFieldValuePropagationTest.java
index a74ac5f..2c8f4b3 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/membervaluepropagation/InstanceFieldValuePropagationTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/membervaluepropagation/InstanceFieldValuePropagationTest.java
@@ -75,12 +75,10 @@
MethodSubject testMaybeNullMethodSubject =
testClassSubject.uniqueMethodWithName("testMaybeNull");
assertThat(testMaybeNullMethodSubject, isPresent());
- // TODO(b/125282093): Should synthesize a null-check and still propagate the field values even
- // when the receiver is nullable.
assertTrue(
testMaybeNullMethodSubject
.streamInstructions()
- .anyMatch(InstructionSubject::isInstanceGet));
+ .noneMatch(InstructionSubject::isInstanceGet));
// TODO(b/125282093): Should be able to remove the new-instance instruction since the instance
// ends up being unused.
assertTrue(
diff --git a/src/test/java/com/android/tools/r8/shaking/FieldReadsJasminTest.java b/src/test/java/com/android/tools/r8/shaking/FieldReadsJasminTest.java
index dfd14da..397c054 100644
--- a/src/test/java/com/android/tools/r8/shaking/FieldReadsJasminTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/FieldReadsJasminTest.java
@@ -17,6 +17,7 @@
import com.android.tools.r8.jasmin.JasminBuilder.ClassBuilder;
import com.android.tools.r8.jasmin.JasminTestBase;
import com.android.tools.r8.naming.MemberNaming.MethodSignature;
+import com.android.tools.r8.utils.ThrowingConsumer;
import com.android.tools.r8.utils.codeinspector.ClassSubject;
import com.android.tools.r8.utils.codeinspector.CodeInspector;
import com.android.tools.r8.utils.codeinspector.FieldSubject;
@@ -37,7 +38,7 @@
@Parameterized.Parameters(name = "{0}")
public static TestParametersCollection data() {
- return getTestParameters().withAllRuntimes().build();
+ return getTestParameters().withAllRuntimesAndApiLevels().build();
}
public FieldReadsJasminTest(TestParameters parameters) {
@@ -99,7 +100,7 @@
if (parameters.isDexRuntime()) {
testForD8()
.addProgramClassFileData(classes)
- .setMinApi(parameters.getRuntime())
+ .setMinApi(parameters.getApiLevel())
.compile()
.inspect(inspector -> ensureNoFieldsRead(inspector, clazz.name, false));
}
@@ -107,7 +108,7 @@
testForR8(parameters.getBackend())
.addProgramClassFileData(classes)
.addKeepRules("-keep class * { <methods>; }")
- .setMinApi(parameters.getRuntime())
+ .setMinApi(parameters.getApiLevel())
.compile()
.inspect(inspector -> ensureNoFieldsRead(inspector, clazz.name, true));
}
@@ -167,20 +168,21 @@
testForR8(parameters.getBackend())
.addProgramClassFileData(app.buildClasses())
.addKeepRules("-keep class * { <methods>; }")
- .setMinApi(parameters.getRuntime())
+ .setMinApi(parameters.getApiLevel())
.compile()
- .inspect(inspector -> {
- FieldSubject fld = inspector.clazz(fieldHolder.name).uniqueFieldWithName(fieldName);
- assertThat(fld, isRenamed());
+ .inspect(
+ inspector -> {
+ FieldSubject fld = inspector.clazz(fieldHolder.name).uniqueFieldWithName(fieldName);
+ assertThat(fld, isRenamed());
- ClassSubject classSubject = inspector.clazz(clazz.name);
- assertThat(classSubject, isPresent());
- MethodSubject methodSubject = classSubject.uniqueMethodWithName(method.name);
- assertThat(methodSubject, isPresent());
- Iterator<InstructionSubject> it =
- methodSubject.iterateInstructions(InstructionSubject::isFieldAccess);
- assertFalse(it.hasNext());
- });
+ ClassSubject classSubject = inspector.clazz(clazz.name);
+ assertThat(classSubject, isPresent());
+ MethodSubject methodSubject = classSubject.uniqueMethodWithName(method.name);
+ assertThat(methodSubject, isPresent());
+ Iterator<InstructionSubject> it =
+ methodSubject.iterateInstructions(InstructionSubject::isFieldAccess);
+ assertFalse(it.hasNext());
+ });
}
@Test
@@ -205,7 +207,27 @@
" invokestatic Empty/foo(L" + CLS + ";)V",
" return");
- ensureFieldExistsAndReadOnlyOnce(builder, empty, foo, empty, "aField");
+ inspect(
+ builder,
+ inspector ->
+ ensureFieldExistsAndReadOnlyOnce(
+ inspector, empty.name, foo.name, empty, "aField", false),
+ inspector -> {
+ ClassSubject emptyClassSubject = inspector.clazz(CLS);
+ assertThat(emptyClassSubject, isPresent());
+ assertTrue(emptyClassSubject.allFields().isEmpty());
+
+ MethodSubject fooMethodSubject = emptyClassSubject.uniqueMethodWithName("foo");
+ assertThat(fooMethodSubject, isPresent());
+ assertTrue(
+ fooMethodSubject
+ .streamInstructions()
+ .filter(InstructionSubject::isInvoke)
+ .anyMatch(
+ invoke ->
+ invoke.getMethod().toSourceString().contains("requireNonNull")
+ || invoke.getMethod().toSourceString().contains("getClass")));
+ });
}
@Test
@@ -275,6 +297,29 @@
ensureFieldExistsAndReadOnlyOnce(builder, main, mainMethod, main, "sField");
}
+ private void inspect(
+ JasminBuilder app,
+ ThrowingConsumer<CodeInspector, RuntimeException> d8Inspector,
+ ThrowingConsumer<CodeInspector, RuntimeException> r8Inspector)
+ throws Exception {
+ List<byte[]> classes = app.buildClasses();
+
+ if (parameters.isDexRuntime()) {
+ testForD8()
+ .addProgramClassFileData(classes)
+ .setMinApi(parameters.getApiLevel())
+ .compile()
+ .inspect(d8Inspector);
+ }
+
+ testForR8(parameters.getBackend())
+ .addProgramClassFileData(classes)
+ .addKeepRules("-keep class * { <methods>; }")
+ .setMinApi(parameters.getApiLevel())
+ .compile()
+ .inspect(r8Inspector);
+ }
+
private void ensureFieldExistsAndReadOnlyOnce(
JasminBuilder app,
ClassBuilder clazz,
@@ -282,24 +327,14 @@
ClassBuilder fieldHolder,
String fieldName)
throws Exception {
- List<byte[]> classes = app.buildClasses();
-
- if (parameters.isDexRuntime()) {
- testForD8()
- .addProgramClassFileData(classes)
- .setMinApi(parameters.getRuntime())
- .compile()
- .inspect(inspector -> ensureFieldExistsAndReadOnlyOnce(
- inspector, clazz.name, method.name, fieldHolder, fieldName, false));
- }
-
- testForR8(parameters.getBackend())
- .addProgramClassFileData(classes)
- .addKeepRules("-keep class * { <methods>; }")
- .setMinApi(parameters.getRuntime())
- .compile()
- .inspect(inspector -> ensureFieldExistsAndReadOnlyOnce(
- inspector, clazz.name, method.name, fieldHolder, fieldName, true));
+ inspect(
+ app,
+ inspector ->
+ ensureFieldExistsAndReadOnlyOnce(
+ inspector, clazz.name, method.name, fieldHolder, fieldName, false),
+ inspector ->
+ ensureFieldExistsAndReadOnlyOnce(
+ inspector, clazz.name, method.name, fieldHolder, fieldName, true));
}
private void ensureFieldExistsAndReadOnlyOnce(