| /* |
| * Copyright 2010 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.dev.cfg.PermutationProperties; |
| import com.google.gwt.dev.jjs.SourceInfo; |
| import com.google.gwt.dev.js.ast.JsBinaryOperation; |
| import com.google.gwt.dev.js.ast.JsBinaryOperator; |
| import com.google.gwt.dev.js.ast.JsContext; |
| 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.JsObjectLiteral; |
| import com.google.gwt.dev.js.ast.JsProgram; |
| import com.google.gwt.dev.js.ast.JsVars; |
| import com.google.gwt.dev.js.ast.JsVars.JsVar; |
| import com.google.gwt.dev.js.ast.JsVisitor; |
| |
| import java.util.HashSet; |
| import java.util.Iterator; |
| import java.util.LinkedHashMap; |
| import java.util.LinkedHashSet; |
| import java.util.Map; |
| import java.util.Map.Entry; |
| import java.util.Set; |
| |
| /** |
| * Rewrite JavaScript to better handle references from one code fragment to |
| * another. For any function defined off the initial download and accessed from |
| * a different island than the one it's defined on, predefine a variable in the |
| * initial download to hold its definition. |
| */ |
| public class HandleCrossFragmentReferences { |
| |
| /** |
| * Find out which islands define and use each named function or variable. This |
| * visitor is not smart about which definitions and uses matter. It blindly |
| * records all of them. |
| */ |
| private class FindNameReferences extends JsVisitor { |
| Map<JsName, Set<Integer>> islandsDefining = new LinkedHashMap<JsName, Set<Integer>>(); |
| Map<JsName, Set<Integer>> islandsUsing = new LinkedHashMap<JsName, Set<Integer>>(); |
| private int currentIsland; |
| |
| @Override |
| public void endVisit(JsFunction x, JsContext ctx) { |
| JsName name = x.getName(); |
| if (name != null) { |
| definitionSeen(name); |
| } |
| } |
| |
| @Override |
| public void endVisit(JsNameRef x, JsContext ctx) { |
| if (x.getQualifier() == null) { |
| JsName name = x.getName(); |
| if (name != null) { |
| referenceSeen(name); |
| } |
| } |
| } |
| |
| @Override |
| public void endVisit(JsVars x, JsContext ctx) { |
| for (JsVar var : x) { |
| JsName name = var.getName(); |
| if (name != null) { |
| definitionSeen(name); |
| } |
| } |
| } |
| |
| @Override |
| public boolean visit(JsProgram x, JsContext ctx) { |
| for (int i = 0; i < x.getFragmentCount(); i++) { |
| currentIsland = i; |
| accept(x.getFragmentBlock(i)); |
| } |
| |
| return false; |
| } |
| |
| private void definitionSeen(JsName name) { |
| /* |
| * Support multiple definitions, because local variables can reuse the |
| * same name. |
| */ |
| Set<Integer> defs = islandsDefining.get(name); |
| if (defs == null) { |
| defs = new LinkedHashSet<Integer>(); |
| islandsDefining.put(name, defs); |
| } |
| defs.add(currentIsland); |
| } |
| |
| private void referenceSeen(JsName name) { |
| Set<Integer> refs = islandsUsing.get(name); |
| if (refs == null) { |
| refs = new HashSet<Integer>(); |
| islandsUsing.put(name, refs); |
| } |
| refs.add(currentIsland); |
| } |
| } |
| |
| /** |
| * Rewrite var and function declarations as assignments, if their name is |
| * accessed cross-island. Rewrite refs to such names correspondingly. |
| */ |
| private class RewriteDeclsAndRefs extends JsModVisitor { |
| @Override |
| public void endVisit(JsFunction x, JsContext ctx) { |
| if (namesToPredefine.contains(x.getName())) { |
| JsBinaryOperation asg = |
| new JsBinaryOperation(x.getSourceInfo(), JsBinaryOperator.ASG, makeRefViaJslink(x |
| .getName(), x.getSourceInfo()), x); |
| x.setName(null); |
| ctx.replaceMe(asg); |
| } |
| } |
| |
| @Override |
| public void endVisit(JsNameRef x, JsContext ctx) { |
| if (namesToPredefine.contains(x.getName())) { |
| ctx.replaceMe(makeRefViaJslink(x.getName(), x.getSourceInfo())); |
| } |
| } |
| |
| @Override |
| public void endVisit(JsVars x, JsContext ctx) { |
| if (!ctx.canInsert()) { |
| return; |
| } |
| |
| /* |
| * Loop through each var and see if it was predefined. If so, then remove |
| * the var. If the var has an initializer, then add back an assignment |
| * statement to initialize it. If there is no initializer, then don't add |
| * anything back; the var will still have undefined as its initial value, |
| * just like before. |
| * |
| * A complication is that the variables that are predefined might be |
| * interspersed with variables that are not. That means the general result |
| * of this transformation has alternating var lists and assignment |
| * statements. The currentVar variable holds the most recently inserted |
| * statement, if that statement was a JsVars; otherwise it holds null. |
| */ |
| |
| JsVars currentVar = null; |
| Iterator<JsVar> varsIterator = x.iterator(); |
| while (varsIterator.hasNext()) { |
| JsVar var = varsIterator.next(); |
| if (namesToPredefine.contains(var.getName())) { |
| // The var was predefined |
| if (var.getInitExpr() != null) { |
| // If it has an initializer, add an assignment statement |
| JsBinaryOperation asg = |
| new JsBinaryOperation(var.getSourceInfo(), JsBinaryOperator.ASG, makeRefViaJslink( |
| var.getName(), var.getSourceInfo()), var.getInitExpr()); |
| ctx.insertBefore(asg.makeStmt()); |
| currentVar = null; |
| } |
| } else { |
| // The var was not predefined; add it to a var list |
| if (currentVar == null) { |
| currentVar = new JsVars(x.getSourceInfo()); |
| ctx.insertBefore(currentVar); |
| } |
| currentVar.add(var); |
| } |
| } |
| |
| ctx.removeMe(); |
| } |
| |
| private JsNameRef makeRefViaJslink(JsName name, SourceInfo sourceInfo) { |
| JsNameRef ref = name.makeRef(sourceInfo); |
| ref.setQualifier(jslink.makeRef(sourceInfo)); |
| return ref; |
| } |
| } |
| |
| public static void exec(JsProgram jsProgram, PermutationProperties properties) { |
| new HandleCrossFragmentReferences(jsProgram, properties).execImpl(); |
| } |
| |
| private static boolean containsOtherThan(Set<Integer> set, int allowed) { |
| for (int elem : set) { |
| if (elem != allowed) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| private JsName jslink; |
| private final JsProgram jsProgram; |
| private final Set<JsName> namesToPredefine = new LinkedHashSet<JsName>(); |
| private final boolean shouldPredeclareReferences; |
| |
| private HandleCrossFragmentReferences(JsProgram jsProgram, PermutationProperties properties) { |
| this.jsProgram = jsProgram; |
| // TODO: should it be a compiler error if soft permutations differ? |
| this.shouldPredeclareReferences = properties.isTrueInAnyPermutation( |
| "compiler.predeclare.cross.fragment.references"); |
| } |
| |
| private void chooseNamesToPredefine(Map<JsName, Set<Integer>> map, |
| Map<JsName, Set<Integer>> islandsUsing) { |
| for (Entry<JsName, Set<Integer>> entry : map.entrySet()) { |
| JsName name = entry.getKey(); |
| Set<Integer> defIslands = entry.getValue(); |
| if (defIslands.size() != 1) { |
| // Only rewrite global variables, which should have exactly one |
| // definition |
| continue; |
| } |
| int defIsland = defIslands.iterator().next(); |
| if (defIsland == 0) { |
| // Variables defined on the base island can be accessed directly from |
| // other islands |
| continue; |
| } |
| Set<Integer> useIslands = islandsUsing.get(name); |
| if (useIslands == null) { |
| // The variable is never used. Leave it alone. |
| continue; |
| } |
| |
| if (containsOtherThan(islandsUsing.get(name), defIsland)) { |
| namesToPredefine.add(name); |
| } |
| } |
| } |
| |
| /** |
| * Define the jslink object that will be used to fix up cross-island |
| * references. |
| */ |
| private void defineJsLink() { |
| SourceInfo info = jsProgram.createSourceInfoSynthetic(HandleCrossFragmentReferences.class); |
| jslink = jsProgram.getScope().declareName("jslink"); |
| JsVars vars = new JsVars(info); |
| JsVar var = new JsVar(info, jslink); |
| var.setInitExpr(JsObjectLiteral.EMPTY); |
| vars.add(var); |
| jsProgram.getFragmentBlock(0).getStatements().add(0, vars); |
| } |
| |
| private void execImpl() { |
| if (jsProgram.getFragmentCount() == 1) { |
| return; |
| } |
| if (!shouldPredeclareReferences) { |
| return; |
| } |
| defineJsLink(); |
| FindNameReferences findNameReferences = new FindNameReferences(); |
| findNameReferences.accept(jsProgram); |
| chooseNamesToPredefine(findNameReferences.islandsDefining, findNameReferences.islandsUsing); |
| new RewriteDeclsAndRefs().accept(jsProgram); |
| } |
| } |