blob: e47f5976aba896886e645e7713adfda11ea35754 [file] [log] [blame]
/*
* Copyright 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.gwt.dev.js;
import com.google.gwt.core.ext.BadPropertyValueException;
import com.google.gwt.core.ext.PropertyOracle;
import com.google.gwt.core.ext.SelectionProperty;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.dev.jjs.HasSourceInfo;
import com.google.gwt.dev.jjs.InternalCompilerException;
import com.google.gwt.dev.jjs.SourceInfo;
import com.google.gwt.dev.js.ast.JsArrayAccess;
import com.google.gwt.dev.js.ast.JsArrayLiteral;
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.JsCatch;
import com.google.gwt.dev.js.ast.JsContext;
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.JsFunction;
import com.google.gwt.dev.js.ast.JsInvocation;
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.JsNew;
import com.google.gwt.dev.js.ast.JsNode;
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.JsReturn;
import com.google.gwt.dev.js.ast.JsStatement;
import com.google.gwt.dev.js.ast.JsStringLiteral;
import com.google.gwt.dev.js.ast.JsThrow;
import com.google.gwt.dev.js.ast.JsTry;
import com.google.gwt.dev.js.ast.JsUnaryOperation;
import com.google.gwt.dev.js.ast.JsUnaryOperator;
import com.google.gwt.dev.js.ast.JsVars;
import com.google.gwt.dev.js.ast.JsVisitor;
import com.google.gwt.dev.js.ast.JsWhile;
import com.google.gwt.dev.js.ast.JsVars.JsVar;
import com.google.gwt.dev.util.collect.Lists;
import com.google.gwt.dev.util.collect.Maps;
import java.io.File;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Emulates the JS stack in order to provide useful stack traces on browers that
* do not provide useful stack information.
*
* @see com.google.gwt.core.client.impl.StackTraceCreator
*/
public class JsStackEmulator {
private static final String PROPERTY_NAME = "compiler.stackMode";
/**
* Resets the global stack depth to the local stack index and top stack frame
* after calls to Exceptions.caught. This is created by
* {@link EntryExitVisitor#visit(JsCatch, JsContext)}.
*/
private class CatchStackReset extends JsModVisitor {
/**
* The local stackIndex variable in the function.
*/
private final EntryExitVisitor eeVisitor;
public CatchStackReset(EntryExitVisitor eeVisitor) {
this.eeVisitor = eeVisitor;
}
@Override
public void endVisit(JsExprStmt x, JsContext<JsStatement> ctx) {
// Looking for e = caught(e);
JsExpression expr = x.getExpression();
if (!(expr instanceof JsBinaryOperation)) {
return;
}
JsBinaryOperation op = (JsBinaryOperation) expr;
if (!(op.getArg2() instanceof JsInvocation)) {
return;
}
JsInvocation i = (JsInvocation) op.getArg2();
JsExpression q = i.getQualifier();
if (!(q instanceof JsNameRef)) {
return;
}
JsName name = ((JsNameRef) q).getName();
if (name == null) {
return;
}
// caughtFunction is the JsFunction translated from Exceptions.caught
if (name.getStaticRef() != caughtFunction) {
return;
}
// $stackDepth = stackIndex
SourceInfo info = x.getSourceInfo().makeChild(JsStackEmulator.class,
"Resetting stack depth");
JsBinaryOperation reset = new JsBinaryOperation(info,
JsBinaryOperator.ASG, stackDepth.makeRef(info),
eeVisitor.stackIndexRef(info));
ctx.insertAfter(reset.makeStmt());
}
}
/**
* The EntryExitVisitor handles pushing and popping frames onto the emulated
* stack. It will operate on exactly one JsFunction. The basic transformation
* is to add a push operation at every function entry, and then a pop
* operation for every statement that might be the final statement executed by
* the function.
* <p>
* General stack depth entry/exit code:
*
* <pre>
* function foo() {
* var stackIndex;
* $stack[stackIndex = ++$stackDepth] = foo;
*
* ... do stuff ..
*
* $stackDepth = stackIndex - 1;
* }
* </pre>
* <p>
* For more complicated control flows involving return statements in try
* blocks with as associated finally block, it is necessary to introduce a
* local variable to indicate if control flow is expected to terminate
* normally at the end of the finally block:
*
* <pre>
* var exitingEarly;
* try {
* if (...) {
* return (exitingEarly = true, new Foo());
* }
* ...
* } finally {
* ... existing finally code ..
* exitingEarly && $stackDepth = stackIndex - 1;
* }
* </pre>
* A separate local variable is used for each try/finally nested within a
* finally block.
* <p>
* Try statements without a catch block will have a catch block added to them
* so that catch blocks are the only places where flow-control may jump to.
* All catch blocks are altered so that the global $stackDepth variable is
* reset to the local stack index value. This allows browser-native exceptions
* to be created with the correct stack trace before the finally code is
* executed with a correct stack depth.
*
* <pre>
* try {
* foo();
* } finally {
* bar();
* }
* </pre>
*
* becomes
*
* <pre>
* try {
* foo();
* } catch (e) {
* e = caught(e);
* $stackDepth = stackIndex;
* throw e;
* } finally {
* bar();
* }
* <p>
* Note that there is no specific handling for explicit throw statements, as
* the stack instrumentation must also handle browser-generated exceptions
* (e.g. <code>null.a()</code>).
*/
private class EntryExitVisitor extends JsModVisitor {
/**
* The name of a function-local variable to hold the invocation's slot in
* the stack.
*/
protected JsName stackIndex;
private final JsFunction currentFunction;
/**
* Maps finally blocks to the local variable name which is used to indicate
* if that finally block will exit the function early. This is a map and not
* a single value because a finally block might be nested in another exit
* block.
*/
private Map<JsBlock, JsName> finallyBlocksToExitVariables = Maps.create();
/**
* This variable will indicate the finally block that contains the last
* statement that will be executed if an unconditional flow control change
* were to occur within the associated try block.
*/
private JsBlock outerFinallyBlock;
/**
* Used if a return statement's expression could potentially trigger an
* exception.
*/
private JsName returnTemp;
/**
* Final cleanup for any new local variables that need to be created.
*/
private List<JsVar> varsToAdd = Lists.create();
public EntryExitVisitor(JsFunction currentFunction) {
this.currentFunction = currentFunction;
}
/**
* If the visitor is exiting the current function's block, add additional
* local variables and the final stack-pop instructions.
*/
@Override
public void endVisit(JsBlock x, JsContext<JsStatement> ctx) {
if (x == currentFunction.getBody()) {
// Add the entry code
List<JsStatement> statements = x.getStatements();
int idx = statements.isEmpty()
|| !(statements.get(0) instanceof JsVars) ? 0 : 1;
// Add push and pop statements
statements.add(idx, push(currentFunction));
addPopAtEndOfBlock(x, false);
// Add any needed variables
JsVars vars;
if (statements.get(0) instanceof JsVars) {
vars = (JsVars) statements.get(0);
} else {
vars = new JsVars(currentFunction.getSourceInfo());
statements.add(0, vars);
}
for (JsVar var : varsToAdd) {
vars.add(var);
}
}
}
@Override
public void endVisit(JsReturn x, JsContext<JsStatement> ctx) {
if (outerFinallyBlock != null) {
// There is a finally block, so we need to set the early-exit flag
JsBinaryOperation asg = new JsBinaryOperation(x.getSourceInfo(),
JsBinaryOperator.ASG, earlyExitRef(outerFinallyBlock),
program.getBooleanLiteral(true));
if (x.getExpr() == null) {
if (ctx.canInsert()) {
// exitingEarly = true; return;
ctx.insertBefore(asg.makeStmt());
} else {
// {exitingEarly = true; return;}
JsBlock block = new JsBlock(x.getSourceInfo());
block.getStatements().add(asg.makeStmt());
block.getStatements().add(x);
ctx.replaceMe(block);
}
} else {
// return (exitingEarly = true, expr);
JsBinaryOperation op = new JsBinaryOperation(x.getSourceInfo(),
JsBinaryOperator.COMMA, asg, x.getExpr());
x.setExpr(op);
}
} else {
if (x.getExpr() != null && x.getExpr().hasSideEffects()) {
// temp = expr; pop(); return temp;
SourceInfo info = x.getSourceInfo().makeChild(JsStackEmulator.class,
"Flow break with side-effect");
JsBinaryOperation asg = new JsBinaryOperation(info,
JsBinaryOperator.ASG, returnTempRef(info), x.getExpr());
x.setExpr(returnTempRef(info));
pop(x, asg, ctx);
} else {
// Otherwise, pop the stack frame
pop(x, null, ctx);
}
}
}
/**
* We want to look at unaltered versions of the catch block, so this is a
* <code>visit<code> and not a <code>endVisit</code>.
*/
@Override
public boolean visit(JsCatch x, JsContext<JsCatch> ctx) {
// Reset the stack depth to the local index
new CatchStackReset(this).accept(x);
return true;
}
@Override
public boolean visit(JsFunction x, JsContext<JsExpression> ctx) {
// Will be taken care of by the Bootstrap visitor
return false;
}
@Override
public boolean visit(JsTry x, JsContext<JsStatement> ctx) {
/*
* Only the outermost finally block needs special treatment; try/finally
* block within try blocks do not receive special treatment.
*/
JsBlock finallyBlock = x.getFinallyBlock();
if (finallyBlock != null && outerFinallyBlock == null) {
outerFinallyBlock = finallyBlock;
// Manual traversal
accept(x.getTryBlock());
if (x.getCatches().isEmpty()) {
JsCatch c = makeSyntheticCatchBlock(x);
x.getCatches().add(c);
}
assert x.getCatches().size() >= 1;
acceptList(x.getCatches());
// Exceptions in the finally block just exit the function
assert outerFinallyBlock == finallyBlock;
outerFinallyBlock = null;
accept(finallyBlock);
// Stack-pop instruction
addPopAtEndOfBlock(finallyBlock, true);
// Clean up entry after adding pop instruction
finallyBlocksToExitVariables = Maps.remove(
finallyBlocksToExitVariables, finallyBlock);
return false;
}
// Normal visit
return true;
}
/**
* Create a reference to the function-local stack index variable, possibly
* allocating it.
*/
protected JsNameRef stackIndexRef(SourceInfo info) {
if (stackIndex == null) {
stackIndex = currentFunction.getScope().declareName(
"JsStackEmulator_stackIndex", "stackIndex");
JsVar var = new JsVar(info, stackIndex);
varsToAdd = Lists.add(varsToAdd, var);
}
return stackIndex.makeRef(info);
}
/**
* Code-gen function for generating the stack-pop statement at the end of a
* block. A no-op if the last statement is a <code>throw</code> or
* <code>return</code> statement, since it will have already caused a pop
* statement to have been added.
*
* @param checkEarlyExit if <code>true</code>, generates
* <code>earlyExit && pop()</code>
*/
private void addPopAtEndOfBlock(JsBlock x, boolean checkEarlyExit) {
JsStatement last = x.getStatements().isEmpty() ? null
: x.getStatements().get(x.getStatements().size() - 1);
if (last instanceof JsReturn || last instanceof JsThrow) {
/*
* Don't need a pop after a throw or break statement. This is an
* optimization for the common case of returning a value as the last
* statement, but doesn't cover all flow-control cases.
*/
return;
} else if (checkEarlyExit && !finallyBlocksToExitVariables.containsKey(x)) {
/*
* No early-exit variable was ever allocated for this block. This means
* that the variable can never be true, and thus the stack-popping
* expression will never be executed.
*/
return;
}
// pop()
SourceInfo info = x.getSourceInfo().makeChild(JsStackEmulator.class,
"Stack exit");
JsExpression op = pop(info);
if (checkEarlyExit) {
// earlyExit && pop()
op = new JsBinaryOperation(info, JsBinaryOperator.AND, earlyExitRef(x),
op);
}
x.getStatements().add(op.makeStmt());
}
/**
* Generate a name reference to the early-exit variable for a given block,
* possibly allocating a new variable.
*/
private JsNameRef earlyExitRef(JsBlock x) {
JsName earlyExitName = finallyBlocksToExitVariables.get(x);
if (earlyExitName == null) {
earlyExitName = currentFunction.getScope().declareName(
"JsStackEmulator_exitingEarly"
+ finallyBlocksToExitVariables.size(), "exitingEarly");
finallyBlocksToExitVariables = Maps.put(finallyBlocksToExitVariables,
x, earlyExitName);
JsVar var = new JsVar(x.getSourceInfo(), earlyExitName);
varsToAdd = Lists.add(varsToAdd, var);
}
return earlyExitName.makeRef(x.getSourceInfo());
}
private JsCatch makeSyntheticCatchBlock(JsTry x) {
/*
* catch (e) { e = caught(e); throw e; }
*/
SourceInfo info = x.getSourceInfo().makeChild(JsStackEmulator.class,
"Synthetic catch block to fix stack depth");
JsCatch c = new JsCatch(info, currentFunction.getScope(), "e");
JsName paramName = c.getParameter().getName();
// caught(e)
JsInvocation caughtCall = new JsInvocation(info);
caughtCall.setQualifier(caughtFunction.getName().makeRef(info));
caughtCall.getArguments().add(paramName.makeRef(info));
// e = caught(e)
JsBinaryOperation asg = new JsBinaryOperation(info, JsBinaryOperator.ASG,
paramName.makeRef(info), caughtCall);
// throw e
JsThrow throwStatement = new JsThrow(info, paramName.makeRef(info));
JsBlock body = new JsBlock(info);
body.getStatements().add(asg.makeStmt());
body.getStatements().add(throwStatement);
c.setBody(body);
return c;
}
/**
* Pops the stack frame.
*
* @param x the statement that will cause the pop
* @param ctx the visitor context
*/
private void pop(JsStatement x, JsExpression expr,
JsContext<JsStatement> ctx) {
// $stackDepth = stackIndex - 1
SourceInfo info = x.getSourceInfo().makeChild(JsStackEmulator.class,
"Stack exit");
JsExpression op = pop(info);
if (ctx.canInsert()) {
if (expr != null) {
ctx.insertBefore(expr.makeStmt());
}
ctx.insertBefore(op.makeStmt());
} else {
JsBlock block = new JsBlock(info);
if (expr != null) {
block.getStatements().add(expr.makeStmt());
}
block.getStatements().add(op.makeStmt());
block.getStatements().add(x);
ctx.replaceMe(block);
}
}
/**
* Decrement the $stackDepth variable.
*/
private JsExpression pop(SourceInfo info) {
JsBinaryOperation sub = new JsBinaryOperation(info, JsBinaryOperator.SUB,
stackIndexRef(info), program.getNumberLiteral(1));
JsBinaryOperation op = new JsBinaryOperation(info, JsBinaryOperator.ASG,
stackDepth.makeRef(info), sub);
return op;
}
/**
* Create the function-entry code.
*/
private JsStatement push(HasSourceInfo x) {
SourceInfo info = x.getSourceInfo().makeChild(JsStackEmulator.class,
"Stack entry code");
JsNameRef stackRef = stack.makeRef(info);
JsNameRef stackDepthRef = stackDepth.makeRef(info);
JsExpression currentFunctionRef;
if (currentFunction.getName() == null) {
// Anonymous
currentFunctionRef = program.getNullLiteral();
} else {
currentFunctionRef = currentFunction.getName().makeRef(info);
}
// ++stackDepth
JsUnaryOperation inc = new JsPrefixOperation(info, JsUnaryOperator.INC,
stackDepthRef);
// stackIndex = ++stackDepth
JsBinaryOperation stackIndexOp = new JsBinaryOperation(info,
JsBinaryOperator.ASG, stackIndexRef(info), inc);
// stack[stackIndex = ++stackDepth]
JsArrayAccess access = new JsArrayAccess(info, stackRef, stackIndexOp);
// stack[stackIndex = ++stackDepth] = currentFunction
JsBinaryOperation op = new JsBinaryOperation(info, JsBinaryOperator.ASG,
access, currentFunctionRef);
return op.makeStmt();
}
private JsNameRef returnTempRef(SourceInfo info) {
if (returnTemp == null) {
returnTemp = currentFunction.getScope().declareName(
"JsStackEmulator_returnTemp", "returnTemp");
JsVar var = new JsVar(info, returnTemp);
varsToAdd = Lists.add(varsToAdd, var);
}
return returnTemp.makeRef(info);
}
}
/**
* Creates a visitor to instrument each JsFunction in the program.
*/
private class InstrumentAllFunctions extends JsVisitor {
@Override
public void endVisit(JsFunction x, JsContext<JsExpression> ctx) {
if (!x.getBody().getStatements().isEmpty()) {
if (recordLineNumbers) {
(new LocationVisitor(x)).accept(x.getBody());
} else {
(new EntryExitVisitor(x)).accept(x.getBody());
}
}
}
}
/**
* Extends EntryExit visitor to record location information in the AST. This
* visitor will modify every JsExpression that can potentially result in a
* change of flow control with file and line number data.
* <p>
* This simply generates code to set entries in the <code>$location</code>
* stack, parallel to <code>$stack</code>:
*
* <pre>
* ($location[stackIndex] = 'Foo.java:' + 42, expr);
* </pre>
*
* Inclusion of file names is dependent on the value of the
* {@link JsStackEmulator#recordFileNames} field.
*/
private class LocationVisitor extends EntryExitVisitor {
private String lastFile;
private int lastLine;
/**
* Nodes in this set are used in a context that expects a reference, not
* just an arbitrary expression. For example, <code>delete</code> takes a
* reference. These are tracked because it wouldn't be safe to rewrite
* <code>delete foo.bar</code> to <code>delete (line='123',foo).bar</code>.
*/
private final Set<JsNode<?>> nodesInRefContext = new HashSet<JsNode<?>>();
public LocationVisitor(JsFunction function) {
super(function);
resetPosition();
}
@Override
public void endVisit(JsArrayAccess x, JsContext<JsExpression> ctx) {
record(x, ctx);
}
@Override
public void endVisit(JsBinaryOperation x, JsContext<JsExpression> ctx) {
if (x.getOperator().isAssignment()) {
record(x, ctx);
}
}
@Override
public void endVisit(JsInvocation x, JsContext<JsExpression> ctx) {
nodesInRefContext.remove(x.getQualifier());
record(x, ctx);
}
@Override
public void endVisit(JsNameRef x, JsContext<JsExpression> ctx) {
record(x, ctx);
}
@Override
public void endVisit(JsNew x, JsContext<JsExpression> ctx) {
record(x, ctx);
}
@Override
public void endVisit(JsPostfixOperation x, JsContext<JsExpression> ctx) {
record(x, ctx);
}
@Override
public void endVisit(JsPrefixOperation x, JsContext<JsExpression> ctx) {
record(x, ctx);
nodesInRefContext.remove(x.getArg());
}
/**
* This is essentially a hacked-up version of JsFor.traverse to account for
* flow control differing from visitation order. It resets lastFile and
* lastLine before the condition and increment expressions in the for loop
* so that location data will be recorded correctly.
*/
@Override
public boolean visit(JsFor x, JsContext<JsStatement> ctx) {
if (x.getInitExpr() != null) {
x.setInitExpr(accept(x.getInitExpr()));
} else if (x.getInitVars() != null) {
x.setInitVars(accept(x.getInitVars()));
}
if (x.getCondition() != null) {
resetPosition();
x.setCondition(accept(x.getCondition()));
}
if (x.getIncrExpr() != null) {
resetPosition();
x.setIncrExpr(accept(x.getIncrExpr()));
}
accept(x.getBody());
return false;
}
@Override
public boolean visit(JsInvocation x, JsContext<JsExpression> ctx) {
nodesInRefContext.add(x.getQualifier());
return true;
}
@Override
public boolean visit(JsPrefixOperation x, JsContext<JsExpression> ctx) {
if (x.getOperator() == JsUnaryOperator.DELETE
|| x.getOperator() == JsUnaryOperator.TYPEOF) {
nodesInRefContext.add(x.getArg());
}
return true;
}
/**
* Similar to JsFor, this resets the current location information before
* evaluating the condition.
*/
@Override
public boolean visit(JsWhile x, JsContext<JsStatement> ctx) {
resetPosition();
x.setCondition(accept(x.getCondition()));
accept(x.getBody());
return false;
}
/**
* Strips off the final name segment.
*/
private String baseName(String fileName) {
// Try the system path separator
int lastIndex = fileName.lastIndexOf(File.separator);
if (lastIndex == -1) {
// Otherwise, try URL path separator
lastIndex = fileName.lastIndexOf('/');
}
if (lastIndex != -1) {
return fileName.substring(lastIndex + 1);
} else {
return fileName;
}
}
private void record(JsExpression x, JsContext<JsExpression> ctx) {
if (ctx.isLvalue()) {
// Assignments to comma expressions aren't legal
return;
} else if (nodesInRefContext.contains(x)) {
// Don't modify references into non-references
return;
} else if (x.getSourceInfo().getStartLine() == lastLine
&& (!recordFileNames || x.getSourceInfo().getFileName().equals(
lastFile))) {
// Same location; ignore
return;
}
SourceInfo info = x.getSourceInfo().makeChild(JsStackEmulator.class,
"Synthetic location data");
// ($locations[stackIndex] = fileName + lineNumber, x)
JsExpression location = program.getStringLiteral(info,
String.valueOf(lastLine = info.getStartLine()));
if (recordFileNames) {
// 'fileName:' + lineNumber
JsStringLiteral stringLit = program.getStringLiteral(info,
baseName(lastFile = info.getFileName()) + ":");
location = new JsBinaryOperation(info, JsBinaryOperator.ADD, stringLit,
location);
}
JsArrayAccess access = new JsArrayAccess(info, lineNumbers.makeRef(info),
stackIndexRef(info));
JsBinaryOperation asg = new JsBinaryOperation(info, JsBinaryOperator.ASG,
access, location);
JsBinaryOperation comma = new JsBinaryOperation(info,
JsBinaryOperator.COMMA, asg, x);
ctx.replaceMe(comma);
}
private void resetPosition() {
lastFile = "";
lastLine = -1;
}
}
/**
* The StackTraceCreator code refers to identifiers defined in JsRootScope,
* which are unobfuscatable. This visitor replaces references to those symbols
* with references to our locally-defined, obfuscatable names.
*/
private class ReplaceUnobfuscatableNames extends JsModVisitor {
private final JsName rootLineNumbers = program.getRootScope().findExistingUnobfuscatableName(
"$location");
// See JsRootScope for the definition of these names
private final JsName rootStack = program.getRootScope().findExistingUnobfuscatableName(
"$stack");
private final JsName rootStackDepth = program.getRootScope().findExistingUnobfuscatableName(
"$stackDepth");
@Override
public void endVisit(JsNameRef x, JsContext<JsExpression> ctx) {
JsName name = x.getName();
JsNameRef newRef = null;
if (name == rootStack) {
newRef = stack.makeRef(x.getSourceInfo());
} else if (name == rootStackDepth) {
newRef = stackDepth.makeRef(x.getSourceInfo());
} else if (name == rootLineNumbers) {
newRef = lineNumbers.makeRef(x.getSourceInfo());
}
if (newRef == null) {
return;
}
assert x.getQualifier() == null;
ctx.replaceMe(newRef);
}
}
/**
* Corresponds to property compiler.stackMode in EmulateJsStack.gwt.xml
* module.
*/
public enum StackMode {
STRIP, NATIVE, EMULATED;
}
public static void exec(JsProgram program, PropertyOracle[] propertyOracles) {
if (getStackMode(propertyOracles) == StackMode.EMULATED) {
(new JsStackEmulator(program, propertyOracles)).execImpl();
}
}
public static StackMode getStackMode(PropertyOracle[] propertyOracles) {
SelectionProperty property;
try {
property = propertyOracles[0].getSelectionProperty(TreeLogger.NULL,
PROPERTY_NAME);
} catch (BadPropertyValueException e) {
// Should be inherited via Core.gwt.xml
throw new InternalCompilerException("Expected property " + PROPERTY_NAME
+ " not defined", e);
}
String value = property.getCurrentValue();
assert value != null : property.getName() + " did not have a value";
StackMode stackMode = StackMode.valueOf(value.toUpperCase());
// Check for multiply defined properties
if (propertyOracles.length > 1) {
for (int i = 1; i < propertyOracles.length; ++i) {
try {
property = propertyOracles[i].getSelectionProperty(TreeLogger.NULL,
PROPERTY_NAME);
} catch (BadPropertyValueException e) {
// OK!
}
assert value.equals(property.getCurrentValue()) :
"compiler.stackMode property has multiple values.";
}
}
return stackMode;
}
private JsFunction caughtFunction;
private JsName lineNumbers;
private final JsProgram program;
private boolean recordFileNames;
private boolean recordLineNumbers;
private JsName stack;
private JsName stackDepth;
private JsStackEmulator(JsProgram program, PropertyOracle[] propertyOracles) {
this.program = program;
assert propertyOracles.length > 0;
PropertyOracle oracle = propertyOracles[0];
try {
List<String> values = oracle.getConfigurationProperty(
"compiler.emulatedStack.recordFileNames").getValues();
recordFileNames = Boolean.valueOf(values.get(0));
values = oracle.getConfigurationProperty(
"compiler.emulatedStack.recordLineNumbers").getValues();
recordLineNumbers = recordFileNames || Boolean.valueOf(values.get(0));
} catch (BadPropertyValueException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
private void execImpl() {
caughtFunction = program.getIndexedFunction("Exceptions.caught");
if (caughtFunction == null) {
// No exceptions caught? Weird, but possible.
return;
}
initNames();
makeVars();
(new ReplaceUnobfuscatableNames()).accept(program);
(new InstrumentAllFunctions()).accept(program);
}
private void initNames() {
stack = program.getScope().declareName("$JsStackEmulator_stack", "$stack");
stackDepth = program.getScope().declareName("$JsStackEmulator_stackDepth",
"$stackDepth");
lineNumbers = program.getScope().declareName("$JsStackEmulator_location",
"$location");
}
private void makeVars() {
SourceInfo info = program.getSourceInfo().makeChild(JsStackEmulator.class,
"Emulated stack data");
JsVar stackVar = new JsVar(info, stack);
stackVar.setInitExpr(new JsArrayLiteral(info));
JsVar stackDepthVar = new JsVar(info, stackDepth);
stackDepthVar.setInitExpr(program.getNumberLiteral(info, -1));
JsVar lineNumbersVar = new JsVar(info, lineNumbers);
lineNumbersVar.setInitExpr(new JsArrayLiteral(info));
JsVars vars;
JsStatement first = program.getGlobalBlock().getStatements().get(0);
if (first instanceof JsVars) {
vars = (JsVars) first;
} else {
vars = new JsVars(info);
program.getGlobalBlock().getStatements().add(0, vars);
}
vars.add(stackVar);
vars.add(stackDepthVar);
vars.add(lineNumbersVar);
}
}