Compile function call expressions.
Mostly reuses the interpreters argument checking and helper functions.
--
MOS_MIGRATED_REVID=107395974
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/FuncallExpression.java b/src/main/java/com/google/devtools/build/lib/syntax/FuncallExpression.java
index cbe5564..e5678cb 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/FuncallExpression.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/FuncallExpression.java
@@ -14,6 +14,8 @@
package com.google.devtools.build.lib.syntax;
+import static com.google.devtools.build.lib.syntax.compiler.ByteCodeUtils.append;
+
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
import com.google.common.cache.CacheBuilder;
@@ -24,8 +26,22 @@
import com.google.common.collect.Lists;
import com.google.devtools.build.lib.events.Location;
import com.google.devtools.build.lib.syntax.EvalException.EvalExceptionWithJavaCause;
+import com.google.devtools.build.lib.syntax.compiler.ByteCodeMethodCalls;
+import com.google.devtools.build.lib.syntax.compiler.ByteCodeUtils;
+import com.google.devtools.build.lib.syntax.compiler.DebugInfo;
+import com.google.devtools.build.lib.syntax.compiler.DebugInfo.AstAccessors;
+import com.google.devtools.build.lib.syntax.compiler.NewObject;
+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.StringUtilities;
+import net.bytebuddy.description.type.TypeDescription;
+import net.bytebuddy.implementation.bytecode.ByteCodeAppender;
+import net.bytebuddy.implementation.bytecode.Removal;
+import net.bytebuddy.implementation.bytecode.StackManipulation;
+import net.bytebuddy.implementation.bytecode.assign.TypeCasting;
+import net.bytebuddy.implementation.bytecode.constant.TextConstant;
+
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
@@ -294,7 +310,7 @@
List<String> names = new ArrayList<>();
for (List<MethodDescriptor> methods : methodCache.get(objClass).values()) {
for (MethodDescriptor method : methods) {
- // TODO(bazel-team): store the Skylark name in the MethodDescriptor.
+ // TODO(bazel-team): store the Skylark name in the MethodDescriptor.
String name = method.annotation.name();
if (name.isEmpty()) {
name = StringUtilities.toPythonStyleFunctionName(method.method.getName());
@@ -410,33 +426,215 @@
}
/**
- * Add one argument to the keyword map, registering a duplicate in case of conflict.
+ * A {@link StackManipulation} invoking addKeywordArg.
+ * <p>Kept close to the definition of the method to avoid reflection errors when changing it.
*/
- private void addKeywordArg(Map<String, Object> kwargs, String name,
- Object value, ImmutableList.Builder<String> duplicates) {
+ private static final StackManipulation addKeywordArg =
+ ByteCodeUtils.invoke(
+ FuncallExpression.class,
+ "addKeywordArg",
+ Map.class,
+ String.class,
+ Object.class,
+ ImmutableList.Builder.class);
+
+ /**
+ * Add one argument to the keyword map, registering a duplicate in case of conflict.
+ *
+ * <p>public for reflection by the compiler and calls from compiled functions
+ */
+ public static void addKeywordArg(
+ Map<String, Object> kwargs,
+ String name,
+ Object value,
+ ImmutableList.Builder<String> duplicates) {
if (kwargs.put(name, value) != null) {
duplicates.add(name);
}
}
/**
- * Add multiple arguments to the keyword map (**kwargs), registering duplicates
+ * A {@link StackManipulation} invoking addKeywordArgs.
+ * <p>Kept close to the definition of the method to avoid reflection errors when changing it.
*/
- private void addKeywordArgs(Map<String, Object> kwargs,
- Object items, ImmutableList.Builder<String> duplicates) throws EvalException {
+ private static final StackManipulation addKeywordArgs =
+ ByteCodeUtils.invoke(
+ FuncallExpression.class,
+ "addKeywordArgs",
+ Map.class,
+ Object.class,
+ ImmutableList.Builder.class,
+ Location.class);
+
+ /**
+ * Add multiple arguments to the keyword map (**kwargs), registering duplicates
+ *
+ * <p>public for reflection by the compiler and calls from compiled functions
+ */
+ public static void addKeywordArgs(
+ Map<String, Object> kwargs,
+ Object items,
+ ImmutableList.Builder<String> duplicates,
+ Location location)
+ throws EvalException {
if (!(items instanceof Map<?, ?>)) {
- throw new EvalException(getLocation(),
+ throw new EvalException(
+ location,
"Argument after ** must be a dictionary, not " + EvalUtils.getDataTypeName(items));
}
for (Map.Entry<?, ?> entry : ((Map<?, ?>) items).entrySet()) {
if (!(entry.getKey() instanceof String)) {
- throw new EvalException(getLocation(),
- "Keywords must be strings, not " + EvalUtils.getDataTypeName(entry.getKey()));
+ throw new EvalException(
+ location, "Keywords must be strings, not " + EvalUtils.getDataTypeName(entry.getKey()));
}
addKeywordArg(kwargs, (String) entry.getKey(), entry.getValue(), duplicates);
}
}
+ /**
+ * A {@link StackManipulation} invoking checkCallable.
+ * <p>Kept close to the definition of the method to avoid reflection errors when changing it.
+ */
+ private static final StackManipulation checkCallable =
+ ByteCodeUtils.invoke(FuncallExpression.class, "checkCallable", Object.class, Location.class);
+
+ /**
+ * Checks whether the given object is a {@link BaseFunction}.
+ *
+ * <p>Public for reflection by the compiler and access from generated byte code.
+ *
+ * @throws EvalException If not a BaseFunction.
+ */
+ public static BaseFunction checkCallable(Object functionValue, Location location)
+ throws EvalException {
+ if (functionValue instanceof BaseFunction) {
+ return (BaseFunction) functionValue;
+ } else {
+ throw new EvalException(
+ location, "'" + EvalUtils.getDataTypeName(functionValue) + "' object is not callable");
+ }
+ }
+
+ /**
+ * A {@link StackManipulation} invoking checkDuplicates.
+ * <p>Kept close to the definition of the method to avoid reflection errors when changing it.
+ */
+ private static final StackManipulation checkDuplicates =
+ ByteCodeUtils.invoke(
+ FuncallExpression.class,
+ "checkDuplicates",
+ ImmutableList.Builder.class,
+ String.class,
+ Location.class);
+
+ /**
+ * Check the list from the builder and report an {@link EvalException} if not empty.
+ *
+ * <p>public for reflection by the compiler and calls from compiled functions
+ */
+ public static void checkDuplicates(
+ ImmutableList.Builder<String> duplicates, String function, Location location)
+ throws EvalException {
+ List<String> dups = duplicates.build();
+ if (!dups.isEmpty()) {
+ throw new EvalException(
+ location,
+ "duplicate keyword"
+ + (dups.size() > 1 ? "s" : "")
+ + " '"
+ + Joiner.on("', '").join(dups)
+ + "' in call to "
+ + function);
+ }
+ }
+
+ /**
+ * A {@link StackManipulation} invoking invokeObjectMethod.
+ * <p>Kept close to the definition of the method to avoid reflection errors when changing it.
+ */
+ private static final StackManipulation invokeObjectMethod =
+ ByteCodeUtils.invoke(
+ FuncallExpression.class,
+ "invokeObjectMethod",
+ String.class,
+ ImmutableList.class,
+ ImmutableMap.class,
+ FuncallExpression.class,
+ Environment.class);
+
+ /**
+ * Call a method depending on the type of an object it is called on.
+ *
+ * <p>Public for reflection by the compiler and access from generated byte code.
+ *
+ * @param positionals The first object is expected to be the object the method is called on.
+ * @param call the original expression that caused this call, needed for rules especially
+ */
+ public static Object invokeObjectMethod(
+ String method,
+ ImmutableList<Object> positionals,
+ ImmutableMap<String, Object> keyWordArgs,
+ FuncallExpression call,
+ Environment env)
+ throws EvalException, InterruptedException {
+ Location location = call.getLocation();
+ Object value = positionals.get(0);
+ ImmutableList<Object> positionalArgs = positionals.subList(1, positionals.size());
+ BaseFunction function = Runtime.getFunction(EvalUtils.getSkylarkType(value.getClass()), method);
+ if (function != null) {
+ if (!isNamespace(value.getClass())) {
+ // Use self as an implicit parameter in front.
+ positionalArgs = positionals;
+ }
+ return function.call(
+ positionalArgs, ImmutableMap.<String, Object>copyOf(keyWordArgs), call, env);
+ } else if (value instanceof ClassObject) {
+ Object fieldValue = ((ClassObject) value).getValue(method);
+ if (fieldValue == null) {
+ throw new EvalException(location, String.format("struct has no method '%s'", method));
+ }
+ if (!(fieldValue instanceof BaseFunction)) {
+ throw new EvalException(
+ location, String.format("struct field '%s' is not a function", method));
+ }
+ function = (BaseFunction) fieldValue;
+ return function.call(
+ positionalArgs, ImmutableMap.<String, Object>copyOf(keyWordArgs), call, env);
+ } else if (env.isSkylark()) {
+ // Only allow native Java calls when using Skylark
+ // When calling a Java method, the name is not in the Environment,
+ // so evaluating 'func' would fail.
+ Class<?> objClass;
+ Object obj;
+ if (value instanceof Class<?>) {
+ // Static call
+ obj = null;
+ objClass = (Class<?>) value;
+ } else {
+ obj = value;
+ objClass = value.getClass();
+ }
+ MethodDescriptor methodDescriptor = call.findJavaMethod(objClass, method, positionalArgs);
+ if (!keyWordArgs.isEmpty()) {
+ throw new EvalException(
+ call.func.getLocation(),
+ String.format(
+ "Keyword arguments are not allowed when calling a java method"
+ + "\nwhile calling method '%s' for type %s",
+ method,
+ EvalUtils.getDataTypeNameFromClass(objClass)));
+ }
+ return callMethod(methodDescriptor, method, obj, positionalArgs.toArray(), location, env);
+ } else {
+ throw new EvalException(
+ location,
+ String.format(
+ "%s is not defined on object of type '%s'",
+ call.functionName(),
+ EvalUtils.getDataTypeName(value)));
+ }
+ }
+
@SuppressWarnings("unchecked")
private void evalArguments(ImmutableList.Builder<Object> posargs, Map<String, Object> kwargs,
Environment env)
@@ -455,18 +653,12 @@
posargs.addAll((Iterable<Object>) value);
}
} else if (arg.isStarStar()) { // expand the kwargs
- addKeywordArgs(kwargs, value, duplicates);
+ addKeywordArgs(kwargs, value, duplicates, getLocation());
} else {
addKeywordArg(kwargs, arg.getName(), value, duplicates);
}
}
- List<String> dups = duplicates.build();
- if (!dups.isEmpty()) {
- throw new EvalException(getLocation(),
- "duplicate keyword" + (dups.size() > 1 ? "s" : "") + " '"
- + Joiner.on("', '").join(dups)
- + "' in call to " + func);
- }
+ checkDuplicates(duplicates, func.getName(), getLocation());
}
@VisibleForTesting
@@ -486,68 +678,13 @@
private Object invokeObjectMethod(Environment env) throws EvalException, InterruptedException {
Object objValue = obj.eval(env);
ImmutableList.Builder<Object> posargs = new ImmutableList.Builder<>();
+ posargs.add(objValue);
// We copy this into an ImmutableMap in the end, but we can't use an ImmutableMap.Builder, or
// we'd still have to have a HashMap on the side for the sake of properly handling duplicates.
Map<String, Object> kwargs = new HashMap<>();
-
- // Strings, lists and dictionaries (maps) have functions that we want to use in
- // MethodLibrary.
- // For other classes, we can call the Java methods.
- BaseFunction function =
- Runtime.getFunction(EvalUtils.getSkylarkType(objValue.getClass()), func.getName());
- if (function != null) {
- if (!isNamespace(objValue.getClass())) {
- // Add self as an implicit parameter in front.
- posargs.add(objValue);
- }
- evalArguments(posargs, kwargs, env);
- return function.call(posargs.build(), ImmutableMap.<String, Object>copyOf(kwargs), this, env);
- } else if (objValue instanceof ClassObject) {
- Object fieldValue = ((ClassObject) objValue).getValue(func.getName());
- if (fieldValue == null) {
- throw new EvalException(
- getLocation(), String.format("struct has no method '%s'", func.getName()));
- }
- if (!(fieldValue instanceof BaseFunction)) {
- throw new EvalException(
- getLocation(), String.format("struct field '%s' is not a function", func.getName()));
- }
- function = (BaseFunction) fieldValue;
- evalArguments(posargs, kwargs, env);
- return function.call(posargs.build(), ImmutableMap.<String, Object>copyOf(kwargs), this, env);
- } else if (env.isSkylark()) {
- // Only allow native Java calls when using Skylark
- // When calling a Java method, the name is not in the Environment,
- // so evaluating 'func' would fail.
- evalArguments(posargs, kwargs, env);
- Class<?> objClass;
- Object obj;
- if (objValue instanceof Class<?>) {
- // Static call
- obj = null;
- objClass = (Class<?>) objValue;
- } else {
- obj = objValue;
- objClass = objValue.getClass();
- }
- String name = func.getName();
- ImmutableList<Object> args = posargs.build();
- MethodDescriptor method = findJavaMethod(objClass, name, args);
- if (!kwargs.isEmpty()) {
- throw new EvalException(
- func.getLocation(),
- String.format(
- "Keyword arguments are not allowed when calling a java method"
- + "\nwhile calling method '%s' for type %s",
- name, EvalUtils.getDataTypeNameFromClass(objClass)));
- }
- return callMethod(method, name, obj, args.toArray(), getLocation(), env);
- } else {
- throw new EvalException(
- getLocation(),
- String.format("%s is not defined on object of type '%s'", functionName(),
- EvalUtils.getDataTypeName(objValue)));
- }
+ evalArguments(posargs, kwargs, env);
+ return invokeObjectMethod(
+ func.getName(), posargs.build(), ImmutableMap.<String, Object>copyOf(kwargs), this, env);
}
/**
@@ -559,14 +696,9 @@
// We copy this into an ImmutableMap in the end, but we can't use an ImmutableMap.Builder, or
// we'd still have to have a HashMap on the side for the sake of properly handling duplicates.
Map<String, Object> kwargs = new HashMap<>();
- if ((funcValue instanceof BaseFunction)) {
- BaseFunction function = (BaseFunction) funcValue;
- evalArguments(posargs, kwargs, env);
- return function.call(posargs.build(), ImmutableMap.<String, Object>copyOf(kwargs), this, env);
- } else {
- throw new EvalException(
- getLocation(), "'" + EvalUtils.getDataTypeName(funcValue) + "' object is not callable");
- }
+ BaseFunction function = checkCallable(funcValue, getLocation());
+ evalArguments(posargs, kwargs, env);
+ return function.call(posargs.build(), ImmutableMap.<String, Object>copyOf(kwargs), this, env);
}
/**
@@ -611,4 +743,119 @@
protected boolean isNewScope() {
return true;
}
+
+ @Override
+ ByteCodeAppender compile(VariableScope scope, DebugInfo debugInfo) throws EvalException {
+ AstAccessors debugAccessors = debugInfo.add(this);
+ List<ByteCodeAppender> code = new ArrayList<>();
+ if (obj != null) {
+ compileObjectMethodCall(scope, debugInfo, debugAccessors, code);
+ } else {
+ compileGlobalFunctionCall(scope, debugInfo, debugAccessors, code);
+ }
+ return ByteCodeUtils.compoundAppender(code);
+ }
+
+ /**
+ * Add code that compiles the argument expressions.
+ *
+ * <p>The byte code leaves the arguments on the stack in order of:
+ * positional arguments, key word arguments, this FuncallExpression, Environment
+ * This is the order required by {@link #invokeObjectMethod} and
+ * {@link BaseFunction#call(List, Map, FuncallExpression, Environment)}.
+ */
+ private void compileArguments(
+ VariableScope scope,
+ DebugInfo debugInfo,
+ AstAccessors debugAccessors,
+ List<ByteCodeAppender> code)
+ throws EvalException {
+ InternalVariable positionalsBuilder = scope.freshVariable(ImmutableList.Builder.class);
+ append(code, ByteCodeMethodCalls.BCImmutableList.builder);
+ code.add(positionalsBuilder.store());
+
+ InternalVariable keyWordArgs = scope.freshVariable(Map.class);
+ append(code, NewObject.fromConstructor(HashMap.class).arguments());
+ code.add(keyWordArgs.store());
+
+ InternalVariable duplicatesBuilder =
+ scope.freshVariable(new TypeDescription.ForLoadedType(ImmutableList.Builder.class));
+ append(code, ByteCodeMethodCalls.BCImmutableList.builder);
+ code.add(duplicatesBuilder.store());
+
+ StackManipulation builderAdd =
+ new StackManipulation.Compound(
+ ByteCodeMethodCalls.BCImmutableList.Builder.add, Removal.SINGLE);
+
+ // add an object the function is called on first
+ if (obj != null) {
+ append(code, positionalsBuilder.load());
+ code.add(obj.compile(scope, debugInfo));
+ append(code, builderAdd);
+ }
+ // add all arguments to their respective builder/map
+ for (Argument.Passed arg : args) {
+ ByteCodeAppender value = arg.getValue().compile(scope, debugInfo);
+ if (arg.isPositional()) {
+ append(code, positionalsBuilder.load());
+ code.add(value);
+ append(code, builderAdd);
+ } else if (arg.isStar()) {
+ // expand the starArg by adding all it's elements to the builder
+ append(code, positionalsBuilder.load());
+ code.add(value);
+ append(
+ code,
+ TypeCasting.to(new TypeDescription.ForLoadedType(Iterable.class)),
+ ByteCodeMethodCalls.BCImmutableList.Builder.addAll,
+ Removal.SINGLE);
+ } else if (arg.isStarStar()) {
+ append(code, keyWordArgs.load());
+ code.add(value);
+ append(code, duplicatesBuilder.load(), debugAccessors.loadLocation, addKeywordArgs);
+ } else {
+ append(code, keyWordArgs.load(), new TextConstant(arg.getName()));
+ code.add(value);
+ append(code, duplicatesBuilder.load(), addKeywordArg);
+ }
+ }
+ append(
+ code,
+ // check for duplicates in the key word arguments
+ duplicatesBuilder.load(),
+ new TextConstant(func.getName()),
+ debugAccessors.loadLocation,
+ checkDuplicates,
+ // load the arguments in the correct order for invokeObjectMethod and BaseFunction.call
+ positionalsBuilder.load(),
+ ByteCodeMethodCalls.BCImmutableList.Builder.build,
+ keyWordArgs.load(),
+ ByteCodeMethodCalls.BCImmutableMap.copyOf,
+ debugAccessors.loadAstNode,
+ TypeCasting.to(new TypeDescription.ForLoadedType(FuncallExpression.class)),
+ scope.loadEnvironment());
+ }
+
+ private void compileObjectMethodCall(
+ VariableScope scope,
+ DebugInfo debugInfo,
+ AstAccessors debugAccessors,
+ List<ByteCodeAppender> code)
+ throws EvalException {
+ append(code, new TextConstant(func.getName()));
+ compileArguments(scope, debugInfo, debugAccessors, code);
+ append(code, invokeObjectMethod);
+ }
+
+ private void compileGlobalFunctionCall(
+ VariableScope scope,
+ DebugInfo debugInfo,
+ AstAccessors debugAccessors,
+ List<ByteCodeAppender> code)
+ throws EvalException {
+ code.add(func.compile(scope, debugInfo));
+ append(code, debugAccessors.loadLocation, checkCallable);
+ compileArguments(scope, debugInfo, debugAccessors, code);
+ append(code, BaseFunction.call);
+ }
}