blob: 5034be5d9e9ddd8308ff3bdfedf74ae8f4ae8760 [file] [log] [blame]
// 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 java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import net.bytebuddy.implementation.bytecode.ByteCodeAppender;
import net.bytebuddy.implementation.bytecode.Removal;
import net.bytebuddy.implementation.bytecode.constant.IntegerConstant;
/**
* 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
// TODO: We currently do not allow slices (e.g. a[2:6] = [3]).
if (lvalue instanceof IndexExpression) {
IndexExpression expression = (IndexExpression) lvalue;
Object key = expression.getKey().eval(env);
Object evaluatedObject = expression.getObject().eval(env);
assignItem(env, loc, evaluatedObject, key, result);
return;
}
throw new EvalException(loc,
"can only assign to variables and tuples, not to '" + lvalue + "'");
}
@SuppressWarnings("unchecked")
private static void assignItem(
Environment env, Location loc, Object o, Object key, Object value)
throws EvalException, InterruptedException {
if (o instanceof SkylarkDict) {
SkylarkDict<Object, Object> dict = (SkylarkDict<Object, Object>) o;
dict.put(key, value, loc, env);
} else if (o instanceof SkylarkList) {
SkylarkList<Object> list = (SkylarkList<Object>) o;
list.set(key, value, loc, env);
} else {
throw new EvalException(
loc,
"can only assign an element in a dictionary or a list, not in a '"
+ EvalUtils.getDataTypeName(o)
+ "'");
}
}
/**
* 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 IndexExpression) {
expr.validate(env);
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.
*
* <p>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<ByteCodeAppender> 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<ByteCodeAppender> code)
throws EvalException {
if (leftValue instanceof Identifier) {
code.add(compileAssignment(scope, (Identifier) leftValue));
} else if (leftValue instanceof ListLiteral) {
List<Expression> 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<Expression> lValueExpressions,
List<ByteCodeAppender> 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));
}
}
}