blob: 126bd529de1b6b469269b465922c7a061b3bd9dc [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.js;
import com.google.gwt.dev.jjs.SourceInfo;
import com.google.gwt.dev.jjs.ast.JProgram;
import com.google.gwt.dev.js.ast.JsBinaryOperation;
import com.google.gwt.dev.js.ast.JsBlock;
import com.google.gwt.dev.js.ast.JsContext;
import com.google.gwt.dev.js.ast.JsExpression;
import com.google.gwt.dev.js.ast.JsModVisitor;
import com.google.gwt.dev.js.ast.JsName;
import com.google.gwt.dev.js.ast.JsPostfixOperation;
import com.google.gwt.dev.js.ast.JsPrefixOperation;
import com.google.gwt.dev.js.ast.JsProgram;
import com.google.gwt.dev.js.ast.JsProgramFragment;
import com.google.gwt.dev.js.ast.JsPropertyInitializer;
import com.google.gwt.dev.js.ast.JsScope;
import com.google.gwt.dev.js.ast.JsStringLiteral;
import com.google.gwt.dev.js.ast.JsVars;
import com.google.gwt.dev.js.ast.JsVars.JsVar;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.SortedMap;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.Map.Entry;
/**
* Interns all String literals in a JsProgram. Each unique String will be
* assigned to a variable in an appropriate program fragment and the
* JsStringLiteral will be replaced with a JsNameRef. This optimization is
* complete in a single pass, although it may be performed multiple times
* without duplicating the intern pool.
*/
public class JsStringInterner {
/**
* Replaces JsStringLiterals with JsNameRefs, creating new JsName allocations
* on the fly.
*/
private static class StringVisitor extends JsModVisitor {
/**
* The current fragment being visited.
*/
private int currentFragment = 0;
/**
* This map records which program fragment the variable for this JsName
* should be created in.
*/
private final SortedMap<JsStringLiteral, Integer> fragmentAssignment = new TreeMap<JsStringLiteral, Integer>(
LITERAL_COMPARATOR);
/**
* A counter used for assigning ids to Strings. Even though it's unlikely
* that someone would actually have two billion strings in their
* application, it doesn't hurt to think ahead.
*/
private long lastId = 0;
/**
* Only used to get fragment load order so strings used in multiple
* fragments need only be downloaded once.
*/
private final JProgram program;
/**
* Records the scope in which the interned identifiers are declared.
*/
private final JsScope scope;
/**
* This is a TreeMap to ensure consistent iteration order, based on the
* lexicographical ordering of the string constant.
*/
private final SortedMap<JsStringLiteral, JsName> toCreate = new TreeMap<JsStringLiteral, JsName>(
LITERAL_COMPARATOR);
/**
* Constructor.
*
* @param scope specifies the scope in which the interned strings should be
* created.
*/
public StringVisitor(JProgram program, JsScope scope) {
this.program = program;
this.scope = scope;
}
@Override
public void endVisit(JsProgramFragment x, JsContext<JsProgramFragment> ctx) {
currentFragment++;
}
/**
* Prevents 'fixing' an otherwise illegal operation.
*/
@Override
public boolean visit(JsBinaryOperation x, JsContext<JsExpression> ctx) {
return !x.getOperator().isAssignment()
|| !(x.getArg1() instanceof JsStringLiteral);
}
/**
* Prevents 'fixing' an otherwise illegal operation.
*/
@Override
public boolean visit(JsPostfixOperation x, JsContext<JsExpression> ctx) {
return !(x.getArg() instanceof JsStringLiteral);
}
/**
* Prevents 'fixing' an otherwise illegal operation.
*/
@Override
public boolean visit(JsPrefixOperation x, JsContext<JsExpression> ctx) {
return !(x.getArg() instanceof JsStringLiteral);
}
/**
* We ignore property initializer labels in object literals, but do process
* the expression. This is because the LHS is always treated as a string,
* and never evaluated as an expression.
*/
@Override
public boolean visit(JsPropertyInitializer x,
JsContext<JsPropertyInitializer> ctx) {
x.setValueExpr(accept(x.getValueExpr()));
return false;
}
/**
* Replace JsStringLiteral instances with JsNameRefs.
*/
@Override
public boolean visit(JsStringLiteral x, JsContext<JsExpression> ctx) {
JsName name = toCreate.get(x);
if (name == null) {
String ident = PREFIX + lastId++;
name = scope.declareName(ident);
toCreate.put(x, name);
}
Integer currentAssignment = fragmentAssignment.get(x);
if (currentAssignment == null) {
// Assign the JsName to the current program fragment
fragmentAssignment.put(x, currentFragment);
} else if (currentAssignment != currentFragment) {
// See if we need to move the assignment to a common ancestor
assert program != null : "JsStringInterner cannot be used with "
+ "fragmented JsProgram without an accompanying JProgram";
int newAssignment = program.lastFragmentLoadingBefore(currentFragment,
currentAssignment);
if (newAssignment != currentAssignment) {
// Assign the JsName to the common ancestor
fragmentAssignment.put(x, newAssignment);
}
}
ctx.replaceMe(name.makeRef(x.getSourceInfo().makeChild(
JsStringInterner.class, "Interned reference")));
return false;
}
/**
* This prevents duplicating the intern pool by not traversing JsVar
* declarations that look like they were created by the interner.
*/
@Override
public boolean visit(JsVar x, JsContext<JsVar> ctx) {
return !(x.getName().getIdent().startsWith(PREFIX));
}
}
public static final String PREFIX = "$intern_";
private static final Comparator<JsStringLiteral> LITERAL_COMPARATOR = new Comparator<JsStringLiteral>() {
public int compare(JsStringLiteral o1, JsStringLiteral o2) {
return o1.getValue().compareTo(o2.getValue());
}
};
/**
* Apply interning of String literals to a JsProgram. The symbol names for the
* interned strings will be defined within the program's top scope and the
* symbol declarations will be added as the first statement in the program's
* global block.
*
* @param jprogram the JProgram that has fragment dependency data for
* <code>program</code>
* @param program the JsProgram
* @return a map describing the interning that occurred
*/
public static Map<JsName, String> exec(JProgram jprogram, JsProgram program) {
StringVisitor v = new StringVisitor(jprogram, program.getScope());
v.accept(program);
Map<Integer, SortedSet<JsStringLiteral>> bins = new HashMap<Integer, SortedSet<JsStringLiteral>>();
for (int i = 0, j = program.getFragmentCount(); i < j; i++) {
bins.put(i, new TreeSet<JsStringLiteral>(LITERAL_COMPARATOR));
}
for (Map.Entry<JsStringLiteral, Integer> entry : v.fragmentAssignment.entrySet()) {
SortedSet<JsStringLiteral> set = bins.get(entry.getValue());
assert set != null;
set.add(entry.getKey());
}
for (Map.Entry<Integer, SortedSet<JsStringLiteral>> entry : bins.entrySet()) {
createVars(program, program.getFragmentBlock(entry.getKey()),
entry.getValue(), v.toCreate);
}
return reverse(v.toCreate);
}
/**
* Intern String literals that occur within a JsBlock. The symbol declarations
* will be added as the first statement in the block.
*
* @param block the block to visit
* @param scope the JsScope in which to reserve the new identifiers
* @return <code>true</code> if any changes were made to the block
*/
public static boolean exec(JsProgram program, JsBlock block, JsScope scope) {
StringVisitor v = new StringVisitor(null, scope);
v.accept(block);
createVars(program, block, v.toCreate.keySet(), v.toCreate);
return v.didChange();
}
/**
* Create variable declarations in <code>block</code> for literal strings
* <code>toCreate</code> using the variable map <code>names</code>.
*/
private static void createVars(JsProgram program, JsBlock block,
Collection<JsStringLiteral> toCreate, Map<JsStringLiteral, JsName> names) {
if (toCreate.size() > 0) {
// Create the pool of variable names.
JsVars vars = new JsVars(program.createSourceInfoSynthetic(
JsStringInterner.class, "Interned string pool"));
SourceInfo sourceInfo = program.createSourceInfoSynthetic(
JsStringInterner.class, "Interned string assignment");
for (JsStringLiteral literal : toCreate) {
JsVar var = new JsVar(sourceInfo, names.get(literal));
var.setInitExpr(literal);
vars.add(var);
}
block.getStatements().add(0, vars);
}
}
private static Map<JsName, String> reverse(
SortedMap<JsStringLiteral, JsName> toCreate) {
Map<JsName, String> reversed = new LinkedHashMap<JsName, String>(
toCreate.size());
for (Entry<JsStringLiteral, JsName> entry : toCreate.entrySet()) {
reversed.put(entry.getValue(), entry.getKey().getValue());
}
return reversed;
}
/**
* Utility class.
*/
private JsStringInterner() {
}
}