blob: aa4e865cb82012e356b2a7d771a00078fc8760f4 [file] [log] [blame]
/*
* Copyright 2008 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.gwt.dev.jjs.impl;
import com.google.gwt.dev.jjs.ast.Context;
import com.google.gwt.dev.jjs.ast.JArrayLength;
import com.google.gwt.dev.jjs.ast.JArrayRef;
import com.google.gwt.dev.jjs.ast.JArrayType;
import com.google.gwt.dev.jjs.ast.JBinaryOperation;
import com.google.gwt.dev.jjs.ast.JBinaryOperator;
import com.google.gwt.dev.jjs.ast.JCastOperation;
import com.google.gwt.dev.jjs.ast.JClassLiteral;
import com.google.gwt.dev.jjs.ast.JClassType;
import com.google.gwt.dev.jjs.ast.JConstructor;
import com.google.gwt.dev.jjs.ast.JDeclarationStatement;
import com.google.gwt.dev.jjs.ast.JDeclaredType;
import com.google.gwt.dev.jjs.ast.JExpression;
import com.google.gwt.dev.jjs.ast.JField;
import com.google.gwt.dev.jjs.ast.JFieldRef;
import com.google.gwt.dev.jjs.ast.JInterfaceType;
import com.google.gwt.dev.jjs.ast.JLocal;
import com.google.gwt.dev.jjs.ast.JLocalRef;
import com.google.gwt.dev.jjs.ast.JMethod;
import com.google.gwt.dev.jjs.ast.JMethodCall;
import com.google.gwt.dev.jjs.ast.JNewArray;
import com.google.gwt.dev.jjs.ast.JNewInstance;
import com.google.gwt.dev.jjs.ast.JNode;
import com.google.gwt.dev.jjs.ast.JParameter;
import com.google.gwt.dev.jjs.ast.JParameterRef;
import com.google.gwt.dev.jjs.ast.JPrimitiveType;
import com.google.gwt.dev.jjs.ast.JProgram;
import com.google.gwt.dev.jjs.ast.JReferenceType;
import com.google.gwt.dev.jjs.ast.JRunAsync;
import com.google.gwt.dev.jjs.ast.JStringLiteral;
import com.google.gwt.dev.jjs.ast.JType;
import com.google.gwt.dev.jjs.ast.JUnsafeTypeCoercion;
import com.google.gwt.dev.jjs.ast.JVariable;
import com.google.gwt.dev.jjs.ast.JVariableRef;
import com.google.gwt.dev.jjs.ast.JVisitor;
import com.google.gwt.dev.jjs.ast.RuntimeConstants;
import com.google.gwt.dev.jjs.ast.js.JsniFieldRef;
import com.google.gwt.dev.jjs.ast.js.JsniMethodBody;
import com.google.gwt.dev.jjs.ast.js.JsniMethodRef;
import com.google.gwt.dev.js.ast.JsContext;
import com.google.gwt.dev.js.ast.JsFunction;
import com.google.gwt.dev.js.ast.JsName;
import com.google.gwt.dev.js.ast.JsNameRef;
import com.google.gwt.dev.js.ast.JsVisitor;
import com.google.gwt.thirdparty.guava.common.collect.ArrayListMultimap;
import com.google.gwt.thirdparty.guava.common.collect.ListMultimap;
import com.google.gwt.thirdparty.guava.common.collect.Lists;
import com.google.gwt.thirdparty.guava.common.collect.Sets;
import java.util.List;
import java.util.Set;
/**
* This class finds out what code in a program is live based on starting
* execution at a specified location.
*/
public class ControlFlowAnalyzer {
/**
* A callback for recording control-flow dependencies as they are discovered.
* See {@link ControlFlowAnalyzer#setDependencyRecorder(DependencyRecorder)}.
*/
public interface DependencyRecorder {
/**
* Used to record the dependencies of a specific method.
*/
void methodIsLiveBecause(JMethod liveMethod, List<JMethod> dependencyChain);
}
/**
* Marks as "referenced" any types, methods, and fields that are reachable.
* Also marks as "instantiable" any classes and interfaces that can possibly
* be instantiated.
*
* TODO(later): make RescueVisitor use less stack?
*/
private class RescueVisitor extends JVisitor {
private final List<JMethod> curMethodStack = Lists.newArrayList();
@Override
public boolean visit(JArrayRef arrayRef, Context ctx) {
maybeRescueJsTypeArray(arrayRef.getInstance().getType());
return true;
}
@Override
public boolean visit(JArrayLength arrayLength, Context ctx) {
maybeRescueJsTypeArray(arrayLength.getInstance().getType());
return true;
}
@Override
public boolean visit(JArrayType type, Context ctx) {
assert (referencedTypes.contains(type));
boolean isInstantiated = instantiatedTypes.contains(type);
JType leafType = type.getLeafType();
int dims = type.getDims();
// Rescue my super array type
boolean didSuperType = false;
if (leafType instanceof JClassType) {
JClassType superClass = ((JClassType) leafType).getSuperClass();
if (superClass != null) {
// FooSub[] -> Foo[]
rescue(program.getOrCreateArrayType(superClass, dims), isInstantiated);
didSuperType = true;
}
} else if (leafType instanceof JInterfaceType) {
// Intf[] -> Object[]
rescue(program.getOrCreateArrayType(program.getTypeJavaLangObject(), dims),
isInstantiated);
didSuperType = true;
}
if (!didSuperType) {
if (dims > 1) {
// anything[][] -> Object[]
rescue(program.getOrCreateArrayType(program.getTypeJavaLangObject(), dims - 1),
isInstantiated);
} else {
// anything[] -> Object
rescue(program.getTypeJavaLangObject(), isInstantiated);
}
}
// Rescue super interface array types.
if (leafType instanceof JDeclaredType) {
JDeclaredType dLeafType = (JDeclaredType) leafType;
for (JInterfaceType intfType : dLeafType.getImplements()) {
JArrayType intfArray = program.getOrCreateArrayType(intfType, dims);
rescue(intfArray, isInstantiated);
}
}
return false;
}
@Override
public boolean visit(JBinaryOperation x, Context ctx) {
if (x.isAssignment() && x.getLhs() instanceof JFieldRef) {
fieldsWritten.add(((JFieldRef) x.getLhs()).getField());
}
// special string concat handling
if ((x.getOp() == JBinaryOperator.CONCAT || x.getOp() == JBinaryOperator.ASG_CONCAT)) {
rescueByConcat(x.getLhs().getType());
rescueByConcat(x.getRhs().getType());
}
JExpression lhs = x.getLhs();
if (x.getOp() != JBinaryOperator.ASG || lhs.hasSideEffects() || isVolatileField(lhs)) {
// Continue the normal visitor process for lhs and rhs.
return true;
}
// Assignments where the lhs does not have side effects (save for volatile fields) are special
// treated here. The idea is to not consider live a field/local/parameter that is only
// written to.
if (lhs instanceof JLocalRef || lhs instanceof JParameterRef) {
// if the lhs is a local or parameter, do not consider it live just because it is being
// written to.
accept(x.getRhs());
return false;
} else if (lhs instanceof JFieldRef) {
JFieldRef fieldRef = (JFieldRef) lhs;
JField field = fieldRef.getField();
if (field.canBeImplementedExternally()) {
// Proceed normally to consider native fields live even if they are only written to.
return true;
}
// Fields that are only written to still need to process their qualifier.
JExpression instance = fieldRef.getInstance();
if (instance != null) {
accept(instance);
}
accept(x.getRhs());
return false;
}
return true;
}
@Override
public boolean visit(JCastOperation x, Context ctx) {
rescueByTypeCoercion(x.getCastType(), x.getExpr().getType());
return true;
}
@Override
public boolean visit(JClassLiteral x, Context ctx) {
JField field = x.getField();
assert field != null;
rescue(field);
return true;
}
@Override
public boolean visit(JClassType type, Context ctx) {
assert (referencedTypes.contains(type));
boolean isInstantiated = instantiatedTypes.contains(type);
// Rescue my super type
rescue(type.getSuperClass(), isInstantiated);
// Rescue my clinit (it won't ever be explicitly referenced)
if (type.hasClinit()) {
rescue(type.getClinitMethod());
}
// If I'm instantiated, let's mark super interfaces as instantiated.
if (isInstantiated) {
rescueMembersAndInstantiateSuperInterfaces(type);
}
return false;
}
@Override
public boolean visit(JDeclarationStatement x, Context ctx) {
/*
* A declaration by itself doesn't rescue a local (even if it has an
* initializer). Writes don't count, only reads.
*/
if (x.getInitializer() != null &&
!isStaticFieldInitializedToLiteral(x.getVariableRef().getTarget())) {
/*
* Don't traverse literal initializers, because those become live when
* the variable is accessed, not when its declaration runs.
*/
accept(x.getInitializer());
if (x.getVariableRef().getTarget() instanceof JField) {
fieldsWritten.add((JField) x.getVariableRef().getTarget());
}
}
// If the lhs is a field ref, we have to visit its qualifier.
JVariableRef variableRef = x.getVariableRef();
if (variableRef instanceof JFieldRef) {
JFieldRef fieldRef = (JFieldRef) variableRef;
JExpression instance = fieldRef.getInstance();
if (instance != null) {
accept(instance);
}
}
return false;
}
@Override
public boolean visit(JFieldRef ref, Context ctx) {
JField target = ref.getField();
/*
* JLS 12.4.1: references to static, non-final, or
* non-compile-time-constant fields rescue the enclosing class. JDT
* already folds in compile-time constants as literals, so we must rescue
* the enclosing types for any static fields that make it here.
*/
if (target.isStatic()) {
rescue(target.getEnclosingType(), false);
}
if (target.isStatic() || instantiatedTypes.contains(target.getEnclosingType())) {
rescue(target);
} else {
// It's a field whose class is not instantiable
if (!liveFieldsAndMethods.contains(target)) {
membersToRescueIfTypeIsInstantiated.add(target);
}
}
return true;
}
@Override
public boolean visit(JInterfaceType type, Context ctx) {
boolean isReferenced = referencedTypes.contains(type);
boolean isInstantiated = instantiatedTypes.contains(type);
assert (isReferenced || isInstantiated);
// Rescue my clinit (it won't ever be explicitly referenced)
if (type.hasClinit()) {
rescue(type.getClinitMethod());
}
// If I'm instantiated, let's mark super interfaces as instantiated.
if (isInstantiated) {
rescueMembersAndInstantiateSuperInterfaces(type);
}
return false;
}
@Override
public boolean visit(JLocalRef ref, Context ctx) {
JLocal target = ref.getLocal();
rescue(target);
return true;
}
@Override
public boolean visit(final JMethod x, Context ctx) {
JReferenceType enclosingType = x.getEnclosingType();
if (enclosingType.isJsoType()) {
// Calls to JavaScriptObject types rescue those types.
boolean instance = !x.isStatic() || program.isStaticImpl(x);
rescue(enclosingType, instance);
} else if (x.isStatic()) {
// JLS 12.4.1: references to static methods rescue the enclosing class
rescue(enclosingType, false);
}
if (x.isJsniMethod()) {
// Manually rescue native parameter references
final JsniMethodBody body = (JsniMethodBody) x.getBody();
final JsFunction func = body.getFunc();
new JsVisitor() {
@Override
public void endVisit(JsNameRef nameRef, JsContext ctx) {
JsName ident = nameRef.getName();
if (ident != null) {
// If we're referencing a parameter, rescue the associated
// JParameter
int index = func.getParameters().indexOf(ident.getStaticRef());
if (index != -1) {
rescue(x.getParams().get(index));
}
}
}
}.accept(func);
}
return true;
}
@Override
public boolean visit(JMethodCall call, Context ctx) {
JMethod method = call.getTarget();
if (call.isVolatile() && method == runAsyncOnSuccess) {
/*
* Note: In order to preserve code splitting, don't allow code flow from the
* AsyncFragmentLoader implementation back into the
* callback.onSuccess(). If we did, the rescue path would look like
* JRunAsync -> AsyncFragmentLoader.runAsync() -> callback.onSuccess().
* This would completely defeat code splitting as all the code on the
* other side of the barrier would become reachable.
*
* Code flow analysis is run separately on methods which implement
* RunAsyncCallback.onSuccess() as top-level entry points.
*/
return true;
}
if (method.isStatic() || isTypeInstantiatedOrJso(method.getEnclosingType())) {
rescue(method);
} else {
// It's a virtual method whose class is not instantiable
if (!liveFieldsAndMethods.contains(method)) {
membersToRescueIfTypeIsInstantiated.add(method);
}
}
if (argumentsToRescueIfParameterRead == null || method.canBePolymorphic()
|| call instanceof JsniMethodRef) {
return true;
}
if (program.instanceMethodForStaticImpl(method) != null) {
// CleanUpRefsVisitor does not prune these params, must rescue.
return true;
}
if (call.getInstance() != null) {
// Explicitly visit instance since we're returning false below.
this.accept(call.getInstance());
}
rescueArgumentsIfParametersCanBeRead(call);
return false;
}
@Override
public boolean visit(JNewArray newArray, Context ctx) {
// rescue and instantiate the array type
JArrayType arrayType = newArray.getArrayType();
if (newArray.getDimensionExpressions() != null) {
// rescue my type and all the implicitly nested types (with fewer dims)
int arrayDimensions = arrayType.getDims();
int initializedDimensions = newArray.getDimensionExpressions().size();
JType leafType = arrayType.getLeafType();
assert (initializedDimensions <= arrayDimensions);
for (int i = 0; i < initializedDimensions; ++i) {
rescue(program.getOrCreateArrayType(leafType, arrayDimensions - i), true);
}
} else {
// just rescue my own specific type
rescue(arrayType, true);
}
return true;
}
@Override
public boolean visit(JNewInstance x, Context ctx) {
// rescue and instantiate the target class!
rescue(x.getClassType(), true);
return super.visit(x, ctx);
}
@Override
public boolean visit(JParameterRef x, Context ctx) {
// rescue the parameter for future pruning purposes
rescue(x.getParameter());
return true;
}
@Override
public boolean visit(JsniFieldRef x, Context ctx) {
/*
* SPECIAL: this could be an assignment that passes a value from
* JavaScript into Java.
*/
if (x.isLvalue()) {
maybeRescueJavaScriptObjectPassingIntoJava(x.getField().getType());
}
// JsniFieldRef rescues as JFieldRef
return visit((JFieldRef) x, ctx);
}
@Override
public boolean visit(JsniMethodBody body, Context ctx) {
liveStrings.addAll(body.getUsedStrings());
return true;
}
@Override
public boolean visit(JsniMethodRef x, Context ctx) {
/*
* SPECIAL: each argument of the call passes a value from JavaScript into
* Java.
*/
for (JParameter param : x.getTarget().getParams()) {
maybeRescueJavaScriptObjectPassingIntoJava(param.getType());
/*
* Because we're not currently tracking methods through JSNI, we need to
* assume that it's not safe to prune parameters of a method referenced
* as such.
*
* A better solution would be to perform basic escape analysis to ensure
* that the function reference never escapes, or at minimum, ensure that
* the method is immediately called after retrieving the method
* reference.
*/
rescue(param);
}
// JsniMethodRef rescues as a JMethodCall
if (x.getTarget() instanceof JConstructor) {
// But if a constructor is targeted, there is an implicit 'new' op.
JConstructor ctor = (JConstructor) x.getTarget();
rescue(ctor.getEnclosingType(), true);
}
return visit((JMethodCall) x, ctx);
}
@Override
public boolean visit(JStringLiteral literal, Context ctx) {
liveStrings.add(literal.getValue());
// rescue and instantiate java.lang.String
rescue(program.getTypeJavaLangString(), true);
return true;
}
@Override
public boolean visit(JUnsafeTypeCoercion x, Context ctx) {
rescueByTypeCoercion(x.getCoercionType(), x.getExpression().getType());
return true;
}
private void rescueByTypeCoercion(JType targetType, JType expressionType) {
// Rescue any JavaScriptObject type that is the target of a cast.
if (!canBeInstantiatedInJavaScript(targetType)) {
return;
}
rescue((JReferenceType) targetType, true);
if (program.typeOracle.isSingleJsoImpl(targetType)) {
/*
* It's a JSO interface, check if the source expr can be a live JSO:
* 1) source is java.lang.Object (JSO could have been assigned to it)
* 2) source is JSO
* 3) source is SingleJSO interface whose implementor is live
*/
if (program.getTypeJavaLangObject() == expressionType
|| program.typeOracle.canBeJavaScriptObject(expressionType)) {
// source is JSO or SingleJso interface whose implementor is live
JClassType jsoImplementor =
program.typeOracle.getSingleJsoImpl((JReferenceType) targetType);
rescue(jsoImplementor, true);
}
}
}
private boolean canBeInstantiatedInJavaScript(JType type) {
// Technically, JsType/JsFunction are also instantiatable in JavaScript but we don't track
// them using similar to JSO as if we do that then after cast normalization, they got pruned.
if (program.typeOracle.canBeJavaScriptObject(type)
|| program.isRepresentedAsNativeJsPrimitive(type)) {
return true;
}
/*
* Hackish: in our own JRE we sometimes create "not quite baked" arrays
* in JavaScript for expediency.
*/
if (type instanceof JArrayType) {
return true;
}
return false;
}
private JMethod getStringValueOfCharMethod() {
JPrimitiveType charType = program.getTypePrimitiveChar();
JClassType stringType = program.getTypeJavaLangString();
if (stringValueOfChar != null) {
return stringValueOfChar;
}
for (JMethod method : stringType.getMethods()) {
if (method.getName().equals("valueOf") &&
method.getOriginalParamTypes().size() == 1 &&
method.getOriginalParamTypes().get(0) == charType) {
stringValueOfChar = method;
return stringValueOfChar;
}
}
assert false;
return null;
}
private boolean isStaticFieldInitializedToLiteral(JVariable var) {
if (!(var instanceof JField)) {
return false;
}
JField field = (JField) var;
return field.isStatic() && field.getLiteralInitializer() != null;
}
private boolean isVolatileField(JExpression x) {
if (!(x instanceof JFieldRef)) {
return false;
}
JFieldRef xFieldRef = (JFieldRef) x;
return xFieldRef.getField().isVolatile();
}
private void maybeRescueClassLiteral(JReferenceType type) {
if (liveFieldsAndMethods.contains(getClassMethod) ||
liveFieldsAndMethods.contains(getClassField)) {
// getClass() already live so rescue class literal immediately
rescue(program.getClassLiteralField(type));
} else {
// getClass() not live yet, so mark for later rescue
classLiteralsToBeRescuedIfGetClassIsLive.add(type);
}
}
/**
* Subclasses of JavaScriptObject are never instantiated directly. They are implicitly created
* when a JSNI method passes a reference to an existing JS object into Java code. If any point
* in the program can pass a value from JS into Java which could potentially be cast to
* JavaScriptObject, we must rescue JavaScriptObject.
*
* @param type The type of the value passing from Java to JavaScript.
* @see com.google.gwt.core.client.JavaScriptObject
*/
private void maybeRescueJavaScriptObjectPassingIntoJava(JType type) {
if (!canBeInstantiatedInJavaScript(type)) {
return;
}
rescue((JReferenceType) type, true);
if (program.typeOracle.isSingleJsoImpl(type)) {
// Cast of JSO into SingleJso interface, rescue the implementor if exists
JClassType singleJsoImpl = program.typeOracle.getSingleJsoImpl((JReferenceType) type);
rescue(singleJsoImpl, true);
}
}
private void rescue(JMethod method) {
if (method == null) {
return;
}
if (liveFieldsAndMethods.add(method)) {
membersToRescueIfTypeIsInstantiated.remove(method);
if (dependencyRecorder != null) {
curMethodStack.add(method);
dependencyRecorder.methodIsLiveBecause(method, curMethodStack);
}
accept(method);
if (dependencyRecorder != null) {
curMethodStack.remove(curMethodStack.size() - 1);
}
if (method.isJsniMethod() || method.canBeImplementedExternally()) {
// Returning from this method passes a value from JavaScript into Java.
maybeRescueJavaScriptObjectPassingIntoJava(method.getType());
}
if (method.canBeReferencedExternally() || method.canBeImplementedExternally()) {
for (JParameter param : method.getParams()) {
// Parameters in JsExport, JsType, JsFunction methods should not be pruned in order to
// keep the API intact.
if (method.canBeReferencedExternally()) {
maybeRescueJavaScriptObjectPassingIntoJava(param.getType());
}
rescue(param);
if (param.isVarargs()) {
assert method.isJsMethodVarargs();
// Rescue the (array) type of varargs parameters as the array creation is implicit.
JArrayType paramType = (JArrayType) param.getType().getUnderlyingType();
rescue(paramType, true);
// Rescue the class literal for the array type as it will be needed when
// ImplementJsVarargs inserts the method prelude to support the JS vararg calling
// convention.
rescue(program.getClassLiteralField(paramType.getLeafType()));
}
}
}
rescueOverridingMethods(method);
if (method == getClassMethod) {
rescueClassLiteralsIfGetClassIsLive();
}
if (method.getSpecialization() != null) {
rescue(method.getSpecialization().getTargetMethod());
}
}
}
private void maybeRescueJsTypeArray(JType type) {
if (!(type instanceof JArrayType)) {
return;
}
JArrayType arrayType = (JArrayType) type;
if (arrayType.canBeImplementedExternally()) {
rescue(arrayType, true);
maybeRescueJsTypeArray(arrayType.getElementType());
}
}
private void rescue(JReferenceType type, boolean isInstantiated) {
if (type == null) {
return;
}
/*
* Track references and instantiability at the granularity of run-time
* types. For example, ignore nullness.
*/
type = type.getUnderlyingType();
boolean doVisit = false;
if (isInstantiated && instantiatedTypes.add(type)) {
maybeRescueClassLiteral(type);
doVisit = true;
}
if (referencedTypes.add(type)) {
doVisit = true;
}
if (!doVisit) {
return;
}
accept(type);
if (!(type instanceof JDeclaredType)) {
return;
}
/*
* We rescue all JsType member and JsFunction methods because we don't know if they'll be
* called from JS or not.
*/
JDeclaredType declaredType = (JDeclaredType) type;
for (JMethod method : declaredType.getMethods()) {
if (method.canBeReferencedExternally()
|| declaredType.isJsNative() && method.isJsConstructor()) {
rescue(method);
}
}
for (JField field : declaredType.getFields()) {
if (field.canBeReferencedExternally()) {
rescue(field);
}
}
}
private void rescue(JVariable var) {
if (var == null) {
return;
}
if (!liveFieldsAndMethods.add(var)) {
// Already rescued.
return;
}
if (var instanceof JField) {
JField field = (JField) var;
membersToRescueIfTypeIsInstantiated.remove(field);
if (field.canBeReferencedExternally() || field.canBeImplementedExternally()) {
maybeRescueJavaScriptObjectPassingIntoJava(field.getType());
}
if (field == getClassField) {
rescueClassLiteralsIfGetClassIsLive();
}
if (isStaticFieldInitializedToLiteral(field)) {
/*
* Rescue literal initializers when the field is rescued, not when
* the static initializer runs. This allows fields initialized to
* string literals to only need the string literals when the field
* itself becomes live.
*
* NOTE: needs to be in sync with {@link JTypeOracle.CheckClinitVistior}.
*/
accept(field.getLiteralInitializer());
} else if (program.getTypeClassLiteralHolder().equals(field.getEnclosingType())) {
/*
* Rescue just slightly less than what would normally be rescued for
* a field reference to the literal's field. Rescue the field
* itself, and its initializer, but do NOT rescue the whole
* enclosing class. That would pull in the clinit of that class,
* which has initializers for all the class literals, which in turn
* have all of the strings of all of the class names.
*
* This work is done in rescue() to allow JSNI references to class
* literals (via the @Foo::class syntax) to correctly rescue class
* literal initializers.
*
* TODO: Model ClassLiteral access a different way to avoid special
* handling. See
* Pruner.transformToNullFieldRef()/transformToNullMethodCall().
*/
accept(field.getInitializer());
referencedTypes.add(field.getEnclosingType());
liveFieldsAndMethods.add(field.getEnclosingType().getClinitMethod());
}
} else if (var instanceof JParameter && argumentsToRescueIfParameterRead != null) {
for (JExpression arg : argumentsToRescueIfParameterRead.removeAll(var)) {
accept(arg);
}
}
}
/**
* The code is very tightly tied to the behavior of Pruner.CleanupRefsVisitor.
* CleanUpRefsVisitor will prune unread parameters, and also prune any matching arguments that
* don't have side effects. We want to make control flow congruent to pruning, to avoid the need
* to iterate over Pruner until reaching a stable point, so we avoid actually rescuing such
* arguments until/unless the parameter is read.
*/
private void rescueArgumentsIfParametersCanBeRead(JMethodCall call) {
JMethod method = call.getTarget();
assert !method.canBePolymorphic();
List<JExpression> args = call.getArgs();
List<JParameter> params = method.getParams();
int i = 0;
for (int c = params.size(); i < c; ++i) {
JExpression arg = args.get(i);
JParameter param = params.get(i);
if (arg.hasSideEffects() || liveFieldsAndMethods.contains(param)) {
this.accept(arg);
continue;
}
argumentsToRescueIfParameterRead.put(param, arg);
}
// Visit any "extra" arguments that exceed the param list.
for (int c = args.size(); i < c; ++i) {
this.accept(args.get(i));
}
}
/**
* Handle special rescues needed implicitly to support concat.
*/
private void rescueByConcat(JType type) {
JPrimitiveType charType = program.getTypePrimitiveChar();
JClassType stringType = program.getTypeJavaLangString();
if (type instanceof JReferenceType
&& !program.typeOracle.castSucceedsTrivially((JReferenceType) type, stringType)
&& !type.isNullType()) {
/*
* Any reference types (except String, which works by default) that take
* part in a concat must rescue java.lang.Object.toString().
*
* TODO: can we narrow the focus by walking up the type hierarchy or
* doing explicit toString calls?
*/
JMethod toStringMethod = program.getIndexedMethod(RuntimeConstants.OBJECT_TO_STRING);
rescue(toStringMethod);
} else if (type == charType) {
/*
* Characters must rescue String.valueOf(char)
*/
rescue(getStringValueOfCharMethod());
}
}
private void rescueClassLiteralsIfGetClassIsLive() {
if (classLiteralsToBeRescuedIfGetClassIsLive != null) {
// guard against re-entrant calls. This only needs to run once.
Set<JReferenceType> toRescue = classLiteralsToBeRescuedIfGetClassIsLive;
classLiteralsToBeRescuedIfGetClassIsLive = null;
for (JReferenceType classLit : toRescue) {
maybeRescueClassLiteral(classLit);
}
}
}
private void rescueMembersAndInstantiateSuperInterfaces(JDeclaredType type) {
for (JInterfaceType intfType : type.getImplements()) {
rescue(intfType, true);
}
rescueMembers(type);
}
/**
* Rescues any of type's virtual methods that a previously seen method call could call.
*/
private void rescueMembers(JDeclaredType type) {
assert instantiatedTypes.contains(type);
for (JMethod method : type.getMethods()) {
if (!method.isStatic() && membersToRescueIfTypeIsInstantiated.contains(method)) {
rescue(method);
}
}
for (JField field : type.getFields()) {
if (!field.isStatic() && membersToRescueIfTypeIsInstantiated.contains(field)) {
rescue(field);
}
}
}
/**
* Assume that <code>method</code> is live. Rescue any overriding methods that might be called
* if <code>method</code> is called through virtual dispatch.
*/
private void rescueOverridingMethods(JMethod method) {
if (method.isStatic()) {
return;
}
for (JMethod overridingMethod : method.getOverridingMethods()) {
if (liveFieldsAndMethods.contains(overridingMethod)) {
// The override is already alive, do nothing.
} else if (instantiatedTypes.contains(overridingMethod.getEnclosingType())) {
// The enclosing class is alive, make my override reachable.
rescue(overridingMethod);
} else {
// The enclosing class is not yet alive, put override in limbo.
membersToRescueIfTypeIsInstantiated.add(overridingMethod);
}
}
}
}
private boolean isTypeInstantiatedOrJso(JDeclaredType type) {
if (type == null) {
return false;
}
return type.isJsoType() || instantiatedTypes.contains(type);
}
/**
* These are arguments that have not yet been rescued on account of the
* associated parameter not having been read yet. If the parameter becomes
* read, we will need to rescue the associated arguments. See comments in
* {@link #rescueArgumentsIfParametersCanBeRead}.
*/
private ListMultimap<JParameter, JExpression> argumentsToRescueIfParameterRead;
private final JMethod asyncFragmentOnLoad;
/**
* Schrodinger set of classLiterals to be rescued if type is instantiated AND getClass()
* is live.
*/
private Set<JReferenceType> classLiteralsToBeRescuedIfGetClassIsLive = Sets.newHashSet();
private DependencyRecorder dependencyRecorder;
private Set<JField> fieldsWritten = Sets.newLinkedHashSet();
private Set<JReferenceType> instantiatedTypes = Sets.newLinkedHashSet();
private Set<JNode> liveFieldsAndMethods = Sets.newLinkedHashSet();
private Set<String> liveStrings = Sets.newLinkedHashSet();
/**
* Schrodinger's members... aka "limbo". :) These are instance methods and
* fields that seem to be reachable, only their enclosing type is
* uninstantiable. We place these methods into purgatory until/unless the
* enclosing type is found to be instantiable.
*/
private Set<JNode> membersToRescueIfTypeIsInstantiated = Sets.newHashSet();
private final JField getClassField;
private final JMethod getClassMethod;
private final JProgram program;
private Set<JReferenceType> referencedTypes = Sets.newHashSet();
private final RescueVisitor rescuer;
private final JMethod runAsyncOnSuccess;
private JMethod stringValueOfChar = null;
public ControlFlowAnalyzer(ControlFlowAnalyzer cfa) {
program = cfa.program;
asyncFragmentOnLoad = cfa.asyncFragmentOnLoad;
runAsyncOnSuccess = cfa.runAsyncOnSuccess;
getClassField = cfa.getClassField;
getClassMethod = cfa.getClassMethod;
fieldsWritten = Sets.newHashSet(cfa.fieldsWritten);
instantiatedTypes = Sets.newHashSet(cfa.instantiatedTypes);
liveFieldsAndMethods = Sets.newHashSet(cfa.liveFieldsAndMethods);
referencedTypes = Sets.newHashSet(cfa.referencedTypes);
stringValueOfChar = cfa.stringValueOfChar;
liveStrings = Sets.newHashSet(cfa.liveStrings);
membersToRescueIfTypeIsInstantiated =
Sets.newHashSet(cfa.membersToRescueIfTypeIsInstantiated);
if (cfa.argumentsToRescueIfParameterRead != null) {
argumentsToRescueIfParameterRead =
ArrayListMultimap.create(cfa.argumentsToRescueIfParameterRead);
}
rescuer = new RescueVisitor();
}
public ControlFlowAnalyzer(JProgram program) {
this.program = program;
asyncFragmentOnLoad = program.getIndexedMethod(RuntimeConstants.ASYNC_FRAGMENT_LOADER_ON_LOAD);
runAsyncOnSuccess = program.getIndexedMethod(RuntimeConstants.RUN_ASYNC_CALLBACK_ON_SUCCESS);
getClassField = program.getIndexedField(RuntimeConstants.OBJECT_CLAZZ);
getClassMethod = program.getIndexedMethod(RuntimeConstants.OBJECT_GET_CLASS);
rescuer = new RescueVisitor();
}
/**
* Return the set of all fields that are written.
*/
public Set<JField> getFieldsWritten() {
return fieldsWritten;
}
/**
* Return the complete set of types that have been instantiated.
*/
public Set<JReferenceType> getInstantiatedTypes() {
return instantiatedTypes;
}
/**
* Return all methods that could be executed, and all variables that could be
* read, based on the given entry points so far.
*/
public Set<? extends JNode> getLiveFieldsAndMethods() {
return liveFieldsAndMethods;
}
public Set<String> getLiveStrings() {
return liveStrings;
}
/**
* Return the complete set of types that have been referenced.
*/
public Set<? extends JReferenceType> getReferencedTypes() {
return referencedTypes;
}
/**
* Specify the {@link DependencyRecorder} to be used for future traversals.
* Specifying <code>null</code> means to stop recording dependencies.
*/
public void setDependencyRecorder(DependencyRecorder dr) {
if (dependencyRecorder != null && dr != null) {
throw new IllegalArgumentException("Attempting to set multiple dependency recorders");
}
this.dependencyRecorder = dr;
}
public void setForPruning() {
assert argumentsToRescueIfParameterRead == null;
argumentsToRescueIfParameterRead = ArrayListMultimap.create();
}
/**
* Traverse the program entry points, but don't traverse any runAsync
* fragments.
*/
public void traverseEntryMethods() {
for (JMethod method : program.getEntryMethods()) {
traverseFrom(method);
}
/*
* All exported methods must be treated as entry points. We need to invent a way to
* scope this down via flags or module properties.
*/
List<JDeclaredType> declaredTypes = program.getDeclaredTypes();
for (JDeclaredType type : declaredTypes) {
// We rescue any JsType/JsFunction interfaces immediately. Although it is not precise, as
// interfaces are mostly free, we are fine. This is simpler than tracking where the objects
// may enter the system.
// More appropriate solution is to track casts and JSNI methods (see
// #canBeInstantiatedInJavaScript) but unfortunately casts are replaced at a later stage
// that causes type and all calls to be pruned.
if (type.canBeImplementedExternally()) {
rescuer.rescue(type, true);
}
// first time through, record all exported methods
for (JMethod method : type.getMethods()) {
if (method.isJsInteropEntryPoint() || method.canBeImplementedExternally()) {
// treat class as instantiated, since a ctor may be called from JS export
rescuer.rescue(method.getEnclosingType(), true);
traverseFrom(method);
}
}
for (JField field : type.getFields()) {
if (field.isJsInteropEntryPoint() || field.canBeImplementedExternally()) {
rescuer.rescue(field.getEnclosingType(), true);
rescuer.rescue(field);
}
}
}
for (JArrayType arrayType : program.getAllArrayTypes()) {
if (arrayType.canBeImplementedExternally()) {
rescuer.rescue(arrayType, true);
}
}
if (program.getRunAsyncs().size() > 0) {
/*
* Explicitly rescue AsyncFragmentLoader.onLoad(). It is never explicitly
* called anyway, until late code gen. Also, we want it in the initial
* fragment so all other fragments can share the code.
*/
traverseFrom(asyncFragmentOnLoad);
}
}
public void traverseEverything() {
traverseEntryMethods();
traverseFromRunAsyncs();
/*
* Keep callback.onSuccess() from being pruned since we explicitly avoid
* visiting it.
*/
liveFieldsAndMethods.add(runAsyncOnSuccess);
}
/**
* Assume <code>method</code> is live, and find out what else might execute.
*/
public void traverseFrom(JMethod method) {
rescuer.rescue(method);
}
/**
* Assume <code>type</code> is instantiated, and find out what else will
* execute as a result.
*/
public void traverseFromInstantiationOf(JDeclaredType type) {
rescuer.rescue(type, true);
}
public void traverseFromReferenceTo(JDeclaredType type) {
rescuer.rescue(type, false);
}
/**
* Traverse the fragment for a specific runAsync.
*/
public void traverseFromRunAsync(JRunAsync runAsync) {
runAsync.traverseOnSuccess(rescuer);
}
/**
* Traverse the fragments for all runAsyncs.
*/
public void traverseFromRunAsyncs() {
for (JRunAsync runAsync : program.getRunAsyncs()) {
traverseFromRunAsync(runAsync);
}
}
}