blob: 5a899c1913b0a7ec38bcef9876f763e1c14f4355 [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.collect.ImmutableList;
import com.google.common.collect.Ordering;
import com.google.common.collect.Sets;
import java.util.Arrays;
import java.util.Set;
import javax.annotation.Nullable;
/** A StarlarkFunction is a function value created by a Starlark {@code def} statement. */
public final class StarlarkFunction implements StarlarkCallable {
private final String name;
private final FunctionSignature signature;
private final Location location;
private final ImmutableList<Statement> statements;
private final Module module; // a function closes over its defining module
private final Tuple<Object> defaultValues;
// isToplevel indicates that this is the <toplevel> function containing
// top-level statements of a file. It causes assignments to unresolved
// identifiers to update the module, not the lexical frame.
// TODO(adonovan): remove this hack when identifier resolution is accurate.
boolean isToplevel;
StarlarkFunction(
String name,
Location location,
FunctionSignature signature,
Tuple<Object> defaultValues,
ImmutableList<Statement> statements,
Module module) {
this.name = name;
this.signature = signature;
this.location = location;
this.statements = statements;
this.module = module;
this.defaultValues = defaultValues;
}
/**
* Returns the optional tuple of default values for optional parameters. For example, the defaults
* for {@code def f(a, b=1, *, c, d=2)} would be {@code (1, 2)}. May return null if the function
* has no optional parameters.
*/
public Tuple<Object> getDefaultValues() {
return defaultValues;
}
/** Returns the signature of this function. */
public FunctionSignature getSignature() {
return signature;
}
@Override
public Location getLocation() {
return location;
}
@Override
public String getName() {
return name;
}
/** Returns the value denoted by the function's doc string literal, or null if absent. */
@Nullable
public String getDocumentation() {
if (statements.isEmpty()) {
return null;
}
Statement first = statements.get(0);
if (!(first instanceof ExpressionStatement)) {
return null;
}
Expression expr = ((ExpressionStatement) first).getExpression();
if (!(expr instanceof StringLiteral)) {
return null;
}
return ((StringLiteral) expr).getValue();
}
public Module getModule() {
return module;
}
@Override
public Object fastcall(StarlarkThread thread, Object[] positional, Object[] named)
throws EvalException, InterruptedException {
if (thread.mutability().isFrozen()) {
throw Starlark.errorf("Trying to call in frozen environment");
}
if (thread.isRecursiveCall(this)) {
throw Starlark.errorf("function '%s' called recursively", name);
}
// Compute the effective parameter values
// and update the corresponding variables.
Object[] arguments =
matchSignature(
getSignature(), this, getDefaultValues(), thread.mutability(), positional, named);
StarlarkThread.Frame fr = thread.frame(0);
ImmutableList<String> names = getSignature().getParameterNames();
for (int i = 0; i < names.size(); ++i) {
fr.locals.put(names.get(i), arguments[i]);
}
return Eval.execFunctionBody(fr, statements);
}
@Override
public void repr(Printer printer) {
Object label = module.getLabel();
printer.append("<function " + getName());
if (label != null) {
printer.append(" from " + label);
}
printer.append(">");
}
/**
* Checks the {@code positional} and {@code named} arguments to ensure they match the {@code
* signature}. It returns an array of effective parameter values corresponding to the parameters
* of the signature. Newly allocated values (e.g. a {@code **kwargs} dict) use the Mutability
* {@code mu}.
*
* <p>If the function has optional parameters, their default values must be supplied by {@code
* defaults}; see {@link getDefaultValues} for details.
*
* <p>The caller is responsible for accessing the correct element and casting to an appropriate
* type.
*
* <p>On failure, it throws an EvalException incorporating {@code func.toString()}.
*/
private static Object[] matchSignature(
FunctionSignature signature,
StarlarkCallable func, // only used in error messages
@Nullable Tuple<Object> defaults,
@Nullable Mutability mu,
Object[] positional,
Object[] named)
throws EvalException {
// TODO(adonovan): simplify this function. Combine cases 1 and 2 without loss of efficiency.
// TODO(adonovan): reduce the verbosity of errors. Printing func.toString is often excessive.
// Make the error messages consistent in form.
// TODO(adonovan): report an error if there were missing values in 'defaults'.
Object[] arguments = new Object[signature.numParameters()];
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 = positional.length;
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.
Object varargs;
if (numPositionalArgs > numPositionalParams) {
varargs =
Tuple.wrap(Arrays.copyOfRange(positional, numPositionalParams, numPositionalArgs));
numPositionalArgs = numPositionalParams; // clip numPositionalArgs
} else {
varargs = Tuple.empty();
}
arguments[numNamedParams] = varargs;
} else if (numPositionalArgs > numPositionalParams) {
throw new EvalException(
null,
numPositionalParams > 0
? "too many (" + numPositionalArgs + ") positional arguments in call to " + func
: func + " does not accept positional arguments, but got " + numPositionalArgs);
}
for (int i = 0; i < numPositionalArgs; i++) {
arguments[i] = positional[i];
}
// (2) handle keyword arguments
if (named.length == 0) {
// 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 Starlark.errorf(
"insufficient arguments received by %s (got %s, expected at least %s)",
func, numPositionalArgs, numMandatoryPositionalParams);
}
// We had no named argument, so fail if there were mandatory named-only parameters
if (numMandatoryNamedOnlyParams > 0) {
throw Starlark.errorf("missing mandatory keyword arguments in call to %s", func);
}
// 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 (defaults != null) {
int endOptionalParams = numPositionalParams + numOptionalNamedOnlyParams;
for (int i = numPositionalArgs; i < endOptionalParams; i++) {
arguments[i] = defaults.get(i - numMandatoryPositionalParams);
}
}
// If there's a kwarg, it's empty.
if (hasKwargs) {
arguments[kwargIndex] = Dict.of(mu);
}
} else {
// general case (2b): some keyword arguments may correspond to named parameters
Dict<String, Object> kwargs = hasKwargs ? Dict.of(mu) : null;
// Accept the named arguments that were passed.
for (int i = 0; i < named.length; i += 2) {
String keyword = (String) named[i]; // safe
Object value = named[i + 1];
int pos = names.indexOf(keyword); // the list should be short, so linear scan is OK.
if (0 <= pos && pos < numNamedParams) {
if (arguments[pos] != null) {
throw Starlark.errorf("%s got multiple values for parameter '%s'", func, keyword);
}
arguments[pos] = value;
} else {
if (!hasKwargs) {
Set<String> unexpected = Sets.newHashSet();
for (int j = 0; j < named.length; j += 2) {
unexpected.add((String) named[j]);
}
unexpected.removeAll(names.subList(0, numNamedParams));
// TODO(adonovan): do spelling check.
throw Starlark.errorf(
"unexpected keyword%s '%s' in call to %s",
unexpected.size() > 1 ? "s" : "",
Joiner.on("', '").join(Ordering.natural().sortedCopy(unexpected)),
func);
}
int sz = kwargs.size();
kwargs.put(keyword, value, null);
if (kwargs.size() == sz) {
throw Starlark.errorf(
"%s got multiple values for keyword argument '%s'", func, keyword);
}
}
}
if (hasKwargs) {
arguments[kwargIndex] = kwargs;
}
// Check that all mandatory parameters were filled in general case 2b.
// Note: it's possible that numPositionalArgs > numMandatoryPositionalParams but that's OK.
for (int i = numPositionalArgs; i < numMandatoryPositionalParams; i++) {
if (arguments[i] == null) {
throw Starlark.errorf(
"missing mandatory positional argument '%s' while calling %s", names.get(i), func);
}
}
int endMandatoryNamedOnlyParams = numPositionalParams + numMandatoryNamedOnlyParams;
for (int i = numPositionalParams; i < endMandatoryNamedOnlyParams; i++) {
if (arguments[i] == null) {
throw Starlark.errorf(
"missing mandatory named-only argument '%s' while calling %s", names.get(i), func);
}
}
// Get defaults for those parameters that weren't passed.
if (defaults != null) {
for (int i = Math.max(numPositionalArgs, numMandatoryPositionalParams);
i < numPositionalParams;
i++) {
if (arguments[i] == null) {
arguments[i] = defaults.get(i - numMandatoryPositionalParams);
}
}
int numMandatoryParams = numMandatoryPositionalParams + numMandatoryNamedOnlyParams;
for (int i = numMandatoryParams + numOptionalPositionalParams; i < numNamedParams; i++) {
if (arguments[i] == null) {
arguments[i] = defaults.get(i - numMandatoryParams);
}
}
}
} // End of general case 2b for argument passing.
return arguments;
}
/** Render this object in the form of an equivalent Python function signature. */
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(getName());
sb.append('(');
getSignature().toStringBuilder(sb, this::printDefaultValue);
sb.append(')');
return sb.toString();
}
private String printDefaultValue(int i) {
Tuple<Object> defaultValues = getDefaultValues();
Object v = defaultValues != null ? defaultValues.get(i) : null;
return v != null ? Starlark.repr(v) : null;
}
@Override
public boolean isImmutable() {
// Only correct because closures are not yet supported.
return true;
}
}