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