Add an optimization to the JavaScript AST to remove delegating/trampoline functions via code inlining. http://groups.google.com/group/Google-Web-Toolkit-Contributors/t/bba1a8b27f98f77d Patch by: bobv Reviewed by: scottb git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@1481 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/dev/core/src/com/google/gwt/dev/jjs/JavaToJavaScriptCompiler.java b/dev/core/src/com/google/gwt/dev/jjs/JavaToJavaScriptCompiler.java index ae8e990..607bae4 100644 --- a/dev/core/src/com/google/gwt/dev/jjs/JavaToJavaScriptCompiler.java +++ b/dev/core/src/com/google/gwt/dev/jjs/JavaToJavaScriptCompiler.java
@@ -49,12 +49,14 @@ import com.google.gwt.dev.jjs.impl.ReplaceRebinds; import com.google.gwt.dev.jjs.impl.TypeMap; import com.google.gwt.dev.jjs.impl.TypeTightener; +import com.google.gwt.dev.js.JsDelegationRemover; import com.google.gwt.dev.js.JsNormalizer; import com.google.gwt.dev.js.JsObfuscateNamer; import com.google.gwt.dev.js.JsPrettyNamer; import com.google.gwt.dev.js.JsSourceGenerationVisitor; import com.google.gwt.dev.js.JsStringInterner; import com.google.gwt.dev.js.JsSymbolResolver; +import com.google.gwt.dev.js.JsUnusedFunctionRemover; import com.google.gwt.dev.js.JsVerboseNamer; import com.google.gwt.dev.js.ast.JsProgram; import com.google.gwt.dev.util.DefaultTextOutput; @@ -394,7 +396,16 @@ // (9) Resolve all unresolved JsNameRefs JsSymbolResolver.exec(jsProgram); - // (10) Obfuscate + // (10) Apply optimizations to JavaScript AST + do { + didChange = false; + // Remove delegating/trampoline functions + didChange = JsDelegationRemover.exec(jsProgram) || didChange; + // Remove unused functions, possible + didChange = JsUnusedFunctionRemover.exec(jsProgram) || didChange; + } while (didChange); + + // (11) Obfuscate if (obfuscate) { JsStringInterner.exec(jsProgram); JsObfuscateNamer.exec(jsProgram);
diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/BuildTypeMap.java b/dev/core/src/com/google/gwt/dev/jjs/impl/BuildTypeMap.java index 83a5f0d..51258e9 100644 --- a/dev/core/src/com/google/gwt/dev/jjs/impl/BuildTypeMap.java +++ b/dev/core/src/com/google/gwt/dev/jjs/impl/BuildTypeMap.java
@@ -556,6 +556,7 @@ List<JsStatement> result = jsParser.parse(jsProgram.getScope(), sr, -1); JsExprStmt jsExprStmt = (JsExprStmt) result.get(0); JsFunction jsFunction = (JsFunction) jsExprStmt.getExpression(); + jsFunction.setFromJava(true); ((JsniMethodBody) newMethod.getBody()).setFunc(jsFunction); } catch (IOException e) { throw new InternalCompilerException(
diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/GenerateJavaScriptAST.java b/dev/core/src/com/google/gwt/dev/jjs/impl/GenerateJavaScriptAST.java index c0b82fb..447af14 100644 --- a/dev/core/src/com/google/gwt/dev/jjs/impl/GenerateJavaScriptAST.java +++ b/dev/core/src/com/google/gwt/dev/jjs/impl/GenerateJavaScriptAST.java
@@ -331,7 +331,7 @@ jsFunction.setName(globalName); } else { // create a new peer JsFunction - jsFunction = new JsFunction(topScope, globalName); + jsFunction = new JsFunction(topScope, globalName, true); methodBodyMap.put(x.getBody(), jsFunction); } push(jsFunction.getScope());
diff --git a/dev/core/src/com/google/gwt/dev/js/JsDelegationRemover.java b/dev/core/src/com/google/gwt/dev/js/JsDelegationRemover.java new file mode 100644 index 0000000..440445d --- /dev/null +++ b/dev/core/src/com/google/gwt/dev/js/JsDelegationRemover.java
@@ -0,0 +1,594 @@ +/* + * Copyright 2007 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.js; + +import com.google.gwt.dev.jjs.InternalCompilerException; +import com.google.gwt.dev.js.ast.JsBinaryOperation; +import com.google.gwt.dev.js.ast.JsBinaryOperator; +import com.google.gwt.dev.js.ast.JsBlock; +import com.google.gwt.dev.js.ast.JsBooleanLiteral; +import com.google.gwt.dev.js.ast.JsCase; +import com.google.gwt.dev.js.ast.JsConditional; +import com.google.gwt.dev.js.ast.JsContext; +import com.google.gwt.dev.js.ast.JsDecimalLiteral; +import com.google.gwt.dev.js.ast.JsDefault; +import com.google.gwt.dev.js.ast.JsExprStmt; +import com.google.gwt.dev.js.ast.JsExpression; +import com.google.gwt.dev.js.ast.JsFor; +import com.google.gwt.dev.js.ast.JsForIn; +import com.google.gwt.dev.js.ast.JsFunction; +import com.google.gwt.dev.js.ast.JsIf; +import com.google.gwt.dev.js.ast.JsIntegralLiteral; +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.JsNullLiteral; +import com.google.gwt.dev.js.ast.JsParameter; +import com.google.gwt.dev.js.ast.JsProgram; +import com.google.gwt.dev.js.ast.JsReturn; +import com.google.gwt.dev.js.ast.JsStatement; +import com.google.gwt.dev.js.ast.JsStringLiteral; +import com.google.gwt.dev.js.ast.JsSwitchMember; +import com.google.gwt.dev.js.ast.JsThisRef; +import com.google.gwt.dev.js.ast.JsVisitor; +import com.google.gwt.dev.js.ast.JsWhile; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Collapses delegating method calls. + */ +public class JsDelegationRemover { + /** + * This is used to clean up duplication invocations of a clinit. Whenever + * there is a possible branch in program flow, the remover will create a new + * instance of itself to handle the possible outcomes. + * + * We don't look at combining the clinits that are called in all branch + * choices. This will not produce the most efficient elimination of clinit + * calls, but it handles the general case and is simple to verify. + */ + private class DuplicateClinitRemover extends JsModVisitor { + /* + * TODO: Most of the special casing below can be removed if complex + * statements always use blocks, rather than plain statements. + */ + + /** + * Retains the names of the clinit functions that we know have been called. + */ + private final Set<JsName> called; + + public DuplicateClinitRemover() { + called = new HashSet<JsName>(); + } + + public DuplicateClinitRemover(Set<JsName> alreadyCalled) { + called = new HashSet<JsName>(alreadyCalled); + } + + @Override + public boolean visit(JsBinaryOperation x, JsContext<JsExpression> ctx) { + if ((x.getOperator() == JsBinaryOperator.COMMA) + && (x.getArg1() instanceof JsInvocation)) { + + JsName clinit = getClinitFromInvocation((JsInvocation) x.getArg1()); + if ((clinit != null) && called.contains(clinit)) { + + // Replace the binary operation with the RHS + ctx.replaceMe(x.getArg2()); + + // Manually call accept on the RHS to eliminate nested comma + // expressions. + accept(x.getArg2()); + + // Don't continue traversing the original binary operation + return false; + } + } + + return true; + } + + /** + * Most of the branching statements (as well as JsFunctions) will visit with + * a JsBlock, so we don't need to explicitly enumerate all JsStatement + * subtypes. + */ + @Override + public boolean visit(JsBlock x, JsContext<JsStatement> ctx) { + (new DuplicateClinitRemover(called)).acceptWithInsertRemove(x + .getStatements()); + return false; + } + + @Override + public boolean visit(JsCase x, JsContext<JsSwitchMember> ctx) { + accept(x.getCaseExpr()); + (new DuplicateClinitRemover(called)).acceptWithInsertRemove(x.getStmts()); + return false; + } + + @Override + public boolean visit(JsConditional x, JsContext<JsExpression> ctx) { + accept(x.getTestExpression()); + (new DuplicateClinitRemover(called)).accept(x.getThenExpression()); + (new DuplicateClinitRemover(called)).accept(x.getElseExpression()); + return false; + } + + @Override + public boolean visit(JsDefault x, JsContext<JsSwitchMember> ctx) { + (new DuplicateClinitRemover(called)).acceptWithInsertRemove(x.getStmts()); + return false; + } + + @Override + public boolean visit(JsFor x, JsContext<JsStatement> ctx) { + // The JsFor may have an expression xor a variable declaration. + if (x.getInitExpr() != null) { + accept(x.getInitExpr()); + } else if (x.getInitVars() != null) { + accept(x.getInitVars()); + } + + // The condition is optional + if (x.getCondition() != null) { + accept(x.getCondition()); + } + + // We don't check the increment expression because even if it exists, it + // is not guaranteed to be called at all + + // The body is not guaranteed to be a JsBlock + (new DuplicateClinitRemover(called)).accept(x.getBody()); + return false; + } + + @Override + public boolean visit(JsForIn x, JsContext<JsStatement> ctx) { + if (x.getIterExpr() != null) { + accept(x.getIterExpr()); + } + + accept(x.getObjExpr()); + + // The body is not guaranteed to be a JsBlock + (new DuplicateClinitRemover(called)).accept(x.getBody()); + return false; + } + + @Override + public boolean visit(JsIf x, JsContext<JsStatement> ctx) { + accept(x.getIfExpr()); + + (new DuplicateClinitRemover(called)).accept(x.getThenStmt()); + if (x.getElseStmt() != null) { + (new DuplicateClinitRemover(called)).accept(x.getElseStmt()); + } + + return false; + } + + /** + * Possibly record that we've seen a clinit in the current context. + */ + @Override + public boolean visit(JsInvocation x, JsContext<JsExpression> ctx) { + JsName name = getClinitFromInvocation(x); + if (name != null) { + called.add(name); + } + return true; + } + + @Override + public boolean visit(JsWhile x, JsContext<JsStatement> ctx) { + accept(x.getCondition()); + + // The body is not guaranteed to be a JsBlock + (new DuplicateClinitRemover(called)).accept(x.getBody()); + return false; + } + } + + /** + * Examines a statement to determine if it matches the delegator pattern. In + * order to be considered a delegator, all JsNameReferences must either be + * references to global/root objects or to the parameters of the enclosing + * function. References to strict parameters must occur exactly once and in + * the order in which the parameters are defined. A flexible parameter (one + * that may be evaluated multiple times without effect) may be evaluated any + * number of times and in any order. + */ + private class IsDelegatingVisitor extends JsVisitor { + private final Set<JsName> parameterNames; + private final List<JsName> strictParameters; + private final List<JsName> flexibleParameters; + private boolean delegating = true; + + public IsDelegatingVisitor(Set<JsName> parameterNames, + List<JsName> strictParameters, List<JsName> flexibleParameters) { + this.parameterNames = parameterNames; + this.strictParameters = strictParameters; + this.flexibleParameters = flexibleParameters; + } + + @Override + public void endVisit(JsNameRef x, JsContext<JsExpression> ctx) { + // Short circuit + if (!delegating) { + return; + } + + // Don't examine JsNameRefs that are intermediate elements in a chain + if (x.getQualifier() != null) { + return; + } + + JsName name = x.getName(); + + if (parameterNames.contains(name)) { + // The name is a reference to a parameter defined in the function + if (flexibleParameters.contains(name)) { + // It doesn't matter how many times an optional parameter is used + + } else if (strictParameters.indexOf(name) != 0) { + // We saw a required parameter being used out of declaration order + delegating = false; + + } else { + // Record that the required parameter was used. + if (strictParameters.remove(0) != name) { + throw new InternalCompilerException( + "Unexpected name removed from strict parameter list."); + } + } + } else { + // See if the name refers to a global/static name. + boolean isStatic = + name.getEnclosing() == program.getScope() + || name.getEnclosing() == program.getRootScope(); + delegating &= isStatic; + } + } + + public boolean isDelegating() { + return delegating && (strictParameters.size() == 0); + } + } + + /** + * Given a call site, replace with an equivalent expression, assuming that the + * callee is a delegation-style function. + */ + private class NameRefReplacerVisitor extends JsModVisitor { + /** + * Set up a map of parameter names back to the expressions that will be + * passed in from the outer call site. + */ + final Map<JsName, JsExpression> originalParameterExpressions = + new HashMap<JsName, JsExpression>(); + + /** + * Constructor. + * + * @param invocation The delegating JsInvocation (the inner delegation) + * @param function The function that encloses the outer delegation + */ + public NameRefReplacerVisitor(JsInvocation invocation, JsFunction function) { + List<JsParameter> parameters = function.getParameters(); + List<JsExpression> arguments = invocation.getArguments(); + + if (parameters.size() != arguments.size()) { + // This shouldn't happen if the cloned JsInvocation has been properly + // configured + throw new InternalCompilerException( + "Mismatch on parameters and arguments"); + } + + for (int i = 0; i < parameters.size(); i++) { + JsParameter p = parameters.get(i); + JsExpression e = arguments.get(i); + originalParameterExpressions.put(p.getName(), e); + } + } + + /** + * Replace JsNameRefs that refer to parameters with the expression passed + * into the function invocation. + */ + @Override + public void endVisit(JsNameRef x, JsContext<JsExpression> ctx) { + if (x.getQualifier() != null) { + return; + } + + JsExpression original = originalParameterExpressions.get(x.getName()); + + if (original != null) { + ctx.replaceMe(original); + } + } + } + + /** + * This looks for all invocations in the program. Any invoked JsFunctions + * derived from Java functions are then examined for being possible delegation + * patterns. + */ + private class RemoveDelegationVisitor extends JsModVisitor { + @Override + public void endVisit(JsInvocation x, JsContext<JsExpression> ctx) { + // Start by determining what is being invoked. + if (!(x.getQualifier() instanceof JsNameRef)) { + return; + } + JsNameRef ref = (JsNameRef) x.getQualifier(); + + /* + * We only want to look at invocations of things that we statically know + * to be functions. Otherwise, we can't know what statements the + * invocation would actually invoke. The static reference would be null + * when trying operate on references to external functions, or functions + * as arguments to another function. + */ + if (!(ref.getName().getStaticRef() instanceof JsFunction)) { + return; + } + + JsFunction f = (JsFunction) ref.getName().getStaticRef(); + List<JsStatement> statements = f.getBody().getStatements(); + JsStatement clinit; + JsStatement toHoist; + + if (statements.size() == 1) { + // The simple case. + clinit = null; + toHoist = statements.get(0); + + } else if (statements.size() == 2) { + /* + * In the case of DOM, or similarly-structured classes that use a static + * "impl" field, we need to account for the static initializer. What + * we'll do is to create a JS comma expression to encapsulate the + * invocation of the static initializer as well as the delegated + * function. As a subsequent optimization, we can go through all + * functions and look for repeated invocations of a clinit, removing the + * JsBinaryOperation and replacing it with its rhs. + */ + clinit = statements.get(0); + toHoist = statements.get(1); + if (!isStaticInitializer(clinit)) { + return; + } + + } else { + // The expression is too complicated for this optimization + return; + } + + // Confirm that the statement conforms to the desired pattern + if (!isDelegatingStatement(x, f, toHoist)) { + return; + } + + /* + * Create a replacement expression to use in place of the invocation. It + * is important that the replacement is newly-minted and therefore not + * referenced by any other AST nodes. Consider the case of a common, + * delegating function. If the hoisted expressions were not distinct + * objects, it would not be possible to substitute different JsNameRefs at + * different call sites. + */ + JsExpression replacement = hoistedExpression(toHoist); + if (replacement == null) { + return; + } + + // Perform the name replacement + NameRefReplacerVisitor v = new NameRefReplacerVisitor(x, f); + replacement = v.accept(replacement); + + // Assemble the (clinit(), hoisted) expression. + if (statements.size() == 2) { + replacement = + new JsBinaryOperation(JsBinaryOperator.COMMA, ((JsExprStmt) clinit) + .getExpression(), replacement); + } + + ctx.replaceMe(replacement); + } + } + + /** + * Static entry point used by JavaToJavaScriptCompiler. + */ + public static boolean exec(JsProgram program) { + return new JsDelegationRemover(program).execImpl(); + } + + /** + * Given a JsInvocation, determine if it is invoking a class's static + * initializer. This just looks for an invocation of a function whose short + * identifier is "$clinit". Not fancy, but it works. + */ + private static JsName getClinitFromInvocation(JsInvocation invocation) { + if (invocation.getQualifier() instanceof JsNameRef) { + JsNameRef nameRef = (JsNameRef) invocation.getQualifier(); + JsName name = nameRef.getName(); + if (name.getShortIdent().equals("$clinit")) { + return name; + } + } + + return null; + } + + /** + * Given a delegated JsStatement, construct an expression to hoist into the + * outer caller. This does not perform any name replacement, but simply + * constructs a mutable copy of the expression that can be manipulated + * at-will. + */ + private static JsExpression hoistedExpression(JsStatement statement) { + JsExpression expression; + if (statement instanceof JsExprStmt) { + JsExprStmt exprStmt = (JsExprStmt) statement; + expression = exprStmt.getExpression(); + + } else if (statement instanceof JsReturn) { + JsReturn ret = (JsReturn) statement; + expression = ret.getExpr(); + + } else { + return null; + } + + return JsHoister.hoist(expression); + } + + /** + * Determines if a statement is an invocation of a static initializer. + */ + private static boolean isStaticInitializer(JsStatement statement) { + if (!(statement instanceof JsExprStmt)) { + return false; + } + + JsExprStmt exprStmt = (JsExprStmt) statement; + JsExpression expression = exprStmt.getExpression(); + + if (!(expression instanceof JsInvocation)) { + return false; + } + + JsInvocation invocation = (JsInvocation) expression; + + if (!(invocation.getQualifier() instanceof JsNameRef)) { + return false; + } + + return getClinitFromInvocation(invocation) != null; + } + + /** + * Indicates if an expression can be repeated multiple times in a delegation + * removal without side-effects. + */ + private static boolean requiresSingleEvaluation(JsExpression e) { + if (e instanceof JsNameRef) { + JsNameRef ref = (JsNameRef) e; + if (ref.getQualifier() != null) { + return requiresSingleEvaluation(ref.getQualifier()); + } else { + return false; + } + } else if (e instanceof JsBooleanLiteral) { + return false; + } else if (e instanceof JsDecimalLiteral) { + return false; + } else if (e instanceof JsIntegralLiteral) { + return false; + } else if (e instanceof JsNullLiteral) { + return false; + } else if (e instanceof JsStringLiteral) { + return false; + } else if (e instanceof JsThisRef) { + return false; + } else { + return true; + } + } + + /** + * The intended victim for the optimizer. + */ + private final JsProgram program; + + /** + * Constructor. + */ + private JsDelegationRemover(JsProgram program) { + this.program = program; + } + + /** + * Instance execution method. + */ + private boolean execImpl() { + RemoveDelegationVisitor v = new RemoveDelegationVisitor(); + v.accept(program); + + DuplicateClinitRemover r = new DuplicateClinitRemover(); + r.accept(program); + + return v.didChange() || r.didChange(); + } + + /** + * Determine if a statement matches the delegator pattern. + */ + private boolean isDelegatingStatement(JsInvocation invocation, + JsFunction enclosing, JsStatement statement) { + + // Build up a map of parameter names + Set<JsName> parameterNames = new HashSet<JsName>(); + + for (JsParameter param : enclosing.getParameters()) { + parameterNames.add(param.getName()); + } + + if (invocation.getArguments().size() != enclosing.getParameters().size()) { + /* + * This will happen with varargs-style JavaScript functions that rely on + * the "arguments" array. The reference to arguments would be detected in + * IsDelegatingVisitor, but the bucketing code below assumes the same + * number of parameters and arguments. + */ + return false; + } + + /* + * Determine which function parameters can safely be duplicated or + * re-ordered in the delegation removal. This will vary between call sites, + * based on whether or not the invocation's arguments can be repeated + * without ill effect. + */ + List<JsName> strictParameters = new ArrayList<JsName>(); + List<JsName> flexibleParameters = new ArrayList<JsName>(); + for (int i = 0; i < invocation.getArguments().size(); i++) { + JsExpression e = invocation.getArguments().get(i); + if (requiresSingleEvaluation(e)) { + strictParameters.add(enclosing.getParameters().get(i).getName()); + } else { + flexibleParameters.add(enclosing.getParameters().get(i).getName()); + } + } + + IsDelegatingVisitor v = + new IsDelegatingVisitor(parameterNames, strictParameters, + flexibleParameters); + v.accept(statement); + return v.isDelegating(); + } +}
diff --git a/dev/core/src/com/google/gwt/dev/js/JsHoister.java b/dev/core/src/com/google/gwt/dev/js/JsHoister.java new file mode 100644 index 0000000..27fede8 --- /dev/null +++ b/dev/core/src/com/google/gwt/dev/js/JsHoister.java
@@ -0,0 +1,262 @@ +/* + * Copyright 2007 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.js; + +import com.google.gwt.dev.jjs.InternalCompilerException; +import com.google.gwt.dev.js.ast.JsArrayAccess; +import com.google.gwt.dev.js.ast.JsArrayLiteral; +import com.google.gwt.dev.js.ast.JsBinaryOperation; +import com.google.gwt.dev.js.ast.JsBooleanLiteral; +import com.google.gwt.dev.js.ast.JsConditional; +import com.google.gwt.dev.js.ast.JsContext; +import com.google.gwt.dev.js.ast.JsDecimalLiteral; +import com.google.gwt.dev.js.ast.JsExpression; +import com.google.gwt.dev.js.ast.JsFunction; +import com.google.gwt.dev.js.ast.JsIntegralLiteral; +import com.google.gwt.dev.js.ast.JsInvocation; +import com.google.gwt.dev.js.ast.JsNameRef; +import com.google.gwt.dev.js.ast.JsNew; +import com.google.gwt.dev.js.ast.JsNullLiteral; +import com.google.gwt.dev.js.ast.JsObjectLiteral; +import com.google.gwt.dev.js.ast.JsPostfixOperation; +import com.google.gwt.dev.js.ast.JsPrefixOperation; +import com.google.gwt.dev.js.ast.JsPropertyInitializer; +import com.google.gwt.dev.js.ast.JsRegExp; +import com.google.gwt.dev.js.ast.JsStringLiteral; +import com.google.gwt.dev.js.ast.JsThisRef; +import com.google.gwt.dev.js.ast.JsVisitor; + +import java.util.List; +import java.util.Stack; + +/** + * A utility class to clone JsExpression AST members for use by + * {@link JsDelegationRemover}. <b>Not all expressions are necessarily + * implemented</b>, only those that are safe to hoist into outer call sites. + */ +final class JsHoister { + /** + * Implements actual cloning logic. We rely on the JsExpressions to provide + * traversal logic. The {@link #stack} field is used to accumulate + * already-cloned JsExpression instances. One gotcha that falls out of this is + * that argument lists are on the stack in reverse order, so lists should be + * constructed via inserts, rather than appends. + */ + private static class Cloner extends JsVisitor { + private final Stack<JsExpression> stack = new Stack<JsExpression>(); + private boolean successful = true; + + @Override + public void endVisit(JsArrayAccess x, JsContext<JsExpression> ctx) { + JsArrayAccess newExpression = new JsArrayAccess(); + newExpression.setIndexExpr(stack.pop()); + newExpression.setArrayExpr(stack.pop()); + stack.push(newExpression); + } + + @Override + public void endVisit(JsArrayLiteral x, JsContext<JsExpression> ctx) { + JsArrayLiteral toReturn = new JsArrayLiteral(); + List<JsExpression> expressions = toReturn.getExpressions(); + for (JsExpression e : x.getExpressions()) { + expressions.add(0, stack.pop()); + } + stack.push(toReturn); + } + + @Override + public void endVisit(JsBinaryOperation x, JsContext<JsExpression> ctx) { + JsBinaryOperation toReturn = new JsBinaryOperation(x.getOperator()); + toReturn.setArg2(stack.pop()); + toReturn.setArg1(stack.pop()); + stack.push(toReturn); + } + + @Override + public void endVisit(JsBooleanLiteral x, JsContext<JsExpression> ctx) { + stack.push(x); + } + + @Override + public void endVisit(JsConditional x, JsContext<JsExpression> ctx) { + JsConditional toReturn = new JsConditional(); + toReturn.setElseExpression(stack.pop()); + toReturn.setThenExpression(stack.pop()); + toReturn.setTestExpression(stack.pop()); + stack.push(toReturn); + } + + @Override + public void endVisit(JsDecimalLiteral x, JsContext<JsExpression> ctx) { + stack.push(x); + } + + /** + * The only functions that would get be visited are those being used as + * first-class objects. + */ + @Override + public void endVisit(JsFunction x, JsContext<JsExpression> ctx) { + // Set a flag to indicate that we cannot continue, and push a null so + // we don't run out of elements on the stack. + successful = false; + stack.push(null); + } + + @Override + public void endVisit(JsIntegralLiteral x, JsContext<JsExpression> ctx) { + stack.push(x); + } + + /** + * Cloning the invocation allows us to modify it without damaging other call + * sites. + */ + @Override + public void endVisit(JsInvocation x, JsContext<JsExpression> ctx) { + JsInvocation toReturn = new JsInvocation(); + List<JsExpression> params = toReturn.getArguments(); + for (JsExpression e : x.getArguments()) { + params.add(0, stack.pop()); + } + toReturn.setQualifier(stack.pop()); + stack.push(toReturn); + } + + /** + * Do a deep clone of a JsNameRef. Because JsNameRef chains are shared + * throughout the AST, you can't just go and change their qualifiers when + * re-writing an invocation. + */ + @Override + public void endVisit(JsNameRef x, JsContext<JsExpression> ctx) { + JsNameRef toReturn = new JsNameRef(x.getName()); + + if (x.getQualifier() != null) { + toReturn.setQualifier(stack.pop()); + } + stack.push(toReturn); + } + + @Override + public void endVisit(JsNew x, JsContext<JsExpression> ctx) { + JsNew toReturn = new JsNew(); + + List<JsExpression> arguments = toReturn.getArguments(); + for (JsExpression a : x.getArguments()) { + arguments.add(0, stack.pop()); + } + toReturn.setConstructorExpression(stack.pop()); + stack.push(toReturn); + } + + @Override + public void endVisit(JsNullLiteral x, JsContext<JsExpression> ctx) { + stack.push(x); + } + + @Override + public void endVisit(JsObjectLiteral x, JsContext<JsExpression> ctx) { + JsObjectLiteral toReturn = new JsObjectLiteral(); + List<JsPropertyInitializer> inits = toReturn.getPropertyInitializers(); + + for (JsPropertyInitializer init : x.getPropertyInitializers()) { + /* + * JsPropertyInitializers are the only non-JsExpression objects that we + * care about, so we just go ahead and create the objects in the loop, + * rather than expecting it to be on the stack and having to perform + * narrowing casts at all stack.pop() invocations. + */ + JsPropertyInitializer newInit = new JsPropertyInitializer(); + newInit.setValueExpr(stack.pop()); + newInit.setLabelExpr(stack.pop()); + + inits.add(0, newInit); + } + stack.push(toReturn); + } + + @Override + public void endVisit(JsPostfixOperation x, JsContext<JsExpression> ctx) { + JsPostfixOperation toReturn = new JsPostfixOperation(x.getOperator()); + toReturn.setArg(stack.pop()); + stack.push(toReturn); + } + + @Override + public void endVisit(JsPrefixOperation x, JsContext<JsExpression> ctx) { + JsPrefixOperation toReturn = new JsPrefixOperation(x.getOperator()); + toReturn.setArg(stack.pop()); + stack.push(toReturn); + } + + @Override + public void endVisit(JsRegExp x, JsContext<JsExpression> ctx) { + stack.push(x); + } + + @Override + public void endVisit(JsStringLiteral x, JsContext<JsExpression> ctx) { + stack.push(x); + } + + /** + * A "this" reference can only effectively be hoisted if the call site is + * qualified by a JsNameRef, so we'll ignore this for now. + */ + @Override + public void endVisit(JsThisRef x, JsContext<JsExpression> ctx) { + // Set a flag to indicate that we cannot continue, and push a null so + // we don't run out of elements on the stack. + successful = false; + stack.push(null); + } + + public JsExpression getExpression() { + return (successful && checkStack()) ? stack.peek() : null; + } + + private boolean checkStack() { + if (stack.size() > 1) { + throw new InternalCompilerException("Too many expressions on stack"); + } + + return stack.size() == 1; + } + } + + /** + * Given a JsStatement, construct an expression to hoist into the outer + * caller. This does not perform any name replacement, nor does it verify the + * scope of referenced elements, but simply constructs a mutable copy of the + * expression that can be manipulated at-will. + * + * @return A copy of the original expression, or <code>null</code> if the + * expression cannot be hoisted. + */ + public static JsExpression hoist(JsExpression expression) { + if (expression == null) { + return null; + } + + Cloner c = new Cloner(); + c.accept(expression); + return c.getExpression(); + } + + private JsHoister() { + } +}
diff --git a/dev/core/src/com/google/gwt/dev/js/JsUnusedFunctionRemover.java b/dev/core/src/com/google/gwt/dev/js/JsUnusedFunctionRemover.java new file mode 100644 index 0000000..ef97683 --- /dev/null +++ b/dev/core/src/com/google/gwt/dev/js/JsUnusedFunctionRemover.java
@@ -0,0 +1,120 @@ +/* + * Copyright 2007 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.js; + +import com.google.gwt.dev.jjs.InternalCompilerException; +import com.google.gwt.dev.js.ast.JsContext; +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.JsModVisitor; +import com.google.gwt.dev.js.ast.JsName; +import com.google.gwt.dev.js.ast.JsNameRef; +import com.google.gwt.dev.js.ast.JsProgram; +import com.google.gwt.dev.js.ast.JsStatement; +import com.google.gwt.dev.js.ast.JsVisitor; + +import java.util.HashMap; +import java.util.Map; + +/** + * Removes JsFunctions that are never referenced in the program. + */ +public class JsUnusedFunctionRemover { + + /** + * Finds all functions in the program. + */ + private class JsFunctionVisitor extends JsVisitor { + + @Override + public void endVisit(JsFunction x, JsContext<JsExpression> ctx) { + // Anonymous function, ignore it + if (x.getName() != null) { + toRemove.put(x.getName(), x); + } + } + } + + /** + * Finds all function references in the program. + */ + private class JsNameRefVisitor extends JsVisitor { + + @Override + public void endVisit(JsNameRef x, JsContext<JsExpression> ctx) { + toRemove.remove(x.getName()); + } + } + + private class RemovalVisitor extends JsModVisitor { + + @Override + public void endVisit(JsExprStmt x, JsContext<JsStatement> ctx) { + if (!(x.getExpression() instanceof JsFunction)) { + return; + } + + JsFunction f = (JsFunction) x.getExpression(); + + if (!f.isFromJava()) { + // This function didn't come from Java source, so we'll assume it's + // magic and ignore it. + return; + } + + JsName name = f.getName(); + + if (toRemove.containsKey(name)) { + // Removing a static initializer indicates a problem in + // JsDelegationRemover. + if (name.getIdent().equals("$clinit")) { + throw new InternalCompilerException("Tried to remove clinit " + + name.getStaticRef().toSource()); + } + + // Remove the statement + ctx.removeMe(); + } + } + } + + public static boolean exec(JsProgram program) { + return (new JsUnusedFunctionRemover(program)).execImpl(); + } + + private final Map<JsName, JsFunction> toRemove = + new HashMap<JsName, JsFunction>(); + private final JsProgram program; + + public JsUnusedFunctionRemover(JsProgram program) { + this.program = program; + } + + public boolean execImpl() { + // Find all functions + (new JsFunctionVisitor()).accept(program); + + // Remove the functions that are referenced from the hit list + (new JsNameRefVisitor()).accept(program); + + // Remove the unused functions from the JsProgram + RemovalVisitor removalVisitor = new RemovalVisitor(); + removalVisitor.accept(program); + + return removalVisitor.didChange(); + } +}
diff --git a/dev/core/src/com/google/gwt/dev/js/ast/JsCatchScope.java b/dev/core/src/com/google/gwt/dev/js/ast/JsCatchScope.java index 51f4294..1791fc5 100644 --- a/dev/core/src/com/google/gwt/dev/js/ast/JsCatchScope.java +++ b/dev/core/src/com/google/gwt/dev/js/ast/JsCatchScope.java
@@ -28,7 +28,7 @@ public JsCatchScope(JsScope parent, String ident) { super(parent, "Catch scope"); - this.name = new JsName(ident, ident); + this.name = new JsName(this, ident, ident); } @Override
diff --git a/dev/core/src/com/google/gwt/dev/js/ast/JsFunction.java b/dev/core/src/com/google/gwt/dev/js/ast/JsFunction.java index c2ce85c..7ae5889 100644 --- a/dev/core/src/com/google/gwt/dev/js/ast/JsFunction.java +++ b/dev/core/src/com/google/gwt/dev/js/ast/JsFunction.java
@@ -27,20 +27,29 @@ protected final List<JsParameter> params = new ArrayList<JsParameter>(); protected final JsScope scope; private JsName name; + private boolean fromJava; /** * Creates an anonymous function. */ public JsFunction(JsScope parent) { - this(parent, null); + this(parent, null, false); } /** - * Creates an named function. + * Creates a function that is not derived from Java source. */ public JsFunction(JsScope parent, JsName name) { + this(parent, name, false); + } + + /** + * Creates a named function, possibly derived from Java source. + */ + public JsFunction(JsScope parent, JsName name, boolean fromJava) { assert (parent != null); - this.name = name; + this.fromJava = fromJava; + setName(name); String scopeName = (name == null) ? "<anonymous>" : name.getIdent(); scopeName = "function " + scopeName; this.scope = new JsScope(parent, scopeName); @@ -62,12 +71,25 @@ return scope; } + public boolean isFromJava() { + return fromJava; + } + public void setBody(JsBlock body) { this.body = body; } + public void setFromJava(boolean fromJava) { + this.fromJava = fromJava; + } + public void setName(JsName name) { this.name = name; + if (name != null) { + if (isFromJava()) { + name.setStaticRef(this); + } + } } public void traverse(JsVisitor v, JsContext<JsExpression> ctx) {
diff --git a/dev/core/src/com/google/gwt/dev/js/ast/JsName.java b/dev/core/src/com/google/gwt/dev/js/ast/JsName.java index 91c0871..30e9497 100644 --- a/dev/core/src/com/google/gwt/dev/js/ast/JsName.java +++ b/dev/core/src/com/google/gwt/dev/js/ast/JsName.java
@@ -20,19 +20,30 @@ */ public class JsName { + private final JsScope enclosing; private final String ident; private boolean isObfuscatable; private String shortIdent; /** + * A back-reference to the JsNode that the JsName refers to. + */ + private JsNode staticRef; + + /** * @param ident the unmangled ident to use for this name */ - JsName(String ident, String shortIdent) { + JsName(JsScope enclosing, String ident, String shortIdent) { + this.enclosing = enclosing; this.ident = ident; this.shortIdent = shortIdent; this.isObfuscatable = true; } + public JsScope getEnclosing() { + return enclosing; + } + public String getIdent() { return ident; } @@ -41,6 +52,10 @@ return shortIdent; } + public JsNode getStaticRef() { + return staticRef; + } + public boolean isObfuscatable() { return isObfuscatable; } @@ -57,6 +72,13 @@ this.shortIdent = shortIdent; } + /** + * Should never be called except on immutable stuff. + */ + public void setStaticRef(JsNode node) { + this.staticRef = node; + } + @Override public String toString() { return ident;
diff --git a/dev/core/src/com/google/gwt/dev/js/ast/JsNameRef.java b/dev/core/src/com/google/gwt/dev/js/ast/JsNameRef.java index 7a4d701..7c4f0e1 100644 --- a/dev/core/src/com/google/gwt/dev/js/ast/JsNameRef.java +++ b/dev/core/src/com/google/gwt/dev/js/ast/JsNameRef.java
@@ -18,7 +18,7 @@ /** * Represents a JavaScript expression that references a name. */ -public final class JsNameRef extends JsExpression /*implements HasName*/ { +public final class JsNameRef extends JsExpression implements HasName { private String ident; private JsName name; @@ -36,6 +36,10 @@ return (name == null) ? ident : name.getIdent(); } + public JsName getName() { + return name; + } + public JsExpression getQualifier() { return qualifier; }
diff --git a/dev/core/src/com/google/gwt/dev/js/ast/JsParameter.java b/dev/core/src/com/google/gwt/dev/js/ast/JsParameter.java index 8e293b6..af2e667 100644 --- a/dev/core/src/com/google/gwt/dev/js/ast/JsParameter.java +++ b/dev/core/src/com/google/gwt/dev/js/ast/JsParameter.java
@@ -24,6 +24,7 @@ public JsParameter(JsName name) { this.name = name; + name.setStaticRef(this); } public JsName getName() {
diff --git a/dev/core/src/com/google/gwt/dev/js/ast/JsScope.java b/dev/core/src/com/google/gwt/dev/js/ast/JsScope.java index 68d73fc..4d2a66a 100644 --- a/dev/core/src/com/google/gwt/dev/js/ast/JsScope.java +++ b/dev/core/src/com/google/gwt/dev/js/ast/JsScope.java
@@ -198,7 +198,7 @@ * Creates a new name in this scope. */ protected JsName doCreateName(String ident, String shortIdent) { - JsName name = new JsName(ident, shortIdent); + JsName name = new JsName(this, ident, shortIdent); names.put(ident, name); return name; }