Consult api database during type strengthening
Bug: b/236294139
Change-Id: I9e070782a2da544eba90b15c41d5b7281142cc3b
diff --git a/src/main/java/com/android/tools/r8/androidapi/ComputedApiLevel.java b/src/main/java/com/android/tools/r8/androidapi/ComputedApiLevel.java
index 2f5ab5e..aa2402c 100644
--- a/src/main/java/com/android/tools/r8/androidapi/ComputedApiLevel.java
+++ b/src/main/java/com/android/tools/r8/androidapi/ComputedApiLevel.java
@@ -5,6 +5,7 @@
package com.android.tools.r8.androidapi;
import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.OptionalBool;
import com.android.tools.r8.utils.structural.Equatable;
import java.util.Objects;
@@ -66,6 +67,10 @@
return this.equals(other);
}
+ OptionalBool isLessThanOrEqualTo(AndroidApiLevel other);
+
+ OptionalBool isLessThanOrEqualTo(ComputedApiLevel other);
+
class NotSetApiLevel implements ComputedApiLevel {
private static final NotSetApiLevel INSTANCE = new NotSetApiLevel();
@@ -73,6 +78,18 @@
private NotSetApiLevel() {}
@Override
+ public OptionalBool isLessThanOrEqualTo(AndroidApiLevel other) {
+ assert false : "Cannot compute relationship for not set";
+ return OptionalBool.unknown();
+ }
+
+ @Override
+ public OptionalBool isLessThanOrEqualTo(ComputedApiLevel other) {
+ assert false : "Cannot compute relationship for not set";
+ return OptionalBool.unknown();
+ }
+
+ @Override
public boolean isNotSetApiLevel() {
return true;
}
@@ -95,6 +112,16 @@
private UnknownApiLevel() {}
@Override
+ public OptionalBool isLessThanOrEqualTo(AndroidApiLevel other) {
+ return OptionalBool.unknown();
+ }
+
+ @Override
+ public OptionalBool isLessThanOrEqualTo(ComputedApiLevel other) {
+ return OptionalBool.unknown();
+ }
+
+ @Override
public boolean isUnknownApiLevel() {
return true;
}
@@ -141,6 +168,20 @@
}
@Override
+ public OptionalBool isLessThanOrEqualTo(AndroidApiLevel other) {
+ return OptionalBool.of(apiLevel.isLessThanOrEqualTo(other));
+ }
+
+ @Override
+ public OptionalBool isLessThanOrEqualTo(ComputedApiLevel other) {
+ if (other.isKnownApiLevel()) {
+ return isLessThanOrEqualTo(other.asKnownApiLevel().getApiLevel());
+ }
+ assert other.isUnknownApiLevel() : "Cannot compute relationship for not set";
+ return OptionalBool.unknown();
+ }
+
+ @Override
public String toString() {
return apiLevel.toString();
}
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorProgramOptimizer.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorProgramOptimizer.java
index 1bc719c..134163d 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorProgramOptimizer.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorProgramOptimizer.java
@@ -6,6 +6,7 @@
import static com.android.tools.r8.utils.MapUtils.ignoreKey;
+import com.android.tools.r8.androidapi.AndroidApiLevelCompute;
import com.android.tools.r8.graph.AppView;
import com.android.tools.r8.graph.DexClass;
import com.android.tools.r8.graph.DexEncodedMethod;
@@ -35,6 +36,7 @@
import com.android.tools.r8.shaking.AppInfoWithLiveness;
import com.android.tools.r8.shaking.KeepFieldInfo;
import com.android.tools.r8.utils.AccessUtils;
+import com.android.tools.r8.utils.AndroidApiLevelUtils;
import com.android.tools.r8.utils.BooleanBox;
import com.android.tools.r8.utils.BooleanUtils;
import com.android.tools.r8.utils.IntBox;
@@ -109,6 +111,7 @@
}
private final AppView<AppInfoWithLiveness> appView;
+ private final AndroidApiLevelCompute androidApiLevelCompute;
private final ImmediateProgramSubtypingInfo immediateSubtypingInfo;
private final Map<Set<DexProgramClass>, DexMethodSignatureSet> interfaceDispatchOutsideProgram;
@@ -120,6 +123,7 @@
ImmediateProgramSubtypingInfo immediateSubtypingInfo,
Map<Set<DexProgramClass>, DexMethodSignatureSet> interfaceDispatchOutsideProgram) {
this.appView = appView;
+ this.androidApiLevelCompute = AndroidApiLevelCompute.create(appView);
this.immediateSubtypingInfo = immediateSubtypingInfo;
this.interfaceDispatchOutsideProgram = interfaceDispatchOutsideProgram;
}
@@ -486,8 +490,9 @@
private DexType getNewFieldType(ProgramField field) {
DynamicType dynamicType = field.getOptimizationInfo().getDynamicType();
+ DexType staticType = field.getType();
if (dynamicType.isUnknown()) {
- return field.getType();
+ return staticType;
}
KeepFieldInfo keepInfo = appView.getKeepInfo(field);
@@ -496,17 +501,17 @@
assert !keepInfo.isPinned(options);
if (!keepInfo.isFieldTypeStrengtheningAllowed(options)) {
- return field.getType();
+ return staticType;
}
if (dynamicType.isNullType()) {
// Don't optimize always null fields; these will be optimized anyway.
- return field.getType();
+ return staticType;
}
if (dynamicType.isNotNullType()) {
// We don't have a more specific type.
- return field.getType();
+ return staticType;
}
DynamicTypeWithUpperBound dynamicTypeWithUpperBound =
@@ -514,15 +519,15 @@
TypeElement dynamicUpperBoundType = dynamicTypeWithUpperBound.getDynamicUpperBoundType();
assert dynamicUpperBoundType.isReferenceType();
- ClassTypeElement staticFieldType = field.getType().toTypeElement(appView).asClassType();
+ ClassTypeElement staticFieldType = staticType.toTypeElement(appView).asClassType();
if (dynamicUpperBoundType.equalUpToNullability(staticFieldType)) {
// We don't have more precise type information.
- return field.getType();
+ return staticType;
}
if (!dynamicUpperBoundType.strictlyLessThan(staticFieldType, appView)) {
assert options.testing.allowTypeErrors;
- return field.getType();
+ return staticType;
}
DexType newStaticFieldType;
@@ -533,7 +538,7 @@
newStaticFieldType =
dynamicUpperBoundClassType.getInterfaces().getSingleKnownInterface();
} else {
- return field.getType();
+ return staticType;
}
} else {
newStaticFieldType = dynamicUpperBoundClassType.getClassType();
@@ -542,8 +547,17 @@
newStaticFieldType = dynamicUpperBoundType.asArrayType().toDexType(dexItemFactory);
}
- if (!AccessUtils.isAccessibleInSameContextsAs(newStaticFieldType, field.getType(), appView)) {
- return field.getType();
+ if (newStaticFieldType == staticType) {
+ return staticType;
+ }
+
+ if (!AccessUtils.isAccessibleInSameContextsAs(newStaticFieldType, staticType, appView)) {
+ return staticType;
+ }
+
+ if (!AndroidApiLevelUtils.isApiSafeForTypeStrengthening(
+ newStaticFieldType, staticType, appView, androidApiLevelCompute)) {
+ return staticType;
}
return newStaticFieldType;
diff --git a/src/main/java/com/android/tools/r8/utils/AndroidApiLevelUtils.java b/src/main/java/com/android/tools/r8/utils/AndroidApiLevelUtils.java
index 089ccce..4c453da 100644
--- a/src/main/java/com/android/tools/r8/utils/AndroidApiLevelUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/AndroidApiLevelUtils.java
@@ -4,11 +4,19 @@
package com.android.tools.r8.utils;
+import static com.android.tools.r8.graph.DexLibraryClass.asLibraryClassOrNull;
+
import com.android.tools.r8.androidapi.AndroidApiLevelCompute;
import com.android.tools.r8.androidapi.ComputedApiLevel;
+import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexClass;
import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexItemFactory;
import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.LibraryClass;
+import com.android.tools.r8.graph.LibraryDefinition;
import com.android.tools.r8.graph.LibraryMethod;
import com.android.tools.r8.graph.ProgramMethod;
@@ -65,6 +73,81 @@
return false;
}
assert options.apiModelingOptions().enableApiCallerIdentification;
- return apiLevel.asKnownApiLevel().getApiLevel().isLessThanOrEqualTo(options.getMinApiLevel());
+ return apiLevel.isLessThanOrEqualTo(options.getMinApiLevel()).isTrue();
+ }
+
+ private static boolean isApiSafeForReference(
+ LibraryDefinition definition,
+ AndroidApiLevelCompute androidApiLevelCompute,
+ InternalOptions options) {
+ assert options.apiModelingOptions().enableApiCallerIdentification;
+ ComputedApiLevel apiLevel =
+ androidApiLevelCompute.computeApiLevelForLibraryReference(
+ definition.getReference(), ComputedApiLevel.unknown());
+ return apiLevel.isLessThanOrEqualTo(options.getMinApiLevel()).isTrue();
+ }
+
+ private static boolean isApiSafeForReference(
+ LibraryDefinition newDefinition,
+ LibraryDefinition oldDefinition,
+ AndroidApiLevelCompute androidApiLevelCompute,
+ InternalOptions options) {
+ assert options.apiModelingOptions().enableApiCallerIdentification;
+ assert !isApiSafeForReference(newDefinition, androidApiLevelCompute, options)
+ : "Clients should first check if the definition is present on all apis since the min api";
+ ComputedApiLevel apiLevel =
+ androidApiLevelCompute.computeApiLevelForLibraryReference(
+ newDefinition.getReference(), ComputedApiLevel.unknown());
+ if (apiLevel.isUnknownApiLevel()) {
+ return false;
+ }
+ ComputedApiLevel apiLevelOfOriginal =
+ androidApiLevelCompute.computeApiLevelForLibraryReference(
+ oldDefinition.getReference(), ComputedApiLevel.unknown());
+ return apiLevel.isLessThanOrEqualTo(apiLevelOfOriginal).isTrue();
+ }
+
+ public static boolean isApiSafeForTypeStrengthening(
+ DexType newType,
+ DexType oldType,
+ AppView<? extends AppInfoWithClassHierarchy> appView,
+ AndroidApiLevelCompute androidApiLevelCompute) {
+ // Type strengthening only applies to reference types.
+ assert newType.isReferenceType();
+ assert oldType.isReferenceType();
+ assert newType != oldType;
+ DexItemFactory dexItemFactory = appView.dexItemFactory();
+ DexType newBaseType = newType.toBaseType(dexItemFactory);
+ if (newBaseType.isPrimitiveType()) {
+ // Array of primitives is available on all api levels.
+ return true;
+ }
+ assert newBaseType.isClassType();
+ DexClass newBaseClass = appView.definitionFor(newBaseType);
+ if (newBaseClass == null) {
+ // This could be a library class that is only available on newer api levels.
+ return false;
+ }
+ if (!newBaseClass.isLibraryClass()) {
+ // Program and classpath classes are not api level dependent.
+ return true;
+ }
+ InternalOptions options = appView.options();
+ if (!options.apiModelingOptions().enableApiCallerIdentification) {
+ // Conservatively bail out if we don't have api modeling.
+ return false;
+ }
+ LibraryClass newBaseLibraryClass = newBaseClass.asLibraryClass();
+ if (isApiSafeForReference(newBaseLibraryClass, androidApiLevelCompute, options)) {
+ // Library class is present on all api levels since min api.
+ return true;
+ }
+ // Check if the new library class is present since the api level of the old type.
+ DexType oldBaseType = oldType.toBaseType(dexItemFactory);
+ assert oldBaseType.isClassType();
+ LibraryClass oldBaseLibraryClass = asLibraryClassOrNull(appView.definitionFor(oldBaseType));
+ return oldBaseLibraryClass != null
+ && isApiSafeForReference(
+ newBaseLibraryClass, oldBaseLibraryClass, androidApiLevelCompute, options);
}
}
diff --git a/src/test/java/com/android/tools/r8/apimodel/ApiModelTypeStrengtheningAboveMinApiTest.java b/src/test/java/com/android/tools/r8/apimodel/ApiModelTypeStrengtheningAboveMinApiTest.java
new file mode 100644
index 0000000..16e5c6c
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/apimodel/ApiModelTypeStrengtheningAboveMinApiTest.java
@@ -0,0 +1,97 @@
+// Copyright (c) 2022, 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.apimodel;
+
+import static com.android.tools.r8.apimodel.ApiModelingTestHelper.setMockApiLevelForClass;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.codeinspector.FieldSubject;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class ApiModelTypeStrengtheningAboveMinApiTest extends TestBase {
+
+ @Parameter(0)
+ public TestParameters parameters;
+
+ @Parameters(name = "{0}")
+ public static TestParametersCollection data() {
+ return getTestParameters().withAllRuntimesAndApiLevels().build();
+ }
+
+ @Test
+ public void test() throws Exception {
+ int sdkInt = parameters.isCfRuntime() ? 0 : parameters.getApiLevel().getLevel();
+ testForR8(parameters.getBackend())
+ .addProgramClasses(Main.class, Version.class)
+ .addLibraryClasses(ApiLevel22.class, ApiLevel23.class)
+ .addDefaultRuntimeLibrary(parameters)
+ .addKeepMainRule(Main.class)
+ .addKeepRules(
+ "-assumevalues class " + Version.class.getTypeName() + " {",
+ " public static int getSdkInt(int) return " + sdkInt + "..42;",
+ "}")
+ .apply(setMockApiLevelForClass(ApiLevel22.class, AndroidApiLevel.L_MR1))
+ .apply(setMockApiLevelForClass(ApiLevel23.class, AndroidApiLevel.M))
+ .apply(ApiModelingTestHelper::enableApiCallerIdentification)
+ .setMinApi(parameters.getApiLevel())
+ .compile()
+ .inspect(
+ inspector -> {
+ FieldSubject fieldSubject = inspector.clazz(Main.class).uniqueFieldWithName("FIELD");
+ assertThat(fieldSubject, isPresent());
+ assertEquals(
+ ApiLevel22.class.getTypeName(), fieldSubject.getField().getType().getTypeName());
+ })
+ .addRunClasspathClasses(ApiLevel22.class, ApiLevel23.class)
+ .run(parameters.getRuntime(), Main.class, Integer.toString(sdkInt))
+ .applyIf(
+ parameters.isDexRuntime()
+ && parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.L_MR1),
+ runResult -> runResult.assertSuccessWithOutputLines("ApiLevel22"),
+ runResult -> runResult.assertSuccessWithOutputLines("null"));
+ }
+
+ public static class ApiLevel23 {}
+
+ public static class ApiLevel22 extends ApiLevel23 {
+
+ @Override
+ public String toString() {
+ return "ApiLevel22";
+ }
+ }
+
+ public static class Main {
+
+ public static ApiLevel23 FIELD;
+
+ public static void main(String[] args) {
+ int sdk = Integer.parseInt(args[0]);
+ if (Version.getSdkInt(sdk) >= 22) {
+ FIELD = new ApiLevel22();
+ }
+ System.out.println(FIELD);
+ }
+ }
+
+ public static class Version {
+
+ // -assumevalues ...
+ public static int getSdkInt(int sdk) {
+ return sdk;
+ }
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/apimodel/ApiModelTypeStrengtheningTest.java b/src/test/java/com/android/tools/r8/apimodel/ApiModelTypeStrengtheningTest.java
new file mode 100644
index 0000000..5df717d
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/apimodel/ApiModelTypeStrengtheningTest.java
@@ -0,0 +1,101 @@
+// Copyright (c) 2022, 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.apimodel;
+
+import static com.android.tools.r8.apimodel.ApiModelingTestHelper.setMockApiLevelForClass;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.codeinspector.FieldSubject;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class ApiModelTypeStrengtheningTest extends TestBase {
+
+ @Parameter(0)
+ public TestParameters parameters;
+
+ @Parameters(name = "{0}")
+ public static TestParametersCollection data() {
+ return getTestParameters().withAllRuntimesAndApiLevels().build();
+ }
+
+ @Test
+ public void test() throws Exception {
+ boolean isTypeStrengtheningSafe =
+ parameters.isDexRuntime()
+ && parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.M);
+ int sdkInt = parameters.isCfRuntime() ? 0 : parameters.getApiLevel().getLevel();
+ testForR8(parameters.getBackend())
+ .addProgramClasses(Main.class, Version.class)
+ .addLibraryClasses(ApiLevel22.class, ApiLevel23.class)
+ .addDefaultRuntimeLibrary(parameters)
+ .addKeepMainRule(Main.class)
+ .addKeepRules(
+ "-assumevalues class " + Version.class.getTypeName() + " {",
+ " public static int getSdkInt(int) return " + sdkInt + "..42;",
+ "}")
+ .apply(setMockApiLevelForClass(ApiLevel22.class, AndroidApiLevel.L_MR1))
+ .apply(setMockApiLevelForClass(ApiLevel23.class, AndroidApiLevel.M))
+ .apply(ApiModelingTestHelper::enableApiCallerIdentification)
+ .setMinApi(parameters.getApiLevel())
+ .compile()
+ .inspect(
+ inspector -> {
+ Class<?> expectedFieldType =
+ isTypeStrengtheningSafe ? ApiLevel23.class : ApiLevel22.class;
+ FieldSubject fieldSubject = inspector.clazz(Main.class).uniqueFieldWithName("FIELD");
+ assertThat(fieldSubject, isPresent());
+ assertEquals(
+ expectedFieldType.getTypeName(), fieldSubject.getField().getType().getTypeName());
+ })
+ .addRunClasspathClasses(ApiLevel22.class, ApiLevel23.class)
+ .run(parameters.getRuntime(), Main.class, Integer.toString(sdkInt))
+ .applyIf(
+ isTypeStrengtheningSafe,
+ runResult -> runResult.assertSuccessWithOutputLines("ApiLevel23"),
+ runResult -> runResult.assertSuccessWithOutputLines("null"));
+ }
+
+ public static class ApiLevel22 {}
+
+ public static class ApiLevel23 extends ApiLevel22 {
+
+ @Override
+ public String toString() {
+ return "ApiLevel23";
+ }
+ }
+
+ public static class Main {
+
+ public static ApiLevel22 FIELD;
+
+ public static void main(String[] args) {
+ int sdk = Integer.parseInt(args[0]);
+ if (Version.getSdkInt(sdk) >= 23) {
+ FIELD = new ApiLevel23();
+ }
+ System.out.println(FIELD);
+ }
+ }
+
+ public static class Version {
+
+ // -assumevalues ...
+ public static int getSdkInt(int sdk) {
+ return sdk;
+ }
+ }
+}