| // 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 com.google.common.base.Joiner; |
| import com.google.common.base.Preconditions; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableSet; |
| import com.google.common.collect.Ordering; |
| import com.google.common.collect.Sets; |
| import com.google.devtools.build.lib.events.Location; |
| import com.google.devtools.build.lib.skylarkinterface.SkylarkPrinter; |
| import com.google.devtools.build.lib.skylarkinterface.SkylarkSignature; |
| import com.google.devtools.build.lib.syntax.SkylarkList.Tuple; |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Objects; |
| import javax.annotation.Nullable; |
| |
| /** |
| * A base class for Skylark functions, whether builtin or user-defined. |
| * |
| * <p>Nomenclature: We call "Parameters" the formal parameters of a function definition. We call |
| * "Arguments" the actual values supplied at the call site. |
| * |
| * <p>The outer calling convention is like that of python3, with named parameters that can be |
| * mandatory or optional, and also be positional or named-only, and rest parameters for extra |
| * positional and keyword arguments. Callers supply a {@code List<Object>} args for positional |
| * arguments and a {@code Map<String, Object>} for keyword arguments, where positional arguments |
| * will be resolved first, then keyword arguments, with errors for a clash between the two, for |
| * missing mandatory parameter, or for unexpected extra positional or keyword argument in absence of |
| * rest parameter. |
| * |
| * <p>The inner calling convention is to pass the underlying method an {@code Object[]} of the |
| * type-checked argument values, one per expected parameter, parameters being sorted as documented |
| * in {@link FunctionSignature}. |
| * |
| * <p>The function may provide default values for optional parameters not provided by the caller. |
| * These default values can be null if there are no optional parameters or for builtin functions, |
| * but not for user-defined functions that have optional parameters. |
| */ |
| // TODO(bazel-team): |
| // Provide optimized argument frobbing depending of FunctionSignature and CallerSignature |
| // (that FuncallExpression must supply), optimizing for the all-positional and all-keyword cases. |
| // Also, use better pure maps to minimize map O(n) re-creation events when processing keyword maps. |
| public abstract class BaseFunction implements StarlarkFunction { |
| |
| /** |
| * The name of the function. |
| * |
| * <p>For safe extensibility, this class only retrieves name via the accessor {@link #getName}. |
| * This field must be null iff {@link #getName} is overridden. |
| */ |
| @Nullable private final String name; |
| |
| // A function signature, including defaults and types |
| // never null after it is configured |
| @Nullable protected FunctionSignature.WithValues<Object, SkylarkType> signature; |
| |
| // Location of the function definition, or null for builtin functions |
| // TODO(bazel-team): Or make non-nullable, and use Location.BUILTIN for builtin functions? |
| @Nullable protected Location location; |
| |
| // Some functions are also Namespaces or other Skylark entities. |
| @Nullable protected Class<?> objectType; |
| |
| // Documentation for variables, if any |
| @Nullable protected List<String> paramDoc; |
| |
| // The types actually enforced by the Skylark runtime, as opposed to those enforced by the JVM, |
| // or those displayed to the user in the documentation. |
| @Nullable protected List<SkylarkType> enforcedArgumentTypes; |
| |
| // Defaults to be used when configure(annotation) is called (after the function is constructed). |
| @Nullable private Iterable<Object> unconfiguredDefaultValues; |
| // The configure(annotation) function will include these defaults in the function signature. |
| // We need to supply these defaultValues to the constructor, that will store them here, because |
| // they can't be supplied via Java annotations, due to the limitations in the annotation facility. |
| // (For extra brownies, we could supply them as Skylark expression strings, to be evaluated by our |
| // evaluator without the help of any unconfigured functions, or to be processed at compile-time; |
| // but we resolve annotations at runtime for now.) |
| // Limitations in Java annotations mean we can't express them in the SkylarkSignature annotation. |
| // (In the future, we could parse and evaluate simple Skylark expression strings, but then |
| // we'd have to be very careful of circularities during initialization). |
| // Note that though we want this list to be immutable, we don't use ImmutableList, |
| // because that can't store nulls and nulls are essential for some BuiltinFunction-s. |
| // We trust the user not to modify the list behind our back. |
| |
| |
| /** |
| * Returns the name of this function. |
| * |
| * <p>A subclass must override this function if a null name is given to this class's constructor. |
| */ |
| public String getName() { |
| Preconditions.checkNotNull(name); |
| return name; |
| } |
| |
| /** Returns the signature of this function. */ |
| @Nullable public FunctionSignature.WithValues<Object, SkylarkType> getSignature() { |
| return signature; |
| } |
| |
| /** This function may also be viewed by Skylark as being of a special ObjectType */ |
| @Nullable public Class<?> getObjectType() { |
| return objectType; |
| } |
| |
| /** Returns true if the BaseFunction is configured. */ |
| public boolean isConfigured() { |
| return signature != null; |
| } |
| |
| /** |
| * Creates an unconfigured BaseFunction with the given name. |
| * |
| * <p>The name must be null if called from a subclass constructor where the subclass overrides |
| * {@link #getName}; otherwise it must be non-null. |
| */ |
| public BaseFunction(@Nullable String name) { |
| this.name = name; |
| } |
| |
| /** |
| * Constructs a BaseFunction with a given name, signature and location. |
| * |
| * @param name the function name; null iff this is a subclass overriding {@link #getName} |
| * @param signature the signature with default values and types |
| * @param location the location of function definition |
| */ |
| public BaseFunction( |
| @Nullable String name, |
| @Nullable FunctionSignature.WithValues<Object, SkylarkType> signature, |
| @Nullable Location location) { |
| this(name); |
| this.signature = signature; |
| this.location = location; |
| } |
| |
| /** |
| * Constructs a BaseFunction with a given name, signature. |
| * |
| * @param name the function name; null iff this is a subclass overriding {@link #getName} |
| * @param signature the signature, with default values and types |
| */ |
| public BaseFunction( |
| @Nullable String name, |
| @Nullable FunctionSignature.WithValues<Object, SkylarkType> signature) { |
| this(name, signature, null); |
| } |
| |
| /** |
| * Constructs a BaseFunction with a given name and signature without default values or types. |
| * |
| * @param name the function name; null iff this is a subclass overriding {@link #getName} |
| * @param signature the signature, without default values or types |
| */ |
| public BaseFunction(@Nullable String name, FunctionSignature signature) { |
| this(name, FunctionSignature.WithValues.create(signature), null); |
| } |
| |
| /** |
| * Constructs a BaseFunction with a given name and list of unconfigured defaults. |
| * |
| * @param name the function name; null iff this is a subclass overriding {@link #getName} |
| * @param defaultValues a list of default values for the optional arguments to be configured. |
| */ |
| public BaseFunction(@Nullable String name, @Nullable Iterable<Object> defaultValues) { |
| this(name); |
| this.unconfiguredDefaultValues = defaultValues; |
| } |
| |
| /** Get parameter documentation as a list corresponding to each parameter */ |
| public List<String> getParamDoc() { |
| return paramDoc; |
| } |
| |
| /** |
| * The size of the array required by the callee. |
| */ |
| protected int getArgArraySize() { |
| return signature.getSignature().getShape().getArguments(); |
| } |
| |
| /** |
| * The types that will be actually enforced by Skylark itself, so we may skip those already |
| * enforced by the JVM during calls to BuiltinFunction, but also so we may lie to the user |
| * in the automatically-generated documentation |
| */ |
| public List<SkylarkType> getEnforcedArgumentTypes() { |
| return enforcedArgumentTypes; |
| } |
| |
| /** |
| * Process the caller-provided arguments into an array suitable for the callee (this function). |
| */ |
| public Object[] processArguments(List<Object> args, |
| @Nullable Map<String, Object> kwargs, |
| @Nullable Location loc, @Nullable Environment env) |
| throws EvalException { |
| |
| Object[] arguments = new Object[getArgArraySize()]; |
| |
| // extract function signature |
| FunctionSignature sig = signature.getSignature(); |
| FunctionSignature.Shape shape = sig.getShape(); |
| ImmutableList<String> names = sig.getNames(); |
| List<Object> defaultValues = signature.getDefaultValues(); |
| |
| // Note that this variable will be adjusted down if there are extra positionals, |
| // after these extra positionals are dumped into starParam. |
| int numPositionalArgs = args.size(); |
| |
| int numMandatoryPositionalParams = shape.getMandatoryPositionals(); |
| int numOptionalPositionalParams = shape.getOptionalPositionals(); |
| int numMandatoryNamedOnlyParams = shape.getMandatoryNamedOnly(); |
| int numOptionalNamedOnlyParams = shape.getOptionalNamedOnly(); |
| boolean hasStarParam = shape.hasStarArg(); |
| boolean hasKwParam = shape.hasKwArg(); |
| int numPositionalParams = numMandatoryPositionalParams + numOptionalPositionalParams; |
| int numNamedOnlyParams = numMandatoryNamedOnlyParams + numOptionalNamedOnlyParams; |
| int numNamedParams = numPositionalParams + numNamedOnlyParams; |
| int kwParamIndex = names.size() - 1; // only valid if hasKwParam |
| |
| // (1) handle positional arguments |
| if (hasStarParam) { |
| // Nota Bene: we collect extra positional arguments in a (tuple,) rather than a [list], |
| // and this is actually the same as in Python. |
| int starParamIndex = numNamedParams; |
| if (numPositionalArgs > numPositionalParams) { |
| arguments[starParamIndex] = |
| Tuple.copyOf(args.subList(numPositionalParams, numPositionalArgs)); |
| numPositionalArgs = numPositionalParams; // clip numPositionalArgs |
| } else { |
| arguments[starParamIndex] = Tuple.empty(); |
| } |
| } else if (numPositionalArgs > numPositionalParams) { |
| throw new EvalException(loc, |
| numPositionalParams > 0 |
| ? "too many (" + numPositionalArgs + ") positional arguments in call to " + this |
| : this + " does not accept positional arguments, but got " + numPositionalArgs); |
| } |
| |
| for (int i = 0; i < numPositionalArgs; i++) { |
| arguments[i] = args.get(i); |
| } |
| |
| // (2) handle keyword arguments |
| if (kwargs == null || kwargs.isEmpty()) { |
| // Easy case (2a): there are no keyword arguments. |
| // All arguments were positional, so check we had enough to fill all mandatory positionals. |
| if (numPositionalArgs < numMandatoryPositionalParams) { |
| throw new EvalException(loc, String.format( |
| "insufficient arguments received by %s (got %s, expected at least %s)", |
| this, numPositionalArgs, numMandatoryPositionalParams)); |
| } |
| // We had no named argument, so fail if there were mandatory named-only parameters |
| if (numMandatoryNamedOnlyParams > 0) { |
| throw new EvalException(loc, String.format( |
| "missing mandatory keyword arguments in call to %s", this)); |
| } |
| // Fill in defaults for missing optional parameters, that were conveniently grouped together, |
| // thanks to the absence of mandatory named-only parameters as checked above. |
| if (defaultValues != null) { |
| int j = numPositionalArgs - numMandatoryPositionalParams; |
| int endOptionalParams = numPositionalParams + numOptionalNamedOnlyParams; |
| for (int i = numPositionalArgs; i < endOptionalParams; i++) { |
| arguments[i] = defaultValues.get(j++); |
| } |
| } |
| // If there's a kwParam, it's empty. |
| if (hasKwParam) { |
| // TODO(bazel-team): create a fresh mutable dict, like Python does |
| arguments[kwParamIndex] = SkylarkDict.of(env); |
| } |
| } else if (hasKwParam && numNamedParams == 0) { |
| // Easy case (2b): there are no named parameters, but there is a **kwParam. |
| // Therefore all keyword arguments go directly to the kwParam. |
| // Note that *starParam and **kwParam themselves don't count as named. |
| // Also note that no named parameters means no mandatory parameters that weren't passed, |
| // and no missing optional parameters for which to use a default. Thus, no loops. |
| // NB: not 2a means kwarg isn't null |
| arguments[kwParamIndex] = SkylarkDict.copyOf(env, kwargs); |
| } else { |
| // Hard general case (2c): some keyword arguments may correspond to named parameters |
| SkylarkDict<String, Object> kwArg = hasKwParam ? SkylarkDict.of(env) : SkylarkDict.empty(); |
| |
| // For nicer stabler error messages, start by checking against |
| // an argument being provided both as positional argument and as keyword argument. |
| ArrayList<String> bothPosKey = new ArrayList<>(); |
| for (int i = 0; i < numPositionalArgs; i++) { |
| String name = names.get(i); |
| if (kwargs.containsKey(name)) { |
| bothPosKey.add(name); |
| } |
| } |
| if (!bothPosKey.isEmpty()) { |
| throw new EvalException(loc, |
| String.format("argument%s '%s' passed both by position and by name in call to %s", |
| (bothPosKey.size() > 1 ? "s" : ""), Joiner.on("', '").join(bothPosKey), this)); |
| } |
| |
| // Accept the arguments that were passed. |
| for (Map.Entry<String, Object> entry : kwargs.entrySet()) { |
| String keyword = entry.getKey(); |
| Object value = entry.getValue(); |
| int pos = names.indexOf(keyword); // the list should be short, so linear scan is OK. |
| if (0 <= pos && pos < numNamedParams) { |
| arguments[pos] = value; |
| } else { |
| if (!hasKwParam) { |
| List<String> unexpected = Ordering.natural().sortedCopy(Sets.difference( |
| kwargs.keySet(), ImmutableSet.copyOf(names.subList(0, numNamedParams)))); |
| throw new EvalException(loc, String.format("unexpected keyword%s '%s' in call to %s", |
| unexpected.size() > 1 ? "s" : "", Joiner.on("', '").join(unexpected), this)); |
| } |
| if (kwArg.containsKey(keyword)) { |
| throw new EvalException(loc, String.format( |
| "%s got multiple values for keyword argument '%s'", this, keyword)); |
| } |
| kwArg.put(keyword, value, loc, env); |
| } |
| } |
| if (hasKwParam) { |
| // TODO(bazel-team): create a fresh mutable dict, like Python does |
| arguments[kwParamIndex] = SkylarkDict.copyOf(env, kwArg); |
| } |
| |
| // Check that all mandatory parameters were filled in general case 2c. |
| // Note: it's possible that numPositionalArgs > numMandatoryPositionalParams but that's OK. |
| for (int i = numPositionalArgs; i < numMandatoryPositionalParams; i++) { |
| if (arguments[i] == null) { |
| throw new EvalException(loc, String.format( |
| "missing mandatory positional argument '%s' while calling %s", |
| names.get(i), this)); |
| } |
| } |
| |
| int endMandatoryNamedOnlyParams = numPositionalParams + numMandatoryNamedOnlyParams; |
| for (int i = numPositionalParams; i < endMandatoryNamedOnlyParams; i++) { |
| if (arguments[i] == null) { |
| throw new EvalException(loc, String.format( |
| "missing mandatory named-only argument '%s' while calling %s", |
| names.get(i), this)); |
| } |
| } |
| |
| // Get defaults for those parameters that weren't passed. |
| if (defaultValues != null) { |
| for (int i = Math.max(numPositionalArgs, numMandatoryPositionalParams); |
| i < numPositionalParams; i++) { |
| if (arguments[i] == null) { |
| arguments[i] = defaultValues.get(i - numMandatoryPositionalParams); |
| } |
| } |
| int numMandatoryParams = numMandatoryPositionalParams + numMandatoryNamedOnlyParams; |
| for (int i = numMandatoryParams + numOptionalPositionalParams; i < numNamedParams; i++) { |
| if (arguments[i] == null) { |
| arguments[i] = defaultValues.get(i - numMandatoryParams); |
| } |
| } |
| } |
| } // End of general case 2c for argument passing. |
| |
| return arguments; |
| } |
| |
| /** check types and convert as required */ |
| protected void canonicalizeArguments(Object[] arguments, Location loc) throws EvalException { |
| // TODO(bazel-team): maybe link syntax.SkylarkType and package.Type, |
| // so we can simultaneously typecheck and convert? |
| // Note that a BuiltinFunction already does typechecking of simple types. |
| |
| List<SkylarkType> types = getEnforcedArgumentTypes(); |
| |
| // Check types, if supplied |
| if (types == null) { |
| return; |
| } |
| int length = types.size(); |
| for (int i = 0; i < length; i++) { |
| Object value = arguments[i]; |
| SkylarkType type = types.get(i); |
| if (value != null && type != null && !type.contains(value)) { |
| List<String> names = signature.getSignature().getNames(); |
| throw new EvalException(loc, |
| String.format("expected %s for '%s' while calling %s but got %s instead: %s", |
| type, names.get(i), getName(), EvalUtils.getDataTypeName(value, true), value)); |
| } |
| } |
| } |
| |
| /** |
| * Returns the environment for the scope of this function. |
| * |
| * <p>Since this is a BaseFunction, we don't create a new environment. |
| */ |
| @SuppressWarnings("unused") // For the exception |
| protected Environment getOrCreateChildEnvironment(Environment parent) throws EvalException { |
| return parent; |
| } |
| |
| /** |
| * The outer calling convention to a BaseFunction. |
| * |
| * @param args a list of all positional arguments (as in *starArg) |
| * @param kwargs a map for key arguments (as in **kwArgs) |
| * @param ast the expression for this function's definition |
| * @param env the Environment in the function is called |
| * @return the value resulting from evaluating the function with the given arguments |
| * @throws EvalException-s containing source information. |
| */ |
| public Object call(List<Object> args, |
| @Nullable Map<String, Object> kwargs, |
| @Nullable FuncallExpression ast, |
| Environment env) |
| throws EvalException, InterruptedException { |
| Preconditions.checkState(isConfigured(), "Function %s was not configured", getName()); |
| |
| // ast is null when called from Java (as there's no Skylark call site). |
| Location loc = ast == null ? Location.BUILTIN : ast.getLocation(); |
| |
| Object[] arguments = processArguments(args, kwargs, loc, env); |
| return callWithArgArray(arguments, ast, env, location); |
| } |
| |
| /** |
| * Inner call to a BaseFunction subclasses need to @Override this method. |
| * |
| * @param args an array of argument values sorted as per the signature. |
| * @param ast the source code for the function if user-defined |
| * @param env the lexical environment of the function call |
| * @throws InterruptedException may be thrown in the function implementations. |
| */ |
| // Don't make it abstract, so that subclasses may be defined that @Override the outer call() only. |
| protected Object call(Object[] args, @Nullable FuncallExpression ast, Environment env) |
| throws EvalException, InterruptedException { |
| throw new EvalException( |
| (ast == null) ? Location.BUILTIN : ast.getLocation(), |
| String.format("function %s not implemented", getName())); |
| } |
| |
| /** |
| * The outer calling convention to a BaseFunction. This function expects all arguments to have |
| * been resolved into positional ones. |
| * |
| * @param ast the expression for this function's definition |
| * @param env the Environment in the function is called |
| * @return the value resulting from evaluating the function with the given arguments |
| * @throws EvalException-s containing source information. |
| */ |
| public Object callWithArgArray( |
| Object[] arguments, @Nullable FuncallExpression ast, Environment env, Location loc) |
| throws EvalException, InterruptedException { |
| Preconditions.checkState(isConfigured(), "Function %s was not configured", getName()); |
| canonicalizeArguments(arguments, loc); |
| |
| try { |
| if (Callstack.enabled) { |
| Callstack.push(this); |
| } |
| return call(arguments, ast, env); |
| } finally { |
| if (Callstack.enabled) { |
| Callstack.pop(); |
| } |
| } |
| } |
| |
| /** |
| * Render this object in the form of an equivalent Python function signature. |
| */ |
| @Override |
| public String toString() { |
| StringBuilder sb = new StringBuilder(); |
| sb.append(getName()); |
| if (signature != null) { |
| sb.append('('); |
| signature.toStringBuilder(sb); |
| sb.append(')'); |
| } // if unconfigured, don't even output parentheses |
| return sb.toString(); |
| } |
| |
| /** Configure a BaseFunction from a @SkylarkSignature annotation */ |
| public void configure(SkylarkSignature annotation) { |
| Preconditions.checkState(!isConfigured()); // must not be configured yet |
| |
| this.paramDoc = new ArrayList<>(); |
| this.signature = SkylarkSignatureProcessor.getSignatureForCallable( |
| getName(), annotation, unconfiguredDefaultValues, paramDoc, getEnforcedArgumentTypes()); |
| this.objectType = annotation.objectType().equals(Object.class) |
| ? null : annotation.objectType(); |
| configure(); |
| } |
| |
| /** Configure a function based on its signature */ |
| protected void configure() { |
| // this function is called after the signature was initialized |
| Preconditions.checkState(signature != null); |
| enforcedArgumentTypes = signature.getTypes(); |
| } |
| |
| protected boolean hasSelfArgument() { |
| Class<?> clazz = getObjectType(); |
| if (clazz == null) { |
| return false; |
| } |
| List<SkylarkType> types = signature.getTypes(); |
| ImmutableList<String> names = signature.getSignature().getNames(); |
| |
| return (!types.isEmpty() && types.get(0).canBeCastTo(clazz)) |
| || (!names.isEmpty() && names.get(0).equals("self")); |
| } |
| |
| protected String getObjectTypeString() { |
| Class<?> clazz = getObjectType(); |
| if (clazz == null) { |
| return ""; |
| } |
| return EvalUtils.getDataTypeNameFromClass(clazz, false) + "."; |
| } |
| |
| /** |
| * Returns [class.]function (depending on whether func belongs to a class). |
| */ |
| public String getFullName() { |
| return String.format("%s%s", getObjectTypeString(), getName()); |
| } |
| |
| /** |
| * Returns the signature as "[className.]methodName(name1: paramType1, name2: paramType2, ...)" |
| */ |
| public String getShortSignature() { |
| StringBuilder builder = new StringBuilder(); |
| boolean hasSelf = hasSelfArgument(); |
| |
| builder.append(getFullName()).append("("); |
| signature.toStringBuilder(builder, false, false, hasSelf); |
| builder.append(")"); |
| |
| return builder.toString(); |
| } |
| |
| /** |
| * Prints the types of the first {@code howManyArgsToPrint} given arguments as |
| * "(type1, type2, ...)" |
| */ |
| protected String printTypeString(Object[] args, int howManyArgsToPrint) { |
| StringBuilder builder = new StringBuilder(); |
| builder.append("("); |
| |
| int start = hasSelfArgument() ? 1 : 0; |
| for (int pos = start; pos < howManyArgsToPrint; ++pos) { |
| builder.append(EvalUtils.getDataTypeName(args[pos])); |
| |
| if (pos < howManyArgsToPrint - 1) { |
| builder.append(", "); |
| } |
| } |
| builder.append(")"); |
| return builder.toString(); |
| } |
| |
| @Override |
| public boolean equals(@Nullable Object other) { |
| if (other instanceof BaseFunction) { |
| BaseFunction that = (BaseFunction) other; |
| // In theory, the location alone unambiguously identifies a given function. However, in |
| // some test cases the location might not have a valid value, thus we also check the name. |
| return Objects.equals(this.getName(), that.getName()) |
| && Objects.equals(this.location, that.location); |
| } |
| return false; |
| } |
| |
| @Override |
| public int hashCode() { |
| return Objects.hash(getName(), location); |
| } |
| |
| @Nullable |
| public Location getLocation() { |
| return location; |
| } |
| |
| @Override |
| public boolean isImmutable() { |
| return true; |
| } |
| |
| @Override |
| public void repr(SkylarkPrinter printer) { |
| printer.append("<function " + getName() + ">"); |
| } |
| } |