/* * 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); } } }