/*
* Copyright 2013 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.template.soy.passes;
import static com.google.common.base.Preconditions.checkState;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.template.soy.base.SourceLocation;
import com.google.template.soy.error.ErrorReporter;
import com.google.template.soy.error.SoyErrorKind;
import com.google.template.soy.exprtree.AbstractExprNodeVisitor;
import com.google.template.soy.exprtree.ExprNode;
import com.google.template.soy.exprtree.ExprNode.ParentExprNode;
import com.google.template.soy.exprtree.ExprRootNode;
import com.google.template.soy.exprtree.GlobalNode;
import com.google.template.soy.exprtree.VarDefn;
import com.google.template.soy.exprtree.VarRefNode;
import com.google.template.soy.soytree.AbstractSoyNodeVisitor;
import com.google.template.soy.soytree.ForNode;
import com.google.template.soy.soytree.ForeachNonemptyNode;
import com.google.template.soy.soytree.LetContentNode;
import com.google.template.soy.soytree.LetValueNode;
import com.google.template.soy.soytree.PrintNode;
import com.google.template.soy.soytree.SoyNode;
import com.google.template.soy.soytree.SoyNode.BlockNode;
import com.google.template.soy.soytree.SoyNode.ExprHolderNode;
import com.google.template.soy.soytree.SoyNode.ParentSoyNode;
import com.google.template.soy.soytree.TemplateNode;
import com.google.template.soy.soytree.defn.InjectedParam;
import com.google.template.soy.soytree.defn.LocalVar;
import com.google.template.soy.soytree.defn.LoopVar;
import com.google.template.soy.soytree.defn.TemplateParam;
import com.google.template.soy.soytree.defn.UndeclaredVar;
import java.util.ArrayDeque;
import java.util.BitSet;
import java.util.Deque;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* Visitor which resolves all variable and parameter references to point to the corresponding
* declaration object.
*
*/
final class ResolveNamesVisitor extends AbstractSoyNodeVisitor<Void> {
private static final SoyErrorKind GLOBAL_MATCHES_VARIABLE =
SoyErrorKind.of(
"Found global reference aliasing a local variable ''{0}'', did you mean " + "''${0}''?");
private static final SoyErrorKind VARIABLE_ALREADY_DEFINED =
SoyErrorKind.of("variable ''${0}'' already defined{1}");
/**
* A data structure that assigns a unique (small) integer to all local variable definitions that
* are active within a given lexical scope.
*
* <p>A 'slot' is a small integer that is assigned to a {@link VarDefn} such that at any given
* point of execution while that variable could be referenced there is only one variable with that
* index.
*/
private final class LocalVariables {
private final BitSet availableSlots = new BitSet();
private final Deque<Map<String, VarDefn>> currentScope = new ArrayDeque<>();
private final BitSet slotsToRelease = new BitSet();
/** Tracks the next unused slot to claim. */
private int nextSlotToClaim = 0;
/**
* A counter that tracks when to release the {@link #slotsToRelease} set.
*
* <p>We add {@link #slotsToRelease} to {@link #availableSlots} only when exiting a scope if
* this value == 0.
*/
private int delayReleaseClaims = 0;
/**
* Enters a new scope. Variables {@link #define defined} will have a lifetime that extends until
* a matching call to {@link #exitScope()}.
*/
void enterScope() {
currentScope.push(new LinkedHashMap<String, VarDefn>());
}
/**
* Enters a new scope.
*
* <p>Variables defined in a lazy scope have a lifetime that extends to the matching {@link
* #exitLazyScope()} call, but the variable slots reserved have their lifetimes extended until
* the parent scope closes.
*/
void enterLazyScope() {
delayReleaseClaims++;
enterScope();
}
/** Exits the current scope. */
void exitLazyScope() {
checkState(delayReleaseClaims > 0, "Exiting a lazy scope when we aren't in one");
exitScope();
delayReleaseClaims--;
}
/**
* Exits the current lazy scope.
*
* <p>This releases all the variable indices associated with the variables defined in this frame
* so that they can be reused.
*/
void exitScope() {
Map<String, VarDefn> variablesGoingOutOfScope = currentScope.pop();
for (VarDefn var : variablesGoingOutOfScope.values()) {
if (var instanceof LoopVar) {
LoopVar loopVar = (LoopVar) var;
slotsToRelease.set(loopVar.currentLoopIndexIndex());
slotsToRelease.set(loopVar.isLastIteratorIndex());
}
slotsToRelease.set(var.localVariableIndex());
}
if (delayReleaseClaims == 0) {
availableSlots.or(slotsToRelease);
slotsToRelease.clear();
}
}
/**
* Returns the {@link VarDefn} associated with the given name by searching through the current
* scope and all parent scopes.
*/
VarDefn lookup(String name) {
for (Map<String, VarDefn> scope : currentScope) {
VarDefn defn = scope.get(name);
if (defn != null) {
return defn;
}
}
return null;
}
/**
* Defines a {@link LoopVar}. Unlike normal local variables and params loop variables get 2
* extra implicit local variables for tracking the current index and whether or not we are at
* the last index.
*/
boolean define(LoopVar defn, SoyNode definingNode) {
if (!define((VarDefn) defn, definingNode)) {
return false;
}
// only allocate the extra slots if definition succeeded
defn.setExtraLoopIndices(claimSlot(), claimSlot());
return true;
}
/** Defines a variable. */
boolean define(VarDefn defn, SoyNode definingNode) {
// Search for the name to see if it is being redefined.
VarDefn preexisting = lookup(defn.name());
if (preexisting != null) {
Optional<SourceLocation> sourceLocation = forVarDefn(preexisting);
String location =
sourceLocation.isPresent() ? " at line " + sourceLocation.get().getBeginLine() : "";
errorReporter.report(
definingNode.getSourceLocation(), VARIABLE_ALREADY_DEFINED, defn.name(), location);
return false;
}
currentScope.peek().put(defn.name(), defn);
defn.setLocalVariableIndex(claimSlot());
return true;
}
/**
* Returns the smallest available local variable slot or claims a new one if there is none
* available.
*/
private int claimSlot() {
int nextSetBit = availableSlots.nextSetBit(0);
int slotToUse;
if (nextSetBit != -1) {
slotToUse = nextSetBit;
availableSlots.clear(nextSetBit);
} else {
slotToUse = nextSlotToClaim;
nextSlotToClaim++;
}
return slotToUse;
}
void verify() {
checkState(delayReleaseClaims == 0, "%s lazy scope(s) are still active", delayReleaseClaims);
checkState(slotsToRelease.isEmpty(), "%s slots are waiting to be released", slotsToRelease);
BitSet unavailableSlots = new BitSet(nextSlotToClaim);
unavailableSlots.set(0, nextSlotToClaim);
// now the only bits on will be the ones where available slots has '0'.
unavailableSlots.xor(availableSlots);
checkState(
unavailableSlots.isEmpty(), "Expected all slots to be available: %s", unavailableSlots);
}
}
/** Scope for injected params. */
private LocalVariables localVariables;
private Map<String, InjectedParam> ijParams;
private final ErrorReporter errorReporter;
ResolveNamesVisitor(ErrorReporter errorReporter) {
this.errorReporter = errorReporter;
}
@Override
protected void visitTemplateNode(TemplateNode node) {
// Create a scope for all parameters.
localVariables = new LocalVariables();
localVariables.enterScope();
ijParams = new HashMap<>();
// Add both injected and regular params to the param scope.
for (TemplateParam param : node.getAllParams()) {
localVariables.define(param, node);
}
visitSoyNode(node);
localVariables.exitScope();
localVariables.verify();
node.setMaxLocalVariableTableSize(localVariables.nextSlotToClaim);
localVariables = null;
ijParams = null;
}
@Override
protected void visitPrintNode(PrintNode node) {
visitSoyNode(node);
}
@Override
protected void visitLetValueNode(LetValueNode node) {
visitExpressions(node);
// Now after the let-block is complete, define the new variable
// in the current scope.
localVariables.define(node.getVar(), node);
}
@Override
protected void visitLetContentNode(LetContentNode node) {
// LetContent nodes may reserve slots in their sub expressions, but due to lazy evaluation will
// not use them immediately, so we can't release the slots until the parent scope is gone.
// however the variable lifetime should be limited
localVariables.enterLazyScope();
visitChildren(node);
localVariables.exitLazyScope();
localVariables.define(node.getVar(), node);
}
@Override
protected void visitForNode(ForNode node) {
// Visit the range expressions.
visitExpressions(node);
localVariables.enterScope();
localVariables.define(node.getVar(), node);
// Visit the node body
visitChildren(node);
localVariables.exitScope();
}
@Override
protected void visitForeachNonemptyNode(ForeachNonemptyNode node) {
// Visit the foreach iterator expression
visitExpressions(node.getParent());
// Create a scope to hold the iteration variable
localVariables.enterScope();
localVariables.define(node.getVar(), node);
// Visit the node body
visitChildren(node);
localVariables.exitScope();
}
@Override
protected void visitSoyNode(SoyNode node) {
if (node instanceof ExprHolderNode) {
visitExpressions((ExprHolderNode) node);
}
if (node instanceof ParentSoyNode<?>) {
if (node instanceof BlockNode) {
localVariables.enterScope();
visitChildren((BlockNode) node);
localVariables.exitScope();
} else {
visitChildren((ParentSoyNode<?>) node);
}
}
}
private void visitExpressions(ExprHolderNode node) {
ResolveNamesExprVisitor exprVisitor = new ResolveNamesExprVisitor();
for (ExprRootNode expr : node.getExprList()) {
exprVisitor.exec(expr);
}
}
private static Optional<SourceLocation> forVarDefn(VarDefn varDefn) {
if (varDefn instanceof LocalVar) {
return Optional.of(((LocalVar) varDefn).declaringNode().getSourceLocation());
} else {
// TODO(user): plumb source locations through to other VarDefn impls.
return Optional.absent();
}
}
// -----------------------------------------------------------------------------------------------
// Expr visitor.
/**
* Visitor which resolves all variable and parameter references in expressions to point to the
* corresponding declaration object.
*/
private final class ResolveNamesExprVisitor extends AbstractExprNodeVisitor<Void> {
@Override
public Void exec(ExprNode node) {
Preconditions.checkArgument(node instanceof ExprRootNode);
visit(node);
return null;
}
@Override
protected void visitExprRootNode(ExprRootNode node) {
visitChildren(node);
}
@Override
protected void visitExprNode(ExprNode node) {
if (node instanceof ParentExprNode) {
visitChildren((ParentExprNode) node);
}
}
@Override
protected void visitGlobalNode(GlobalNode node) {
// Check for a typo involving a global reference. If the author forgets the leading '$' on a
// variable reference then it will get parsed as a global. In some compiler configurations
// unknown globals are not an error. To ensure that typos are caught we check for this case
// here. Making 'unknown globals' an error consistently would be a better solution, though
// even then we would probably want some typo checking like this.
// Note. This also makes it impossible for a global to share the same name as a local. This
// should be fine since global names are typically qualified strings.
String globalName = node.getName();
VarDefn varDefn = localVariables.lookup(globalName);
if (varDefn != null) {
node.suppressUnknownGlobalErrors();
// This means that this global has the same name as an in-scope local or param. It is
// likely that they just forgot the leading '$'
errorReporter.report(node.getSourceLocation(), GLOBAL_MATCHES_VARIABLE, globalName);
}
}
@Override
protected void visitVarRefNode(VarRefNode varRef) {
if (varRef.isDollarSignIjParameter()) {
InjectedParam ijParam = ijParams.get(varRef.getName());
if (ijParam == null) {
ijParam = new InjectedParam(varRef.getName());
ijParams.put(varRef.getName(), ijParam);
}
varRef.setDefn(ijParam);
return;
}
VarDefn varDefn = localVariables.lookup(varRef.getName());
if (varDefn == null) {
// this case is mostly about supporting v1 templates. Undeclared vars for v2 templates are
// flagged as errors in the CheckTemplateParamsVisitor
varDefn = new UndeclaredVar(varRef.getName());
}
varRef.setDefn(varDefn);
}
}
}