blob: a522b6ea575461d70c72c0dd4338633d68f3e833 [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.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.JsName;
import com.google.gwt.dev.js.ast.JsNameRef;
import com.google.gwt.dev.js.ast.JsNew;
import com.google.gwt.dev.js.ast.JsNumberLiteral;
import com.google.gwt.dev.js.ast.JsObjectLiteral;
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 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;
}
}
/**
* <p>
* A predicate on whether statements and variables should be considered live.
* </p>
*
*
* <p>
* 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:
* </p>
*
* <ul>
* <li>A class literal depends on the strings contained in its instantiation
* instruction.</li>
*
* <li>Types depend on their supertype.</li>
*
* <li>Instance methods depend on their enclosing type.</li>
*
* <li>Static fields that are initialized to strings depend on the string they
* are initialized to.</li>
* </ul>
*/
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 <code>true</code>, but does return <code>false</code> 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 <code>stat</code>, or
* <code>null</code> if there isn't one. It recognizes JavaScript of the form
* <code>function foo(...) { ...}</code>, where <code>foo</code> 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<JsStatement> 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<JsStatement> newStats = Collections.<JsStatement> singletonList(call.makeStmt());
return newStats;
}
/**
* Assume that all code described by <code>alreadyLoadedPredicate</code> has
* been downloaded. Extract enough JavaScript statements that the code
* described by <code>livenessPredicate</code> can also run. The caller should
* ensure that <code>livenessPredicate</code> includes strictly more live code
* than <code>alreadyLoadedPredicate</code>.
*/
public List<JsStatement> extractStatements(LivenessPredicate livenessPredicate,
LivenessPredicate alreadyLoadedPredicate) {
List<JsStatement> extractedStats = new ArrayList<JsStatement>();
/**
* The type whose vtables can currently be installed.
*/
JClassType currentVtableType = null;
// Since we haven't run yet.
assert jsprogram.getFragmentCount() == 1;
List<JsStatement> stats = jsprogram.getGlobalBlock().getStatements();
for (JsStatement stat : stats) {
boolean keepIt;
JClassType vtableTypeAssigned = vtableTypeAssigned(stat);
if (vtableTypeAssigned != null && livenessPredicate.isLive(vtableTypeAssigned)) {
JsExprStmt result =
extractPrototypeSetup(livenessPredicate, alreadyLoadedPredicate, stat,
vtableTypeAssigned);
if (result != null) {
stat = result;
keepIt = true;
} else {
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) {
extractedStats.add(vtableStatFor(vtableType));
currentVtableType = vtableType;
}
extractedStats.add(stat);
}
}
return extractedStats;
}
/**
* Find all Java methods that still exist in the resulting JavaScript, even
* after JavaScript inlining and pruning.
*/
public Set<JMethod> findAllMethodsInJavaScript() {
Set<JMethod> methodsInJs = new HashSet<JMethod>();
for (int frag = 0; frag < jsprogram.getFragmentCount(); frag++) {
List<JsStatement> 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 <code>JsVars</code> 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;
}
/**
* 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. We essentially have to break up the
* prototype chain according to exactly what's newly live.
*/
private JsExprStmt extractPrototypeSetup(final LivenessPredicate livenessPredicate,
final LivenessPredicate alreadyLoadedPredicate, JsStatement stat,
final JClassType vtableTypeAssigned) {
final boolean[] anyLiveCode = new boolean[1];
Cloner c = new Cloner() {
@Override
public void endVisit(JsBinaryOperation x, JsContext ctx) {
JsExpression rhs = stack.pop();
JsNameRef lhs = (JsNameRef) stack.pop();
if (rhs instanceof JsNew || rhs instanceof JsObjectLiteral) {
// The super op is being assigned to the seed prototype.
if (alreadyLoadedPredicate.isLive(vtableTypeAssigned)) {
stack.push(lhs);
return;
} else {
anyLiveCode[0] = true;
}
} else if (lhs.getQualifier() == null) {
// The underscore is being assigned to.
assert "_".equals(lhs.getIdent());
} else {
// A constructor function is being assigned to.
assert "prototype".equals(lhs.getIdent());
JsNameRef ctorRef = (JsNameRef) lhs.getQualifier();
JConstructor ctor = (JConstructor) map.nameToMethod(ctorRef.getName());
assert ctor != null;
if (livenessPredicate.isLive(ctor) && !alreadyLoadedPredicate.isLive(ctor)) {
anyLiveCode[0] = true;
} else {
stack.push(rhs);
return;
}
}
JsBinaryOperation toReturn = new JsBinaryOperation(x.getSourceInfo(), x.getOperator());
toReturn.setArg2(rhs);
toReturn.setArg1(lhs);
stack.push(toReturn);
}
};
c.accept(((JsExprStmt) stat).getExpression());
JsExprStmt result = anyLiveCode[0] ? c.getExpression().makeStmt() : null;
return result;
}
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 <code>true</code>.
*
* 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();
}
/**
* Return the Java method corresponding to <code>stat</code>, or
* <code>null</code> if there isn't one. It recognizes JavaScript of the form
* <code>function foo(...) { ...}</code>, where <code>foo</code> 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
* <code>currentLivenessPredicate</code> but not by
* <code>alreadyLoadedPredicate</code>.
*/
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());
}
}
/**
* Compute a statement that can be used to set up for installing instance
* methods into a vtable. It will be of the form
* <code>_ = foo.prototype</code>, where <code>foo</code> is the constructor
* function for <code>vtableType</code>.
*/
private JsStatement vtableStatFor(JClassType vtableType) {
JsNameRef prototypeField =
new JsNameRef(jsprogram.createSourceInfoSynthetic(FragmentExtractor.class), "prototype");
JsExpression constructorRef;
SourceInfo sourceInfoVtableSetup = jsprogram.createSourceInfoSynthetic(FragmentExtractor.class);
if (vtableType == jprogram.getTypeJavaLangString()) {
// The methods of java.lang.String are put onto JavaScript's String
// prototype
SourceInfo sourceInfoConstructorRef =
jsprogram.createSourceInfoSynthetic(FragmentExtractor.class);
constructorRef = new JsNameRef(sourceInfoConstructorRef, "String");
} else {
constructorRef = map.nameForType(vtableType).makeRef(sourceInfoVtableSetup);
}
prototypeField.setQualifier(constructorRef);
SourceInfo underlineSourceInfo = jsprogram.createSourceInfoSynthetic(FragmentExtractor.class);
return (new JsBinaryOperation(sourceInfoVtableSetup, JsBinaryOperator.ASG, jsprogram.getScope()
.declareName("_").makeRef(underlineSourceInfo), prototypeField)).makeStmt();
}
/**
* If <code>state</code> is of the form <code>_ = Foo.prototype = exp</code>,
* then return <code>Foo</code>. Otherwise return <code>null</code>.
*/
private JClassType vtableTypeAssigned(JsStatement stat) {
if (!(stat instanceof JsExprStmt)) {
return null;
}
JsExprStmt expr = (JsExprStmt) stat;
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();
if (lhs.getQualifier() != null) {
return null;
}
if (lhs.getName() == null) {
return null;
}
if (!lhs.getName().getShortIdent().equals("_")) {
return null;
}
if (!(binExpr.getArg2() instanceof JsBinaryOperation)) {
return null;
}
JsBinaryOperation binExprRhs = (JsBinaryOperation) binExpr.getArg2();
if (binExprRhs.getOperator() != JsBinaryOperator.ASG) {
return null;
}
if (!(binExprRhs.getArg1() instanceof JsNameRef)) {
return null;
}
JsNameRef middleNameRef = (JsNameRef) binExprRhs.getArg1();
if (!middleNameRef.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;
}
}