// 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 static com.google.devtools.build.lib.syntax.compiler.ByteCodeUtils.append; import com.google.devtools.build.lib.events.Location; import com.google.devtools.build.lib.syntax.compiler.ByteCodeUtils; import com.google.devtools.build.lib.syntax.compiler.DebugInfo.AstAccessors; import com.google.devtools.build.lib.syntax.compiler.Variable.InternalVariable; import com.google.devtools.build.lib.syntax.compiler.VariableScope; import com.google.devtools.build.lib.util.Preconditions; import net.bytebuddy.implementation.bytecode.ByteCodeAppender; import net.bytebuddy.implementation.bytecode.Removal; import net.bytebuddy.implementation.bytecode.constant.IntegerConstant; import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; import java.util.List; /** * Class representing an LValue. * It appears in assignment, for loop and comprehensions, e.g. * lvalue = 2 * [for lvalue in exp] * for lvalue in exp: pass * An LValue can be a simple variable or something more complex like a tuple. */ public class LValue implements Serializable { private final Expression expr; public LValue(Expression expr) { this.expr = expr; } public Expression getExpression() { return expr; } /** * Assign a value to an LValue and update the environment. */ public void assign(Environment env, Location loc, Object result) throws EvalException, InterruptedException { assign(env, loc, expr, result); } private static void assign(Environment env, Location loc, Expression lvalue, Object result) throws EvalException, InterruptedException { if (lvalue instanceof Identifier) { assign(env, loc, (Identifier) lvalue, result); return; } if (lvalue instanceof ListLiteral) { ListLiteral variables = (ListLiteral) lvalue; Collection rvalue = EvalUtils.toCollection(result, loc); int len = variables.getElements().size(); if (len != rvalue.size()) { throw new EvalException(loc, String.format( "lvalue has length %d, but rvalue has has length %d", len, rvalue.size())); } int i = 0; for (Object o : rvalue) { assign(env, loc, variables.getElements().get(i), o); i++; } return; } // Support syntax for setting an element in an array, e.g. a[5] = 2 // We currently do not allow slices (e.g. a[2:6] = [3]). if (lvalue instanceof FuncallExpression) { FuncallExpression func = (FuncallExpression) lvalue; List args = func.getArguments(); if (func.getFunction().getName().equals("$index") && func.getObject() instanceof Identifier && args.size() == 1) { Object key = args.get(0).getValue().eval(env); assignItem(env, loc, (Identifier) func.getObject(), key, result); return; } } throw new EvalException(loc, "can only assign to variables and tuples, not to '" + lvalue + "'"); } // Since dict is still immutable, the expression 'a[x] = b' creates a new dictionary and // assigns it to 'a'. @SuppressWarnings("unchecked") private static void assignItem( Environment env, Location loc, Identifier ident, Object key, Object value) throws EvalException, InterruptedException { Object o = ident.eval(env); if (!(o instanceof SkylarkDict)) { throw new EvalException( loc, "can only assign an element in a dictionary, not in a '" + EvalUtils.getDataTypeName(o) + "'"); } SkylarkDict dict = (SkylarkDict) o; dict.put(key, value, loc, env); } /** * Assign value to a single variable. */ private static void assign(Environment env, Location loc, Identifier ident, Object result) throws EvalException, InterruptedException { Preconditions.checkNotNull(result, "trying to assign null to %s", ident); // The variable may have been referenced successfully if a global variable // with the same name exists. In this case an Exception needs to be thrown. 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(), result); } void validate(ValidationEnvironment env, Location loc) throws EvalException { validate(env, loc, expr); } private static void validate(ValidationEnvironment env, Location loc, Expression expr) throws EvalException { if (expr instanceof Identifier) { Identifier ident = (Identifier) expr; env.declare(ident.getName(), loc); return; } if (expr instanceof ListLiteral) { for (Expression e : ((ListLiteral) expr).getElements()) { validate(env, loc, e); } return; } if (expr instanceof FuncallExpression) { FuncallExpression func = (FuncallExpression) expr; if (func.getFunction().getName().equals("$index") && func.getObject() instanceof Identifier) { return; } } throw new EvalException(loc, "can only assign to variables and tuples, not to '" + expr + "'"); } @Override public String toString() { return expr.toString(); } /** * Compile an assignment within the given ASTNode to these l-values. * *

The value to possibly destructure and assign must already be on the stack. */ public ByteCodeAppender compileAssignment( ASTNode node, AstAccessors debugAccessors, VariableScope scope) throws EvalException { List code = new ArrayList<>(); compileAssignment(node, debugAccessors, expr, scope, code); return ByteCodeUtils.compoundAppender(code); } /** * Called recursively to compile the tree of l-values we might have. */ private static void compileAssignment( ASTNode node, AstAccessors debugAccessors, Expression leftValue, VariableScope scope, List code) throws EvalException { if (leftValue instanceof Identifier) { code.add(compileAssignment(scope, (Identifier) leftValue)); } else if (leftValue instanceof ListLiteral) { List lValueExpressions = ((ListLiteral) leftValue).getElements(); compileAssignment(node, debugAccessors, scope, lValueExpressions, code); } else { String message = String.format( "Can't assign to expression '%s', only to variables or nested tuples of variables", leftValue); throw new EvalExceptionWithStackTrace(new EvalException(node.getLocation(), message), node); } } /** * Assumes a collection of values on the top of the stack and assigns them to the l-value * expressions given. */ private static void compileAssignment( ASTNode node, AstAccessors debugAccessors, VariableScope scope, List lValueExpressions, List code) throws EvalException { InternalVariable objects = scope.freshVariable(Collection.class); InternalVariable iterator = scope.freshVariable(Iterator.class); // convert the object on the stack into a collection and store it to a variable for loading // multiple times below below code.add(new ByteCodeAppender.Simple(debugAccessors.loadLocation, EvalUtils.toCollection)); code.add(objects.store()); append( code, // check that we got exactly the amount of objects in the collection that we need IntegerConstant.forValue(lValueExpressions.size()), objects.load(), debugAccessors.loadLocation, // TODO(bazel-team) load better location within tuple ByteCodeUtils.invoke( LValue.class, "checkSize", int.class, Collection.class, Location.class), // get an iterator to assign the objects objects.load(), ByteCodeUtils.invoke(Collection.class, "iterator")); code.add(iterator.store()); // assign each object to the corresponding l-value for (Expression lValue : lValueExpressions) { code.add( new ByteCodeAppender.Simple( iterator.load(), ByteCodeUtils.invoke(Iterator.class, "next"))); compileAssignment(node, debugAccessors, lValue, scope, code); } } /** * Compile assignment to a single identifier. */ private static ByteCodeAppender compileAssignment(VariableScope scope, Identifier identifier) { // don't store to/create the _ "variable" the value is not needed, just remove it if (identifier.getName().equals("_")) { return new ByteCodeAppender.Simple(Removal.SINGLE); } return scope.getVariable(identifier).store(); } /** * Checks that the size of a collection at runtime conforms to the amount of l-value expressions * we have to assign to. */ public static void checkSize(int expected, Collection collection, Location location) throws EvalException { int actual = collection.size(); if (expected != actual) { throw new EvalException( location, String.format("lvalue has length %d, but rvalue has has length %d", expected, actual)); } } }