blob: d5485270fea38469899abd48e3730ed8b8743770 [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.devtools.starlark.spelling.SpellChecker;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
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 Resolver.Function rfn;
private final Module module; // a function closes over its defining module
private final Tuple<Object> defaultValues;
StarlarkFunction(Resolver.Function rfn, Tuple<Object> defaultValues, Module module) {
this.rfn = rfn;
this.module = module;
this.defaultValues = defaultValues;
}
boolean isToplevel() {
return rfn.isToplevel;
}
/**
* Returns the default value of the ith parameter ({@code 0 <= i < getParameterNames().size()}),
* or null if the parameter is not optional. Residual parameters, if any, are always last, and
* have no default value.
*/
@Nullable
public Object getDefaultValue(int i) {
if (i >= 0) {
// def f(a, b=1, *args, c, d=2, **kwargs) has defaults tuple (b=1, d=2).
// TODO(adonovan): eliminate hole using a sentinel, to simplify this
// and other run-time logic.
int a = rfn.numMandatoryPositional;
int b = rfn.numOptionalPositional;
int c = rfn.numMandatoryNamedOnly; // the hole
int d = rfn.numOptionalNamedOnly;
if (i < a) {
return null;
} else if (i < a + b) {
return defaultValues.get(i - a);
} else if (i < a + b + c) {
return null;
} else if (i < a + b + c + d) {
return defaultValues.get(i - a - c);
} else if (i < rfn.parameterNames.size()) {
return null; // *args or **kwargs TODO(adonovan): make this an error.
}
}
throw new IndexOutOfBoundsException();
}
/**
* Returns the names of this function's parameters. The residual {@code *args} and {@code
* **kwargs} parameters, if any, are always last.
*/
public ImmutableList<String> getParameterNames() {
return rfn.parameterNames;
}
/**
* Reports whether this function has a residual positional arguments parameter, {@code def
* f(*args)}.
*/
public boolean hasVarargs() {
return rfn.varargs != null;
}
/**
* Reports whether this function has a residual keyword arguments parameter, {@code def
* f(**kwargs)}.
*/
public boolean hasKwargs() {
return rfn.kwargs != null;
}
@Override
public Location getLocation() {
return rfn.location;
}
@Override
public String getName() {
return rfn.name;
}
/** Returns the value denoted by the function's doc string literal, or null if absent. */
@Nullable
public String getDocumentation() {
if (rfn.body.isEmpty()) {
return null;
}
Statement first = rfn.body.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", getName());
}
// Compute the effective parameter values
// and update the corresponding variables.
Object[] arguments = processArgs(thread.mutability(), positional, named);
StarlarkThread.Frame fr = thread.frame(0);
ImmutableList<String> names = rfn.parameterNames;
for (int i = 0; i < names.size(); ++i) {
fr.locals.put(names.get(i), arguments[i]);
}
return Eval.execFunctionBody(fr, rfn.body);
}
@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 positional and named arguments to ensure they match the signature. It returns a new
// array of effective parameter values corresponding to the parameters of the signature. Newly
// allocated values (e.g. a **kwargs dict) use the Mutability mu.
//
// If the function has optional parameters, their default values are supplied by getDefaultValues.
private Object[] processArgs(Mutability mu, Object[] positional, Object[] named)
throws EvalException {
ImmutableList<String> names = rfn.parameterNames;
// TODO(adonovan): when we have flat frames, pass in the locals array here instead of
// allocating.
Object[] arguments = new Object[names.size()];
// 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 numPositionalParams = rfn.numPositional();
int numNamedParams = rfn.numPositional() + rfn.numNamedOnly();
// positional arguments
if (rfn.varargs != null) {
Object varargs;
if (numPositionalArgs > rfn.numPositional()) {
varargs =
Tuple.wrap(Arrays.copyOfRange(positional, rfn.numPositional(), numPositionalArgs));
numPositionalArgs = numPositionalParams; // clip numPositionalArgs
} else {
varargs = Tuple.empty();
}
arguments[numNamedParams] = varargs;
} else if (numPositionalArgs > numPositionalParams) {
if (numPositionalParams > 0) {
throw Starlark.errorf(
"%s() accepts no more than %d positional argument%s but got %d",
getName(), numPositionalParams, plural(numPositionalParams), numPositionalArgs);
} else {
throw Starlark.errorf(
"%s() does not accept positional arguments, but got %d", getName(), numPositionalArgs);
}
}
for (int i = 0; i < numPositionalArgs; i++) {
arguments[i] = positional[i];
}
// **kwargs
Dict<String, Object> kwargs = null;
if (rfn.kwargs != null) {
kwargs = Dict.of(mu);
arguments[names.size() - 1] = kwargs;
}
List<String> missing = null;
// named arguments
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) {
// keyword is the name of a named parameter
if (arguments[pos] != null) {
throw Starlark.errorf("%s() got multiple values for parameter '%s'", getName(), keyword);
}
arguments[pos] = value;
} else if (kwargs != null) {
// residual keyword argument
int sz = kwargs.size();
kwargs.put(keyword, value, null);
if (kwargs.size() == sz) {
throw Starlark.errorf(
"%s() got multiple values for keyword argument '%s'", getName(), keyword);
}
} else {
// unexpected keyword argument
if (missing == null) {
missing = new ArrayList<>();
}
missing.add(keyword);
}
}
if (missing != null) {
// Give a spelling hint if there is exactly one.
// More than that suggests the wrong function was called.
throw Starlark.errorf(
"%s() got unexpected keyword argument%s: %s%s",
getName(),
plural(missing.size()),
Joiner.on(", ").join(missing),
missing.size() == 1 ? SpellChecker.didYouMean(missing.get(0), names) : "");
}
// missing mandatory positionals?
// numPositionalArgs > rfn.numMandatoryPositional is OK
for (int i = numPositionalArgs; i < rfn.numMandatoryPositional; i++) {
if (arguments[i] == null) {
if (missing == null) {
missing = new ArrayList<>();
}
missing.add(names.get(i));
}
}
if (missing != null) {
throw Starlark.errorf(
"%s() missing %d required positional argument%s: %s",
getName(), missing.size(), plural(missing.size()), Joiner.on(", ").join(missing));
}
// missing mandatory named-onlys?
int endMandatoryNamedOnlyParams = rfn.numPositional() + rfn.numMandatoryNamedOnly;
for (int i = numPositionalParams; i < endMandatoryNamedOnlyParams; i++) {
if (arguments[i] == null) {
if (missing == null) {
missing = new ArrayList<>();
}
missing.add(names.get(i));
}
}
if (missing != null) {
throw Starlark.errorf(
"%s() missing %d required keyword-only argument%s: %s",
getName(), missing.size(), plural(missing.size()), Joiner.on(", ").join(missing));
}
// default values
for (int i = Math.max(numPositionalArgs, rfn.numMandatoryPositional);
i < numPositionalParams;
i++) {
if (arguments[i] == null) {
arguments[i] = defaultValues.get(i - rfn.numMandatoryPositional);
}
}
int numMandatoryParams = rfn.numMandatoryPositional + rfn.numMandatoryNamedOnly;
for (int i = numMandatoryParams + rfn.numOptionalPositional; i < numNamedParams; i++) {
if (arguments[i] == null) {
arguments[i] = defaultValues.get(i - numMandatoryParams);
}
}
return arguments;
}
private static String plural(int n) {
return n == 1 ? "" : "s";
}
@Override
public String toString() {
StringBuilder out = new StringBuilder();
out.append(getName());
out.append('(');
String sep = "";
for (String param : getParameterNames()) {
out.append(sep).append(param);
sep = ", ";
}
out.append(')');
return out.toString();
}
@Override
public boolean isImmutable() {
// Only correct because closures are not yet supported.
return true;
}
}