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