// 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.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.SkylarkSignature;
import com.google.devtools.build.lib.skylarkinterface.SkylarkValue;
import com.google.devtools.build.lib.syntax.SkylarkList.Tuple;
import com.google.devtools.build.lib.syntax.compiler.ByteCodeUtils;
import com.google.devtools.build.lib.util.Preconditions;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import javax.annotation.Nullable;
import net.bytebuddy.implementation.bytecode.StackManipulation;
/**
* A base class for Skylark functions, whether builtin or user-defined.
*
*
Nomenclature:
* We call "Parameters" the formal parameters of a function definition.
* We call "Arguments" the actual values supplied at the call site.
*
*
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} args for positional arguments
* and a {@code Map} 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.
*
* 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}.
*
*
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 SkylarkValue {
// The name of the function
private final String name;
// A function signature, including defaults and types
// never null after it is configured
@Nullable protected FunctionSignature.WithValues signature;
// Location of the function definition, or null 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 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 enforcedArgumentTypes;
// Defaults to be used when configure(annotation) is called (after the function is constructed).
@Nullable private Iterable 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. */
public String getName() {
return name;
}
/** Returns the signature of this function. */
@Nullable public FunctionSignature.WithValues 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.
*
* @param name the function name
*/
public BaseFunction(String name) {
this.name = name;
}
/**
* Constructs a BaseFunction with a given name, signature and location.
*
* @param name the function name
* @param signature the signature with default values and types
* @param location the location of function definition
*/
public BaseFunction(String name,
@Nullable FunctionSignature.WithValues 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
* @param signature the signature, with default values and types
*/
public BaseFunction(String name,
@Nullable FunctionSignature.WithValues signature) {
this(name, signature, null);
}
/**
* Constructs a BaseFunction with a given name and signature without default values or types.
*
* @param name the function name
* @param signature the signature, without default values or types
*/
public BaseFunction(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
* @param defaultValues a list of default values for the optional arguments to be configured.
*/
public BaseFunction(String name, @Nullable Iterable defaultValues) {
this(name);
this.unconfiguredDefaultValues = defaultValues;
}
/** Get parameter documentation as a list corresponding to each parameter */
public List 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 getEnforcedArgumentTypes() {
return enforcedArgumentTypes;
}
/**
* Process the caller-provided arguments into an array suitable for the callee (this function).
*/
public Object[] processArguments(List args,
@Nullable Map 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 names = sig.getNames();
List 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 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 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 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 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 types = getEnforcedArgumentTypes();
// Check types, if supplied
if (types == null) {
return;
}
List names = signature.getSignature().getNames();
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)) {
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.
*
* 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;
}
public static final StackManipulation call =
ByteCodeUtils.invoke(
BaseFunction.class,
"call",
List.class,
Map.class,
FuncallExpression.class,
Environment.class);
/**
* 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 args,
@Nullable Map 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);
canonicalizeArguments(arguments, loc);
return call(arguments, ast, env);
}
/**
* 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, @Nullable Environment env)
throws EvalException, InterruptedException {
throw new EvalException(
(ast == null) ? Location.BUILTIN : ast.getLocation(),
String.format("function %s not implemented", getName()));
}
/**
* 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.getSignature(
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 types = signature.getTypes();
ImmutableList 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, ...)"
* or "[className.]methodName(paramType1, paramType2, ...)", depending on the value of showNames.
*/
public String getShortSignature(boolean showNames) {
StringBuilder builder = new StringBuilder();
boolean hasSelf = hasSelfArgument();
builder.append(getFullName()).append("(");
signature.toStringBuilder(builder, showNames, 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.name, that.name) && Objects.equals(this.location, that.location);
}
return false;
}
@Override
public int hashCode() {
return Objects.hash(name, location);
}
@Nullable
public Location getLocation() {
return location;
}
@Override
public boolean isImmutable() {
return true;
}
@Override
public void write(Appendable buffer, char quotationMark) {
Printer.append(buffer, "");
}
}