blob: 0346d45f6727ec8412d1cb29973c11443bd0b399 [file] [log] [blame]
// 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.syntax.SkylarkList.Tuple;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
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.
*/
public abstract class BaseFunction implements StarlarkCallable {
// TODO(adonovan): this class has too many fields and relies too heavily on side effects and the
// class hierarchy (the configure methods are the worse offenders). Turn fields into abstract
// methods. Make processArguments a static function with multiple parameters, instead of a
// "mix-in" that accesses instance fields.
/**
* 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;
/** The function signature; non-null after configure(). */
@Nullable protected FunctionSignature signature;
/**
* The default values of optional parameters. Not defined until after configure(), at which point
* both the list and its elements may be null. A null list is equivalent to a list containing only
* null elements.
*/
// TODO(adonovan): investigate why null elements are permitted. I would expect one one-null
// element per optional parameter, without exception. Also, try to eliminate separate configure
// step.
@Nullable protected List<Object> defaultValues;
/**
* The types of parameters, for annotation-based methods; null for others. May contain null
* elements. These "official" types are not necessarily the same as the "enforced" types used in
* the actual run-time checks.
*/
@Nullable protected List<SkylarkType> paramTypes;
// 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;
// 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 List<SkylarkType> enforcedArgumentTypes;
/**
* 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 getSignature() {
return signature;
}
/**
* Returns the tuple of parameter default values of this function value. May be null and may
* contain null elements.
*/
@Nullable
public List<Object> getDefaultValues() {
return defaultValues;
}
/** 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 (signature-less) 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.
*/
protected 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
*/
protected BaseFunction(
@Nullable String name,
FunctionSignature signature,
@Nullable List<Object> defaultValues,
@Nullable Location location) {
this(name);
this.signature = Preconditions.checkNotNull(signature);
this.defaultValues = defaultValues;
this.location = location;
if (defaultValues != null) {
Preconditions.checkArgument(defaultValues.size() == signature.numOptionals());
}
if (paramTypes != null) {
Preconditions.checkArgument(paramTypes.size() == signature.numParameters());
}
}
/**
* 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 function signature
*/
protected BaseFunction(@Nullable String name, FunctionSignature signature) {
this(name, signature, /*defaultValues=*/ null, /*location=*/ null);
}
/**
* The size of the array required by the callee.
*/
protected int getArgArraySize() {
return signature.numParameters();
}
/**
* 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
*/
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 StarlarkThread thread)
throws EvalException {
Object[] arguments = new Object[getArgArraySize()];
ImmutableList<String> names = signature.getParameterNames();
// 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 = signature.numMandatoryPositionals();
int numOptionalPositionalParams = signature.numOptionalPositionals();
int numMandatoryNamedOnlyParams = signature.numMandatoryNamedOnly();
int numOptionalNamedOnlyParams = signature.numOptionalNamedOnly();
boolean hasVarargs = signature.hasVarargs();
boolean hasKwargs = signature.hasKwargs();
int numPositionalParams = numMandatoryPositionalParams + numOptionalPositionalParams;
int numNamedOnlyParams = numMandatoryNamedOnlyParams + numOptionalNamedOnlyParams;
int numNamedParams = numPositionalParams + numNamedOnlyParams;
int kwargIndex = names.size() - 1; // only valid if hasKwargs
// (1) handle positional arguments
if (hasVarargs) {
// 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 kwarg, it's empty.
if (hasKwargs) {
// TODO(bazel-team): create a fresh mutable dict, like Python does
arguments[kwargIndex] = SkylarkDict.of(thread);
}
} else if (hasKwargs && numNamedParams == 0) {
// Easy case (2b): there are no named parameters, but there is a **kwargs.
// Therefore all keyword arguments go directly to the kwarg.
// Note that *args and **kwargs 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[kwargIndex] = SkylarkDict.copyOf(thread, kwargs);
} else {
// Hard general case (2c): some keyword arguments may correspond to named parameters
SkylarkDict<String, Object> kwArg = hasKwargs ? SkylarkDict.of(thread) : 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 (!hasKwargs) {
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, thread);
}
}
if (hasKwargs) {
// TODO(bazel-team): create a fresh mutable dict, like Python does
arguments[kwargIndex] = SkylarkDict.copyOf(thread, 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 */
private void canonicalizeArguments(Object[] arguments, Location loc) throws EvalException {
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.getParameterNames();
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));
}
}
}
/**
* The outer calling convention to a BaseFunction.
*
* @param args a list of all positional arguments (as in *args)
* @param kwargs a map for key arguments (as in **kwargs)
* @param ast the expression for this function's definition
* @param thread the StarlarkThread 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,
StarlarkThread thread)
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, thread);
return callWithArgArray(arguments, ast, thread, 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 thread the Starlark thread for the 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, StarlarkThread thread)
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 thread the StarlarkThread in the function is called
* @return the value resulting from evaluating the function with the given arguments
* @throws EvalException-s containing source information.
*/
// TODO(adonovan): make this private. The sole external caller has a location but no ast.
public Object callWithArgArray(
Object[] arguments, @Nullable FuncallExpression ast, StarlarkThread thread, 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, thread);
} 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 unconfigured, don't even output parentheses.
if (signature != null) {
sb.append('(');
signature.toStringBuilder(sb, this::printDefaultValue, this::printType, false);
sb.append(')');
}
return sb.toString();
}
private String printDefaultValue(int i) {
Object v = defaultValues != null ? defaultValues.get(i) : null;
return v != null ? Printer.repr(v) : null;
}
private String printType(int i) {
SkylarkType t = paramTypes != null ? paramTypes.get(i) : null;
return t != null ? t.toString() : null;
}
/** Configure a function based on its signature */
// This function is called after the signature is initialized.
void configure() {
Preconditions.checkState(signature != null);
// BuiltinFunction overrides this method without calling this
// implementation, so this statement does not clobber the
// enforcedArgumentTypes computed by getSignatureForCallable.
// Still it is hard to explain what the configure method does.
// TODO(adonovan): eliminate SkylarkSignature then simplify.
this.enforcedArgumentTypes = this.paramTypes;
}
protected boolean hasSelfArgument() {
Class<?> clazz = getObjectType();
if (clazz == null) {
return false;
}
// TODO(adonovan): paramTypes can be null. How does this work?
List<SkylarkType> types = paramTypes;
ImmutableList<String> names = signature.getParameterNames();
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, /*defaultValuePrinter=*/ null, /*typePrinter=*/ null, 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();
}
@Nullable
public Location getLocation() {
return location;
}
@Override
public boolean isImmutable() {
return true;
}
@Override
public void repr(SkylarkPrinter printer) {
printer.append("<function " + getName() + ">");
}
}