aboutsummaryrefslogtreecommitdiff
path: root/src/jdk/nashorn/internal/codegen/LocalVariableTypesCalculator.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/jdk/nashorn/internal/codegen/LocalVariableTypesCalculator.java')
-rw-r--r--src/jdk/nashorn/internal/codegen/LocalVariableTypesCalculator.java1562
1 files changed, 1562 insertions, 0 deletions
diff --git a/src/jdk/nashorn/internal/codegen/LocalVariableTypesCalculator.java b/src/jdk/nashorn/internal/codegen/LocalVariableTypesCalculator.java
new file mode 100644
index 00000000..ac3c2934
--- /dev/null
+++ b/src/jdk/nashorn/internal/codegen/LocalVariableTypesCalculator.java
@@ -0,0 +1,1562 @@
+/*
+ * Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package jdk.nashorn.internal.codegen;
+
+import static jdk.nashorn.internal.codegen.CompilerConstants.RETURN;
+import static jdk.nashorn.internal.ir.Expression.isAlwaysFalse;
+import static jdk.nashorn.internal.ir.Expression.isAlwaysTrue;
+
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Deque;
+import java.util.HashSet;
+import java.util.IdentityHashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
+import jdk.nashorn.internal.codegen.types.Type;
+import jdk.nashorn.internal.ir.AccessNode;
+import jdk.nashorn.internal.ir.BaseNode;
+import jdk.nashorn.internal.ir.BinaryNode;
+import jdk.nashorn.internal.ir.Block;
+import jdk.nashorn.internal.ir.BreakNode;
+import jdk.nashorn.internal.ir.BreakableNode;
+import jdk.nashorn.internal.ir.CaseNode;
+import jdk.nashorn.internal.ir.CatchNode;
+import jdk.nashorn.internal.ir.ContinueNode;
+import jdk.nashorn.internal.ir.Expression;
+import jdk.nashorn.internal.ir.ForNode;
+import jdk.nashorn.internal.ir.FunctionNode;
+import jdk.nashorn.internal.ir.FunctionNode.CompilationState;
+import jdk.nashorn.internal.ir.IdentNode;
+import jdk.nashorn.internal.ir.IfNode;
+import jdk.nashorn.internal.ir.IndexNode;
+import jdk.nashorn.internal.ir.JoinPredecessor;
+import jdk.nashorn.internal.ir.JoinPredecessorExpression;
+import jdk.nashorn.internal.ir.JumpStatement;
+import jdk.nashorn.internal.ir.LabelNode;
+import jdk.nashorn.internal.ir.LexicalContext;
+import jdk.nashorn.internal.ir.LexicalContextNode;
+import jdk.nashorn.internal.ir.LiteralNode;
+import jdk.nashorn.internal.ir.LocalVariableConversion;
+import jdk.nashorn.internal.ir.LoopNode;
+import jdk.nashorn.internal.ir.Node;
+import jdk.nashorn.internal.ir.PropertyNode;
+import jdk.nashorn.internal.ir.ReturnNode;
+import jdk.nashorn.internal.ir.RuntimeNode;
+import jdk.nashorn.internal.ir.RuntimeNode.Request;
+import jdk.nashorn.internal.ir.SplitReturn;
+import jdk.nashorn.internal.ir.Statement;
+import jdk.nashorn.internal.ir.SwitchNode;
+import jdk.nashorn.internal.ir.Symbol;
+import jdk.nashorn.internal.ir.TernaryNode;
+import jdk.nashorn.internal.ir.ThrowNode;
+import jdk.nashorn.internal.ir.TryNode;
+import jdk.nashorn.internal.ir.UnaryNode;
+import jdk.nashorn.internal.ir.VarNode;
+import jdk.nashorn.internal.ir.WhileNode;
+import jdk.nashorn.internal.ir.visitor.NodeVisitor;
+import jdk.nashorn.internal.parser.TokenType;
+
+/**
+ * Calculates types for local variables. For purposes of local variable type calculation, the only types used are
+ * Undefined, boolean, int, long, double, and Object. The calculation eagerly widens types of local variable to their
+ * widest at control flow join points.
+ * TODO: investigate a more sophisticated solution that uses use/def information to only widens the type of a local
+ * variable to its widest used type after the join point. That would eliminate some widenings of undefined variables to
+ * object, most notably those used only in loops. We need a full liveness analysis for that. Currently, we can establish
+ * per-type liveness, which eliminates most of unwanted dead widenings.
+ * NOTE: the way this class is implemented, it actually processes the AST in two passes. The first pass is top-down and
+ * implemented in {@code enterXxx} methods. This pass does not mutate the AST (except for one occurrence, noted below),
+ * as being able to find relevant labels for control flow joins is sensitive to their reference identity, and mutated
+ * label-carrying nodes will create copies of their labels. A second bottom-up pass applying the changes is implemented
+ * in the separate visitor sitting in {@link #leaveFunctionNode(FunctionNode)}. This visitor will also instantiate new
+ * instances of the calculator to be run on nested functions (when not lazy compiling).
+ *
+ */
+final class LocalVariableTypesCalculator extends NodeVisitor<LexicalContext>{
+
+ private static class JumpOrigin {
+ final JoinPredecessor node;
+ final Map<Symbol, LvarType> types;
+
+ JumpOrigin(final JoinPredecessor node, final Map<Symbol, LvarType> types) {
+ this.node = node;
+ this.types = types;
+ }
+ }
+
+ private static class JumpTarget {
+ private final List<JumpOrigin> origins = new LinkedList<>();
+ private Map<Symbol, LvarType> types = Collections.emptyMap();
+
+ void addOrigin(final JoinPredecessor originNode, final Map<Symbol, LvarType> originTypes) {
+ origins.add(new JumpOrigin(originNode, originTypes));
+ this.types = getUnionTypes(this.types, originTypes);
+ }
+ }
+ private enum LvarType {
+ UNDEFINED(Type.UNDEFINED),
+ BOOLEAN(Type.BOOLEAN),
+ INT(Type.INT),
+ LONG(Type.LONG),
+ DOUBLE(Type.NUMBER),
+ OBJECT(Type.OBJECT);
+
+ private final Type type;
+ private LvarType(final Type type) {
+ this.type = type;
+ }
+ }
+
+ private static final Map<Type, LvarType> TO_LVAR_TYPE = new IdentityHashMap<>();
+
+ static {
+ for(final LvarType lvarType: LvarType.values()) {
+ TO_LVAR_TYPE.put(lvarType.type, lvarType);
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ private static IdentityHashMap<Symbol, LvarType> cloneMap(final Map<Symbol, LvarType> map) {
+ return (IdentityHashMap<Symbol, LvarType>)((IdentityHashMap<?,?>)map).clone();
+ }
+
+ private LocalVariableConversion createConversion(final Symbol symbol, final LvarType branchLvarType,
+ final Map<Symbol, LvarType> joinLvarTypes, final LocalVariableConversion next) {
+ final LvarType targetType = joinLvarTypes.get(symbol);
+ assert targetType != null;
+ if(targetType == branchLvarType) {
+ return next;
+ }
+ // NOTE: we could naively just use symbolIsUsed(symbol, branchLvarType) here, but that'd be wrong. While
+ // technically a conversion will read the value of the symbol with that type, but it will also write it to a new
+ // type, and that type might be dead (we can't know yet). For this reason, we don't treat conversion reads as
+ // real uses until we know their target type is live. If we didn't do this, and just did a symbolIsUsed here,
+ // we'd introduce false live variables which could nevertheless turn into dead ones in a subsequent
+ // deoptimization, causing a shift in the list of live locals that'd cause erroneous restoration of
+ // continuations (since RewriteException's byteCodeSlots carries an array and not a name-value map).
+
+ symbolIsConverted(symbol, branchLvarType, targetType);
+ //symbolIsUsed(symbol, branchLvarType);
+ return new LocalVariableConversion(symbol, branchLvarType.type, targetType.type, next);
+ }
+
+ private static Map<Symbol, LvarType> getUnionTypes(final Map<Symbol, LvarType> types1, final Map<Symbol, LvarType> types2) {
+ if(types1 == types2 || types1.isEmpty()) {
+ return types2;
+ } else if(types2.isEmpty()) {
+ return types1;
+ }
+ final Set<Symbol> commonSymbols = new HashSet<>(types1.keySet());
+ commonSymbols.retainAll(types2.keySet());
+ // We have a chance of returning an unmodified set if both sets have the same keys and one is strictly wider
+ // than the other.
+ final int commonSize = commonSymbols.size();
+ final int types1Size = types1.size();
+ final int types2Size = types2.size();
+ if(commonSize == types1Size && commonSize == types2Size) {
+ boolean matches1 = true, matches2 = true;
+ Map<Symbol, LvarType> union = null;
+ for(final Symbol symbol: commonSymbols) {
+ final LvarType type1 = types1.get(symbol);
+ final LvarType type2 = types2.get(symbol);
+ final LvarType widest = widestLvarType(type1, type2);
+ if(widest != type1 && matches1) {
+ matches1 = false;
+ if(!matches2) {
+ union = cloneMap(types1);
+ }
+ }
+ if (widest != type2 && matches2) {
+ matches2 = false;
+ if(!matches1) {
+ union = cloneMap(types2);
+ }
+ }
+ if(!(matches1 || matches2) && union != null) { //remove overly enthusiastic "union can be null" warning
+ assert union != null;
+ union.put(symbol, widest);
+ }
+ }
+ return matches1 ? types1 : matches2 ? types2 : union;
+ }
+ // General case
+ final Map<Symbol, LvarType> union;
+ if(types1Size > types2Size) {
+ union = cloneMap(types1);
+ union.putAll(types2);
+ } else {
+ union = cloneMap(types2);
+ union.putAll(types1);
+ }
+ for(final Symbol symbol: commonSymbols) {
+ final LvarType type1 = types1.get(symbol);
+ final LvarType type2 = types2.get(symbol);
+ union.put(symbol, widestLvarType(type1, type2));
+ }
+ return union;
+ }
+
+ private static void symbolIsUsed(final Symbol symbol, final LvarType type) {
+ if(type != LvarType.UNDEFINED) {
+ symbol.setHasSlotFor(type.type);
+ }
+ }
+
+ private static class SymbolConversions {
+ private static byte I2L = 1 << 0;
+ private static byte I2D = 1 << 1;
+ private static byte I2O = 1 << 2;
+ private static byte L2D = 1 << 3;
+ private static byte L2O = 1 << 4;
+ private static byte D2O = 1 << 5;
+
+ private byte conversions;
+
+ void recordConversion(final LvarType from, final LvarType to) {
+ switch (from) {
+ case UNDEFINED:
+ return;
+ case INT:
+ case BOOLEAN:
+ switch (to) {
+ case LONG:
+ recordConversion(I2L);
+ return;
+ case DOUBLE:
+ recordConversion(I2D);
+ return;
+ case OBJECT:
+ recordConversion(I2O);
+ return;
+ default:
+ illegalConversion(from, to);
+ return;
+ }
+ case LONG:
+ switch (to) {
+ case DOUBLE:
+ recordConversion(L2D);
+ return;
+ case OBJECT:
+ recordConversion(L2O);
+ return;
+ default:
+ illegalConversion(from, to);
+ return;
+ }
+ case DOUBLE:
+ if(to == LvarType.OBJECT) {
+ recordConversion(D2O);
+ }
+ return;
+ default:
+ illegalConversion(from, to);
+ }
+ }
+
+ private static void illegalConversion(final LvarType from, final LvarType to) {
+ throw new AssertionError("Invalid conversion from " + from + " to " + to);
+ }
+
+ void recordConversion(final byte convFlag) {
+ conversions = (byte)(conversions | convFlag);
+ }
+
+ boolean hasConversion(final byte convFlag) {
+ return (conversions & convFlag) != 0;
+ }
+
+ void calculateTypeLiveness(final Symbol symbol) {
+ if(symbol.hasSlotFor(Type.OBJECT)) {
+ if(hasConversion(D2O)) {
+ symbol.setHasSlotFor(Type.NUMBER);
+ }
+ if(hasConversion(L2O)) {
+ symbol.setHasSlotFor(Type.LONG);
+ }
+ if(hasConversion(I2O)) {
+ symbol.setHasSlotFor(Type.INT);
+ }
+ }
+ if(symbol.hasSlotFor(Type.NUMBER)) {
+ if(hasConversion(L2D)) {
+ symbol.setHasSlotFor(Type.LONG);
+ }
+ if(hasConversion(I2D)) {
+ symbol.setHasSlotFor(Type.INT);
+ }
+ }
+ if(symbol.hasSlotFor(Type.LONG)) {
+ if(hasConversion(I2L)) {
+ symbol.setHasSlotFor(Type.INT);
+ }
+ }
+ }
+ }
+
+ private void symbolIsConverted(final Symbol symbol, final LvarType from, final LvarType to) {
+ SymbolConversions conversions = symbolConversions.get(symbol);
+ if(conversions == null) {
+ conversions = new SymbolConversions();
+ symbolConversions.put(symbol, conversions);
+ }
+ conversions.recordConversion(from, to);
+ }
+
+ private static LvarType toLvarType(final Type type) {
+ assert type != null;
+ final LvarType lvarType = TO_LVAR_TYPE.get(type);
+ if(lvarType != null) {
+ return lvarType;
+ }
+ assert type.isObject();
+ return LvarType.OBJECT;
+ }
+ private static LvarType widestLvarType(final LvarType t1, final LvarType t2) {
+ if(t1 == t2) {
+ return t1;
+ }
+ // Undefined or boolean to anything always widens to object.
+ if(t1.ordinal() < LvarType.INT.ordinal() || t2.ordinal() < LvarType.INT.ordinal()) {
+ return LvarType.OBJECT;
+ }
+ // NOTE: we allow "widening" of long to double even though it can lose precision. ECMAScript doesn't have an
+ // Int64 type anyway, so this loss of precision is actually more conformant to the specification...
+ return LvarType.values()[Math.max(t1.ordinal(), t2.ordinal())];
+ }
+ private final Compiler compiler;
+ private final Map<Label, JumpTarget> jumpTargets = new IdentityHashMap<>();
+ // Local variable type mapping at the currently evaluated point. No map instance is ever modified; setLvarType() always
+ // allocates a new map. Immutability of maps allows for cheap snapshots by just keeping the reference to the current
+ // value.
+ private Map<Symbol, LvarType> localVariableTypes = new IdentityHashMap<>();
+
+ // Whether the current point in the AST is reachable code
+ private boolean reachable = true;
+ // Return type of the function
+ private Type returnType = Type.UNKNOWN;
+ // Synthetic return node that we must insert at the end of the function if it's end is reachable.
+ private ReturnNode syntheticReturn;
+
+ private boolean alreadyEnteredTopLevelFunction;
+
+ // LvarType and conversion information gathered during the top-down pass; applied to nodes in the bottom-up pass.
+ private final Map<JoinPredecessor, LocalVariableConversion> localVariableConversions = new IdentityHashMap<>();
+
+ private final Map<IdentNode, LvarType> identifierLvarTypes = new IdentityHashMap<>();
+ private final Map<Symbol, SymbolConversions> symbolConversions = new IdentityHashMap<>();
+
+ private SymbolToType symbolToType = new SymbolToType();
+
+ // Stack of open labels for starts of catch blocks, one for every currently traversed try block; for inserting
+ // control flow edges to them. Note that we currently don't insert actual control flow edges, but instead edges that
+ // help us with type calculations. This means that some operations that can result in an exception being thrown
+ // aren't considered (function calls, side effecting property getters and setters etc.), while some operations that
+ // don't result in control flow transfers do originate an edge to the catch blocks (namely, assignments to local
+ // variables).
+ private final Deque<Label> catchLabels = new ArrayDeque<>();
+
+ LocalVariableTypesCalculator(final Compiler compiler) {
+ super(new LexicalContext());
+ this.compiler = compiler;
+ }
+
+ private JumpTarget createJumpTarget(final Label label) {
+ assert !jumpTargets.containsKey(label);
+ final JumpTarget jumpTarget = new JumpTarget();
+ jumpTargets.put(label, jumpTarget);
+ return jumpTarget;
+ }
+
+ private void doesNotContinueSequentially() {
+ reachable = false;
+ localVariableTypes = Collections.emptyMap();
+ }
+
+
+ @Override
+ public boolean enterBinaryNode(final BinaryNode binaryNode) {
+ // NOTE: regardless of operator's lexical associativity, lhs is always evaluated first.
+ final Expression lhs = binaryNode.lhs();
+ final boolean isAssignment = binaryNode.isAssignment();
+ LvarType lhsTypeOnLoad = null;
+ if(isAssignment) {
+ if(lhs instanceof BaseNode) {
+ ((BaseNode)lhs).getBase().accept(this);
+ if(lhs instanceof IndexNode) {
+ ((IndexNode)lhs).getIndex().accept(this);
+ } else {
+ assert lhs instanceof AccessNode;
+ }
+ } else {
+ assert lhs instanceof IdentNode;
+ if(binaryNode.isSelfModifying()) {
+ final IdentNode ident = ((IdentNode)lhs);
+ ident.accept(this);
+ // Self-assignment can cause a change in the type of the variable. For purposes of evaluating
+ // the type of the operation, we must use its type as it was when it was loaded. If we didn't
+ // do this, some awkward expressions would end up being calculated incorrectly, e.g.
+ // "var x; x += x = 0;". In this case we have undefined+int so the result type is double (NaN).
+ // However, if we used the type of "x" on LHS after we evaluated RHS, we'd see int+int, so the
+ // result type would be either optimistic int or pessimistic long, which would be wrong.
+ lhsTypeOnLoad = getLocalVariableTypeIfBytecode(ident.getSymbol());
+ }
+ }
+ } else {
+ lhs.accept(this);
+ }
+
+ final boolean isLogical = binaryNode.isLogical();
+ assert !(isAssignment && isLogical); // there are no logical assignment operators in JS
+ final Label joinLabel = isLogical ? new Label("") : null;
+ if(isLogical) {
+ jumpToLabel((JoinPredecessor)lhs, joinLabel);
+ }
+
+ final Expression rhs = binaryNode.rhs();
+ rhs.accept(this);
+ if(isLogical) {
+ jumpToLabel((JoinPredecessor)rhs, joinLabel);
+ }
+ joinOnLabel(joinLabel);
+
+ if(isAssignment && lhs instanceof IdentNode) {
+ if(binaryNode.isSelfModifying()) {
+ onSelfAssignment((IdentNode)lhs, binaryNode, lhsTypeOnLoad);
+ } else {
+ onAssignment((IdentNode)lhs, rhs);
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean enterBlock(final Block block) {
+ for(final Symbol symbol: block.getSymbols()) {
+ if(symbol.isBytecodeLocal() && getLocalVariableTypeOrNull(symbol) == null) {
+ setType(symbol, LvarType.UNDEFINED);
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public boolean enterBreakNode(final BreakNode breakNode) {
+ return enterJumpStatement(breakNode);
+ }
+
+ @Override
+ public boolean enterContinueNode(final ContinueNode continueNode) {
+ return enterJumpStatement(continueNode);
+ }
+
+ private boolean enterJumpStatement(final JumpStatement jump) {
+ if(!reachable) {
+ return false;
+ }
+ final BreakableNode target = jump.getTarget(lc);
+ jumpToLabel(jump, jump.getTargetLabel(target), getBreakTargetTypes(target));
+ doesNotContinueSequentially();
+ return false;
+ }
+
+ @Override
+ protected boolean enterDefault(final Node node) {
+ return reachable;
+ }
+
+ private void enterDoWhileLoop(final WhileNode loopNode) {
+ final JoinPredecessorExpression test = loopNode.getTest();
+ final Block body = loopNode.getBody();
+ final Label continueLabel = loopNode.getContinueLabel();
+ final Label breakLabel = loopNode.getBreakLabel();
+ final Map<Symbol, LvarType> beforeLoopTypes = localVariableTypes;
+ final Label repeatLabel = new Label("");
+ for(;;) {
+ jumpToLabel(loopNode, repeatLabel, beforeLoopTypes);
+ final Map<Symbol, LvarType> beforeRepeatTypes = localVariableTypes;
+ body.accept(this);
+ if(reachable) {
+ jumpToLabel(body, continueLabel);
+ }
+ joinOnLabel(continueLabel);
+ if(!reachable) {
+ break;
+ }
+ test.accept(this);
+ jumpToLabel(test, breakLabel);
+ if(isAlwaysFalse(test)) {
+ break;
+ }
+ jumpToLabel(test, repeatLabel);
+ joinOnLabel(repeatLabel);
+ if(localVariableTypes.equals(beforeRepeatTypes)) {
+ break;
+ }
+ resetJoinPoint(continueLabel);
+ resetJoinPoint(breakLabel);
+ resetJoinPoint(repeatLabel);
+ }
+
+ if(isAlwaysTrue(test)) {
+ doesNotContinueSequentially();
+ }
+
+ leaveBreakable(loopNode);
+ }
+
+ @Override
+ public boolean enterForNode(final ForNode forNode) {
+ if(!reachable) {
+ return false;
+ }
+
+ final Expression init = forNode.getInit();
+ if(forNode.isForIn()) {
+ final JoinPredecessorExpression iterable = forNode.getModify();
+ iterable.accept(this);
+ enterTestFirstLoop(forNode, null, init,
+ // If we're iterating over property names, and we can discern from the runtime environment
+ // of the compilation that the object being iterated over must use strings for property
+ // names (e.g., it is a native JS object or array), then we'll not bother trying to treat
+ // the property names optimistically.
+ !compiler.useOptimisticTypes() || (!forNode.isForEach() && compiler.hasStringPropertyIterator(iterable.getExpression())));
+ } else {
+ if(init != null) {
+ init.accept(this);
+ }
+ enterTestFirstLoop(forNode, forNode.getModify(), null, false);
+ }
+ return false;
+ }
+
+ @Override
+ public boolean enterFunctionNode(final FunctionNode functionNode) {
+ if(alreadyEnteredTopLevelFunction) {
+ return false;
+ }
+ int pos = 0;
+ if(!functionNode.isVarArg()) {
+ for (final IdentNode param : functionNode.getParameters()) {
+ final Symbol symbol = param.getSymbol();
+ // Parameter is not necessarily bytecode local as it can be scoped due to nested context use, but it
+ // must have a slot if we aren't in a function with vararg signature.
+ assert symbol.hasSlot();
+ final Type callSiteParamType = compiler.getParamType(functionNode, pos);
+ final LvarType paramType = callSiteParamType == null ? LvarType.OBJECT : toLvarType(callSiteParamType);
+ setType(symbol, paramType);
+ // Make sure parameter slot for its incoming value is not marked dead. NOTE: this is a heuristic. Right
+ // now, CodeGenerator.expandParameters() relies on the fact that every parameter's final slot width will
+ // be at least the same as incoming width, therefore even if a parameter is never read, we'll still keep
+ // its slot.
+ symbolIsUsed(symbol);
+ setIdentifierLvarType(param, paramType);
+ pos++;
+ }
+ }
+ setCompilerConstantAsObject(functionNode, CompilerConstants.THIS);
+
+ // TODO: coarse-grained. If we wanted to solve it completely precisely,
+ // we'd also need to push/pop its type when handling WithNode (so that
+ // it can go back to undefined after a 'with' block.
+ if(functionNode.hasScopeBlock() || functionNode.needsParentScope()) {
+ setCompilerConstantAsObject(functionNode, CompilerConstants.SCOPE);
+ }
+ if(functionNode.needsCallee()) {
+ setCompilerConstantAsObject(functionNode, CompilerConstants.CALLEE);
+ }
+ if(functionNode.needsArguments()) {
+ setCompilerConstantAsObject(functionNode, CompilerConstants.ARGUMENTS);
+ }
+
+ alreadyEnteredTopLevelFunction = true;
+ return true;
+ }
+
+ @Override
+ public boolean enterIdentNode(final IdentNode identNode) {
+ final Symbol symbol = identNode.getSymbol();
+ if(symbol.isBytecodeLocal()) {
+ symbolIsUsed(symbol);
+ setIdentifierLvarType(identNode, getLocalVariableType(symbol));
+ }
+ return false;
+ }
+
+ @Override
+ public boolean enterIfNode(final IfNode ifNode) {
+ if(!reachable) {
+ return false;
+ }
+
+ final Expression test = ifNode.getTest();
+ final Block pass = ifNode.getPass();
+ final Block fail = ifNode.getFail();
+
+ test.accept(this);
+
+ final Map<Symbol, LvarType> afterTestLvarTypes = localVariableTypes;
+ if(!isAlwaysFalse(test)) {
+ pass.accept(this);
+ }
+ final Map<Symbol, LvarType> passLvarTypes = localVariableTypes;
+ final boolean reachableFromPass = reachable;
+
+ reachable = true;
+ localVariableTypes = afterTestLvarTypes;
+ if(!isAlwaysTrue(test) && fail != null) {
+ fail.accept(this);
+ final boolean reachableFromFail = reachable;
+ reachable |= reachableFromPass;
+ if(!reachable) {
+ return false;
+ }
+
+ if(reachableFromFail) {
+ if(reachableFromPass) {
+ final Map<Symbol, LvarType> failLvarTypes = localVariableTypes;
+ localVariableTypes = getUnionTypes(passLvarTypes, failLvarTypes);
+ setConversion(pass, passLvarTypes, localVariableTypes);
+ setConversion(fail, failLvarTypes, localVariableTypes);
+ }
+ return false;
+ }
+ }
+
+ if(reachableFromPass) {
+ localVariableTypes = getUnionTypes(afterTestLvarTypes, passLvarTypes);
+ // IfNode itself is associated with conversions that might need to be performed after the test if there's no
+ // else branch. E.g.
+ // if(x = 1, cond) { x = 1.0 } must widen "x = 1" to a double.
+ setConversion(pass, passLvarTypes, localVariableTypes);
+ setConversion(ifNode, afterTestLvarTypes, localVariableTypes);
+ } else {
+ localVariableTypes = afterTestLvarTypes;
+ }
+
+ return false;
+ }
+
+ @Override
+ public boolean enterPropertyNode(final PropertyNode propertyNode) {
+ // Avoid falsely adding property keys to the control flow graph
+ if(propertyNode.getValue() != null) {
+ propertyNode.getValue().accept(this);
+ }
+ return false;
+ }
+
+ @Override
+ public boolean enterReturnNode(final ReturnNode returnNode) {
+ if(!reachable) {
+ return false;
+ }
+
+ final Expression returnExpr = returnNode.getExpression();
+ final Type returnExprType;
+ if(returnExpr != null) {
+ returnExpr.accept(this);
+ returnExprType = getType(returnExpr);
+ } else {
+ returnExprType = Type.UNDEFINED;
+ }
+ returnType = Type.widestReturnType(returnType, returnExprType);
+ doesNotContinueSequentially();
+ return false;
+ }
+
+ @Override
+ public boolean enterSplitReturn(final SplitReturn splitReturn) {
+ doesNotContinueSequentially();
+ return false;
+ }
+
+ @Override
+ public boolean enterSwitchNode(final SwitchNode switchNode) {
+ if(!reachable) {
+ return false;
+ }
+
+ final Expression expr = switchNode.getExpression();
+ expr.accept(this);
+
+ final List<CaseNode> cases = switchNode.getCases();
+ if(cases.isEmpty()) {
+ return false;
+ }
+
+ // Control flow is different for all-integer cases where we dispatch by switch table, and for all other cases
+ // where we do sequential comparison. Note that CaseNode objects act as join points.
+ final boolean isInteger = switchNode.isUniqueInteger();
+ final Label breakLabel = switchNode.getBreakLabel();
+ final boolean hasDefault = switchNode.getDefaultCase() != null;
+
+ boolean tagUsed = false;
+ for(final CaseNode caseNode: cases) {
+ final Expression test = caseNode.getTest();
+ if(!isInteger && test != null) {
+ test.accept(this);
+ if(!tagUsed) {
+ symbolIsUsed(switchNode.getTag(), LvarType.OBJECT);
+ tagUsed = true;
+ }
+ }
+ // CaseNode carries the conversions that need to be performed on its entry from the test.
+ // CodeGenerator ensures these are only emitted when arriving on the branch and not through a
+ // fallthrough.
+ jumpToLabel(caseNode, caseNode.getBody().getEntryLabel());
+ }
+ if(!hasDefault) {
+ // No default case means we can arrive at the break label without entering any cases. In that case
+ // SwitchNode will carry the conversions that need to be performed before it does that jump.
+ jumpToLabel(switchNode, breakLabel);
+ }
+
+ // All cases are arrived at through jumps
+ doesNotContinueSequentially();
+
+ Block previousBlock = null;
+ for(final CaseNode caseNode: cases) {
+ final Block body = caseNode.getBody();
+ final Label entryLabel = body.getEntryLabel();
+ if(previousBlock != null && reachable) {
+ jumpToLabel(previousBlock, entryLabel);
+ }
+ joinOnLabel(entryLabel);
+ assert reachable == true;
+ body.accept(this);
+ previousBlock = body;
+ }
+ if(previousBlock != null && reachable) {
+ jumpToLabel(previousBlock, breakLabel);
+ }
+ leaveBreakable(switchNode);
+ return false;
+ }
+
+ @Override
+ public boolean enterTernaryNode(final TernaryNode ternaryNode) {
+ final Expression test = ternaryNode.getTest();
+ final Expression trueExpr = ternaryNode.getTrueExpression();
+ final Expression falseExpr = ternaryNode.getFalseExpression();
+
+ test.accept(this);
+
+ final Map<Symbol, LvarType> testExitLvarTypes = localVariableTypes;
+ if(!isAlwaysFalse(test)) {
+ trueExpr.accept(this);
+ }
+ final Map<Symbol, LvarType> trueExitLvarTypes = localVariableTypes;
+ localVariableTypes = testExitLvarTypes;
+ if(!isAlwaysTrue(test)) {
+ falseExpr.accept(this);
+ }
+ final Map<Symbol, LvarType> falseExitLvarTypes = localVariableTypes;
+ localVariableTypes = getUnionTypes(trueExitLvarTypes, falseExitLvarTypes);
+ setConversion((JoinPredecessor)trueExpr, trueExitLvarTypes, localVariableTypes);
+ setConversion((JoinPredecessor)falseExpr, falseExitLvarTypes, localVariableTypes);
+ return false;
+ }
+
+ private void enterTestFirstLoop(final LoopNode loopNode, final JoinPredecessorExpression modify,
+ final Expression iteratorValues, final boolean iteratorValuesAreObject) {
+ final JoinPredecessorExpression test = loopNode.getTest();
+ if(isAlwaysFalse(test)) {
+ test.accept(this);
+ return;
+ }
+
+ final Label continueLabel = loopNode.getContinueLabel();
+ final Label breakLabel = loopNode.getBreakLabel();
+
+ final Label repeatLabel = modify == null ? continueLabel : new Label("");
+ final Map<Symbol, LvarType> beforeLoopTypes = localVariableTypes;
+ for(;;) {
+ jumpToLabel(loopNode, repeatLabel, beforeLoopTypes);
+ final Map<Symbol, LvarType> beforeRepeatTypes = localVariableTypes;
+ if(test != null) {
+ test.accept(this);
+ }
+ if(!isAlwaysTrue(test)) {
+ jumpToLabel(test, breakLabel);
+ }
+ if(iteratorValues instanceof IdentNode) {
+ final IdentNode ident = (IdentNode)iteratorValues;
+ // Receives iterator values; the optimistic type of the iterator values is tracked on the
+ // identifier, but we override optimism if it's known that the object being iterated over will
+ // never have primitive property names.
+ onAssignment(ident, iteratorValuesAreObject ? LvarType.OBJECT :
+ toLvarType(compiler.getOptimisticType(ident)));
+ }
+ final Block body = loopNode.getBody();
+ body.accept(this);
+ if(reachable) {
+ jumpToLabel(body, continueLabel);
+ }
+ joinOnLabel(continueLabel);
+ if(!reachable) {
+ break;
+ }
+ if(modify != null) {
+ modify.accept(this);
+ jumpToLabel(modify, repeatLabel);
+ joinOnLabel(repeatLabel);
+ }
+ if(localVariableTypes.equals(beforeRepeatTypes)) {
+ break;
+ }
+ // Reset the join points and repeat the analysis
+ resetJoinPoint(continueLabel);
+ resetJoinPoint(breakLabel);
+ resetJoinPoint(repeatLabel);
+ }
+
+ if(isAlwaysTrue(test) && iteratorValues == null) {
+ doesNotContinueSequentially();
+ }
+
+ leaveBreakable(loopNode);
+ }
+
+ @Override
+ public boolean enterThrowNode(final ThrowNode throwNode) {
+ if(!reachable) {
+ return false;
+ }
+
+ throwNode.getExpression().accept(this);
+ jumpToCatchBlock(throwNode);
+ doesNotContinueSequentially();
+ return false;
+ }
+
+ @Override
+ public boolean enterTryNode(final TryNode tryNode) {
+ if(!reachable) {
+ return false;
+ }
+
+ // This is the label for the join point at the entry of the catch blocks.
+ final Label catchLabel = new Label("");
+ catchLabels.push(catchLabel);
+
+ // Presume that even the start of the try block can immediately go to the catch
+ jumpToLabel(tryNode, catchLabel);
+
+ final Block body = tryNode.getBody();
+ body.accept(this);
+ catchLabels.pop();
+
+ // Final exit label for the whole try/catch construct (after the try block and after all catches).
+ final Label endLabel = new Label("");
+
+ boolean canExit = false;
+ if(reachable) {
+ jumpToLabel(body, endLabel);
+ canExit = true;
+ }
+ doesNotContinueSequentially();
+
+ joinOnLabel(catchLabel);
+ for(final CatchNode catchNode: tryNode.getCatches()) {
+ final IdentNode exception = catchNode.getException();
+ onAssignment(exception, LvarType.OBJECT);
+ final Expression condition = catchNode.getExceptionCondition();
+ if(condition != null) {
+ condition.accept(this);
+ }
+ final Map<Symbol, LvarType> afterConditionTypes = localVariableTypes;
+ final Block catchBody = catchNode.getBody();
+ // TODO: currently, we consider that the catch blocks are always reachable from the try block as currently
+ // we lack enough analysis to prove that no statement before a break/continue/return in the try block can
+ // throw an exception.
+ reachable = true;
+ catchBody.accept(this);
+ final Symbol exceptionSymbol = exception.getSymbol();
+ if(reachable) {
+ localVariableTypes = cloneMap(localVariableTypes);
+ localVariableTypes.remove(exceptionSymbol);
+ jumpToLabel(catchBody, endLabel);
+ canExit = true;
+ }
+ localVariableTypes = cloneMap(afterConditionTypes);
+ localVariableTypes.remove(exceptionSymbol);
+ }
+ // NOTE: if we had one or more conditional catch blocks with no unconditional catch block following them, then
+ // there will be an unconditional rethrow, so the join point can never be reached from the last
+ // conditionExpression.
+ doesNotContinueSequentially();
+
+ if(canExit) {
+ joinOnLabel(endLabel);
+ }
+
+ return false;
+ }
+
+
+ @Override
+ public boolean enterUnaryNode(final UnaryNode unaryNode) {
+ final Expression expr = unaryNode.getExpression();
+ expr.accept(this);
+
+ if(unaryNode.isSelfModifying()) {
+ if(expr instanceof IdentNode) {
+ final IdentNode ident = (IdentNode)expr;
+ onSelfAssignment(ident, unaryNode, getLocalVariableTypeIfBytecode(ident.getSymbol()));
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean enterVarNode(final VarNode varNode) {
+ if (!reachable) {
+ return false;
+ }
+ final Expression init = varNode.getInit();
+ if(init != null) {
+ init.accept(this);
+ onAssignment(varNode.getName(), init);
+ }
+ return false;
+ }
+
+ @Override
+ public boolean enterWhileNode(final WhileNode whileNode) {
+ if(!reachable) {
+ return false;
+ }
+ if(whileNode.isDoWhile()) {
+ enterDoWhileLoop(whileNode);
+ } else {
+ enterTestFirstLoop(whileNode, null, null, false);
+ }
+ return false;
+ }
+
+ private Map<Symbol, LvarType> getBreakTargetTypes(final BreakableNode target) {
+ // Remove symbols defined in the the blocks that are being broken out of.
+ Map<Symbol, LvarType> types = localVariableTypes;
+ for(final Iterator<LexicalContextNode> it = lc.getAllNodes(); it.hasNext();) {
+ final LexicalContextNode node = it.next();
+ if(node instanceof Block) {
+ for(final Symbol symbol: ((Block)node).getSymbols()) {
+ if(localVariableTypes.containsKey(symbol)) {
+ if(types == localVariableTypes) {
+ types = cloneMap(localVariableTypes);
+ }
+ types.remove(symbol);
+ }
+ }
+ }
+ if(node == target) {
+ break;
+ }
+ }
+ return types;
+ }
+
+ /**
+ * Returns the current type of the local variable represented by the symbol. This is the most strict of all
+ * {@code getLocalVariableType*} methods, as it will throw an assertion if the type is null. Therefore, it is only
+ * safe to be invoked on symbols known to be bytecode locals, and only after they have been initialized.
+ * Regardless, it is recommended to use this method in majority of cases, as because of its strictness it is the
+ * best suited for catching missing type calculation bugs early.
+ * @param symbol a symbol representing a bytecode local variable.
+ * @return the current type of the local variable represented by the symbol
+ */
+ private LvarType getLocalVariableType(final Symbol symbol) {
+ final LvarType type = getLocalVariableTypeOrNull(symbol);
+ assert type != null;
+ return type;
+ }
+
+ /**
+ * Gets the type for a local variable if it is a bytecode local, otherwise null. Can be used in circumstances where
+ * the type is irrelevant if the symbol is not a bytecode local. Note that for bytecode locals, it delegates to
+ * {@link #getLocalVariableType(Symbol)}, so it will still assert that the type for such variable is already
+ * defined (that is, not null).
+ * @param symbol the symbol representing the variable.
+ * @return the current variable type, if it is a bytecode local, otherwise null.
+ */
+ private LvarType getLocalVariableTypeIfBytecode(final Symbol symbol) {
+ return symbol.isBytecodeLocal() ? getLocalVariableType(symbol) : null;
+ }
+
+ /**
+ * Gets the type for a variable represented by a symbol, or null if the type is not know. This is the least strict
+ * of all local variable type getters, and as such its use is discouraged except in initialization scenarios (where
+ * a just-defined symbol might still be null).
+ * @param symbol the symbol
+ * @return the current type for the symbol, or null if the type is not known either because the symbol has not been
+ * initialized, or because the symbol does not represent a bytecode local variable.
+ */
+ private LvarType getLocalVariableTypeOrNull(final Symbol symbol) {
+ return localVariableTypes.get(symbol);
+ }
+
+ private JumpTarget getOrCreateJumpTarget(final Label label) {
+ JumpTarget jumpTarget = jumpTargets.get(label);
+ if(jumpTarget == null) {
+ jumpTarget = createJumpTarget(label);
+ }
+ return jumpTarget;
+ }
+
+
+ /**
+ * If there's a join point associated with a label, insert the join point into the flow.
+ * @param label the label to insert a join point for.
+ */
+ private void joinOnLabel(final Label label) {
+ final JumpTarget jumpTarget = jumpTargets.remove(label);
+ if(jumpTarget == null) {
+ return;
+ }
+ assert !jumpTarget.origins.isEmpty();
+ reachable = true;
+ localVariableTypes = getUnionTypes(jumpTarget.types, localVariableTypes);
+ for(final JumpOrigin jumpOrigin: jumpTarget.origins) {
+ setConversion(jumpOrigin.node, jumpOrigin.types, localVariableTypes);
+ }
+ }
+
+ /**
+ * If we're in a try/catch block, add an edge from the specified node to the try node's pre-catch label.
+ */
+ private void jumpToCatchBlock(final JoinPredecessor jumpOrigin) {
+ final Label currentCatchLabel = catchLabels.peek();
+ if(currentCatchLabel != null) {
+ jumpToLabel(jumpOrigin, currentCatchLabel);
+ }
+ }
+
+ private void jumpToLabel(final JoinPredecessor jumpOrigin, final Label label) {
+ jumpToLabel(jumpOrigin, label, localVariableTypes);
+ }
+
+ private void jumpToLabel(final JoinPredecessor jumpOrigin, final Label label, final Map<Symbol, LvarType> types) {
+ getOrCreateJumpTarget(label).addOrigin(jumpOrigin, types);
+ }
+
+ @Override
+ public Node leaveBlock(final Block block) {
+ if(lc.isFunctionBody()) {
+ if(reachable) {
+ // reachable==true means we can reach the end of the function without an explicit return statement. We
+ // need to insert a synthetic one then. This logic used to be in Lower.leaveBlock(), but Lower's
+ // reachability analysis (through Terminal.isTerminal() flags) is not precise enough so
+ // Lower$BlockLexicalContext.afterSetStatements will sometimes think the control flow terminates even
+ // when it didn't. Example: function() { switch((z)) { default: {break; } throw x; } }.
+ createSyntheticReturn(block);
+ assert !reachable;
+ }
+ // We must calculate the return type here (and not in leaveFunctionNode) as it can affect the liveness of
+ // the :return symbol and thus affect conversion type liveness calculations for it.
+ calculateReturnType();
+ }
+
+ boolean cloned = false;
+ for(final Symbol symbol: block.getSymbols()) {
+ // Undefine the symbol outside the block
+ if(localVariableTypes.containsKey(symbol)) {
+ if(!cloned) {
+ localVariableTypes = cloneMap(localVariableTypes);
+ cloned = true;
+ }
+ localVariableTypes.remove(symbol);
+ }
+
+ if(symbol.hasSlot()) {
+ final SymbolConversions conversions = symbolConversions.get(symbol);
+ if(conversions != null) {
+ // Potentially make some currently dead types live if they're needed as a source of a type
+ // conversion at a join.
+ conversions.calculateTypeLiveness(symbol);
+ }
+ if(symbol.slotCount() == 0) {
+ // This is a local variable that is never read. It won't need a slot.
+ symbol.setNeedsSlot(false);
+ }
+ }
+ }
+
+ if(reachable) {
+ // TODO: this is totally backwards. Block should not be breakable, LabelNode should be breakable.
+ final LabelNode labelNode = lc.getCurrentBlockLabelNode();
+ if(labelNode != null) {
+ jumpToLabel(labelNode, block.getBreakLabel());
+ }
+ }
+ leaveBreakable(block);
+ return block;
+ }
+
+ private void calculateReturnType() {
+ // NOTE: if return type is unknown, then the function does not explicitly return a value. Such a function under
+ // ECMAScript rules returns Undefined, which has Type.OBJECT. We might consider an optimization in the future
+ // where we can return void functions.
+ if(returnType.isUnknown()) {
+ returnType = Type.OBJECT;
+ }
+ }
+
+ private void createSyntheticReturn(final Block body) {
+ final FunctionNode functionNode = lc.getCurrentFunction();
+ final long token = functionNode.getToken();
+ final int finish = functionNode.getFinish();
+ final List<Statement> statements = body.getStatements();
+ final int lineNumber = statements.isEmpty() ? functionNode.getLineNumber() : statements.get(statements.size() - 1).getLineNumber();
+ final IdentNode returnExpr;
+ if(functionNode.isProgram()) {
+ returnExpr = new IdentNode(token, finish, RETURN.symbolName()).setSymbol(getCompilerConstantSymbol(functionNode, RETURN));
+ } else {
+ returnExpr = null;
+ }
+ syntheticReturn = new ReturnNode(lineNumber, token, finish, returnExpr);
+ syntheticReturn.accept(this);
+ }
+
+ /**
+ * Leave a breakable node. If there's a join point associated with its break label (meaning there was at least one
+ * break statement to the end of the node), insert the join point into the flow.
+ * @param breakable the breakable node being left.
+ */
+ private void leaveBreakable(final BreakableNode breakable) {
+ joinOnLabel(breakable.getBreakLabel());
+ }
+
+ @Override
+ public Node leaveFunctionNode(final FunctionNode functionNode) {
+ // Sets the return type of the function and also performs the bottom-up pass of applying type and conversion
+ // information to nodes as well as doing the calculation on nested functions as required.
+ FunctionNode newFunction = functionNode;
+ final NodeVisitor<LexicalContext> applyChangesVisitor = new NodeVisitor<LexicalContext>(new LexicalContext()) {
+ private boolean inOuterFunction = true;
+ private final Deque<JoinPredecessor> joinPredecessors = new ArrayDeque<>();
+
+ @Override
+ protected boolean enterDefault(final Node node) {
+ if(!inOuterFunction) {
+ return false;
+ }
+ if(node instanceof JoinPredecessor) {
+ joinPredecessors.push((JoinPredecessor)node);
+ }
+ return inOuterFunction;
+ }
+
+ @Override
+ public boolean enterFunctionNode(final FunctionNode fn) {
+ if(compiler.isOnDemandCompilation()) {
+ // Only calculate nested function local variable types if we're doing eager compilation
+ return false;
+ }
+ inOuterFunction = false;
+ return true;
+ }
+
+ @SuppressWarnings("fallthrough")
+ @Override
+ public Node leaveBinaryNode(final BinaryNode binaryNode) {
+ if(binaryNode.isComparison()) {
+ final Expression lhs = binaryNode.lhs();
+ final Expression rhs = binaryNode.rhs();
+
+ Type cmpWidest = Type.widest(lhs.getType(), rhs.getType());
+ boolean newRuntimeNode = false, finalized = false;
+ final TokenType tt = binaryNode.tokenType();
+ switch (tt) {
+ case EQ_STRICT:
+ case NE_STRICT:
+ // Specialize comparison with undefined
+ final Expression undefinedNode = createIsUndefined(binaryNode, lhs, rhs,
+ tt == TokenType.EQ_STRICT ? Request.IS_UNDEFINED : Request.IS_NOT_UNDEFINED);
+ if(undefinedNode != binaryNode) {
+ return undefinedNode;
+ }
+ // Specialize comparison of boolean with non-boolean
+ if (lhs.getType().isBoolean() != rhs.getType().isBoolean()) {
+ newRuntimeNode = true;
+ cmpWidest = Type.OBJECT;
+ finalized = true;
+ }
+ // fallthrough
+ default:
+ if (newRuntimeNode || cmpWidest.isObject()) {
+ return new RuntimeNode(binaryNode).setIsFinal(finalized);
+ }
+ }
+ } else if(binaryNode.isOptimisticUndecidedType()) {
+ // At this point, we can assign a static type to the optimistic binary ADD operator as now we know
+ // the types of its operands.
+ return binaryNode.decideType();
+ }
+ return binaryNode;
+ }
+
+ @Override
+ protected Node leaveDefault(final Node node) {
+ if(node instanceof JoinPredecessor) {
+ final JoinPredecessor original = joinPredecessors.pop();
+ assert original.getClass() == node.getClass() : original.getClass().getName() + "!=" + node.getClass().getName();
+ return (Node)setLocalVariableConversion(original, (JoinPredecessor)node);
+ }
+ return node;
+ }
+
+ @Override
+ public Node leaveBlock(final Block block) {
+ if(inOuterFunction && syntheticReturn != null && lc.isFunctionBody()) {
+ final ArrayList<Statement> stmts = new ArrayList<>(block.getStatements());
+ stmts.add((ReturnNode)syntheticReturn.accept(this));
+ return block.setStatements(lc, stmts);
+ }
+ return super.leaveBlock(block);
+ }
+
+ @Override
+ public Node leaveFunctionNode(final FunctionNode nestedFunctionNode) {
+ inOuterFunction = true;
+ final FunctionNode newNestedFunction = (FunctionNode)nestedFunctionNode.accept(
+ new LocalVariableTypesCalculator(compiler));
+ lc.replace(nestedFunctionNode, newNestedFunction);
+ return newNestedFunction;
+ }
+
+ @Override
+ public Node leaveIdentNode(final IdentNode identNode) {
+ final IdentNode original = (IdentNode)joinPredecessors.pop();
+ final Symbol symbol = identNode.getSymbol();
+ if(symbol == null) {
+ assert identNode.isPropertyName();
+ return identNode;
+ } else if(symbol.hasSlot()) {
+ assert !symbol.isScope() || symbol.isParam(); // Only params can be slotted and scoped.
+ assert original.getName().equals(identNode.getName());
+ final LvarType lvarType = identifierLvarTypes.remove(original);
+ if(lvarType != null) {
+ return setLocalVariableConversion(original, identNode.setType(lvarType.type));
+ }
+ // If there's no type, then the identifier must've been in unreachable code. In that case, it can't
+ // have assigned conversions either.
+ assert localVariableConversions.get(original) == null;
+ } else {
+ assert identIsDeadAndHasNoLiveConversions(original);
+ }
+ return identNode;
+ }
+
+ @Override
+ public Node leaveLiteralNode(final LiteralNode<?> literalNode) {
+ //for e.g. ArrayLiteralNodes the initial types may have been narrowed due to the
+ //introduction of optimistic behavior - hence ensure that all literal nodes are
+ //reinitialized
+ return literalNode.initialize(lc);
+ }
+
+ @Override
+ public Node leaveRuntimeNode(final RuntimeNode runtimeNode) {
+ final Request request = runtimeNode.getRequest();
+ final boolean isEqStrict = request == Request.EQ_STRICT;
+ if(isEqStrict || request == Request.NE_STRICT) {
+ return createIsUndefined(runtimeNode, runtimeNode.getArgs().get(0), runtimeNode.getArgs().get(1),
+ isEqStrict ? Request.IS_UNDEFINED : Request.IS_NOT_UNDEFINED);
+ }
+ return runtimeNode;
+ }
+
+ @SuppressWarnings("unchecked")
+ private <T extends JoinPredecessor> T setLocalVariableConversion(final JoinPredecessor original, final T jp) {
+ // NOTE: can't use Map.remove() as our copy-on-write AST semantics means some nodes appear twice (in
+ // finally blocks), so we need to be able to access conversions for them multiple times.
+ return (T)jp.setLocalVariableConversion(lc, localVariableConversions.get(original));
+ }
+ };
+
+ newFunction = newFunction.setBody(lc, (Block)newFunction.getBody().accept(applyChangesVisitor));
+ newFunction = newFunction.setReturnType(lc, returnType);
+
+
+ newFunction = newFunction.setState(lc, CompilationState.LOCAL_VARIABLE_TYPES_CALCULATED);
+ newFunction = newFunction.setParameters(lc, newFunction.visitParameters(applyChangesVisitor));
+ return newFunction;
+ }
+
+ private static Expression createIsUndefined(final Expression parent, final Expression lhs, final Expression rhs, final Request request) {
+ if (isUndefinedIdent(lhs) || isUndefinedIdent(rhs)) {
+ return new RuntimeNode(parent, request, lhs, rhs);
+ }
+ return parent;
+ }
+
+ private static boolean isUndefinedIdent(final Expression expr) {
+ return expr instanceof IdentNode && "undefined".equals(((IdentNode)expr).getName());
+ }
+
+ private boolean identIsDeadAndHasNoLiveConversions(final IdentNode identNode) {
+ final LocalVariableConversion conv = localVariableConversions.get(identNode);
+ return conv == null || !conv.isLive();
+ }
+
+ private void onAssignment(final IdentNode identNode, final Expression rhs) {
+ onAssignment(identNode, toLvarType(getType(rhs)));
+ }
+
+ private void onAssignment(final IdentNode identNode, final LvarType type) {
+ final Symbol symbol = identNode.getSymbol();
+ assert symbol != null : identNode.getName();
+ if(!symbol.isBytecodeLocal()) {
+ return;
+ }
+ assert type != null;
+ final LvarType finalType;
+ if(type == LvarType.UNDEFINED && getLocalVariableType(symbol) != LvarType.UNDEFINED) {
+ // Explicit assignment of a known undefined local variable to a local variable that is not undefined will
+ // materialize that undefined in the assignment target. Note that assigning known undefined to known
+ // undefined will *not* initialize the variable, e.g. "var x; var y = x;" compiles to no-op.
+ finalType = LvarType.OBJECT;
+ symbol.setFlag(Symbol.HAS_OBJECT_VALUE);
+ } else {
+ finalType = type;
+ }
+ setType(symbol, finalType);
+ // Explicit assignment of an undefined value. Make sure the variable can store an object
+ // TODO: if we communicated the fact to codegen with a flag on the IdentNode that the value was already
+ // undefined before the assignment, we could just ignore it. In general, we could ignore an assignment if we
+ // know that the value assigned is the same as the current value of the variable, but we'd need constant
+ // propagation for that.
+ setIdentifierLvarType(identNode, finalType);
+ // For purposes of type calculation, we consider an assignment to a local variable to be followed by
+ // the catch nodes of the current (if any) try block. This will effectively enforce that narrower
+ // assignments to a local variable in a try block will also have to store a widened value as well. Code
+ // within the try block will be able to keep loading the narrower value, but after the try block only
+ // the widest value will remain live.
+ // Rationale for this is that if there's an use for that variable in any of the catch blocks, or
+ // following the catch blocks, they must use the widest type.
+ // Example:
+ /*
+ Originally:
+ ===========
+ var x;
+ try {
+ x = 1; <-- stores into int slot for x
+ f(x); <-- loads the int slot for x
+ x = 3.14 <-- stores into the double slot for x
+ f(x); <-- loads the double slot for x
+ x = 1; <-- stores into int slot for x
+ f(x); <-- loads the int slot for x
+ } finally {
+ f(x); <-- loads the double slot for x, but can be reached by a path where x is int, so we need
+ to go back and ensure that double values are also always stored along with int
+ values.
+ }
+
+ After correction:
+ =================
+
+ var x;
+ try {
+ x = 1; <-- stores into both int and double slots for x
+ f(x); <-- loads the int slot for x
+ x = 3.14 <-- stores into the double slot for x
+ f(x); <-- loads the double slot for x
+ x = 1; <-- stores into both int and double slots for x
+ f(x); <-- loads the int slot for x
+ } finally {
+ f(x); <-- loads the double slot for x
+ }
+ */
+ jumpToCatchBlock(identNode);
+ }
+
+ private void onSelfAssignment(final IdentNode identNode, final Expression assignment, final LvarType typeOnLoad) {
+ final Symbol symbol = identNode.getSymbol();
+ assert symbol != null : identNode.getName();
+ if(!symbol.isBytecodeLocal()) {
+ return;
+ }
+ final LvarType type = toLvarType(getType(assignment, symbol, typeOnLoad.type));
+ // Self-assignment never produce either a boolean or undefined
+ assert type != null && type != LvarType.UNDEFINED && type != LvarType.BOOLEAN;
+ setType(symbol, type);
+ jumpToCatchBlock(identNode);
+ }
+
+ private void resetJoinPoint(final Label label) {
+ jumpTargets.remove(label);
+ }
+
+ private void setCompilerConstantAsObject(final FunctionNode functionNode, final CompilerConstants cc) {
+ final Symbol symbol = getCompilerConstantSymbol(functionNode, cc);
+ setType(symbol, LvarType.OBJECT);
+ // never mark compiler constants as dead
+ symbolIsUsed(symbol);
+ }
+
+ private static Symbol getCompilerConstantSymbol(final FunctionNode functionNode, final CompilerConstants cc) {
+ return functionNode.getBody().getExistingSymbol(cc.symbolName());
+ }
+
+ private void setConversion(final JoinPredecessor node, final Map<Symbol, LvarType> branchLvarTypes, final Map<Symbol, LvarType> joinLvarTypes) {
+ if(node == null) {
+ return;
+ }
+ if(branchLvarTypes.isEmpty() || joinLvarTypes.isEmpty()) {
+ localVariableConversions.remove(node);
+ }
+
+ LocalVariableConversion conversion = null;
+ if(node instanceof IdentNode) {
+ // conversions on variable assignment in try block are special cases, as they only apply to the variable
+ // being assigned and all other conversions should be ignored.
+ final Symbol symbol = ((IdentNode)node).getSymbol();
+ conversion = createConversion(symbol, branchLvarTypes.get(symbol), joinLvarTypes, null);
+ } else {
+ for(final Map.Entry<Symbol, LvarType> entry: branchLvarTypes.entrySet()) {
+ final Symbol symbol = entry.getKey();
+ final LvarType branchLvarType = entry.getValue();
+ conversion = createConversion(symbol, branchLvarType, joinLvarTypes, conversion);
+ }
+ }
+ if(conversion != null) {
+ localVariableConversions.put(node, conversion);
+ } else {
+ localVariableConversions.remove(node);
+ }
+ }
+
+ private void setIdentifierLvarType(final IdentNode identNode, final LvarType type) {
+ assert type != null;
+ identifierLvarTypes.put(identNode, type);
+ }
+
+ /**
+ * Marks a local variable as having a specific type from this point onward. Invoked by stores to local variables.
+ * @param symbol the symbol representing the variable
+ * @param type the type
+ */
+ @SuppressWarnings("unused")
+ private void setType(final Symbol symbol, final LvarType type) {
+ if(getLocalVariableTypeOrNull(symbol) == type) {
+ return;
+ }
+ assert symbol.hasSlot();
+ assert !symbol.isGlobal();
+ localVariableTypes = localVariableTypes.isEmpty() ? new IdentityHashMap<Symbol, LvarType>() : cloneMap(localVariableTypes);
+ localVariableTypes.put(symbol, type);
+ }
+
+ /**
+ * Set a flag in the symbol marking it as needing to be able to store a value of a particular type. Every symbol for
+ * a local variable will be assigned between 1 and 6 local variable slots for storing all types it is known to need
+ * to store.
+ * @param symbol the symbol
+ */
+ private void symbolIsUsed(final Symbol symbol) {
+ symbolIsUsed(symbol, getLocalVariableType(symbol));
+ }
+
+ /**
+ * Gets the type of the expression, dependent on the current types of the local variables.
+ *
+ * @param expr the expression
+ * @return the current type of the expression dependent on the current types of the local variables.
+ */
+ private Type getType(final Expression expr) {
+ return expr.getType(getSymbolToType());
+ }
+
+ /**
+ * Returns a function object from symbols to their types, used by the expressions to evaluate their type.
+ * {@link BinaryNode} specifically uses identity of the function to cache type calculations. This method makes
+ * sure to return the same function object while the local variable types don't change, and create a new function
+ * object if the local variable types have been changed.
+ * @return a function object representing a mapping from symbols to their types.
+ */
+ private Function<Symbol, Type> getSymbolToType() {
+ if(symbolToType.isStale()) {
+ symbolToType = new SymbolToType();
+ }
+ return symbolToType;
+ }
+
+ private class SymbolToType implements Function<Symbol, Type> {
+ private final Object boundTypes = localVariableTypes;
+ @Override
+ public Type apply(final Symbol t) {
+ return getLocalVariableType(t).type;
+ }
+
+ boolean isStale() {
+ return boundTypes != localVariableTypes;
+ }
+ }
+
+ /**
+ * Gets the type of the expression, dependent on the current types of the local variables and a single overridden
+ * symbol type. Used by type calculation on compound operators to ensure the type of the LHS at the time it was
+ * loaded (which can potentially be different after RHS evaluation, e.g. "var x; x += x = 0;") is preserved for
+ * the calculation.
+ *
+ * @param expr the expression
+ * @param overriddenSymbol the overridden symbol
+ * @param overriddenType the overridden type
+ * @return the current type of the expression dependent on the current types of the local variables and the single
+ * potentially overridden type.
+ */
+ private Type getType(final Expression expr, final Symbol overriddenSymbol, final Type overriddenType) {
+ return expr.getType(getSymbolToType(overriddenSymbol, overriddenType));
+ }
+
+ private Function<Symbol, Type> getSymbolToType(final Symbol overriddenSymbol, final Type overriddenType) {
+ return getLocalVariableType(overriddenSymbol).type == overriddenType ? getSymbolToType() :
+ new SymbolToTypeOverride(overriddenSymbol, overriddenType);
+ }
+
+ private class SymbolToTypeOverride implements Function<Symbol, Type> {
+ private final Function<Symbol, Type> originalSymbolToType = getSymbolToType();
+ private final Symbol overriddenSymbol;
+ private final Type overriddenType;
+
+ SymbolToTypeOverride(final Symbol overriddenSymbol, final Type overriddenType) {
+ this.overriddenSymbol = overriddenSymbol;
+ this.overriddenType = overriddenType;
+ }
+
+ @Override
+ public Type apply(final Symbol symbol) {
+ return symbol == overriddenSymbol ? overriddenType : originalSymbolToType.apply(symbol);
+ }
+ }
+}