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 a2e9e87..3ff4dcb 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;
@@ -70,6 +71,10 @@
return this.equals(other);
}
+ OptionalBool isLessThanOrEqualTo(AndroidApiLevel other);
+
+ OptionalBool isLessThanOrEqualTo(ComputedApiLevel other);
+
class NotSetApiLevel implements ComputedApiLevel {
private static final NotSetApiLevel INSTANCE = new NotSetApiLevel();
@@ -77,6 +82,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;
}
@@ -99,6 +116,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;
}
@@ -145,6 +172,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 bd3fee8..df0f324 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
@@ -40,6 +40,7 @@
import com.android.tools.r8.shaking.KeepFieldInfo;
import com.android.tools.r8.shaking.KeepMethodInfo;
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.IntBox;
import com.android.tools.r8.utils.InternalOptions;
@@ -642,8 +643,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);
@@ -652,17 +654,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 =
@@ -670,15 +672,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;
@@ -689,7 +691,7 @@
newStaticFieldType =
dynamicUpperBoundClassType.getInterfaces().getSingleKnownInterface();
} else {
- return field.getType();
+ return staticType;
}
} else {
newStaticFieldType = dynamicUpperBoundClassType.getClassType();
@@ -698,8 +700,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)) {
+ return staticType;
}
return newStaticFieldType;
@@ -965,10 +976,13 @@
if (!appView.appInfo().isSubtype(newReturnType, staticType)) {
return null;
}
- return AccessUtils.isAccessibleInSameContextsAs(
- newReturnType, method.getReturnType(), appView)
- ? newReturnType
- : null;
+ if (!AccessUtils.isAccessibleInSameContextsAs(newReturnType, staticType, appView)) {
+ return null;
+ }
+ if (!AndroidApiLevelUtils.isApiSafeForTypeStrengthening(newReturnType, staticType, appView)) {
+ return null;
+ }
+ return newReturnType;
}
private SingleValue getReturnValue(ProgramMethod method) {
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 374459a..189d9f9 100644
--- a/src/main/java/com/android/tools/r8/utils/AndroidApiLevelUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/AndroidApiLevelUtils.java
@@ -4,12 +4,20 @@
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.DexMethod;
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;
import com.android.tools.r8.ir.optimize.inliner.NopWhyAreYouNotInliningReporter;
@@ -95,11 +103,78 @@
if (apiLevelOfOriginal.isUnknownApiLevel()) {
return false;
}
- return apiLevelOfOriginal
- .asKnownApiLevel()
- .max(apiLevel)
- .asKnownApiLevel()
- .getApiLevel()
- .isLessThanOrEqualTo(options.getMinApiLevel());
+ return apiLevelOfOriginal.max(apiLevel).isLessThanOrEqualTo(options.getMinApiLevel()).isTrue();
+ }
+
+ private static boolean isApiSafeForReference(LibraryDefinition definition, AppView<?> appView) {
+ return isApiSafeForReference(definition, appView.apiLevelCompute(), appView.options());
+ }
+
+ 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, AppView<?> appView) {
+ assert appView.options().apiModelingOptions().enableApiCallerIdentification;
+ assert !isApiSafeForReference(newDefinition, appView)
+ : "Clients should first check if the definition is present on all apis since the min api";
+ AndroidApiLevelCompute androidApiLevelCompute = appView.apiLevelCompute();
+ 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) {
+ // 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;
+ }
+ if (!appView.options().apiModelingOptions().enableApiCallerIdentification) {
+ // Conservatively bail out if we don't have api modeling.
+ return false;
+ }
+ LibraryClass newBaseLibraryClass = newBaseClass.asLibraryClass();
+ if (isApiSafeForReference(newBaseLibraryClass, appView)) {
+ // 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, appView);
}
}
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..d974c7a
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/apimodel/ApiModelTypeStrengtheningAboveMinApiTest.java
@@ -0,0 +1,98 @@
+// 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)
+ .apply(ApiModelingTestHelper::disableOutliningAndStubbing)
+ .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..1d17e5c
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/apimodel/ApiModelTypeStrengtheningTest.java
@@ -0,0 +1,102 @@
+// 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)
+ .apply(ApiModelingTestHelper::disableOutliningAndStubbing)
+ .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;
+ }
+ }
+}