blob: 94ee9cf376c1f59bebec974ef1a12571e2f8ee87 [file] [log] [blame]
// 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.ir.analysis;
import com.android.tools.r8.errors.Unreachable;
import com.android.tools.r8.graph.AppView;
import com.android.tools.r8.graph.DexClass;
import com.android.tools.r8.graph.DexDefinition;
import com.android.tools.r8.graph.DexEncodedField;
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.DexType;
import com.android.tools.r8.graph.ResolutionResult;
import com.android.tools.r8.ir.analysis.type.TypeElement;
import com.android.tools.r8.ir.code.BasicBlock;
import com.android.tools.r8.ir.code.CatchHandlers.CatchHandler;
import com.android.tools.r8.ir.code.DominatorTree;
import com.android.tools.r8.ir.code.DominatorTree.Assumption;
import com.android.tools.r8.ir.code.DominatorTree.Inclusive;
import com.android.tools.r8.ir.code.FieldInstruction;
import com.android.tools.r8.ir.code.IRCode;
import com.android.tools.r8.ir.code.InitClass;
import com.android.tools.r8.ir.code.InstanceGet;
import com.android.tools.r8.ir.code.InstancePut;
import com.android.tools.r8.ir.code.Instruction;
import com.android.tools.r8.ir.code.InstructionIterator;
import com.android.tools.r8.ir.code.InvokeDirect;
import com.android.tools.r8.ir.code.InvokeInterface;
import com.android.tools.r8.ir.code.InvokeStatic;
import com.android.tools.r8.ir.code.InvokeSuper;
import com.android.tools.r8.ir.code.InvokeVirtual;
import com.android.tools.r8.ir.code.NewInstance;
import com.android.tools.r8.ir.code.StaticGet;
import com.android.tools.r8.ir.code.StaticPut;
import com.android.tools.r8.ir.code.Value;
import com.android.tools.r8.shaking.AppInfoWithLiveness;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Sets;
import com.google.common.collect.Streams;
import java.util.ArrayDeque;
import java.util.BitSet;
import java.util.Deque;
import java.util.List;
import java.util.Set;
/**
* Analysis that given an instruction determines if a given type is guaranteed to be class
* initialized prior the given instruction.
*/
public class ClassInitializationAnalysis {
public enum AnalysisAssumption {
INSTRUCTION_DOES_NOT_THROW,
NONE
}
public enum Query {
DIRECTLY,
DIRECTLY_OR_INDIRECTLY
}
private static final ClassInitializationAnalysis TRIVIAL =
new ClassInitializationAnalysis() {
@Override
public boolean isClassDefinitelyLoadedBeforeInstruction(
DexType type, Instruction instruction) {
return false;
}
};
private final AppView<AppInfoWithLiveness> appView;
private final IRCode code;
private final DexItemFactory dexItemFactory;
private DominatorTree dominatorTree = null;
private int markingColor = -1;
private ClassInitializationAnalysis() {
this.appView = null;
this.code = null;
this.dexItemFactory = null;
}
public ClassInitializationAnalysis(AppView<AppInfoWithLiveness> appView, IRCode code) {
this.appView = appView;
this.code = code;
this.dexItemFactory = appView.dexItemFactory();
}
// Returns a trivial, conservative analysis that always returns false.
public static ClassInitializationAnalysis trivial() {
return TRIVIAL;
}
public boolean isClassDefinitelyLoadedBeforeInstruction(DexType type, Instruction instruction) {
DexType context = code.method.holder();
BasicBlock block = instruction.getBlock();
// Visit the instructions in `block` prior to `instruction`.
for (Instruction previous : block.getInstructions()) {
if (previous == instruction) {
break;
}
if (previous.definitelyTriggersClassInitialization(
type,
context,
appView,
Query.DIRECTLY_OR_INDIRECTLY,
// The given instruction is only reached if none of the instructions in the same
// basic block throws, so we can safely assume that they will not.
AnalysisAssumption.INSTRUCTION_DOES_NOT_THROW)) {
return true;
}
}
if (dominatorTree == null) {
dominatorTree = new DominatorTree(code, Assumption.MAY_HAVE_UNREACHABLE_BLOCKS);
}
// Visit all the instructions in all the blocks that dominate `block`.
for (BasicBlock dominator : dominatorTree.dominatorBlocks(block, Inclusive.NO)) {
AnalysisAssumption assumption = getAssumptionForDominator(dominator, block);
InstructionIterator instructionIterator = dominator.iterator();
while (instructionIterator.hasNext()) {
Instruction previous = instructionIterator.next();
if (previous.definitelyTriggersClassInitialization(
type, context, appView, Query.DIRECTLY_OR_INDIRECTLY, assumption)) {
return true;
}
if (dominator.hasCatchHandlers() && previous.instructionTypeCanThrow()) {
// All of the instructions that follow the first instruction that may throw are
// guaranteed to be non-throwing. Hence they cannot cause any class initializations.
assert Streams.stream(instructionIterator)
.noneMatch(Instruction::instructionTypeCanThrow);
break;
}
}
}
return false;
}
/**
* Returns the analysis assumption to use when analyzing the instructions in the given dominator
* block.
*
* <p>If the given block has no catch handlers, then we can safely assume that the instruction
* does not throw, because execution would otherwise exit the method.
*
* <p>As a simple example, consider the method below. In order for the execution to get from the
* call to A.foo() to the call to A.bar() the call A.foo() must not throw.
*
* <pre>
* public static void method() {
* A.foo();
* A.bar();
* }
* </pre>
*
* This assumption cannot be made in the presence of intraprocedural exceptional control flow.
* Consider the following example.
*
* <pre>
* public static void method(A instance) {
* try {
* instance.field = 42;
* } catch (Exception e) {
* A.foo();
* return;
* }
* A.bar();
* }
* </pre>
*
* <p>At the call to A.foo() it is not guaranteed that the class A has been initialized, since
* `instance` could always be null.
*
* <p>At the call to A.bar() it is guaranteed, since the instance field assignment succeeded.
*/
private AnalysisAssumption getAssumptionForDominator(BasicBlock dominator, BasicBlock block) {
if (!dominator.hasCatchHandlers()) {
return AnalysisAssumption.INSTRUCTION_DOES_NOT_THROW;
}
Instruction exceptionalExit = dominator.exceptionalExit();
if (exceptionalExit == null) {
// The block cannot throw after all.
return AnalysisAssumption.INSTRUCTION_DOES_NOT_THROW;
}
if (markingColor < 0) {
markingColor = code.reserveMarkingColor();
code.markTransitivePredecessors(block, markingColor);
}
for (CatchHandler<BasicBlock> catchHandler : dominator.getCatchHandlers()) {
if (catchHandler.target.isMarked(markingColor)) {
// There is a path from this catch handler to the instruction of interest, so we can't make
// any assumptions.
return AnalysisAssumption.NONE;
}
}
// There are no paths from any of the catch handlers to the instruction of interest, so we can
// assume that no instructions in the given block will throw (otherwise, the instruction of
// interest will not be reached).
return AnalysisAssumption.INSTRUCTION_DOES_NOT_THROW;
}
/**
* The analysis reuses the dominator tree and basic block markings. If the underlying structure of
* the IR changes, then this method must be called to reset the dominator tree, return the current
* marking color, and clear all marks.
*/
public void notifyCodeHasChanged() {
dominatorTree = null;
returnMarkingColor();
}
/** Returns the marking color, if any, and clears all marks. */
public void finish() {
returnMarkingColor();
}
private void returnMarkingColor() {
if (markingColor >= 0) {
code.returnMarkingColor(markingColor);
markingColor = -1;
}
}
public static class InstructionUtils {
public static boolean forInitClass(
InitClass instruction,
DexType type,
AppView<?> appView,
Query mode,
AnalysisAssumption assumption) {
if (assumption == AnalysisAssumption.NONE) {
// Class initialization may fail with ExceptionInInitializerError.
return false;
}
DexClass clazz = appView.definitionFor(instruction.getClassValue());
return clazz != null && isTypeInitializedBy(instruction, type, clazz, appView, mode);
}
public static boolean forInstanceGet(
InstanceGet instruction,
DexType type,
AppView<?> appView,
Query mode,
AnalysisAssumption assumption) {
return forInstanceGetOrPut(instruction, type, appView, mode, assumption);
}
public static boolean forInstancePut(
InstancePut instruction,
DexType type,
AppView<?> appView,
Query mode,
AnalysisAssumption assumption) {
return forInstanceGetOrPut(instruction, type, appView, mode, assumption);
}
private static boolean forInstanceGetOrPut(
FieldInstruction instruction,
DexType type,
AppView<?> appView,
Query mode,
AnalysisAssumption assumption) {
assert instruction.isInstanceGet() || instruction.isInstancePut();
if (assumption == AnalysisAssumption.NONE) {
Value object =
instruction.isInstanceGet()
? instruction.asInstanceGet().object()
: instruction.asInstancePut().object();
if (object.getType().isNullable()) {
// If the receiver is null we cannot be sure that the holder has been initialized.
return false;
}
}
DexEncodedField field = appView.appInfo().resolveField(instruction.getField());
return field != null && isTypeInitializedBy(instruction, type, field, appView, mode);
}
public static boolean forInvokeDirect(
InvokeDirect instruction,
DexType type,
AppView<?> appView,
Query mode,
AnalysisAssumption assumption) {
if (assumption == AnalysisAssumption.NONE) {
if (instruction.getReceiver().getType().isNullable()) {
// If the receiver is null we cannot be sure that the holder has been initialized.
return false;
}
}
DexEncodedMethod method = appView.definitionFor(instruction.getInvokedMethod());
return method != null && isTypeInitializedBy(instruction, type, method, appView, mode);
}
public static boolean forInvokeInterface(
InvokeInterface instruction,
DexType type,
DexType context,
AppView<?> appView,
Query mode,
AnalysisAssumption assumption) {
if (assumption == AnalysisAssumption.NONE) {
if (instruction.getReceiver().getType().isNullable()) {
// If the receiver is null we cannot be sure that the holder has been initialized.
return false;
}
}
if (mode == Query.DIRECTLY) {
// We cannot ensure exactly which class is being loaded because it depends on the runtime
// type of the receiver.
// TODO(christofferqa): We can do better if there is a unique target.
return false;
}
if (appView.appInfo().hasLiveness()) {
DexEncodedMethod singleTarget =
instruction.lookupSingleTarget(appView.withLiveness(), context);
if (singleTarget != null) {
return isTypeInitializedBy(instruction, type, singleTarget, appView, mode);
}
}
DexMethod method = instruction.getInvokedMethod();
ResolutionResult resolutionResult =
appView.appInfo().resolveMethodOnInterface(method.holder, method);
if (!resolutionResult.isSingleResolution()) {
return false;
}
DexType holder = resolutionResult.getSingleTarget().holder();
return appView.isSubtype(holder, type).isTrue();
}
public static boolean forInvokeStatic(
InvokeStatic instruction,
DexType type,
DexType context,
AppView<?> appView,
Query mode,
AnalysisAssumption assumption) {
if (assumption == AnalysisAssumption.NONE) {
// Class initialization may fail with ExceptionInInitializerError.
return false;
}
DexEncodedMethod method = instruction.lookupSingleTarget(appView, context);
return method != null && isTypeInitializedBy(instruction, type, method, appView, mode);
}
public static boolean forInvokeSuper(
InvokeSuper instruction,
DexType type,
DexType context,
AppView<?> appView,
Query mode,
AnalysisAssumption assumption) {
if (assumption == AnalysisAssumption.NONE) {
if (instruction.getReceiver().getType().isNullable()) {
// If the receiver is null we cannot be sure that the holder has been initialized.
return false;
}
}
if (mode == Query.DIRECTLY) {
// We cannot ensure exactly which class is being loaded because it depends on the runtime
// type of the receiver.
// TODO(christofferqa): We can do better if there is a unique target.
return false;
}
if (appView.appInfo().hasLiveness()) {
DexEncodedMethod singleTarget =
instruction.lookupSingleTarget(appView.withLiveness(), context);
if (singleTarget != null) {
return isTypeInitializedBy(instruction, type, singleTarget, appView, mode);
}
}
DexMethod method = instruction.getInvokedMethod();
DexClass enclosingClass = appView.definitionFor(method.holder);
if (enclosingClass == null) {
return false;
}
DexType superType = enclosingClass.superType;
if (superType == null) {
return false;
}
ResolutionResult resolutionResult =
appView.appInfo().resolveMethod(superType, method, instruction.itf);
if (!resolutionResult.isSingleResolution()) {
return false;
}
DexType holder = resolutionResult.getSingleTarget().holder();
return appView.isSubtype(holder, type).isTrue();
}
public static boolean forInvokeVirtual(
InvokeVirtual instruction,
DexType type,
DexType context,
AppView<?> appView,
Query mode,
AnalysisAssumption assumption) {
if (assumption == AnalysisAssumption.NONE) {
if (instruction.getReceiver().getType().isNullable()) {
// If the receiver is null we cannot be sure that the holder has been initialized.
return false;
}
}
if (mode == Query.DIRECTLY) {
// We cannot ensure exactly which class is being loaded because it depends on the runtime
// type of the receiver.
// TODO(christofferqa): We can do better if there is a unique target.
return false;
}
if (appView.appInfo().hasLiveness()) {
DexEncodedMethod singleTarget =
instruction.lookupSingleTarget(appView.withLiveness(), context);
if (singleTarget != null) {
return isTypeInitializedBy(instruction, type, singleTarget, appView, mode);
}
}
DexMethod method = instruction.getInvokedMethod();
ResolutionResult resolutionResult =
appView.appInfo().resolveMethodOnClass(method.holder, method);
if (!resolutionResult.isSingleResolution()) {
return false;
}
DexType holder = resolutionResult.getSingleTarget().holder();
return appView.isSubtype(holder, type).isTrue();
}
public static boolean forNewInstance(
NewInstance instruction,
DexType type,
AppView<?> appView,
Query mode,
AnalysisAssumption assumption) {
if (assumption == AnalysisAssumption.NONE) {
// Instruction may throw.
return false;
}
DexClass clazz = appView.definitionFor(instruction.clazz);
return clazz != null && isTypeInitializedBy(instruction, type, clazz, appView, mode);
}
public static boolean forStaticGet(
StaticGet instruction,
DexType type,
AppView<?> appView,
Query mode,
AnalysisAssumption assumption) {
return forStaticGetOrPut(instruction, type, appView, mode, assumption);
}
public static boolean forStaticPut(
StaticPut instruction,
DexType type,
AppView<?> appView,
Query mode,
AnalysisAssumption assumption) {
return forStaticGetOrPut(instruction, type, appView, mode, assumption);
}
private static boolean forStaticGetOrPut(
FieldInstruction instruction,
DexType type,
AppView<?> appView,
Query mode,
AnalysisAssumption assumption) {
assert instruction.isStaticGet() || instruction.isStaticPut();
if (assumption == AnalysisAssumption.NONE) {
// Class initialization may fail with ExceptionInInitializerError.
return false;
}
DexEncodedField field = appView.appInfo().resolveField(instruction.getField());
return field != null && isTypeInitializedBy(instruction, type, field, appView, mode);
}
private static boolean isTypeInitializedBy(
Instruction instruction,
DexType typeToBeInitialized,
DexDefinition definition,
AppView<?> appView,
Query mode) {
if (mode == Query.DIRECTLY) {
if (definition.isDexClass()) {
return definition.asDexClass().type == typeToBeInitialized;
}
if (definition.isDexEncodedMember()) {
return definition.asDexEncodedMember().toReference().holder == typeToBeInitialized;
}
throw new Unreachable();
}
Set<DexType> visited = Sets.newIdentityHashSet();
Deque<DexType> worklist = new ArrayDeque<>();
if (definition.isDexClass()) {
DexClass clazz = definition.asDexClass();
enqueue(clazz.type, visited, worklist);
} else if (definition.isDexEncodedField()) {
DexEncodedField field = definition.asDexEncodedField();
enqueue(field.holder(), visited, worklist);
} else if (definition.isDexEncodedMethod()) {
assert instruction.isInvokeMethod();
DexEncodedMethod method = definition.asDexEncodedMethod();
enqueue(method.holder(), visited, worklist);
enqueueInitializedClassesOnNormalExit(method, instruction.inValues(), visited, worklist);
} else {
assert false;
}
while (!worklist.isEmpty()) {
DexType typeKnownToBeInitialized = worklist.removeFirst();
assert visited.contains(typeKnownToBeInitialized);
if (appView.isSubtype(typeKnownToBeInitialized, typeToBeInitialized).isTrue()) {
return true;
}
DexClass clazz = appView.definitionFor(typeKnownToBeInitialized);
if (clazz != null) {
DexEncodedMethod classInitializer = clazz.getClassInitializer();
if (classInitializer != null) {
enqueueInitializedClassesOnNormalExit(
classInitializer, ImmutableList.of(), visited, worklist);
}
}
}
return false;
}
private static void enqueue(DexType type, Set<DexType> visited, Deque<DexType> worklist) {
if (type.isClassType() && visited.add(type)) {
worklist.add(type);
}
}
private static void enqueueInitializedClassesOnNormalExit(
DexEncodedMethod method,
List<Value> arguments,
Set<DexType> visited,
Deque<DexType> worklist) {
for (DexType type : method.getOptimizationInfo().getInitializedClassesOnNormalExit()) {
enqueue(type, visited, worklist);
}
// If an invoke to an instance method succeeds, then the receiver must be non-null, which
// implies that the type of the receiver must be initialized.
if (!method.isStatic()) {
assert arguments.size() > 0;
TypeElement type = arguments.get(0).getType();
if (type.isClassType()) {
enqueue(type.asClassType().getClassType(), visited, worklist);
}
}
// If an invoke to a method succeeds, and the method would have thrown and exception if the
// i'th argument was null, then the i'th argument must be non-null, which implies that the
// type of the i'th argument must be initialized.
BitSet nonNullParamOrThrowFacts = method.getOptimizationInfo().getNonNullParamOrThrow();
if (nonNullParamOrThrowFacts != null) {
for (int i = 0; i < arguments.size(); i++) {
if (nonNullParamOrThrowFacts.get(i)) {
TypeElement type = arguments.get(i).getType();
if (type.isClassType()) {
enqueue(type.asClassType().getClassType(), visited, worklist);
}
}
}
}
}
}
}