/* * Copyright 2014 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.jjs.impl.OptimizerStats; 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.JsCase; import com.google.gwt.dev.js.ast.JsConditional; import com.google.gwt.dev.js.ast.JsContext; import com.google.gwt.dev.js.ast.JsDefault; import com.google.gwt.dev.js.ast.JsEmpty; 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.JsInvocation; import com.google.gwt.dev.js.ast.JsModVisitor; import com.google.gwt.dev.js.ast.JsNode; import com.google.gwt.dev.js.ast.JsNullLiteral; import com.google.gwt.dev.js.ast.JsProgram; import com.google.gwt.dev.js.ast.JsWhile; import com.google.gwt.dev.util.log.speedtracer.CompilerEventType; import com.google.gwt.dev.util.log.speedtracer.SpeedTracerLogger; import com.google.gwt.dev.util.log.speedtracer.SpeedTracerLogger.Event; import java.util.HashSet; import java.util.List; import java.util.Set; /** * This is used to clean up duplication invocations of clinit function. Whenever there is a * possible branch in program flow, the remover will create a new instance of * itself to handle the possible branches. * * We don't look at combining branch choices. This will not produce the most * efficient elimination of duplicated calls, but it handles the general case * and is simple to verify. */ public class DuplicateClinitRemover extends JsModVisitor { private static final String NAME = JsInliner.class.getSimpleName(); /* * TODO: Most of the special casing below can be removed if complex * statements always use blocks, rather than plain statements. */ /** * Retains the functions that we know have been called. */ private final Set<JsFunction> called; private final JsProgram program; public DuplicateClinitRemover(JsProgram program) { this.program = program; called = new HashSet<JsFunction>(); } public DuplicateClinitRemover(JsProgram program, Set<JsFunction> alreadyCalled) { this.program = program; called = new HashSet<JsFunction>(alreadyCalled); } /** * Look for comma expressions that contain duplicate calls and handle the * conditional-evaluation case of logical and/or operations. */ @Override public boolean visit(JsBinaryOperation x, JsContext ctx) { if (x.getOperator() == JsBinaryOperator.COMMA) { boolean left = isDuplicateCall(x.getArg1()); boolean right = isDuplicateCall(x.getArg2()); if (left && right) { /* * (clinit(), clinit()) --> delete or null. * * This construct is very unlikely since the InliningVisitor builds * the comma expressions in a right-nested manner. */ if (ctx.canRemove()) { ctx.removeMe(); return false; } else { // The return value from an XO function is never used ctx.replaceMe(JsNullLiteral.INSTANCE); return false; } } else if (left) { // (clinit(), xyz) --> xyz // This is the common case ctx.replaceMe(accept(x.getArg2())); return false; } else if (right) { // (xyz, clinit()) --> xyz // Possible if a clinit() were the last element ctx.replaceMe(accept(x.getArg1())); return false; } } else if (x.getOperator().equals(JsBinaryOperator.AND) || x.getOperator().equals(JsBinaryOperator.OR)) { x.setArg1(accept(x.getArg1())); // Possibility of conditional evaluation of second parameter x.setArg2(branch(x.getArg2())); 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 ctx) { branch(x.getStatements()); return false; } @Override public boolean visit(JsCase x, JsContext ctx) { x.setCaseExpr(accept(x.getCaseExpr())); branch(x.getStmts()); return false; } @Override public boolean visit(JsConditional x, JsContext ctx) { x.setTestExpression(accept(x.getTestExpression())); x.setThenExpression(branch(x.getThenExpression())); x.setElseExpression(branch(x.getElseExpression())); return false; } @Override public boolean visit(JsDefault x, JsContext ctx) { branch(x.getStmts()); return false; } @Override public boolean visit(JsExprStmt x, JsContext ctx) { if (isDuplicateCall(x.getExpression())) { if (ctx.canRemove()) { ctx.removeMe(); } else { ctx.replaceMe(new JsEmpty(x.getSourceInfo())); } return false; } else { return true; } } @Override public boolean visit(JsFor x, JsContext ctx) { // The JsFor may have an expression xor a variable declaration. if (x.getInitExpr() != null) { x.setInitExpr(accept(x.getInitExpr())); } else if (x.getInitVars() != null) { x.setInitVars(accept(x.getInitVars())); } // The condition is optional if (x.getCondition() != null) { x.setCondition(accept(x.getCondition())); } // The increment expression is optional if (x.getIncrExpr() != null) { x.setIncrExpr(branch(x.getIncrExpr())); } // The body is not guaranteed to be a JsBlock x.setBody(branch(x.getBody())); return false; } @Override public boolean visit(JsForIn x, JsContext ctx) { if (x.getIterExpr() != null) { x.setIterExpr(accept(x.getIterExpr())); } x.setObjExpr(accept(x.getObjExpr())); // The body is not guaranteed to be a JsBlock x.setBody(branch(x.getBody())); return false; } @Override public boolean visit(JsIf x, JsContext ctx) { x.setIfExpr(accept(x.getIfExpr())); x.setThenStmt(branch(x.getThenStmt())); if (x.getElseStmt() != null) { x.setElseStmt(branch(x.getElseStmt())); } return false; } /** * Possibly record that we've seen a call in the current context. */ @Override public boolean visit(JsInvocation x, JsContext ctx) { JsFunction func = JsUtils.isExecuteOnce(x); while (func != null) { called.add(func); func = func.getSuperClinit(); } return true; } @Override public boolean visit(JsWhile x, JsContext ctx) { x.setCondition(accept(x.getCondition())); // The body is not guaranteed to be a JsBlock x.setBody(branch(x.getBody())); return false; } /** * Static entry point used by JavaToJavaScriptCompiler. */ public static OptimizerStats exec(JsProgram program) { Event optimizeJsEvent = SpeedTracerLogger.start( CompilerEventType.OPTIMIZE_JS, "duplicateXOremover", NAME); OptimizerStats stats = execImpl(program); optimizeJsEvent.end("didChange", "" + stats.didChange()); return stats; } private static OptimizerStats execImpl(JsProgram program) { OptimizerStats stats = new OptimizerStats(NAME); DuplicateClinitRemover r = new DuplicateClinitRemover(program); r.accept(program); if (r.didChange()) { stats.recordModified(); } return stats; } private <T extends JsNode> void branch(List<T> x) { DuplicateClinitRemover dup = new DuplicateClinitRemover(program, called); dup.acceptWithInsertRemove(x); didChange |= dup.didChange(); } private <T extends JsNode> T branch(T x) { DuplicateClinitRemover dup = new DuplicateClinitRemover(program, called); T toReturn = dup.accept(x); if ((toReturn != x) && !dup.didChange()) { throw new InternalCompilerException( "node replacement should imply didChange()"); } didChange |= dup.didChange(); return toReturn; } private boolean isDuplicateCall(JsExpression x) { if (!(x instanceof JsInvocation)) { return false; } JsFunction func = JsUtils.isExecuteOnce((JsInvocation) x); return (func != null && called.contains(func)); } }