Fixes a bug with JsStaticEval that was preventing unreachable functions from being declared. Gotta love JavaScript, eh?
Found by: cromwellian
Review by: spoon
git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@2392 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/dev/core/src/com/google/gwt/dev/js/JsInliner.java b/dev/core/src/com/google/gwt/dev/js/JsInliner.java
index 3d40811..d6ae5ac 100644
--- a/dev/core/src/com/google/gwt/dev/js/JsInliner.java
+++ b/dev/core/src/com/google/gwt/dev/js/JsInliner.java
@@ -839,6 +839,22 @@
boolean sawReturnStatement = false;
for (JsStatement statement : statements) {
+ if (sawReturnStatement) {
+ /*
+ * We've already seen a return statement, but there are still more
+ * statements. The target is unsafe to inline, so bail. Note: in most
+ * cases JsStaticEval will have removed any statements following a
+ * return statement.
+ *
+ * The reason we have to bail is that the return statement's
+ * expression MUST be the last thing evaluated.
+ *
+ * TODO(bobv): maybe it could still be inlined with smart
+ * transformation?
+ */
+ return;
+ }
+
/*
* Create replacement expressions to use in place of the original
* statements. It is important that the replacement is newly-minted and
@@ -857,7 +873,6 @@
if (isReturnStatement(statement)) {
sawReturnStatement = true;
- break;
}
}
@@ -1461,6 +1476,9 @@
// Extract the return value
JsReturn ret = (JsReturn) statement;
expression = ret.getExpr();
+ if (expression == null) {
+ expression = program.getUndefinedLiteral();
+ }
} else if (statement instanceof JsVars) {
// Create a comma expression for variable initializers
diff --git a/dev/core/src/com/google/gwt/dev/js/JsStaticEval.java b/dev/core/src/com/google/gwt/dev/js/JsStaticEval.java
index 48fa96e..daaa3b3 100644
--- a/dev/core/src/com/google/gwt/dev/js/JsStaticEval.java
+++ b/dev/core/src/com/google/gwt/dev/js/JsStaticEval.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2007 Google Inc.
+ * 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
@@ -27,14 +27,22 @@
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.JsFunction;
import com.google.gwt.dev.js.ast.JsIf;
import com.google.gwt.dev.js.ast.JsModVisitor;
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.JsVisitor;
import com.google.gwt.dev.js.ast.JsWhile;
+import com.google.gwt.dev.js.ast.JsVars.JsVar;
+import java.util.ArrayList;
+import java.util.HashSet;
import java.util.List;
+import java.util.ListIterator;
+import java.util.Set;
+import java.util.Stack;
/**
* Removes JsFunctions that are never referenced in the program.
@@ -68,6 +76,140 @@
}
/**
+ * Force all functions to be evaluated at the top of the lexical scope in
+ * which they reside. This makes {@link StaticEvalVisitor} simpler in that we
+ * no longer have to worry about function declarations within expressions.
+ * After this runs, only statements can contain declarations. Moved functions
+ * will end up just before the statement in which they presently reside.
+ */
+ private class EvalFunctionsAtTopScope extends JsModVisitor {
+ private final Set<JsFunction> dontMove = new HashSet<JsFunction>();
+ private final Stack<JsBlock> scopeStack = new Stack<JsBlock>();
+ private final Stack<ListIterator<JsStatement>> itrStack = new Stack<ListIterator<JsStatement>>();
+
+ @Override
+ public void endVisit(JsFunction x, JsContext<JsExpression> ctx) {
+ scopeStack.pop();
+ }
+
+ @Override
+ public void endVisit(JsProgram x, JsContext<JsProgram> ctx) {
+ scopeStack.pop();
+ }
+
+ @Override
+ public boolean visit(JsBlock x, JsContext<JsStatement> ctx) {
+ if (x == scopeStack.peek()) {
+ ListIterator<JsStatement> itr = x.getStatements().listIterator();
+ itrStack.push(itr);
+ while (itr.hasNext()) {
+ JsStatement stmt = itr.next();
+ JsFunction func = isFunctionDecl(stmt);
+ // Already at the top level.
+ if (func != null) {
+ dontMove.add(func);
+ }
+ accept(stmt);
+ if (func != null) {
+ dontMove.remove(func);
+ }
+ }
+ itrStack.pop();
+ // Already visited.
+ return false;
+ } else {
+ // Just do normal visitation.
+ return true;
+ }
+ }
+
+ @Override
+ public boolean visit(JsFunction x, JsContext<JsExpression> ctx) {
+ /*
+ * We do this during visit() to preserve first-to-last evaluation order.
+ */
+ if (x.getName() != null && !dontMove.contains(x)) {
+ /*
+ * Reinsert this function into the statement immediately before the
+ * current statement. The current statement will have already been
+ * returned from the current iterator's next(), so we have to
+ * backshuffle one step to get in front of it.
+ */
+ ListIterator<JsStatement> itr = itrStack.peek();
+ itr.previous();
+ itr.add(x.makeStmt());
+ itr.next();
+ ctx.replaceMe(x.getName().makeRef());
+ }
+
+ // Dive into the function itself.
+ scopeStack.push(x.getBody());
+ return true;
+ }
+
+ @Override
+ public boolean visit(JsProgram x, JsContext<JsProgram> ctx) {
+ scopeStack.push(x.getGlobalBlock());
+ return true;
+ }
+ }
+
+ /**
+ * Creates a minimalist list of statements that must be run in order to
+ * achieve the same declaration effect as the visited statements.
+ *
+ * For example, a JsFunction declaration should be run as a JsExprStmt. JsVars
+ * should be run without any initializers.
+ *
+ * This visitor is called from
+ * {@link StaticEvalVisitor#ensureDeclarations(JsStatement)} on any statements
+ * that are removed from a function.
+ */
+ private static class MustExecVisitor extends JsVisitor {
+
+ private final List<JsStatement> mustExec = new ArrayList<JsStatement>();
+
+ public MustExecVisitor() {
+ }
+
+ @Override
+ public void endVisit(JsExprStmt x, JsContext<JsStatement> ctx) {
+ JsFunction func = isFunctionDecl(x);
+ if (func != null) {
+ mustExec.add(x);
+ }
+ }
+
+ @Override
+ public void endVisit(JsVars x, JsContext<JsStatement> ctx) {
+ JsVars strippedVars = new JsVars();
+ boolean mustReplace = false;
+ for (JsVar var : x) {
+ JsVar strippedVar = new JsVar(var.getName());
+ strippedVars.add(strippedVar);
+ if (var.getInitExpr() != null) {
+ mustReplace = true;
+ }
+ }
+ if (mustReplace) {
+ mustExec.add(strippedVars);
+ } else {
+ mustExec.add(x);
+ }
+ }
+
+ public List<JsStatement> getStatements() {
+ return mustExec;
+ }
+
+ @Override
+ public boolean visit(JsFunction x, JsContext<JsExpression> ctx) {
+ // Don't dive into nested functions.
+ return false;
+ }
+ }
+
+ /**
* Does static evals.
*
* TODO: borrow more concepts from
@@ -118,8 +260,17 @@
if (stmt.unconditionalControlBreak()) {
// Abrupt change in flow, chop the remaining items from this block
for (int j = i + 1; j < stmts.size();) {
- stmts.remove(j);
- didChange = true;
+ JsStatement toRemove = stmts.get(j);
+ JsStatement toReplace = ensureDeclarations(toRemove);
+ if (toReplace == null) {
+ stmts.remove(j);
+ didChange = true;
+ } else if (toReplace == toRemove) {
+ ++j;
+ } else {
+ stmts.set(j, toReplace);
+ didChange = true;
+ }
}
}
}
@@ -169,7 +320,7 @@
JsBlock block = new JsBlock();
block.getStatements().add(x.getBody());
block.getStatements().add(expr.makeStmt());
- ctx.replaceMe(block);
+ ctx.replaceMe(accept(block));
}
}
}
@@ -205,7 +356,11 @@
block.getStatements().add(x.getInitVars());
}
block.getStatements().add(expr.makeStmt());
- ctx.replaceMe(block);
+ JsStatement decls = ensureDeclarations(x.getBody());
+ if (decls != null) {
+ block.getStatements().add(decls);
+ }
+ ctx.replaceMe(accept(block));
}
}
}
@@ -221,10 +376,13 @@
if (expr instanceof CanBooleanEval) {
CanBooleanEval cond = (CanBooleanEval) expr;
JsStatement onlyStmtToExecute;
+ JsStatement removed;
if (cond.isBooleanTrue()) {
onlyStmtToExecute = thenStmt;
+ removed = elseStmt;
} else if (cond.isBooleanFalse()) {
onlyStmtToExecute = elseStmt;
+ removed = thenStmt;
} else {
return;
}
@@ -236,7 +394,11 @@
block.getStatements().add(onlyStmtToExecute);
}
- ctx.replaceMe(block);
+ JsStatement decls = ensureDeclarations(removed);
+ if (decls != null) {
+ block.getStatements().add(decls);
+ }
+ ctx.replaceMe(accept(block));
} else if (isEmpty(thenStmt) && isEmpty(elseStmt)) {
ctx.replaceMe(expr.makeStmt());
}
@@ -253,10 +415,46 @@
// If false, replace with condition.
if (cond.isBooleanFalse()) {
- ctx.replaceMe(expr.makeStmt());
+ JsBlock block = new JsBlock();
+ block.getStatements().add(expr.makeStmt());
+ JsStatement decls = ensureDeclarations(x.getBody());
+ if (decls != null) {
+ block.getStatements().add(decls);
+ }
+ ctx.replaceMe(accept(block));
}
}
}
+
+ /**
+ * This method MUST be called whenever any statements are removed from a
+ * function. This is because some statements, such as JsVars or JsFunction
+ * have the effect of defining local variables, no matter WHERE they are in
+ * the function. The returned statement (if any), must be executed. It is
+ * also possible for stmt to be directly returned, in which case the caller
+ * should not perform AST changes that would cause an infinite optimization
+ * loop.
+ *
+ * Note: EvalFunctionsAtTopScope will have changed any JsFunction
+ * declarations into statements before this visitor runs.
+ */
+ private JsStatement ensureDeclarations(JsStatement stmt) {
+ if (stmt == null) {
+ return null;
+ }
+ MustExecVisitor mev = new MustExecVisitor();
+ mev.accept(stmt);
+ List<JsStatement> stmts = mev.getStatements();
+ if (stmts.isEmpty()) {
+ return null;
+ } else if (stmts.size() == 1) {
+ return stmts.get(0);
+ } else {
+ JsBlock jsBlock = new JsBlock();
+ jsBlock.getStatements().addAll(stmts);
+ return jsBlock;
+ }
+ }
}
public static boolean exec(JsProgram program) {
@@ -271,6 +469,24 @@
}
/**
+ * If the statement is a JsExprStmt that declares a function with no other
+ * side effects, returns that function; otherwise <code>null</code>.
+ */
+ protected static JsFunction isFunctionDecl(JsStatement stmt) {
+ if (stmt instanceof JsExprStmt) {
+ JsExprStmt exprStmt = (JsExprStmt) stmt;
+ JsExpression expr = exprStmt.getExpression();
+ if (expr instanceof JsFunction) {
+ JsFunction func = (JsFunction) expr;
+ if (func.getName() != null) {
+ return func;
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
* Simplify short circuit AND expressions.
*
* <pre>
@@ -324,10 +540,11 @@
}
public boolean execImpl() {
- // Remove the unused functions from the JsProgram
- StaticEvalVisitor evalVisitor = new StaticEvalVisitor();
- evalVisitor.accept(program);
+ EvalFunctionsAtTopScope fev = new EvalFunctionsAtTopScope();
+ fev.accept(program);
+ StaticEvalVisitor sev = new StaticEvalVisitor();
+ sev.accept(program);
- return evalVisitor.didChange();
+ return fev.didChange() || sev.didChange();
}
}
diff --git a/user/test/com/google/gwt/dev/jjs/CompilerSuite.java b/user/test/com/google/gwt/dev/jjs/CompilerSuite.java
index 37775c7..7df07d3 100644
--- a/user/test/com/google/gwt/dev/jjs/CompilerSuite.java
+++ b/user/test/com/google/gwt/dev/jjs/CompilerSuite.java
@@ -27,6 +27,7 @@
import com.google.gwt.dev.jjs.test.HostedTest;
import com.google.gwt.dev.jjs.test.InnerClassTest;
import com.google.gwt.dev.jjs.test.InnerOuterSuperTest;
+import com.google.gwt.dev.jjs.test.JsStaticEvalTest;
import com.google.gwt.dev.jjs.test.JsniConstructorTest;
import com.google.gwt.dev.jjs.test.JsoTest;
import com.google.gwt.dev.jjs.test.MemberShadowingTest;
@@ -63,6 +64,7 @@
suite.addTestSuite(InnerOuterSuperTest.class);
suite.addTestSuite(JsniConstructorTest.class);
suite.addTestSuite(JsoTest.class);
+ suite.addTestSuite(JsStaticEvalTest.class);
suite.addTestSuite(MemberShadowingTest.class);
suite.addTestSuite(MethodBindTest.class);
suite.addTestSuite(MethodCallTest.class);
diff --git a/user/test/com/google/gwt/dev/jjs/test/JsStaticEvalTest.java b/user/test/com/google/gwt/dev/jjs/test/JsStaticEvalTest.java
new file mode 100644
index 0000000..a4e5ac2
--- /dev/null
+++ b/user/test/com/google/gwt/dev/jjs/test/JsStaticEvalTest.java
@@ -0,0 +1,220 @@
+/*
+ * 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.test;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.junit.client.GWTTestCase;
+
+/**
+ * Tests that declarations in pruned code still happen. NOTE: this test does not
+ * run in hosted mode due to browser inconsistencies; however it should run in
+ * web mode due to our normalizations.
+ */
+public class JsStaticEvalTest extends GWTTestCase {
+
+ @Override
+ public String getModuleName() {
+ return "com.google.gwt.dev.jjs.CompilerSuite";
+ }
+
+ public void testAfterReturn() {
+ if (GWT.isScript()) {
+ doTestAfterReturn();
+ }
+ }
+
+ public void testAfterReturnInBlock() {
+ if (GWT.isScript()) {
+ doTestAfterReturnInBlock();
+ }
+ }
+
+ public void testConditionalFalse() {
+ if (GWT.isScript()) {
+ doTestConditionalFalse();
+ }
+ }
+
+ public void testConditionalTrue() {
+ if (GWT.isScript()) {
+ doTestConditionalTrue();
+ }
+ }
+
+ public void testForFalse() {
+ if (GWT.isScript()) {
+ doTestForFalse();
+ }
+ }
+
+ public void testIfFalse() {
+ if (GWT.isScript()) {
+ doTestIfFalse();
+ }
+ }
+
+ public void testOrder1() {
+ if (GWT.isScript()) {
+ doTestOrder1();
+ }
+ }
+
+ public void testOrder2() {
+ if (GWT.isScript()) {
+ doTestOrder2();
+ }
+ }
+
+ public void testShortCircuitAnd() {
+ if (GWT.isScript()) {
+ doTestShortCircuitAnd();
+ }
+ }
+
+ public void testShortCircuitOr() {
+ if (GWT.isScript()) {
+ doTestShortCircuitOr();
+ }
+ }
+
+ public void testWhileFalse() {
+ if (GWT.isScript()) {
+ doTestWhileFalse();
+ }
+ }
+
+ private native void doTestAfterReturn() /*-{
+ func();
+ @junit.framework.Assert::assertTrue(Z)(result);
+ return;
+ var result;
+ function func() {
+ result = true;
+ }
+ }-*/;
+
+ private native void doTestAfterReturnInBlock() /*-{
+ func();
+ @junit.framework.Assert::assertTrue(Z)(result);
+ return;
+ {
+ var result;
+ function func() {
+ result = true;
+ }
+ }
+ }-*/;
+
+ private native void doTestConditionalFalse() /*-{
+ func();
+ @junit.framework.Assert::assertTrue(Z)(result);
+ return;
+ var result;
+ var toss = false ? function func() {
+ result = true;
+ } : false;
+ }-*/;
+
+ private native void doTestConditionalTrue() /*-{
+ func();
+ @junit.framework.Assert::assertTrue(Z)(result);
+ return;
+ var result;
+ var toss = true ? true : function func() {
+ result = true;
+ };
+ }-*/;
+
+ private native void doTestForFalse() /*-{
+ func();
+ @junit.framework.Assert::assertTrue(Z)(result);
+ return;
+ for (;false;) {
+ var result;
+ function func() {
+ result = true;
+ }
+ }
+ }-*/;
+
+ private native void doTestIfFalse() /*-{
+ func();
+ @junit.framework.Assert::assertTrue(Z)(result);
+ return;
+ if (false) {
+ var result;
+ function func() {
+ result = true;
+ }
+ }
+ }-*/;
+
+ private native void doTestOrder1() /*-{
+ func();
+ @junit.framework.Assert::assertTrue(Z)(result);
+ return;
+ var result;
+ var toss = function func() {
+ };
+ var toss = function func() {
+ result = true;
+ };
+ }-*/;
+
+ private native void doTestOrder2() /*-{
+ var toss = function func() {
+ };
+ func();
+ @junit.framework.Assert::assertTrue(Z)(result);
+ return;
+ var result;
+ var toss = function func() {
+ result = true;
+ };
+ }-*/;
+
+ private native void doTestShortCircuitAnd() /*-{
+ func();
+ @junit.framework.Assert::assertTrue(Z)(result);
+ return;
+ var result;
+ var toss = false && function func() {
+ result = true;
+ };
+ }-*/;
+
+ private native void doTestShortCircuitOr() /*-{
+ func();
+ @junit.framework.Assert::assertTrue(Z)(result);
+ return;
+ var result;
+ var toss = true || function func() {
+ result = true;
+ };
+ }-*/;
+
+ private native void doTestWhileFalse() /*-{
+ func();
+ @junit.framework.Assert::assertTrue(Z)(result);
+ return;
+ while (false) {
+ var result;
+ function func() {
+ result = true;
+ }
+ }
+ }-*/;
+}