blob: d3c57fcc6df482f1ac63305c7df8a447ecd4ec0e [file] [log] [blame]
// Copyright (c) 2023, 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.conversion.passes;
import static com.android.tools.r8.graph.DexProgramClass.asProgramClassOrNull;
import com.android.tools.r8.contexts.CompilationContext.MethodProcessingContext;
import com.android.tools.r8.graph.AccessControl;
import com.android.tools.r8.graph.AppView;
import com.android.tools.r8.graph.DexClass;
import com.android.tools.r8.graph.DexProgramClass;
import com.android.tools.r8.graph.DexType;
import com.android.tools.r8.graph.ProgramMethod;
import com.android.tools.r8.ir.analysis.type.DynamicType;
import com.android.tools.r8.ir.analysis.type.TypeElement;
import com.android.tools.r8.ir.analysis.type.TypeUtils;
import com.android.tools.r8.ir.code.Assume;
import com.android.tools.r8.ir.code.BasicBlock;
import com.android.tools.r8.ir.code.CheckCast;
import com.android.tools.r8.ir.code.IRCode;
import com.android.tools.r8.ir.code.InstanceOf;
import com.android.tools.r8.ir.code.Instruction;
import com.android.tools.r8.ir.code.InstructionListIterator;
import com.android.tools.r8.ir.code.InvokeStatic;
import com.android.tools.r8.ir.code.Value;
import com.android.tools.r8.ir.conversion.MethodProcessor;
import com.android.tools.r8.ir.conversion.passes.result.CodeRewriterResult;
import com.android.tools.r8.ir.optimize.AffectedValues;
import com.android.tools.r8.ir.optimize.CodeRewriter;
import com.android.tools.r8.ir.optimize.UtilityMethodsForCodeOptimizations;
import com.android.tools.r8.ir.optimize.UtilityMethodsForCodeOptimizations.UtilityMethodForCodeOptimizations;
import com.android.tools.r8.shaking.AppInfoWithLiveness;
import java.util.Set;
public class TrivialCheckCastAndInstanceOfRemover extends CodeRewriterPass<AppInfoWithLiveness> {
public TrivialCheckCastAndInstanceOfRemover(AppView<?> appView) {
super(appView);
}
@Override
protected String getRewriterId() {
return "TrivialCheckCastAndInstanceOfRemover";
}
@Override
protected boolean shouldRewriteCode(IRCode code, MethodProcessor methodProcessor) {
return appView.enableWholeProgramOptimizations()
&& appView.options().testing.enableCheckCastAndInstanceOfRemoval
&& (code.metadata().mayHaveCheckCast() || code.metadata().mayHaveInstanceOf());
}
@Override
protected CodeRewriterResult rewriteCode(
IRCode code,
MethodProcessor methodProcessor,
MethodProcessingContext methodProcessingContext) {
assert appView.appInfo().hasLiveness();
AppView<AppInfoWithLiveness> appViewWithLiveness = appView.withLiveness();
boolean hasChanged = false;
// If we can remove a CheckCast it is due to us having at least as much information about the
// type as the CheckCast gives. We then need to propagate that information to the users of
// the CheckCast to ensure further optimizations and removals of CheckCast:
//
// : 1: NewArrayEmpty v2 <- v1(1) java.lang.String[] <-- v2 = String[]
// ...
// : 2: CheckCast v5 <- v2; java.lang.Object[] <-- v5 = Object[]
// ...
// : 3: ArrayGet v7 <- v5, v6(0) <-- v7 = Object
// : 4: CheckCast v8 <- v7; java.lang.String <-- v8 = String
// ...
//
// When looking at line 2 we can conclude that the CheckCast is trivial because v2 is String[]
// and remove it. However, v7 is still only known to be Object and we cannot remove the
// CheckCast at line 4 unless we update v7 with the most precise information by narrowing the
// affected values of v5. We therefore have to run the type analysis after each CheckCast
// removal.
boolean needToRemoveTrivialPhis = false;
for (BasicBlock block : code.getBlocks()) {
InstructionListIterator it = block.listIterator(code);
while (it.hasNext()) {
Instruction current = it.next();
if (current.isCheckCast()) {
boolean hasPhiUsers = current.outValue().hasPhiUsers();
AffectedValues affectedValues = new AffectedValues();
RemoveCheckCastInstructionIfTrivialResult removeResult =
removeCheckCastInstructionIfTrivial(
appViewWithLiveness,
current.asCheckCast(),
it,
code,
code.context(),
affectedValues,
methodProcessor,
methodProcessingContext);
if (removeResult != RemoveCheckCastInstructionIfTrivialResult.NO_REMOVALS) {
hasChanged = true;
needToRemoveTrivialPhis |= hasPhiUsers;
int blockSizeBeforeAssumeRemoval = block.size();
Instruction previous = it.peekPrevious();
if (removeResult == RemoveCheckCastInstructionIfTrivialResult.REMOVED_CAST_DO_NARROW) {
affectedValues.narrowingWithAssumeRemoval(
appView,
code,
typeAnalysis -> typeAnalysis.setKeepRedundantBlocksAfterAssumeRemoval(true));
} else {
assert removeResult
== RemoveCheckCastInstructionIfTrivialResult.REMOVED_CAST_DO_PROPAGATE;
affectedValues.propagateWithAssumeRemoval(
appView,
code,
typeAnalysis -> typeAnalysis.setKeepRedundantBlocksAfterAssumeRemoval(true));
}
if (block.size() != blockSizeBeforeAssumeRemoval) {
it = previous != null ? block.listIterator(code, previous) : block.listIterator(code);
}
}
} else if (current.isInstanceOf()) {
boolean hasPhiUsers = current.outValue().hasPhiUsers();
if (removeInstanceOfInstructionIfTrivial(
appViewWithLiveness, current.asInstanceOf(), it, code)) {
hasChanged = true;
needToRemoveTrivialPhis |= hasPhiUsers;
}
}
}
}
// ... v1
// ...
// v2 <- check-cast v1, T
// v3 <- phi(v1, v2)
// Removing check-cast may result in a trivial phi:
// v3 <- phi(v1, v1)
if (needToRemoveTrivialPhis) {
AffectedValues affectedValues = new AffectedValues();
code.removeAllDeadAndTrivialPhis(affectedValues);
affectedValues.narrowingWithAssumeRemoval(
appView,
code,
typeAnalysis -> typeAnalysis.setKeepRedundantBlocksAfterAssumeRemoval(true));
}
if (hasChanged) {
code.removeRedundantBlocks();
}
return CodeRewriterResult.hasChanged(hasChanged);
}
enum RemoveCheckCastInstructionIfTrivialResult {
NO_REMOVALS,
REMOVED_CAST_DO_NARROW,
REMOVED_CAST_DO_PROPAGATE
}
// Returns true if the given check-cast instruction was removed.
private RemoveCheckCastInstructionIfTrivialResult removeCheckCastInstructionIfTrivial(
AppView<AppInfoWithLiveness> appViewWithLiveness,
CheckCast checkCast,
InstructionListIterator it,
IRCode code,
ProgramMethod context,
Set<Value> affectedValues,
MethodProcessor methodProcessor,
MethodProcessingContext methodProcessingContext) {
Value inValue = checkCast.object();
Value outValue = checkCast.outValue();
DexType castType = checkCast.getType();
DexType baseCastType = castType.toBaseType(dexItemFactory);
// If the cast type is not accessible in the current context, we should not remove the cast
// in order to preserve runtime errors. Note that JVM and ART behave differently: see
// {@link com.android.tools.r8.ir.optimize.checkcast.IllegalAccessErrorTest}.
if (baseCastType.isClassType()) {
DexClass baseCastClass = appView.definitionFor(baseCastType);
if (baseCastClass == null
|| AccessControl.isClassAccessible(baseCastClass, code.context(), appViewWithLiveness)
.isPossiblyFalse()) {
return RemoveCheckCastInstructionIfTrivialResult.NO_REMOVALS;
}
}
if (!appView
.getOpenClosedInterfacesCollection()
.isDefinitelyInstanceOfStaticType(appViewWithLiveness, inValue)) {
return RemoveCheckCastInstructionIfTrivialResult.NO_REMOVALS;
}
// If the in-value is `null` and the cast-type is a float-array type, then trivial check-cast
// elimination may lead to verification errors. See b/123269162.
if (options.canHaveArtCheckCastVerifierBug()) {
if (inValue.getType().isNullType()
&& castType.isArrayType()
&& castType.toBaseType(dexItemFactory).isFloatType()) {
return RemoveCheckCastInstructionIfTrivialResult.NO_REMOVALS;
}
}
// If casting to an array of an interface type elimination may lead to verification errors.
// See b/132420510 and b/223424356.
if (options.canHaveIncorrectJoinForArrayOfInterfacesBug()) {
if (castType.isArrayType()) {
DexType baseType = castType.toBaseType(dexItemFactory);
if (baseType.isClassType() && baseType.isInterface(appViewWithLiveness)) {
return RemoveCheckCastInstructionIfTrivialResult.NO_REMOVALS;
}
}
}
TypeElement inTypeLattice = inValue.getType();
TypeElement outTypeLattice = outValue.getType();
TypeElement castTypeLattice = castType.toTypeElement(appView, inTypeLattice.nullability());
assert inTypeLattice.nullability().lessThanOrEqual(outTypeLattice.nullability());
if (inTypeLattice.lessThanOrEqual(castTypeLattice, appView)) {
// 1) Trivial cast.
// A a = ...
// A a' = (A) a;
// 2) Up-cast: we already have finer type info.
// A < B
// A a = ...
// B b = (B) a;
assert inTypeLattice.lessThanOrEqual(outTypeLattice, appView);
// The removeOrReplaceByDebugLocalWrite will propagate the incoming value for the CheckCast
// to the users of the CheckCast's out value.
//
// v2 = CheckCast A v1 ~~> DebugLocalWrite $v0 <- v1
//
// The DebugLocalWrite is not a user of the outvalue, we therefore have to wait and take the
// CheckCast invalue users that includes the potential DebugLocalWrite.
CodeRewriter.removeOrReplaceByDebugLocalWrite(checkCast, it, inValue, outValue);
affectedValues.addAll(inValue.affectedValues());
return RemoveCheckCastInstructionIfTrivialResult.REMOVED_CAST_DO_NARROW;
}
// If values of cast type are guaranteed to be null, then the out-value must be null if the cast
// succeeds. After removing all usages of the out-value, the check-cast instruction is replaced
// by a call to throwClassCastExceptionIfNotNull() to allow dead code elimination of the cast
// type.
if (castType.isClassType()
&& castType.isAlwaysNull(appViewWithLiveness)
&& !outValue.hasDebugUsers()
&& !appView.getSyntheticItems().isFinalized()) {
// Replace all usages of the out-value by null.
it.previous();
Value nullValue = it.insertConstNullInstruction(code, options);
it.next();
checkCast.outValue().replaceUsers(nullValue);
affectedValues.addAll(nullValue.affectedValues());
// Replace the check-cast instruction by throwClassCastExceptionIfNotNull().
UtilityMethodForCodeOptimizations throwClassCastExceptionIfNotNullMethod =
UtilityMethodsForCodeOptimizations.synthesizeThrowClassCastExceptionIfNotNullMethod(
appView, methodProcessor.getEventConsumer(), methodProcessingContext);
throwClassCastExceptionIfNotNullMethod.optimize(methodProcessor);
InvokeStatic replacement =
InvokeStatic.builder()
.setMethod(throwClassCastExceptionIfNotNullMethod.getMethod())
.setSingleArgument(checkCast.object())
.setPosition(checkCast)
.build();
it.replaceCurrentInstruction(replacement);
assert replacement.lookupSingleTarget(appView, context) != null;
if (checkCast.object().getType().isNullable()) {
return RemoveCheckCastInstructionIfTrivialResult.REMOVED_CAST_DO_NARROW;
}
return RemoveCheckCastInstructionIfTrivialResult.REMOVED_CAST_DO_PROPAGATE;
}
// If the cast is guaranteed to succeed and only there to ensure the program type checks, then
// check if the program would still type check after removing the cast.
if (checkCast.isSafeCheckCast()
|| checkCast
.getFirstOperand()
.getDynamicType(appViewWithLiveness)
.getDynamicUpperBoundType()
.lessThanOrEqualUpToNullability(castTypeLattice, appView)) {
TypeElement useType =
TypeUtils.computeUseType(appViewWithLiveness, context, checkCast.outValue());
if (inTypeLattice.lessThanOrEqualUpToNullability(useType, appView)) {
return RemoveCheckCastInstructionIfTrivialResult.REMOVED_CAST_DO_NARROW;
}
}
// Otherwise, keep the checkcast to preserve verification errors. E.g., down-cast:
// A < B < C
// c = ... // Even though we know c is of type A,
// a' = (B) c; // (this could be removed, since chained below.)
// a'' = (A) a'; // this should remain for runtime verification.
assert !inTypeLattice.isDefinitelyNull() || (inValue.isPhi() && !inTypeLattice.isNullType());
assert outTypeLattice.equalUpToNullability(castTypeLattice);
return RemoveCheckCastInstructionIfTrivialResult.NO_REMOVALS;
}
// Returns true if the given instance-of instruction was removed.
private boolean removeInstanceOfInstructionIfTrivial(
AppView<AppInfoWithLiveness> appViewWithLiveness,
InstanceOf instanceOf,
InstructionListIterator it,
IRCode code) {
ProgramMethod context = code.context();
// If the instance-of type is not accessible in the current context, we should not remove the
// instance-of instruction in order to preserve IllegalAccessError.
DexType instanceOfBaseType = instanceOf.type().toBaseType(dexItemFactory);
if (instanceOfBaseType.isClassType()) {
DexClass instanceOfClass = appView.definitionFor(instanceOfBaseType);
if (instanceOfClass == null
|| AccessControl.isClassAccessible(instanceOfClass, context, appViewWithLiveness)
.isPossiblyFalse()) {
return false;
}
}
Value inValue = instanceOf.value();
if (!appView
.getOpenClosedInterfacesCollection()
.isDefinitelyInstanceOfStaticType(appViewWithLiveness, inValue)) {
return false;
}
TypeElement inType = inValue.getType();
TypeElement instanceOfType =
TypeElement.fromDexType(instanceOf.type(), inType.nullability(), appView);
Value aliasValue = inValue.getAliasedValue();
if (inType.lessThanOrEqual(instanceOfType, appView)) {
if (inType.isDefinitelyNull()) {
return replaceInstanceOfByFalse(code, it);
}
if (inType.isDefinitelyNotNull()) {
return replaceInstanceOfByTrue(code, it);
}
if (options.canUseJavaUtilObjectsNonNull()) {
return replaceInstanceOfByNonNull(it, instanceOf);
}
}
if (aliasValue.isDefinedByInstructionSatisfying(Instruction::isCreatingInstanceOrArray)
&& instanceOfType.strictlyLessThan(inType, appView)) {
return replaceInstanceOfByFalse(code, it);
}
if (instanceOf.type().isClassType()
&& isNeverInstantiatedDirectlyOrIndirectly(instanceOf.type())) {
// The type of the instance-of instruction is a program class, and is never instantiated
// directly or indirectly. Thus, the in-value must be null, meaning that the instance-of
// instruction will always evaluate to false.
return replaceInstanceOfByFalse(code, it);
}
if (inType.isClassType()
&& isNeverInstantiatedDirectlyOrIndirectly(inType.asClassType().getClassType())) {
// The type of the in-value is a program class, and is never instantiated directly or
// indirectly. This, the in-value must be null, meaning that the instance-of instruction
// will always evaluate to false.
return replaceInstanceOfByFalse(code, it);
}
Value aliasedValue =
inValue.getSpecificAliasedValue(
value ->
value.isDefinedByInstructionSatisfying(
Instruction::isAssumeWithDynamicTypeAssumption));
if (aliasedValue != null) {
Assume assumeInstruction = aliasedValue.getDefinition().asAssume();
DynamicType dynamicType = assumeInstruction.getDynamicType();
if (dynamicType.getNullability().isDefinitelyNull()) {
return replaceInstanceOfByFalse(code, it);
} else if (dynamicType.isDynamicTypeWithUpperBound()
&& dynamicType
.asDynamicTypeWithUpperBound()
.getDynamicUpperBoundType()
.lessThanOrEqual(instanceOfType, appView)
&& (!inType.isNullable() || dynamicType.getNullability().isDefinitelyNotNull())) {
return replaceInstanceOfByTrue(code, it);
}
}
return false;
}
private boolean replaceInstanceOfByFalse(
IRCode code, InstructionListIterator instructionIterator) {
instructionIterator.replaceCurrentInstructionWithConstBoolean(code, false);
return true;
}
private boolean replaceInstanceOfByTrue(
IRCode code, InstructionListIterator instructionIterator) {
instructionIterator.replaceCurrentInstructionWithConstBoolean(code, true);
return true;
}
private boolean replaceInstanceOfByNonNull(
InstructionListIterator instructionIterator, InstanceOf instanceOf) {
InvokeStatic replacement =
InvokeStatic.builder()
.setMethod(dexItemFactory.objectsMethods.nonNull)
.setSingleArgument(instanceOf.value())
.setOutValue(instanceOf.outValue())
.build();
instructionIterator.replaceCurrentInstruction(replacement);
return true;
}
private boolean isNeverInstantiatedDirectlyOrIndirectly(DexType type) {
assert appView.appInfo().hasLiveness();
assert type.isClassType();
DexProgramClass clazz = asProgramClassOrNull(appView.definitionFor(type));
return clazz != null
&& !appView.appInfo().withLiveness().isInstantiatedDirectlyOrIndirectly(clazz);
}
}