blob: a6080e81d1281673853103af13f062538a57f54b [file] [log] [blame]
/*
* Copyright 2008 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.gwt.dev.jjs.impl;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.dev.jjs.SourceInfo;
import com.google.gwt.dev.jjs.ast.Context;
import com.google.gwt.dev.jjs.ast.JBinaryOperation;
import com.google.gwt.dev.jjs.ast.JBinaryOperator;
import com.google.gwt.dev.jjs.ast.JDeclaredType;
import com.google.gwt.dev.jjs.ast.JExpression;
import com.google.gwt.dev.jjs.ast.JExpressionStatement;
import com.google.gwt.dev.jjs.ast.JField;
import com.google.gwt.dev.jjs.ast.JFieldRef;
import com.google.gwt.dev.jjs.ast.JLocal;
import com.google.gwt.dev.jjs.ast.JLocalRef;
import com.google.gwt.dev.jjs.ast.JMethod;
import com.google.gwt.dev.jjs.ast.JMethodBody;
import com.google.gwt.dev.jjs.ast.JMethodCall;
import com.google.gwt.dev.jjs.ast.JModVisitor;
import com.google.gwt.dev.jjs.ast.JNode;
import com.google.gwt.dev.jjs.ast.JProgram;
import com.google.gwt.dev.jjs.ast.JReferenceType;
import com.google.gwt.dev.jjs.ast.JReturnStatement;
import com.google.gwt.dev.jjs.ast.JStatement;
import com.google.gwt.dev.jjs.ast.JType;
import com.google.gwt.dev.jjs.ast.JVisitor;
import com.google.gwt.dev.jjs.ast.js.JMultiExpression;
import com.google.gwt.dev.util.UnitTestTreeLogger;
import com.google.gwt.thirdparty.guava.common.base.Function;
import com.google.gwt.thirdparty.guava.common.base.Joiner;
import com.google.gwt.thirdparty.guava.common.base.Preconditions;
import com.google.gwt.thirdparty.guava.common.collect.FluentIterable;
import com.google.gwt.thirdparty.guava.common.collect.ImmutableSet;
import com.google.gwt.thirdparty.guava.common.collect.Lists;
import com.google.gwt.thirdparty.guava.common.collect.Sets;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;
import javax.annotation.Nullable;
/**
* Test case for testing Jjs optimizers. Adds a convenient Result class.
*/
public abstract class OptimizerTestBase extends JJSTestBase {
protected boolean runDeadCodeElimination = false;
/**
* Holds the result of optimization to compare against expected results.
*/
protected final class Result {
private final String returnType;
private final String originalCode;
private final boolean madeChanges;
private final JProgram optimizedProgram;
private final String methodName;
public Result(JProgram optimizedProgram, String returnType,
String methodName, String originalCode, boolean madeChanges) {
this.optimizedProgram = optimizedProgram;
this.returnType = returnType;
this.methodName = methodName;
this.originalCode = originalCode;
this.madeChanges = madeChanges;
}
public void classHasMethods(String className, List<String> expectedMethodSnippets) {
final JDeclaredType targetClass = findClass(className);
Set<String> actualMethodSignatures = Sets.newHashSet();
for (JMethod method : targetClass.getMethods()) {
actualMethodSignatures.add(method.toString());
}
ImmutableSet<String> expectedMethodSignatures = FluentIterable.from(expectedMethodSnippets)
.transform(new Function<String, String>() {
@Nullable
@Override
public String apply(String unqualifiedMethodSignature) {
return targetClass.getName() + "." + unqualifiedMethodSignature;
}
}).toSet();
assertContainsAll(
expectedMethodSignatures, actualMethodSignatures);
}
/**
* Check whether the resulting is equivalent to {@code expected}.<p>
*
* Caveat: {@code expected} needs to be syntactically and type correct (as it will be compiled).
* Normalizer passes for the most part are not expected to produce type correct transformations
* as at the end the will be translated into an untyped language.
* In some cases the test might be able to use this function even if it is testing a pass
* that does not produce type correct program by replacing some standard mocked resources
* (tweaking method parameter and return types).
*/
public void into(String... expected) throws UnableToCompleteException {
// We can't compile expected code into non-main method.
Preconditions.checkState(methodName.equals(MAIN_METHOD_NAME));
JProgram program = compileSnippet(returnType, Joiner.on("\n").join(expected), true);
String expectedSource =
OptimizerTestBase.findMethod(program, methodName).getBody().toSource();
String actualSource =
OptimizerTestBase.findMethod(optimizedProgram, methodName)
.getBody().toSource();
assertEquals(originalCode, expectedSource, actualSource);
}
public void intoString(String... expected) {
String expectedSource = Joiner.on("\n").join(expected);
String actualSource =
OptimizerTestBase.findMethod(optimizedProgram, methodName)
.getBody().toSource();
// Trim surrounding {} and unindent body once
assertTrue(actualSource.startsWith("{"));
assertTrue(actualSource.endsWith("}"));
actualSource = actualSource.substring(1, actualSource.length() - 2).trim();
actualSource = Pattern.compile("^ ", Pattern.MULTILINE)
.matcher(actualSource).replaceAll("");
assertEquals(originalCode, expectedSource, actualSource);
}
public void noChange() {
assertFalse(madeChanges);
}
public JMethod findMethod(String methodName) {
return OptimizerTestBase.findMethod(optimizedProgram, methodName);
}
public JField findField(String fieldName) {
return OptimizerTestBase.findField(optimizedProgram,
"EntryPoint." + fieldName);
}
public JDeclaredType findClass(String className) {
return OptimizerTestBase.findDeclaredType(optimizedProgram, className);
}
public JProgram getOptimizedProgram() {
return optimizedProgram;
}
}
/**
* Asserts that there {@code method} calls all and only {@code expectedTargets}.
*/
protected static void assertCallsAndOnlyCalls(JMethod method, JMethod... expectedTargets) {
final Set<JMethod> actualTargets = Sets.newHashSet();
new JVisitor() {
@Override
public void endVisit(JMethodCall x, Context ctx) {
actualTargets.add(x.getTarget());
}
}.accept(method);
assertEquals(ImmutableSet.copyOf(expectedTargets), actualTargets);
}
/**
* Asserts that the compile fails with {@code expectedErrors}.
*/
public final void assertCompileFails(String code, String... expectedErrors) {
assert expectedErrors != null : "Failed compiles must specify error messages.";
UnitTestTreeLogger.Builder builder = new UnitTestTreeLogger.Builder();
builder.setLowestLogLevel(TreeLogger.ERROR);
for (String expectedError : expectedErrors) {
builder.expectError(expectedError, null);
}
UnitTestTreeLogger errorLogger = builder.createLogger();
try {
optimize(errorLogger, "void", code);
fail("Compile should have failed but succeeded.");
} catch (Exception e) {
if (!(e.getCause() instanceof UnableToCompleteException)
&& !(e instanceof UnableToCompleteException)) {
e.printStackTrace();
fail();
}
}
errorLogger.assertCorrectLogEntries();
}
/**
* Asserts that the compile succeeds with {@code expectedWarnings}.
*/
public final Result assertCompileSucceeds(String code,
String... expectedWarnings) throws UnableToCompleteException {
UnitTestTreeLogger.Builder builder = new UnitTestTreeLogger.Builder();
builder.setLowestLogLevel(TreeLogger.WARN);
if (expectedWarnings != null) {
for (String expectedWarning : expectedWarnings) {
builder.expectWarn(expectedWarning, null);
}
}
UnitTestTreeLogger errorLogger = builder.createLogger();
try {
return optimize(errorLogger, "void", code);
} catch (UnableToCompleteException e) {
fail("Compile failed");
} finally {
errorLogger.assertCorrectLogEntries();
}
return null;
}
/**
* Asserts that there {@code method} only calls {@code forwardsToMethod}.
*/
protected static void assertForwardsTo(JMethod method, JMethod forwardsToMethod) {
assertCallsAndOnlyCalls(method, forwardsToMethod);
}
protected static void assertOverrides(
Result result, String fullMethodSignature, String... overriddenMethodSignatures) {
assertEquals(ImmutableSet.copyOf(overriddenMethodSignatures),
findOverrides(result, fullMethodSignature));
}
protected static void assertParameterTypes(
final Result result, String methodName, String... parameterTypeNames) {
JMethod method = findMethod(result, methodName);
assertNotNull("Did not find method " + methodName, method);
assertEquals(parameterTypeNames.length, method.getParams().size());
JType[] parameterTypes = FluentIterable.from(Arrays.asList(parameterTypeNames))
.transform(new Function<String, JType>() {
@Nullable
@Override
public JType apply(String typeName) {
return findType(result, typeName);
}
})
.toArray(JType.class);
assertParameterTypes(method, parameterTypes);
}
protected static <T extends JNode> Collection<T> getNodes(
final Class<T> expressionClass, JNode inNode, final boolean exactClass) {
final List<T> result = Lists.newArrayList();
new JVisitor() {
@Override
public boolean visit(JNode x, Context ctx) {
if (x.getClass() == expressionClass || expressionClass.isInstance(x) && !exactClass) {
result.add(expressionClass.cast(x));
}
return true;
}
}.accept(inNode);
return result;
}
protected static void assertReturnType(
Result result, String methodName, String resultTypeName) {
JMethod method = findMethod(result, methodName);
assertNotNull("Did not find method " + methodName, method);
JDeclaredType resultType = result.findClass(resultTypeName);
assertNotNull("Did not find class " + resultTypeName, resultType);
assertEquals(resultType, method.getType().getUnderlyingType());
}
protected static JMethod findMethod(Result result, String methodName) {
int lastDot = methodName.lastIndexOf(".");
JMethod method = null;
if (lastDot != -1) {
String className = methodName.substring(0, lastDot);
JDeclaredType clazz = result.findClass(className);
assertNotNull("Did not find class " + className, clazz);
method = clazz.findMethod(methodName.substring(lastDot + 1), true);
} else {
method = result.findMethod(methodName);
}
return method;
}
protected static ImmutableSet<String> findOverrides(Result result, String fullMethodSignature) {
final Function<JMethod, String> METHOD_TO_STRING =
new Function<JMethod, String>() {
@Nullable
@Override
public String apply(JMethod method) {
return method.toString();
}
};
JMethod method = findMethod(result, fullMethodSignature);
assertNotNull("Method " + fullMethodSignature + " not found", method);
assertEquals(fullMethodSignature, method.toString());
return FluentIterable
.from(method.getOverriddenMethods())
.transform(METHOD_TO_STRING)
.toSet();
}
private static JReferenceType findType(Result result, String parameterTypeName) {
JReferenceType parameterType;
if (parameterTypeName.equals("null")) {
parameterType = JReferenceType.NULL_TYPE;
} else {
parameterType = result.findClass(parameterTypeName);
}
return parameterType;
}
/**
* Makes implicit <code>$clinit()</code> calls explicit to mimic the effect of other
* optimizations. Otherwise can not test optimizations that involve <code>$clinit</code> calls
* as they don't appear when compiling small snippets.
*
* @param method method to transform to make <code>$clinit</code> calls explicit.
*/
private void insertImplicitClinitCalls(final JMethod method) {
// Mimic the method inliner which inserts clinits calls prior to method or field dereference.
// The actual clinit() calls might be inserted as a result of optimizations: e,g,
// DeadCodeElimination inserts clinit calls when it removes (some) field accesses or method
// calls.
final JMethodBody body = (JMethodBody) method.getBody();
new JModVisitor() {
private JMethodCall createClinitCall(SourceInfo sourceInfo, JDeclaredType targetType) {
JMethod clinit = targetType.getClinitTarget().getClinitMethod();
assert (JProgram.isClinit(clinit));
return new JMethodCall(sourceInfo, null, clinit);
}
private JMultiExpression createMultiExpressionForInstanceAndClinit(JExpression x) {
JMultiExpression multi = new JMultiExpression(x.getSourceInfo());
JMethodCall clinit = null;
if (x instanceof JMethodCall) {
JExpression instance = ((JMethodCall) x).getInstance();
// Any instance expression goes first (this can happen even with statics).
if (instance != null) {
multi.addExpressions(instance);
JLocal var = JProgram.createLocal(instance.getSourceInfo(), "$t", instance.getType(),
false, body);
JLocalRef localRef = var.makeRef(var.getSourceInfo());
instance = new JBinaryOperation(instance.getSourceInfo(), localRef.getType(),
JBinaryOperator.ASG, localRef, instance);
}
clinit = createClinitCall(x.getSourceInfo(),
((JMethodCall) x).getTarget().getEnclosingType());
} else if (x instanceof JFieldRef) {
clinit = createClinitCall(x.getSourceInfo(), ((JFieldRef) x).getEnclosingType());
}
// If we need a clinit call, add it first
if (clinit != null) {
multi.addExpressions(clinit);
}
multi.addExpressions(x);
return multi;
}
@Override
public void endVisit(JMethodCall x, Context ctx) {
ctx.replaceMe(createMultiExpressionForInstanceAndClinit(x));
}
@Override
public void endVisit(JFieldRef x, Context ctx) {
ctx.replaceMe(createMultiExpressionForInstanceAndClinit(x));
}
}.accept(method);
}
protected final Result optimize(TreeLogger logger, final String returnType,
final String... codeSnippet) throws UnableToCompleteException {
return optimizeMethod(logger, MAIN_METHOD_NAME, returnType, codeSnippet);
}
protected final Result optimize(final String returnType,
final String... codeSnippet) {
UnitTestTreeLogger.Builder builder = new UnitTestTreeLogger.Builder();
builder.setLowestLogLevel(TreeLogger.WARN);
try {
return optimize(null, returnType, codeSnippet);
} catch (UnableToCompleteException e) {
fail();
return null;
}
}
/**
* Test the effect of an optimization on a JMultiExpression.
* JMultiExpression can not be constructed from source code at the moment as it is not a valid
* java source construct.
*
* @param addClinitCalls whether to insert the implicit clinit calls. This is necessary because
* clinit() methods are synthetic can not be inserted explicitly as source
* code calls.
* @param returnType the return type of the JMultiExpression. Must be <code>void</code> or
* compatible with the last expression.
* @param expressionSnippets source code of the expressions.
* @return the optimization result.
* @throws UnableToCompleteException
*/
protected final Result optimizeExpressions(boolean addClinitCalls, final String returnType,
final String... expressionSnippets)
throws UnableToCompleteException {
// TODO(rluble): Not very elegant to require that the snippets be statements instead of
// expressions.
assert expressionSnippets.length > 0;
// Compile as statements
if (!returnType.equals("void")) {
expressionSnippets[expressionSnippets.length - 1] =
"return " + expressionSnippets[expressionSnippets.length - 1];
}
String snippet = Joiner.on(";\n").join(expressionSnippets) + ";\n";
final JProgram program = compileSnippet(returnType, snippet, true);
JMethod method = findMethod(program, MAIN_METHOD_NAME);
JMethodBody body = (JMethodBody) method.getBody();
JMultiExpression multi = new JMultiExpression(body.getSourceInfo());
// Transform statement sequence into a JMultiExpression
for (JStatement stmt : body.getStatements()) {
if (stmt instanceof JExpressionStatement) {
JExpressionStatement exprStmt = (JExpressionStatement) stmt;
JExpression expr = exprStmt.getExpr();
multi.addExpressions(expr);
} else if (stmt instanceof JReturnStatement) {
JReturnStatement returnStatement = (JReturnStatement) stmt;
JExpression expr = returnStatement.getExpr();
if (expr != null) {
multi.addExpressions(expr);
}
} else {
assert false : "Not a valid multiexpression";
}
}
// Take care of the return type
JStatement multiStm =
returnType.equals("void") ? multi.makeStatement() : multi.makeReturnStatement();
// Replace the method body
JMethodBody newBody = new JMethodBody(method.getBody().getSourceInfo());
newBody.getBlock().addStmt(multiStm);
method.setBody(newBody);
newBody.setMethod(method);
if (addClinitCalls) {
insertImplicitClinitCalls(method);
}
// Finally optimize.
boolean madeChanges = doOptimizeMethod(TreeLogger.NULL, program, method);
if (madeChanges && runDeadCodeElimination) {
DeadCodeElimination.exec(program);
}
return new Result(program, returnType, MAIN_METHOD_NAME, snippet, madeChanges);
}
protected final Result optimizeMethod(final String methodName,
final String mainMethodReturnType, final String... mainMethodSnippet)
throws UnableToCompleteException {
return optimizeMethod(null, methodName, mainMethodReturnType, mainMethodSnippet);
}
protected final Result optimizeMethod(TreeLogger logger, final String methodName,
final String mainMethodReturnType, final String... mainMethodSnippet)
throws UnableToCompleteException {
if (logger == null) {
logger = this.logger;
}
String snippet = Joiner.on("\n").join(mainMethodSnippet);
JProgram program = compileSnippet(logger, mainMethodReturnType, snippet, true);
JMethod method = findMethod(program, methodName);
boolean madeChanges = doOptimizeMethod(logger, program, method);
if (madeChanges && runDeadCodeElimination) {
DeadCodeElimination.exec(program);
}
return new Result(program, mainMethodReturnType, methodName, snippet, madeChanges);
}
protected abstract boolean doOptimizeMethod(TreeLogger logger, JProgram program, JMethod method)
throws UnableToCompleteException;
}