com.google.gwt.dev.jjs.impl.FragmentExtractor Maven / Gradle / Ivy
/*
* 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.SourceInfo;
import com.google.gwt.dev.jjs.ast.JClassType;
import com.google.gwt.dev.jjs.ast.JConstructor;
import com.google.gwt.dev.jjs.ast.JDeclaredType;
import com.google.gwt.dev.jjs.ast.JField;
import com.google.gwt.dev.jjs.ast.JMethod;
import com.google.gwt.dev.jjs.ast.JProgram;
import com.google.gwt.dev.js.JsHoister.Cloner;
import com.google.gwt.dev.js.ast.JsBinaryOperation;
import com.google.gwt.dev.js.ast.JsBinaryOperator;
import com.google.gwt.dev.js.ast.JsContext;
import com.google.gwt.dev.js.ast.JsEmpty;
import com.google.gwt.dev.js.ast.JsExprStmt;
import com.google.gwt.dev.js.ast.JsExpression;
import com.google.gwt.dev.js.ast.JsFunction;
import com.google.gwt.dev.js.ast.JsInvocation;
import com.google.gwt.dev.js.ast.JsModVisitor;
import com.google.gwt.dev.js.ast.JsName;
import com.google.gwt.dev.js.ast.JsNameRef;
import com.google.gwt.dev.js.ast.JsNumberLiteral;
import com.google.gwt.dev.js.ast.JsProgram;
import com.google.gwt.dev.js.ast.JsStatement;
import com.google.gwt.dev.js.ast.JsVars;
import com.google.gwt.dev.js.ast.JsVars.JsVar;
import com.google.gwt.dev.js.ast.JsVisitable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* A class that extracts a fragment of code based on a supplied liveness
* condition.
*/
public class FragmentExtractor {
/**
* A {@link LivenessPredicate} that bases liveness on a single
* {@link ControlFlowAnalyzer}.
*/
public static class CfaLivenessPredicate implements LivenessPredicate {
private final ControlFlowAnalyzer cfa;
public CfaLivenessPredicate(ControlFlowAnalyzer cfa) {
this.cfa = cfa;
}
public boolean isLive(JDeclaredType type) {
return cfa.getInstantiatedTypes().contains(type);
}
public boolean isLive(JField field) {
return cfa.getLiveFieldsAndMethods().contains(field)
|| cfa.getFieldsWritten().contains(field);
}
public boolean isLive(JMethod method) {
return cfa.getLiveFieldsAndMethods().contains(method);
}
public boolean isLive(String string) {
return cfa.getLiveStrings().contains(string);
}
public boolean miscellaneousStatementsAreLive() {
return true;
}
}
/**
*
* A predicate on whether statements and variables should be considered live.
*
*
*
*
* Any supplied predicate must satisfy load-order dependencies. For any atom
* considered live, the atoms it depends on at load time should also be live.
* The following load-order dependencies exist:
*
*
*
* - A class literal depends on the strings contained in its instantiation
* instruction.
*
* - Types depend on their supertype.
*
* - Instance methods depend on their enclosing type.
*
* - Static fields that are initialized to strings depend on the string they
* are initialized to.
*
*/
public static interface LivenessPredicate {
boolean isLive(JDeclaredType type);
boolean isLive(JField field);
boolean isLive(JMethod method);
boolean isLive(String literal);
/**
* Whether miscellelaneous statements should be considered live.
* Miscellaneous statements are any that the fragment extractor does not
* recognize as being in any particular category. This method should almost
* always return true
, but does return false
for
* {@link NothingAlivePredicate}.
*/
boolean miscellaneousStatementsAreLive();
}
/**
* A {@link LivenessPredicate} where nothing is alive.
*/
public static class NothingAlivePredicate implements LivenessPredicate {
public boolean isLive(JDeclaredType type) {
return false;
}
public boolean isLive(JField field) {
return false;
}
public boolean isLive(JMethod method) {
return false;
}
public boolean isLive(String string) {
return false;
}
public boolean miscellaneousStatementsAreLive() {
return false;
}
}
/**
* A logger for statements that the fragment extractor encounters. Install one
* using
* {@link FragmentExtractor#setStatementLogger(com.google.gwt.fragserv.FragmentExtractor.StatementLogger)}
* .
*/
public static interface StatementLogger {
void logStatement(JsStatement stat, boolean isIncluded);
}
private static class NullStatementLogger implements StatementLogger {
public void logStatement(JsStatement method, boolean isIncluded) {
}
}
/**
* Return the Java method corresponding to stat
, or
* null
if there isn't one. It recognizes JavaScript of the form
* function foo(...) { ...}
, where foo
is the name
* of the JavaScript translation of a Java method.
*/
public static JMethod methodFor(JsStatement stat, JavaToJavaScriptMap map) {
if (stat instanceof JsExprStmt) {
JsExpression exp = ((JsExprStmt) stat).getExpression();
if (exp instanceof JsFunction) {
JsFunction func = (JsFunction) exp;
if (func.getName() != null) {
JMethod method = map.nameToMethod(func.getName());
if (method != null) {
return method;
}
}
}
}
return map.vtableInitToMethod(stat);
}
private final JProgram jprogram;
private final JsProgram jsprogram;
private final JavaToJavaScriptMap map;
private StatementLogger statementLogger = new NullStatementLogger();
public FragmentExtractor(JavaAndJavaScript javaAndJavaScript) {
this(javaAndJavaScript.jprogram, javaAndJavaScript.jsprogram, javaAndJavaScript.map);
}
public FragmentExtractor(JProgram jprogram, JsProgram jsprogram, JavaToJavaScriptMap map) {
this.jprogram = jprogram;
this.jsprogram = jsprogram;
this.map = map;
}
/**
* Create a call to {@link AsyncFragmentLoader#onLoad}.
*/
public List createOnLoadedCall(int splitPoint) {
JMethod loadMethod = jprogram.getIndexedMethod("AsyncFragmentLoader.onLoad");
JsName loadMethodName = map.nameForMethod(loadMethod);
SourceInfo sourceInfo = jsprogram.getSourceInfo();
JsInvocation call = new JsInvocation(sourceInfo);
call.setQualifier(wrapWithEntry(loadMethodName.makeRef(sourceInfo)));
call.getArguments().add(new JsNumberLiteral(sourceInfo, splitPoint));
List newStats = Collections. singletonList(call.makeStmt());
return newStats;
}
/**
* Assume that all code described by alreadyLoadedPredicate
has
* been downloaded. Extract enough JavaScript statements that the code
* described by livenessPredicate
can also run. The caller should
* ensure that livenessPredicate
includes strictly more live code
* than alreadyLoadedPredicate
.
*/
public List extractStatements(LivenessPredicate livenessPredicate,
LivenessPredicate alreadyLoadedPredicate) {
List extractedStats = new ArrayList();
/**
* The type whose vtables can currently be installed.
*/
JClassType currentVtableType = null;
JClassType pendingVtableType = null;
JsExprStmt pendingDefineSeed = null;
// Since we haven't run yet.
assert jsprogram.getFragmentCount() == 1;
List stats = jsprogram.getGlobalBlock().getStatements();
for (JsStatement stat : stats) {
boolean keepIt;
JClassType vtableTypeAssigned = vtableTypeAssigned(stat);
if (vtableTypeAssigned != null
&& livenessPredicate.isLive(vtableTypeAssigned)) {
boolean[] anyCtorsSetup = new boolean[1];
JsExprStmt result = maybeRemoveCtorsFromDefineSeedStmt(livenessPredicate,
alreadyLoadedPredicate, stat, anyCtorsSetup);
boolean anyWorkDone = anyCtorsSetup[0]
|| !alreadyLoadedPredicate.isLive(vtableTypeAssigned);
if (anyWorkDone) {
stat = result;
keepIt = true;
} else {
pendingDefineSeed = result;
pendingVtableType = vtableTypeAssigned;
keepIt = false;
}
} else if (containsRemovableVars(stat)) {
stat = removeSomeVars((JsVars) stat, livenessPredicate, alreadyLoadedPredicate);
keepIt = !(stat instanceof JsEmpty);
} else {
keepIt = isLive(stat, livenessPredicate) && !isLive(stat, alreadyLoadedPredicate);
}
statementLogger.logStatement(stat, keepIt);
if (keepIt) {
if (vtableTypeAssigned != null) {
currentVtableType = vtableTypeAssigned;
}
JClassType vtableType = vtableTypeNeeded(stat);
if (vtableType != null && vtableType != currentVtableType) {
assert pendingVtableType == vtableType;
extractedStats.add(pendingDefineSeed);
currentVtableType = pendingVtableType;
pendingDefineSeed = null;
pendingVtableType = null;
}
extractedStats.add(stat);
}
}
return extractedStats;
}
/**
* Find all Java methods that still exist in the resulting JavaScript, even
* after JavaScript inlining and pruning.
*/
public Set findAllMethodsInJavaScript() {
Set methodsInJs = new HashSet();
for (int frag = 0; frag < jsprogram.getFragmentCount(); frag++) {
List stats = jsprogram.getFragmentBlock(frag).getStatements();
for (JsStatement stat : stats) {
JMethod method = methodFor(stat);
if (method != null) {
methodsInJs.add(method);
}
}
}
return methodsInJs;
}
public void setStatementLogger(StatementLogger logger) {
statementLogger = logger;
}
/**
* Check whether this statement is a JsVars
that contains
* individual vars that could be removed. If it does, then
* {@link #removeSomeVars(JsVars, LivenessPredicate, LivenessPredicate)} is
* sensible for this statement and should be used instead of
* {@link #isLive(JsStatement, com.google.gwt.fragserv.FragmentExtractor.LivenessPredicate)}
* .
*/
private boolean containsRemovableVars(JsStatement stat) {
if (stat instanceof JsVars) {
for (JsVar var : (JsVars) stat) {
JField field = map.nameToField(var.getName());
if (field != null) {
return true;
}
}
}
return false;
}
private boolean isLive(JsStatement stat, LivenessPredicate livenessPredicate) {
JClassType type = map.typeForStatement(stat);
if (type != null) {
// This is part of the code only needed once a type is instantiable
return livenessPredicate.isLive(type);
}
JMethod meth = methodFor(stat);
if (meth != null) {
/*
* This statement either defines a method or installs it in a vtable.
*/
if (!livenessPredicate.isLive(meth)) {
// The method is not live. Skip it.
return false;
}
// The method is live. Check that its enclosing type is instantiable.
// TODO(spoon): this check should not be needed once the CFA is updated
return !meth.needsVtable() || livenessPredicate.isLive(meth.getEnclosingType());
}
return livenessPredicate.miscellaneousStatementsAreLive();
}
/**
* Check whether a variable is needed. If the variable is an intern variable,
* then return whether the interned value is live. If the variable corresponds
* to a Java field, then return whether the Java field is live. Otherwise,
* assume the variable is needed and return true
.
*
* Whenever this method is updated, also look at
* {@link #containsRemovableVars(JsStatement)}.
*/
private boolean isLive(JsVar var, LivenessPredicate livenessPredicate) {
JField field = map.nameToField(var.getName());
if (field != null) {
// It's a field
return livenessPredicate.isLive(field);
}
// It's not an intern variable at all
return livenessPredicate.miscellaneousStatementsAreLive();
}
/**
* Weird case: the seed function's liveness is associated with the type
* itself. However, individual constructors can have a liveness that is a
* subset of the type's liveness.
*/
private JsExprStmt maybeRemoveCtorsFromDefineSeedStmt(
final LivenessPredicate livenessPredicate,
final LivenessPredicate alreadyLoadedPredicate, JsStatement stat,
final boolean[] anyCtorsSetup) {
Cloner c = new Cloner();
c.accept(((JsExprStmt) stat).getExpression());
JsExprStmt result = c.getExpression().makeStmt();
new JsModVisitor() {
public void endVisit(JsNameRef x, JsContext ctx) {
JMethod maybeCtor = map.nameToMethod(x.getName());
if (maybeCtor instanceof JConstructor) {
JConstructor ctor = (JConstructor) maybeCtor;
if (!livenessPredicate.isLive(ctor)
|| alreadyLoadedPredicate.isLive(ctor)) {
ctx.removeMe();
} else {
anyCtorsSetup[0] = true;
}
}
};
/**
* Overridden to allow insert/remove on the varargs portion.
*/
protected void doAcceptList(List collection) {
doAcceptWithInsertRemove(collection);
};
}.accept(result);
return result;
}
/**
* Return the Java method corresponding to stat
, or
* null
if there isn't one. It recognizes JavaScript of the form
* function foo(...) { ...}
, where foo
is the name
* of the JavaScript translation of a Java method.
*/
private JMethod methodFor(JsStatement stat) {
return methodFor(stat, map);
}
/**
* If stat is a {@link JsVars} that initializes a bunch of intern vars, return
* a modified statement that skips any vars are needed by
* currentLivenessPredicate
but not by
* alreadyLoadedPredicate
.
*/
private JsStatement removeSomeVars(JsVars stat, LivenessPredicate currentLivenessPredicate,
LivenessPredicate alreadyLoadedPredicate) {
JsVars newVars = new JsVars(stat.getSourceInfo());
for (JsVar var : stat) {
if (isLive(var, currentLivenessPredicate) && !isLive(var, alreadyLoadedPredicate)) {
newVars.add(var);
}
}
if (newVars.getNumVars() == stat.getNumVars()) {
// no change
return stat;
}
if (newVars.iterator().hasNext()) {
/*
* The new variables are non-empty; return them.
*/
return newVars;
} else {
/*
* An empty JsVars seems possibly surprising; return a true empty
* statement instead.
*/
return new JsEmpty(stat.getSourceInfo());
}
}
/**
* If state
is of the form _ = String.prototype
,
* then return String
. If the form is
* defineSeed(id, superId, cTM, ctor1, ctor2, ...)
return the type
* corresponding to that id. Otherwise return null
.
*/
private JClassType vtableTypeAssigned(JsStatement stat) {
if (!(stat instanceof JsExprStmt)) {
return null;
}
JsExprStmt expr = (JsExprStmt) stat;
if (expr.getExpression() instanceof JsInvocation) {
// Handle a defineSeed call.
JsInvocation call = (JsInvocation) expr.getExpression();
if (!(call.getQualifier() instanceof JsNameRef)) {
return null;
}
JsNameRef func = (JsNameRef) call.getQualifier();
if (func.getName() != jsprogram.getIndexedFunction("SeedUtil.defineSeed").getName()) {
return null;
}
return map.typeForStatement(stat);
}
// Handle String.
if (!(expr.getExpression() instanceof JsBinaryOperation)) {
return null;
}
JsBinaryOperation binExpr = (JsBinaryOperation) expr.getExpression();
if (binExpr.getOperator() != JsBinaryOperator.ASG) {
return null;
}
if (!(binExpr.getArg1() instanceof JsNameRef)) {
return null;
}
JsNameRef lhs = (JsNameRef) binExpr.getArg1();
JsName underBar = jsprogram.getScope().findExistingName("_");
assert underBar != null;
if (lhs.getName() != underBar) {
return null;
}
if (!(binExpr.getArg2() instanceof JsNameRef)) {
return null;
}
JsNameRef rhsRef = (JsNameRef) binExpr.getArg2();
if (!(rhsRef.getQualifier() instanceof JsNameRef)) {
return null;
}
if (!((JsNameRef) rhsRef.getQualifier()).getShortIdent().equals("String")) {
return null;
}
if (!rhsRef.getName().getShortIdent().equals("prototype")) {
return null;
}
return map.typeForStatement(stat);
}
private JClassType vtableTypeNeeded(JsStatement stat) {
JMethod meth = map.vtableInitToMethod(stat);
if (meth != null) {
if (meth.needsVtable()) {
return (JClassType) meth.getEnclosingType();
}
}
return null;
}
/**
* Wrap an expression with a call to $entry.
*/
private JsInvocation wrapWithEntry(JsExpression exp) {
SourceInfo sourceInfo = exp.getSourceInfo();
JsInvocation call = new JsInvocation(sourceInfo);
JsName entryFunctionName = jsprogram.getScope().findExistingName("$entry");
call.setQualifier(entryFunctionName.makeRef(sourceInfo));
call.getArguments().add(exp);
return call;
}
}