// Copyright (C) 2009 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.caja.ancillary.opt;
import com.google.caja.lexer.FilePosition;
import com.google.caja.parser.AncestorChain;
import com.google.caja.parser.MutableParseTreeNode;
import com.google.caja.parser.ParseTreeNode;
import com.google.caja.parser.Visitor;
import com.google.caja.parser.js.Block;
import com.google.caja.parser.js.Declaration;
import com.google.caja.parser.js.FunctionConstructor;
import com.google.caja.parser.js.Identifier;
import com.google.caja.parser.js.Literal;
import com.google.caja.parser.js.MultiDeclaration;
import com.google.caja.parser.js.ObjProperty;
import com.google.caja.parser.js.ObjectConstructor;
import com.google.caja.parser.js.Reference;
import com.google.caja.parser.js.RegexpLiteral;
import com.google.caja.parser.js.Statement;
import com.google.caja.parser.js.StringLiteral;
import com.google.caja.reporting.RenderContext;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import java.util.Collections;
import java.util.List;
import java.util.Map;
/**
* Collects frequently used literals, e.g. long strings and the like, into
* a constant pool at the top of each top level function.
*
* @author mikesamuel@gmail.com
*/
public class ConstantPooler {
public static Block optimize(Block program) {
program = (Block) program.clone();
optimizeTopLevelFunctions(program);
return program;
}
private static void optimizeTopLevelFunctions(ParseTreeNode node) {
if (node instanceof FunctionConstructor) {
optimizeWithin((FunctionConstructor) node);
} else {
for (ParseTreeNode child : node.children()) {
optimizeTopLevelFunctions(child);
}
}
}
private static void optimizeWithin(FunctionConstructor fc) {
final Map<LitVal, LitVal> uses = Maps.newLinkedHashMap();
Block body = fc.getBody();
body.acceptPreOrder(new Visitor() {
public boolean visit(AncestorChain<?> chain) {
if (chain.node instanceof Literal
&& !(chain.node instanceof RegexpLiteral)) {
AncestorChain<Literal> litAc = chain.cast(Literal.class);
LitVal key = new LitVal(litAc);
LitVal stored = uses.get(key);
if (stored == null) {
uses.put(key, stored = key);
}
stored.uses.add(litAc);
} else if (chain.node instanceof ObjectConstructor) {
List<? extends ObjProperty> children = chain
.cast(ObjectConstructor.class).node.children();
for (ObjProperty prop : children) {
visit(chain.child(prop).child(prop.children().get(1)));
}
return false;
}
return true;
}
}, null);
List<Declaration> decls = Lists.newArrayList();
FilePosition pos = FilePosition.startOf(body.getFilePosition());
for (LitVal v : uses.values()) {
// Size now = canonLen * nUses.
// Size after = "var aa=".length + canonLen + ";".length + "aa" * nUses
// Switch if now > after;
// canonLen * nUses > 8 + canonLen + 2 * nUses
int requiredSavings = 30; // TUNING PARAMETER
int canonLen = v.canonForm().length();
int nUses = v.uses.size();
if (canonLen * nUses > 8 + canonLen + 2 * nUses + requiredSavings) {
// TODO(mikesamuel): choose a guaranteed non-interfering name.
String name = "$_$__litpool__" + decls.size() + "$_$";
decls.add(new Declaration(
pos, new Identifier(pos, name), v.uses.get(0).node));
for (AncestorChain<Literal> use : v.uses) {
Reference ref = new Reference(
new Identifier(use.node.getFilePosition(), name));
use.parent.cast(MutableParseTreeNode.class).node
.replaceChild(ref, use.node);
}
}
}
if (!decls.isEmpty()) {
Statement first = body.children().get(0);
MultiDeclaration md;
if (first instanceof MultiDeclaration) {
md = (MultiDeclaration) first;
} else if (first instanceof Declaration) {
md = new MultiDeclaration(
FilePosition.span(pos, first.getFilePosition()),
Collections.singletonList((Declaration) first));
body.replaceChild(md, first);
} else if (decls.size() == 1) {
body.insertBefore(decls.get(0), first);
return;
} else {
md = new MultiDeclaration(pos, Collections.<Declaration>emptyList());
body.insertBefore(md, first);
}
MutableParseTreeNode.Mutation mut = md.createMutation();
Declaration firstDecl = md.children().get(0);
for (Declaration decl : decls) {
mut = mut.insertBefore(decl, firstDecl);
}
mut.execute();
}
}
private static class LitVal {
final Object canonValue;
final List<AncestorChain<Literal>> uses = Lists.newArrayList();
LitVal(AncestorChain<Literal> useAc) {
Literal use = useAc.node;
canonValue = use instanceof StringLiteral
? ((StringLiteral) use).getUnquotedValue() : use.getValue();
}
String canonForm() {
StringBuilder sb = new StringBuilder();
Literal use = uses.get(0).node;
RenderContext rc = new RenderContext(use.makeRenderer(sb, null));
use.render(rc);
rc.getOut().noMoreTokens();
return sb.toString();
}
@Override
public int hashCode() { return canonValue.hashCode(); }
@Override
public boolean equals(Object o) {
if (!(o instanceof LitVal)) { return false; }
return this.canonValue.equals(((LitVal) o).canonValue);
}
}
}