diff --git a/src/main/java/com/google/devtools/build/lib/syntax/AssignmentStatement.java b/src/main/java/com/google/devtools/build/lib/syntax/AssignmentStatement.java index f731ed54b160bc..e3dc7d1788f143 100644 --- a/src/main/java/com/google/devtools/build/lib/syntax/AssignmentStatement.java +++ b/src/main/java/com/google/devtools/build/lib/syntax/AssignmentStatement.java @@ -14,6 +14,13 @@ package com.google.devtools.build.lib.syntax; +import com.google.common.base.Optional; +import com.google.devtools.build.lib.syntax.compiler.DebugInfo; +import com.google.devtools.build.lib.syntax.compiler.LoopLabels; +import com.google.devtools.build.lib.syntax.compiler.VariableScope; + +import net.bytebuddy.implementation.bytecode.ByteCodeAppender; + /** * Syntax node for an assignment statement. */ @@ -66,4 +73,13 @@ void validate(ValidationEnvironment env) throws EvalException { expression.validate(env); lvalue.validate(env, getLocation()); } + + @Override + ByteCodeAppender compile( + VariableScope scope, Optional loopLabels, DebugInfo debugInfo) + throws EvalException { + return new ByteCodeAppender.Compound( + expression.compile(scope, debugInfo), + lvalue.compileAssignment(this, debugInfo.add(this), scope)); + } } diff --git a/src/main/java/com/google/devtools/build/lib/syntax/Expression.java b/src/main/java/com/google/devtools/build/lib/syntax/Expression.java index 76c2d66894b6ee..5e6a716ac36d1c 100644 --- a/src/main/java/com/google/devtools/build/lib/syntax/Expression.java +++ b/src/main/java/com/google/devtools/build/lib/syntax/Expression.java @@ -75,8 +75,12 @@ final Object eval(Environment env) throws EvalException, InterruptedException { /** * Builds a {@link ByteCodeAppender} that implements this expression by consuming its operands * from the byte code stack and pushing its result. + * + * @throws EvalException for any error that would have occurred during evaluation of the + * function definition that contains this statement, e.g. type errors. */ - ByteCodeAppender compile(VariableScope scope, DebugInfo debugInfo) { + ByteCodeAppender compile(VariableScope scope, DebugInfo debugInfo) + throws EvalException { throw new UnsupportedOperationException(this.getClass().getSimpleName() + " unsupported."); } } diff --git a/src/main/java/com/google/devtools/build/lib/syntax/LValue.java b/src/main/java/com/google/devtools/build/lib/syntax/LValue.java index 7fe31872dccc69..1b6cf65ec51329 100644 --- a/src/main/java/com/google/devtools/build/lib/syntax/LValue.java +++ b/src/main/java/com/google/devtools/build/lib/syntax/LValue.java @@ -14,11 +14,24 @@ package com.google.devtools.build.lib.syntax; +import static com.google.devtools.build.lib.syntax.compiler.ByteCodeUtils.append; + import com.google.common.base.Preconditions; 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 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. @@ -121,4 +134,103 @@ private static void validate(ValidationEnvironment env, Location loc, Expression 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)); + } + } } diff --git a/src/main/java/com/google/devtools/build/lib/syntax/ListLiteral.java b/src/main/java/com/google/devtools/build/lib/syntax/ListLiteral.java index 18484cccb98b99..e3447fca83224d 100644 --- a/src/main/java/com/google/devtools/build/lib/syntax/ListLiteral.java +++ b/src/main/java/com/google/devtools/build/lib/syntax/ListLiteral.java @@ -118,7 +118,7 @@ void validate(ValidationEnvironment env) throws EvalException { } @Override - ByteCodeAppender compile(VariableScope scope, DebugInfo debugInfo) { + ByteCodeAppender compile(VariableScope scope, DebugInfo debugInfo) throws EvalException { AstAccessors debugAccessors = debugInfo.add(this); List listConstruction = new ArrayList<>(); if (isTuple()) { diff --git a/src/main/java/com/google/devtools/build/lib/syntax/ReturnStatement.java b/src/main/java/com/google/devtools/build/lib/syntax/ReturnStatement.java index 82c0c97ef7d9d6..c1161554d0969c 100644 --- a/src/main/java/com/google/devtools/build/lib/syntax/ReturnStatement.java +++ b/src/main/java/com/google/devtools/build/lib/syntax/ReturnStatement.java @@ -83,7 +83,8 @@ void validate(ValidationEnvironment env) throws EvalException { @Override ByteCodeAppender compile( - VariableScope scope, Optional loopLabels, DebugInfo debugInfo) { + VariableScope scope, Optional loopLabels, DebugInfo debugInfo) + throws EvalException { ByteCodeAppender compiledExpression = returnExpression.compile(scope, debugInfo); return new ByteCodeAppender.Compound( compiledExpression, new ByteCodeAppender.Simple(MethodReturn.REFERENCE)); diff --git a/src/main/java/com/google/devtools/build/lib/syntax/Statement.java b/src/main/java/com/google/devtools/build/lib/syntax/Statement.java index 3db9b1cdd5ae5f..0d8498ea8c4fa3 100644 --- a/src/main/java/com/google/devtools/build/lib/syntax/Statement.java +++ b/src/main/java/com/google/devtools/build/lib/syntax/Statement.java @@ -72,9 +72,13 @@ final void exec(Environment env) throws EvalException, InterruptedException { * *

A statement implementation should never require any particular state of the byte code * stack and should leave it in the state it was before. + * + * @throws EvalException for any error that would have occurred during evaluation of the + * function definition that contains this statement, e.g. type errors. */ ByteCodeAppender compile( - VariableScope scope, Optional loopLabels, DebugInfo debugInfo) { + VariableScope scope, Optional loopLabels, DebugInfo debugInfo) + throws EvalException { throw new UnsupportedOperationException(this.getClass().getSimpleName() + " unsupported."); } } diff --git a/src/main/java/com/google/devtools/build/lib/syntax/UserDefinedFunction.java b/src/main/java/com/google/devtools/build/lib/syntax/UserDefinedFunction.java index cf68aeb6f4255d..ededb169997daf 100644 --- a/src/main/java/com/google/devtools/build/lib/syntax/UserDefinedFunction.java +++ b/src/main/java/com/google/devtools/build/lib/syntax/UserDefinedFunction.java @@ -81,7 +81,8 @@ public class UserDefinedFunction extends BaseFunction { protected UserDefinedFunction(Identifier function, FunctionSignature.WithValues signature, - ImmutableList statements, Environment.Frame definitionGlobals) { + ImmutableList statements, Environment.Frame definitionGlobals) + throws EvalException { super(function.getName(), signature, function.getLocation()); this.statements = statements; this.definitionGlobals = definitionGlobals; @@ -174,7 +175,7 @@ private Object callCompiledFunction(Object[] arguments, FuncallExpression ast, E * *

The "call" method contains the compiled version of this function's AST. */ - private Optional buildCompiledFunction() { + private Optional buildCompiledFunction() throws EvalException { // replace the / character in the path so we have file system compatible class names // the java specification mentions that $ should be used in generated code // see http://docs.oracle.com/javase/specs/jls/se7/html/jls-3.html#jls-3.8 @@ -253,11 +254,14 @@ private Optional buildCompiledFunction() { "call", parameterTypes.toArray(new Class[parameterTypes.size()])) .getLoadedMethod()); + } catch (EvalException e) { + // don't capture EvalExceptions + throw e; } catch (Throwable e) { compilerDebug("Error while compiling", e); // TODO(bazel-team) don't capture all throwables? couldn't compile this, log somewhere? - return Optional.absent(); } + return Optional.absent(); } /** @@ -280,7 +284,8 @@ private void saveByteCode(Unloaded unloadedImplementation) { /** * Builds a byte code implementation of the AST. */ - private Implementation compileBody(VariableScope scope, DebugInfo debugInfo) { + private Implementation compileBody(VariableScope scope, DebugInfo debugInfo) + throws EvalException { List code = new ArrayList<>(statements.size()); code.add(null); // reserve space for later addition of the local variable initializer