New class hierarchy for Skylark functions
* New hierarchy BaseFunction > UserModeFunction, BuiltinFunction.
The old hierarchy still exists for now, to be deleted after migration:
Function > AbstractFunction > MixedModeFunction >
(UserModeFunction, SkylarkFunction > SimpleSkylarkFunction)
(UserModeFunction is already migrated, and
BaseFunction implements Function, for now.)
* Function supports *args and **kwargs when calling functions, and
mandatory named-only parameters in the style of Python 3.
Notable difference with Python: *args binds the variable to a tuple,
because a Skylark list would have to be monomorphic.
* A better, simpler, safer FFI using reflection with BuiltinFunction.
Handles typechecking, passes parameters in a more Java style.
(Not used for now, will be used later.)
* A new annotation @SkylarkSignature, intended to replace @SkylarkBuiltin,
supports the full function call protocol, including default arguments.
* Support for annotating function Factory-s rather than functions.
--
MOS_MIGRATED_REVID=88958581
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/BuiltinFunction.java b/src/main/java/com/google/devtools/build/lib/syntax/BuiltinFunction.java
new file mode 100644
index 0000000..23b5c7d
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/syntax/BuiltinFunction.java
@@ -0,0 +1,330 @@
+// Copyright 2014 Google Inc. 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.devtools.build.lib.events.Location;
+import com.google.devtools.build.lib.packages.Type.ConversionException;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.NoSuchElementException;
+import java.util.concurrent.ExecutionException;
+
+import javax.annotation.Nullable;
+
+/**
+ * A class for Skylark functions provided as builtins by the Skylark implementation
+ */
+public class BuiltinFunction extends BaseFunction {
+
+ /** ExtraArgKind so you can tweek your function's own calling convention */
+ public static enum ExtraArgKind {
+ LOCATION,
+ SYNTAX_TREE,
+ ENVIRONMENT;
+ }
+ // Predefined system add-ons to function signatures
+ public static final ExtraArgKind[] USE_LOC =
+ new ExtraArgKind[] {ExtraArgKind.LOCATION};
+ public static final ExtraArgKind[] USE_LOC_ENV =
+ new ExtraArgKind[] {ExtraArgKind.LOCATION, ExtraArgKind.ENVIRONMENT};
+ public static final ExtraArgKind[] USE_AST =
+ new ExtraArgKind[] {ExtraArgKind.SYNTAX_TREE};
+ public static final ExtraArgKind[] USE_AST_ENV =
+ new ExtraArgKind[] {ExtraArgKind.SYNTAX_TREE, ExtraArgKind.ENVIRONMENT};
+
+
+ // The underlying invoke() method.
+ @Nullable private Method invokeMethod;
+
+ // extra arguments required beside signature.
+ @Nullable private ExtraArgKind[] extraArgs;
+
+ // The count of arguments in the inner invoke method,
+ // to be used as size of argument array by the outer call method.
+ private int innerArgumentCount;
+
+
+ /** Create unconfigured function from its name */
+ public BuiltinFunction(String name) {
+ super(name);
+ }
+
+ /** Creates an unconfigured BuiltinFunction with the given name and defaultValues */
+ public BuiltinFunction(String name, Iterable<Object> defaultValues) {
+ super(name, defaultValues);
+ }
+
+ /** Creates a BuiltinFunction with the given name and signature */
+ public BuiltinFunction(String name, FunctionSignature signature) {
+ super(name, signature);
+ configure();
+ }
+
+ /** Creates a BuiltinFunction with the given name and signature with values */
+ public BuiltinFunction(String name,
+ FunctionSignature.WithValues<Object, SkylarkType> signature) {
+ super(name, signature);
+ configure();
+ }
+
+ /** Creates a BuiltinFunction with the given name and signature and extra arguments */
+ public BuiltinFunction(String name, FunctionSignature signature, ExtraArgKind[] extraArgs) {
+ super(name, signature);
+ this.extraArgs = extraArgs;
+ configure();
+ }
+
+ /** Creates a BuiltinFunction with the given name, signature with values, and extra arguments */
+ public BuiltinFunction(String name,
+ FunctionSignature.WithValues<Object, SkylarkType> signature, ExtraArgKind[] extraArgs) {
+ super(name, signature);
+ this.extraArgs = extraArgs;
+ configure();
+ }
+
+
+ /** Creates a BuiltinFunction from the given name and a Factory */
+ public BuiltinFunction(String name, Factory factory) {
+ super(name);
+ configure(factory);
+ }
+
+ @Override
+ protected int getArgArraySize () {
+ return innerArgumentCount;
+ }
+
+ protected ExtraArgKind[] getExtraArgs () {
+ return extraArgs;
+ }
+
+ @Override
+ @Nullable
+ public Object call(Object[] args,
+ @Nullable FuncallExpression ast, @Nullable Environment env)
+ throws EvalException, InterruptedException {
+ final Location loc = (ast == null) ? location : ast.getLocation();
+
+ // Add extra arguments, if needed
+ if (extraArgs != null) {
+ int i = args.length - extraArgs.length;
+ for (BuiltinFunction.ExtraArgKind extraArg : extraArgs) {
+ switch(extraArg) {
+ case LOCATION:
+ args[i] = loc;
+ break;
+
+ case SYNTAX_TREE:
+ args[i] = ast;
+ break;
+
+ case ENVIRONMENT:
+ args[i] = env;
+ break;
+ }
+ i++;
+ }
+ }
+
+ // Last but not least, actually make an inner call to the function with the resolved arguments.
+ try {
+ return invokeMethod.invoke(this, args);
+ } catch (InvocationTargetException x) {
+ Throwable e = x.getCause();
+ if (e instanceof EvalException) {
+ throw (EvalException) e;
+ } else if (e instanceof InterruptedException) {
+ throw (InterruptedException) e;
+ } else if (e instanceof ConversionException
+ || e instanceof ClassCastException
+ || e instanceof ExecutionException
+ || e instanceof IllegalStateException) {
+ throw new EvalException(loc, e.getMessage(), e);
+ } else if (e instanceof IllegalArgumentException) {
+ // Assume it was thrown by SkylarkType.cast and has a good message.
+ throw new EvalException(loc, String.format("%s\nin call to %s", e.getMessage(), this), e);
+ } else {
+ throw badCallException(loc, e, args);
+ }
+ } catch (IllegalArgumentException e) {
+ // Either this was thrown by Java itself, or it's a bug
+ // To cover the first case, let's manually check the arguments.
+ final int len = args.length - ((extraArgs == null) ? 0 : extraArgs.length);
+ final Class<?>[] types = invokeMethod.getParameterTypes();
+ for (int i = 0; i < args.length; i++) {
+ if (args[i] != null && !types[i].isAssignableFrom(args[i].getClass())) {
+ final String paramName = i < len
+ ? signature.getSignature().getNames().get(i) : extraArgs[i - len].name();
+ throw new EvalException(loc, String.format(
+ "expected %s for '%s' while calling %s but got %s instead",
+ EvalUtils.getDataTypeNameFromClass(types[i]), paramName, getName(),
+ EvalUtils.getDataTypeName(args[i])), e);
+ }
+ }
+ throw badCallException(loc, e, args);
+ } catch (IllegalAccessException e) {
+ throw badCallException(loc, e, args);
+ }
+ }
+
+ private IllegalStateException badCallException(Location loc, Throwable e, Object... args) {
+ // If this happens, it's a bug in our code.
+ return new IllegalStateException(String.format("%s%s (%s)\n"
+ + "while calling %s with args %s\nJava parameter types: %s\nSkylark type checks: %s",
+ (loc == null) ? "" : loc + ": ",
+ e.getClass().getName(), e.getMessage(), this,
+ Arrays.asList(args),
+ Arrays.asList(invokeMethod.getParameterTypes()),
+ signature.getTypes()), e);
+ }
+
+
+ /** Configure the reflection mechanism */
+ @Override
+ public void configure(SkylarkSignature annotation) {
+ Preconditions.checkState(!isConfigured()); // must not be configured yet
+ enforcedArgumentTypes = new ArrayList<>();
+ this.extraArgs = SkylarkSignatureProcessor.getExtraArgs(annotation);
+ super.configure(annotation);
+ }
+
+ // finds the method and makes it accessible (which is needed to find it, and later to use it)
+ protected Method findMethod(final String name) {
+ Method found = null;
+ for (Method method : this.getClass().getDeclaredMethods()) {
+ method.setAccessible(true);
+ if (name.equals(method.getName())) {
+ if (method != null) {
+ throw new IllegalArgumentException(String.format(
+ "function %s has more than one method named %s", getName(), name));
+ }
+ found = method;
+ }
+ }
+ if (found == null) {
+ throw new NoSuchElementException(String.format(
+ "function %s doesn't have a method named %s", getName(), name));
+ }
+ return found;
+ }
+
+ /** Configure the reflection mechanism */
+ @Override
+ protected void configure() {
+ invokeMethod = findMethod("invoke");
+
+ int arguments = signature.getSignature().getShape().getArguments();
+ innerArgumentCount = arguments + (extraArgs == null ? 0 : extraArgs.length);
+ Class<?>[] parameterTypes = invokeMethod.getParameterTypes();
+ Preconditions.checkArgument(innerArgumentCount == parameterTypes.length, getName());
+
+ // TODO(bazel-team): also grab the returnType from the annotations,
+ // and check it against method return type
+ if (enforcedArgumentTypes != null) {
+ for (int i = 0; i < arguments; i++) {
+ SkylarkType enforcedType = enforcedArgumentTypes.get(i);
+ if (enforcedType != null) {
+ Class<?> parameterType = parameterTypes[i];
+ String msg = String.format("fun %s, param %s, enforcedType: %s (%s); parameterType: %s",
+ getName(), signature.getSignature().getNames().get(i),
+ enforcedType, enforcedType.getClass(), parameterType);
+ if (enforcedType instanceof SkylarkType.Simple) {
+ Preconditions.checkArgument(
+ enforcedType == SkylarkType.of(parameterType), msg);
+ // No need to enforce Simple types on the Skylark side, the JVM will do it for us.
+ enforcedArgumentTypes.set(i, null);
+ } else if (enforcedType instanceof SkylarkType.Combination) {
+ Preconditions.checkArgument(
+ enforcedType.getType() == parameterType, msg);
+ } else {
+ Preconditions.checkArgument(
+ parameterType == Object.class || parameterType == null, msg);
+ }
+ }
+ }
+ }
+ // No need for the enforcedArgumentTypes List if all the types were Simple
+ enforcedArgumentTypes = FunctionSignature.<SkylarkType>valueListOrNull(enforcedArgumentTypes);
+ }
+
+ /** Configure by copying another function's configuration */
+ // Alternatively, we could have an extension BuiltinFunctionSignature of FunctionSignature,
+ // and use *that* instead of a Factory.
+ public void configure(BuiltinFunction.Factory factory) {
+ // this function must not be configured yet, but the factory must be
+ Preconditions.checkState(!isConfigured());
+ Preconditions.checkState(factory.isConfigured(),
+ "function factory is not configured for %s", getName());
+
+ this.paramDoc = factory.getParamDoc();
+ this.signature = factory.getSignature();
+ this.extraArgs = factory.getExtraArgs();
+ this.objectType = factory.getObjectType();
+ this.onlyLoadingPhase = factory.isOnlyLoadingPhase();
+ configure();
+ }
+
+ /**
+ * A Factory allows for a @SkylarkSignature annotation to be provided and processed in advance
+ * for a function that will be defined later as a closure (see e.g. in PackageFactory).
+ *
+ * <p>Each instance of this class must define a method create that closes over some (final)
+ * variables and returns a BuiltinFunction.
+ */
+ public abstract static class Factory extends BuiltinFunction {
+ @Nullable private Method createMethod;
+
+ /** Create unconfigured function Factory from its name */
+ public Factory(String name) {
+ super(name);
+ }
+
+ /** Creates an unconfigured function Factory with the given name and defaultValues */
+ public Factory(String name, Iterable<Object> defaultValues) {
+ super(name, defaultValues);
+ }
+
+ @Override
+ public void configure() {
+ if (createMethod != null) {
+ return;
+ }
+ createMethod = findMethod("create");
+ }
+
+ @Override
+ public Object call(Object[] args, @Nullable FuncallExpression ast, @Nullable Environment env)
+ throws EvalException {
+ throw new EvalException(null, "Tried to invoke a Factory for function " + this);
+ }
+
+ /** Instantiate the Factory
+ * @param args arguments to pass to the create method
+ * @return a new BuiltinFunction that closes over the arguments
+ */
+ public BuiltinFunction apply(Object... args) {
+ try {
+ return (BuiltinFunction) createMethod.invoke(this, args);
+ } catch (InvocationTargetException | IllegalArgumentException | IllegalAccessException e) {
+ throw new RuntimeException(String.format(
+ "Exception while applying BuiltinFunction.Factory %s: %s",
+ this, e.getMessage()), e);
+ }
+ }
+ }
+}