// Copyright 2014 The Bazel Authors. All rights reserved. // // 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.devtools.build.lib.syntax; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableSet; import com.google.devtools.build.lib.events.Location; import com.google.devtools.build.lib.syntax.SkylarkList.MutableList; import java.io.IOException; import java.util.Collection; /** * A term that can appear on the left-hand side of an assignment statement, for loop, comprehension * clause, etc. E.g., * * * *

An {@code LValue}'s expression must have one of the following forms: * *

* * In particular and unlike Python, slice expressions, dot expressions, and starred expressions * cannot appear in {@code LValue}s. */ // TODO(bazel-team): Add support for assigning to slices (e.g. a[2:6] = [3]). public final class LValue extends ASTNode { private final Expression expr; public LValue(Expression expr) { this.expr = expr; setLocation(expr.getLocation()); } public Expression getExpression() { return expr; } /** * Updates the environment bindings, and possibly mutates objects, so as to assign the given value * to this {@code LValue}. */ public void assign(Object value, Environment env, Location loc) throws EvalException, InterruptedException { assign(expr, value, env, loc); } /** * Updates the environment bindings, and possibly mutates objects, so as to assign the given * value to the given expression. The expression must be valid for an {@code LValue}. */ private static void assign(Expression expr, Object value, Environment env, Location loc) throws EvalException, InterruptedException { if (expr instanceof Identifier) { assignIdentifier((Identifier) expr, value, env, loc); } else if (expr instanceof IndexExpression) { Object object = ((IndexExpression) expr).getObject().eval(env); Object key = ((IndexExpression) expr).getKey().eval(env); assignItem(object, key, value, env, loc); } else if (expr instanceof ListLiteral) { ListLiteral list = (ListLiteral) expr; assignList(list, value, env, loc); } else { // Not possible for validated ASTs. throw new EvalException(loc, "cannot assign to '" + expr + "'"); } } /** * Binds a variable to the given value in the environment. * * @throws EvalException if we're currently in a function's scope, and the identifier has * previously resolved to a global variable in the same function */ private static void assignIdentifier( Identifier ident, Object value, Environment env, Location loc) throws EvalException { Preconditions.checkNotNull(value, "trying to assign null to %s", ident); if (env.isKnownGlobalVariable(ident.getName())) { throw new EvalException( loc, String.format( "Variable '%s' is referenced before assignment. " + "The variable is defined in the global scope.", ident.getName())); } env.update(ident.getName(), value); } /** * Adds or changes an object-key-value relationship for a list or dict. * *

For a list, the key is an in-range index. For a dict, it is a hashable value. * * @throws EvalException if the object is not a list or dict */ @SuppressWarnings("unchecked") private static void assignItem( Object object, Object key, Object value, Environment env, Location loc) throws EvalException { if (object instanceof SkylarkDict) { SkylarkDict dict = (SkylarkDict) object; dict.put(key, value, loc, env); } else if (object instanceof MutableList) { MutableList list = (MutableList) object; int index = EvalUtils.getSequenceIndex(key, list.size(), loc); list.set(index, value, loc, env.mutability()); } else { throw new EvalException( loc, "can only assign an element in a dictionary or a list, not in a '" + EvalUtils.getDataTypeName(object) + "'"); } } /** * Recursively assigns an iterable value to a list literal. * * @throws EvalException if the list literal has length 0, or if the value is not an iterable of * matching length */ private static void assignList(ListLiteral list, Object value, Environment env, Location loc) throws EvalException, InterruptedException { Collection collection = EvalUtils.toCollection(value, loc, env); int len = list.getElements().size(); if (len == 0) { throw new EvalException( loc, "lists or tuples on the left-hand side of assignments must have at least one item"); } if (len != collection.size()) { throw new EvalException(loc, String.format( "assignment length mismatch: left-hand side has length %d, but right-hand side evaluates " + "to value of length %d", len, collection.size())); } int i = 0; for (Object item : collection) { assign(list.getElements().get(i), item, env, loc); i++; } } /** * Evaluates an augmented assignment that mutates this {@code LValue} with the given right-hand * side's value. * *

The left-hand side expression is evaluated only once, even when it is an {@link * IndexExpression}. The left-hand side is evaluated before the right-hand side to match Python's * behavior (hence why the right-hand side is passed as an expression rather than as an evaluated * value). */ public void assignAugmented(Operator operator, Expression rhs, Environment env, Location loc) throws EvalException, InterruptedException { if (expr instanceof Identifier) { Object result = BinaryOperatorExpression.evaluateAugmented( operator, expr.eval(env), rhs.eval(env), env, loc); assignIdentifier((Identifier) expr, result, env, loc); } else if (expr instanceof IndexExpression) { IndexExpression indexExpression = (IndexExpression) expr; // The object and key should be evaluated only once, so we don't use expr.eval(). Object object = indexExpression.getObject().eval(env); Object key = indexExpression.getKey().eval(env); Object oldValue = IndexExpression.evaluate(object, key, env, loc); // Evaluate rhs after lhs. Object rhsValue = rhs.eval(env); Object result = BinaryOperatorExpression.evaluateAugmented(operator, oldValue, rhsValue, env, loc); assignItem(object, key, result, env, loc); } else if (expr instanceof ListLiteral) { throw new EvalException(loc, "cannot perform augmented assignment on a list literal"); } else { // Not possible for validated ASTs. throw new EvalException(loc, "cannot perform augmented assignment on '" + expr + "'"); } } /** * Returns all names bound by this LValue. * *

Examples: * *

    *
  • <{@code x = ...} binds x. *
  • <{@code x, [y,z] = ..} binds x, y, z. *
  • <{@code x[5] = ..} does not bind any names. *
*/ public ImmutableSet boundIdentifiers() { if (expr instanceof Identifier) { // Common case/fast path - skip the builder. return ImmutableSet.of((Identifier) expr); } else { ImmutableSet.Builder result = ImmutableSet.builder(); collectBoundIdentifiers(expr, result); return result.build(); } } private static void collectBoundIdentifiers( Expression lhs, ImmutableSet.Builder result) { if (lhs instanceof Identifier) { result.add((Identifier) lhs); return; } if (lhs instanceof ListLiteral) { ListLiteral variables = (ListLiteral) lhs; for (Expression expression : variables.getElements()) { collectBoundIdentifiers(expression, result); } } } @Override public void accept(SyntaxTreeVisitor visitor) { visitor.visit(this); } @Override public void prettyPrint(Appendable buffer, int indentLevel) throws IOException { expr.prettyPrint(buffer); } }