blob: 6c0490b9cc2baf089bde2ec524b246c7b4145843 [file] [log] [blame]
/*
* 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.core.ext.BadPropertyValueException;
import com.google.gwt.core.ext.PropertyOracle;
import com.google.gwt.core.ext.TreeLogger;
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 String PROP_PREDECLARE_VARS = "compiler.predeclare.cross.fragment.references";
public static void exec(TreeLogger logger, JsProgram jsProgram, PropertyOracle[] propertyOracles) {
new HandleCrossFragmentReferences(logger, jsProgram, propertyOracles).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 TreeLogger logger;
private final Set<JsName> namesToPredefine = new LinkedHashSet<JsName>();
private final PropertyOracle[] propertyOracles;
private HandleCrossFragmentReferences(TreeLogger logger, JsProgram jsProgram,
PropertyOracle[] propertyOracles) {
this.logger = logger;
this.jsProgram = jsProgram;
this.propertyOracles = propertyOracles;
}
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(new JsObjectLiteral(info));
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);
}
/**
* Check the property oracles for whether references should be predeclared or
* not. If any of them say yes, then do the rewrite.
*/
private boolean shouldPredeclareReferences() {
for (PropertyOracle props : propertyOracles) {
try {
String propValue =
props.getSelectionProperty(logger, PROP_PREDECLARE_VARS).getCurrentValue();
if (Boolean.parseBoolean(propValue)) {
return true;
}
} catch (BadPropertyValueException e) {
// Property not defined; don't rewrite
}
}
return false;
}
}