| /* |
| * 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; |
| } |
| } |