
com.google.javascript.jscomp.LinkedFlowScope Maven / Gradle / Ivy
/*
* Copyright 2008 The Closure Compiler Authors.
*
* 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.javascript.jscomp;
import static com.google.common.base.Preconditions.checkState;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Sets;
import com.google.javascript.jscomp.type.FlowScope;
import com.google.javascript.rhino.JSDocInfo;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.jstype.JSType;
import com.google.javascript.rhino.jstype.StaticTypedRef;
import com.google.javascript.rhino.jstype.StaticTypedScope;
import com.google.javascript.rhino.jstype.StaticTypedSlot;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* A flow scope that tries to store as little symbol information as possible,
* instead delegating to its parents. Optimized for low memory use.
*
* @author [email protected] (Nick Santos)
*/
class LinkedFlowScope implements FlowScope {
// The closest flow scope cache.
private final FlatFlowScopeCache cache;
// The parent flow scope.
private final LinkedFlowScope parent;
// The TypedScope for the block that this flow scope is defined for.
private final TypedScope syntacticScope;
// The distance between this flow scope and the closest flat flow scope.
private int depth;
static final int MAX_DEPTH = 250;
// A FlatFlowScopeCache equivalent to this scope.
private FlatFlowScopeCache flattened;
// Flow scopes assume that all their ancestors are immutable.
// So once a child scope is created, this flow scope may not be modified.
private boolean frozen = false;
// The last slot defined in this flow instruction, and the head of the
// linked list of slots.
private LinkedFlowSlot lastSlot;
/**
* Creates a flow scope without a direct parent. This can happen in three cases: (1) the "bottom"
* scope for a CFG root, (2) a direct child of a parent at the maximum depth, or (3) a joined
* scope with more than one direct parent. The parent is non-null only in the second case.
*/
private LinkedFlowScope(FlatFlowScopeCache cache, TypedScope syntacticScope) {
this.cache = cache;
this.lastSlot = null;
this.depth = 0;
this.parent = cache.linkedEquivalent;
this.syntacticScope = syntacticScope;
}
/** Creates a child flow scope with a single parent. */
private LinkedFlowScope(LinkedFlowScope directParent, TypedScope syntacticScope) {
this.cache = directParent.cache;
this.lastSlot = directParent.lastSlot;
this.depth = directParent.depth + 1;
this.parent = directParent;
this.syntacticScope = syntacticScope;
}
/** Whether this flows from a bottom scope. */
private boolean flowsFromBottom() {
return cache.functionScope.isBottom();
}
/**
* Creates an entry lattice for the flow.
*/
public static LinkedFlowScope createEntryLattice(TypedScope scope) {
return new LinkedFlowScope(new FlatFlowScopeCache(scope), scope);
}
@Override
public void inferSlotType(String symbol, JSType type) {
checkState(!frozen);
ScopedName var = getVarFromSyntacticScope(symbol);
lastSlot = new LinkedFlowSlot(var, type, lastSlot);
depth++;
cache.dirtySymbols.add(var);
}
@Override
public void inferQualifiedSlot(Node node, String symbol, JSType bottomType,
JSType inferredType, boolean declared) {
if (cache.functionScope.isGlobal()) {
// Do not infer qualified names on the global scope. Ideally these would be
// added to the scope by TypedScopeCreator, but if they are not, adding them
// here causes scaling problems (large projects can have tens of thousands of
// undeclared qualified names in the global scope) with no real benefit.
return;
}
TypedVar v = syntacticScope.getVar(symbol);
if (v == null && !cache.functionScope.isBottom()) {
// NOTE(sdh): Qualified names are declared on scopes lazily via this method.
// The difficulty is that it's not always clear which scope they need to be
// defined on. In particular, syntacticScope is wrong because it is often a
// nested block scope that is ignored when branches are joined; functionScope
// is also wrong because it could lead to ambiguity if the same root name is
// declared in multiple different blocks. Instead, the qualified name is declared
// on the scope that owns the root, when possible.
TypedVar rootVar = syntacticScope.getVar(getRootOfQualifiedName(symbol));
TypedScope rootScope =
rootVar != null ? rootVar.getScope() : syntacticScope.getClosestHoistScope();
v = rootScope.declare(symbol, node, bottomType, null, !declared);
}
JSType declaredType = v != null ? v.getType() : null;
if (v != null) {
if (!v.isTypeInferred()) {
// Use the inferred type over the declared type only if the
// inferred type is a strict subtype of the declared type.
if (declaredType == null
|| !inferredType.isSubtypeOf(declaredType)
|| declaredType.isSubtypeOf(inferredType)
|| inferredType.isEquivalentTo(declaredType)) {
return;
}
} else if (declaredType != null && !inferredType.isSubtypeOf(declaredType)) {
// If this inferred type is incompatible with another type previously
// inferred and stored on the scope, then update the scope.
v.setType(v.getType().getLeastSupertype(inferredType));
}
}
inferSlotType(symbol, inferredType);
}
@Override
public JSType getTypeOfThis() {
return cache.functionScope.getTypeOfThis();
}
@Override
public Node getRootNode() {
return syntacticScope.getRootNode();
}
@Override
public StaticTypedScope getParentScope() {
throw new UnsupportedOperationException();
}
/**
* Get the slot for the given symbol.
*/
@Override
public StaticTypedSlot getSlot(String name) {
return getSlot(getVarFromSyntacticScope(name));
}
private StaticTypedSlot getSlot(ScopedName var) {
if (cache.dirtySymbols.contains(var)) {
for (LinkedFlowSlot slot = lastSlot; slot != null; slot = slot.parent) {
if (slot.var.equals(var)) {
return slot;
}
}
}
LinkedFlowSlot slot = cache.symbols.get(var);
return slot != null ? slot : syntacticScope.getSlot(var.getName());
}
private static String getRootOfQualifiedName(String name) {
int index = name.indexOf('.');
return index < 0 ? name : name.substring(0, index);
}
// Returns a ScopedName that uniquely identifies the given name in this scope.
// If the scope does not have a var for the name (this should only be the case
// for qualified names, though some unit tests fail to declare simple names as
// well), a simple ScopedName will be created, using the scope of the qualified
// name's root, but not registered on the scope.
private ScopedName getVarFromSyntacticScope(String name) {
TypedVar v = syntacticScope.getVar(name);
if (v != null) {
return v;
}
TypedVar rootVar = syntacticScope.getVar(getRootOfQualifiedName(name));
TypedScope rootScope = rootVar != null ? rootVar.getScope() : null;
rootScope = rootScope != null ? rootScope : cache.functionScope;
return ScopedName.of(name, rootScope.getRootNode());
}
@Override
public StaticTypedSlot getOwnSlot(String name) {
throw new UnsupportedOperationException();
}
@Override
public FlowScope createChildFlowScope() {
return createChildFlowScope(syntacticScope);
}
@Override
public FlowScope createChildFlowScope(StaticTypedScope scope) {
frozen = true;
TypedScope typedScope = (TypedScope) scope;
if (depth > MAX_DEPTH) {
if (flattened == null) {
flattened = new FlatFlowScopeCache(this);
}
return new LinkedFlowScope(flattened, typedScope);
}
return new LinkedFlowScope(this, typedScope);
}
/**
* Remove flow scopes that add nothing to the flow.
*/
@Override
public LinkedFlowScope optimize() {
LinkedFlowScope current = this;
// NOTE(sdh): This function does not take syntacticScope into account.
// This means that an optimized scope cannot be used to look up names
// by string without first creating a child in the correct block. This
// is not a problem, since this is only used for (a) determining whether
// to join two scopes, (b) determining whether two scopes are equal, or
// (c) optimizing away unnecessary children generated by flowing through
// an expression. In (a) and (b) the result is only inspected locally and
// not escaped. In (c) the result is fed directly into further joins and
// will always have a block scope reassigned before flowing into another
// node. In all cases, it's therefore safe to ignore block scope changes
// when optimizing.
while (current.parent != null && current.lastSlot == current.parent.lastSlot) {
current = current.parent;
}
return current;
}
/** Returns whether this.optimize() == that.optimize(), but without walking up the chain. */
private boolean optimizesToSameScope(LinkedFlowScope that) {
// If lastSlot is null then there are no changes overlayed on top of the cache. In this
// case, the flow scopes are the same only if 'that' also has a null lastSlot and has the
// same cache.
if (this.lastSlot == null) {
return that.lastSlot == null && this.cache == that.cache;
}
// If lastSlot is non-null, then the scopes optimize to the same thing if and only if their
// lastSlots are the same object. In that case, the caches *must* be the same as well, since
// there's no way to change the cache without also changing lastSlot (which we verify).
checkState((this.cache == that.cache) || (this.lastSlot != that.lastSlot));
return this.lastSlot == that.lastSlot;
}
@Override
public TypedScope getDeclarationScope() {
return syntacticScope;
}
/** Join the two FlowScopes. */
static class FlowScopeJoinOp extends JoinOp.BinaryJoinOp {
// NOTE(sdh): When joining flow scopes with different syntactic scopes,
// we do not attempt to recover the correct syntactic scope. This is
// okay because joins only occur in two situations: (1) performed by
// the DataFlowAnalysis class automatically between CFG nodes, and (2)
// requested manually while traversing a single expression within a CFG
// node. The syntactic scope is always set at the beginning of flowing
// through a CFG node. In the case of (1), the join result's syntactic
// scope is immediately replaced with the correct one when we flow through
// the next node. In the case of (2), both inputs will always have the
// same syntactic scope. So simply propagating either input's scope is
// perfectly fine.
@SuppressWarnings("ReferenceEquality")
@Override
public FlowScope apply(FlowScope a, FlowScope b) {
// To join the two scopes, we have to
LinkedFlowScope linkedA = (LinkedFlowScope) a;
LinkedFlowScope linkedB = (LinkedFlowScope) b;
linkedA.frozen = true;
linkedB.frozen = true;
if (linkedA.optimizesToSameScope(linkedB)) {
return linkedA.createChildFlowScope();
}
// NOTE: it would be nice to put 'null' as the syntactic scope if they're not
// equal, but this is not currently feasible. For joins that occur within a
// single CFG node's flow, it's irrelevant, but for joins between separate
// CFG nodes, there is *one* place where the syntactic scope is actually used:
// when joining more than two scopes, the first two scopes are joined, and
// then the join result is joined with the third. When joining, we look up
// the types (and existence) of vars in one scope in the other; so when a var
// from the third scope (say, a local) is missing from the join result, it
// looks through the syntactic scope before realizing this. A quick fix
// might be to just check that the scope is non-null before trying to join;
// a better long-term fix would be to improve how we do joins to avoid
// excessive map entry creation: find a common ancestor, etc. One
// interesting consequence of the current approach is that we may end up
// adding irrelevant block-local variables to the joined scope unnecessarily.
TypedScope common = getCommonParentDeclarationScope(linkedA, linkedB);
// TODO(sdh): Consider reusing the input cache if both inputs are identical.
// We can evaluate how often this happens to see whather this would be a win.
FlatFlowScopeCache cache = new FlatFlowScopeCache(linkedA, linkedB, common.getRootNode());
return new LinkedFlowScope(cache, common);
}
}
static TypedScope getCommonParentDeclarationScope(LinkedFlowScope left, LinkedFlowScope right) {
if (left.flowsFromBottom()) {
return right.syntacticScope;
} else if (right.flowsFromBottom()) {
return left.syntacticScope;
}
return left.syntacticScope.getCommonParent(right.syntacticScope);
}
@Override
public boolean equals(Object other) {
if (!(other instanceof LinkedFlowScope)) {
return false;
}
LinkedFlowScope that = (LinkedFlowScope) other;
if (this.optimizesToSameScope(that)) {
return true;
}
// If two flow scopes are in the same function, then they could have
// two possible function scopes: the real one and the BOTTOM scope.
// If they have different function scopes, we *should* iterate through all
// the variables in each scope and compare. However, 99.9% of the time,
// they're not equal. And the other .1% of the time, we can pretend
// they're equal--this just means that data flow analysis will have
// to propagate the entry lattice a little bit further than it
// really needs to. Everything will still come out ok.
if (this.cache.functionScope != that.cache.functionScope) {
return false;
}
if (cache == that.cache) {
// If the two flow scopes have the same cache, then we can check
// equality a lot faster: by just looking at the "dirty" elements
// in the cache, and comparing them in both scopes.
for (ScopedName var : cache.dirtySymbols) {
if (diffSlots(getSlot(var), that.getSlot(var))) {
return false;
}
}
return true;
}
Map myFlowSlots = allFlowSlots();
Map otherFlowSlots = that.allFlowSlots();
for (ScopedName name : Sets.union(myFlowSlots.keySet(), otherFlowSlots.keySet())) {
if (diffSlots(myFlowSlots.get(name), otherFlowSlots.get(name))) {
return false;
}
}
return true;
}
/**
* Determines whether two slots are meaningfully different for the
* purposes of data flow analysis.
*/
private static boolean diffSlots(StaticTypedSlot slotA, StaticTypedSlot slotB) {
boolean aIsNull = slotA == null || slotA.getType() == null;
boolean bIsNull = slotB == null || slotB.getType() == null;
if (aIsNull || bIsNull) {
return aIsNull != bIsNull;
}
// Both slots and types must be non-null.
return slotA.getType().differsFrom(slotB.getType());
}
/**
* Gets all the symbols that have been defined before this point
* in the current flow. Does not return slots that have not changed during
* the flow.
*
* For example, consider the code:
*
* var x = 3;
* function f() {
* var y = 5;
* y = 6; // FLOW POINT
* var z = y;
* return z;
* }
*
* A FlowScope at FLOW POINT will return a slot for y, but not
* a slot for x or z.
*/
private Map allFlowSlots() {
Map slots = new HashMap<>();
for (LinkedFlowSlot slot = lastSlot; slot != null; slot = slot.parent) {
slots.putIfAbsent(slot.var, slot);
}
for (Map.Entry symbolEntry : cache.symbols.entrySet()) {
slots.putIfAbsent(symbolEntry.getKey(), symbolEntry.getValue());
}
return slots;
}
@Override
public int hashCode() {
throw new UnsupportedOperationException();
}
/** A static slot with a linked list built in. */
private static class LinkedFlowSlot implements StaticTypedSlot {
final ScopedName var;
final JSType type;
final LinkedFlowSlot parent;
LinkedFlowSlot(ScopedName var, JSType type, LinkedFlowSlot parent) {
this.var = var;
this.type = type;
this.parent = parent;
}
@Override
public String getName() {
return var.getName();
}
@Override
public JSType getType() {
return type;
}
@Override
public boolean isTypeInferred() {
return true;
}
@Override
public StaticTypedRef getDeclaration() {
return null;
}
@Override
public JSDocInfo getJSDocInfo() {
return null;
}
@Override
public StaticTypedScope getScope() {
throw new UnsupportedOperationException();
}
}
/**
* A map that tries to cache as much symbol table information
* as possible in a map. Optimized for fast lookup.
*/
private static class FlatFlowScopeCache {
// The TypedScope for the entire function or for the global scope.
final TypedScope functionScope;
// The linked flow scope that this cache represents.
final LinkedFlowScope linkedEquivalent;
// All the symbols defined before this point in the local flow.
// May not include lazily declared qualified names.
final Map symbols;
// Used to help make lookup faster for LinkedFlowScopes by recording
// symbols that may be redefined "soon", for an arbitrary definition
// of "soon". ;)
//
// More rigorously, if a symbol is redefined in a LinkedFlowScope,
// and this is the closest FlatFlowScopeCache, then that symbol is marked
// "dirty". In this way, we don't waste time looking in the LinkedFlowScope
// list for symbols that aren't defined anywhere nearby.
final Set dirtySymbols = new HashSet<>();
// The cache at the bottom of the lattice.
FlatFlowScopeCache(TypedScope functionScope) {
this.functionScope = functionScope;
this.symbols = ImmutableMap.of();
this.linkedEquivalent = null;
}
// A cache in the middle of a long scope chain.
FlatFlowScopeCache(LinkedFlowScope directParent) {
FlatFlowScopeCache cache = directParent.cache;
this.functionScope = cache.functionScope;
this.symbols = directParent.allFlowSlots();
this.linkedEquivalent = directParent;
}
// A cache at the join of two scope chains. The 'common' node is the root of the closest shared
// ancestor scope between the two joined scopes. Any symbols in more deeply nested scopes than
// this are excluded from the join operations.
@SuppressWarnings("ReferenceEquality")
FlatFlowScopeCache(LinkedFlowScope joinedScopeA, LinkedFlowScope joinedScopeB, Node common) {
this.linkedEquivalent = null;
// Always prefer the "real" function scope to the faked-out
// bottom scope.
this.functionScope =
joinedScopeA.flowsFromBottom()
? joinedScopeB.cache.functionScope
: joinedScopeA.cache.functionScope;
Map slotsA = joinedScopeA.allFlowSlots();
Map slotsB = joinedScopeB.allFlowSlots();
Set commonAncestorScopeRootNodes = new HashSet<>();
commonAncestorScopeRootNodes.add(common);
if (common.getParent() != null) {
for (Node n : common.getAncestors()) {
if (NodeUtil.createsScope(n)) {
commonAncestorScopeRootNodes.add(n);
}
}
}
this.symbols = slotsA;
// There are 5 different join cases:
// 1) The type is declared in joinedScopeA, not in joinedScopeB,
// and not in functionScope. Just use the one in A.
// 2) The type is declared in joinedScopeB, not in joinedScopeA,
// and not in functionScope. Just use the one in B.
// 3) The type is declared in functionScope and joinedScopeA, but
// not in joinedScopeB. Join the two types.
// 4) The type is declared in functionScope and joinedScopeB, but
// not in joinedScopeA. Join the two types.
// 5) The type is declared in joinedScopeA and joinedScopeB. Join
// the two types.
// Stores names that are not in a common ancestor of slotsA and slotsB for later removal
Set obsoleteNames = new HashSet<>();
for (ScopedName var : Sets.union(slotsA.keySet(), slotsB.keySet())) {
if (!commonAncestorScopeRootNodes.contains(var.getScopeRoot())) {
// Variables not defined in a common ancestor no longer exist after the join.
// Since this.symbols is initialized to slotsA, this.symbols may already contain var.
// Remove obsolete names after this for loop (to avoid a ConcurrentModificationException)
obsoleteNames.add(var);
continue;
}
LinkedFlowSlot slotA = slotsA.get(var);
LinkedFlowSlot slotB = slotsB.get(var);
JSType joinedType = null;
if (slotB == null || slotB.getType() == null) {
TypedVar fnSlot = joinedScopeB.syntacticScope.getSlot(var.getName());
JSType fnSlotType = fnSlot == null ? null : fnSlot.getType();
if (fnSlotType == null) {
// Case #1 -- already inserted.
} else if (fnSlotType != slotA.getType()) {
// Case #3
joinedType = slotA.getType().getLeastSupertype(fnSlotType);
}
} else if (slotA == null || slotA.getType() == null) {
TypedVar fnSlot = joinedScopeA.syntacticScope.getSlot(var.getName());
JSType fnSlotType = fnSlot == null ? null : fnSlot.getType();
if (fnSlotType == null || fnSlotType == slotB.getType()) {
// Case #2
symbols.put(var, slotB);
} else {
// Case #4
joinedType = slotB.getType().getLeastSupertype(fnSlotType);
}
} else if (slotA.getType() != slotB.getType()) {
// Case #5
joinedType = slotA.getType().getLeastSupertype(slotB.getType());
}
if (joinedType != null && (slotA == null || joinedType != slotA.getType())) {
symbols.put(var, new LinkedFlowSlot(var, joinedType, null));
}
}
for (ScopedName var : obsoleteNames) {
this.symbols.remove(var);
}
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy