blob: 3136a3bc3e6de916fb5cd0d69b839b72023338c6 [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 net.starlark.java.eval;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Maps;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import javax.annotation.Nullable;
import net.starlark.java.annot.StarlarkBuiltin;
import net.starlark.java.spelling.SpellChecker;
import net.starlark.java.syntax.Location;
import net.starlark.java.syntax.Resolver;
/** A StarlarkFunction is a function value created by a Starlark {@code def} statement. */
@StarlarkBuiltin(
name = "function",
category = "core",
doc = "The type of functions declared in Starlark.")
public final class StarlarkFunction implements StarlarkCallable {
final Resolver.Function rfn;
private final Module module; // a function closes over its defining module
// Index in Module.globals of ith Program global (Resolver.Binding(GLOBAL).index).
// See explanation at Starlark.execFileProgram.
final int[] globalIndex;
// Default values of optional parameters.
// Indices correspond to the subsequence of parameters after the initial
// required parameters and before *args/**kwargs.
// Contain MANDATORY for the required keyword-only parameters.
private final Tuple defaultValues;
// Cells (shared locals) of enclosing functions.
// Indexed by Resolver.Binding(FREE).index values.
private final Tuple freevars;
StarlarkFunction(
Resolver.Function rfn,
Module module,
int[] globalIndex,
Tuple defaultValues,
Tuple freevars) {
this.rfn = rfn;
this.module = module;
this.globalIndex = globalIndex;
this.defaultValues = defaultValues;
this.freevars = freevars;
}
// Sets a global variable, given its index in this function's compiled Program.
void setGlobal(int progIndex, Object value) {
module.setGlobalByIndex(globalIndex[progIndex], value);
}
// Gets the value of a global variable, given its index in this function's compiled Program.
@Nullable
Object getGlobal(int progIndex) {
return module.getGlobalByIndex(globalIndex[progIndex]);
}
boolean isToplevel() {
return rfn.isToplevel();
}
// TODO(adonovan): many functions would be simpler if
// parameterNames excluded the *args and **kwargs parameters,
// (whose names are immaterial to the callee anyway). Do that.
// Also, reject getDefaultValue for varargs and kwargs.
/**
* Returns the default value of the ith parameter ({@code 0 <= i < getParameterNames().size()}),
* or null if the parameter is required. Residual parameters, if any, are always last, and have no
* default value.
*/
@Nullable
public Object getDefaultValue(int i) {
if (i < 0 || i >= rfn.getParameters().size()) {
throw new IndexOutOfBoundsException();
}
int nparams =
rfn.getParameters().size() - (rfn.hasKwargs() ? 1 : 0) - (rfn.hasVarargs() ? 1 : 0);
int prefix = nparams - defaultValues.size();
if (i < prefix) {
return null; // implicit prefix of mandatory parameters
}
if (i < nparams) {
Object v = defaultValues.get(i - prefix);
return v == MANDATORY ? null : v;
}
return null; // *args or *kwargs
}
/**
* 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.getParameterNames();
}
/**
* Reports whether this function has a residual positional arguments parameter, {@code def
* f(*args)}.
*/
public boolean hasVarargs() {
return rfn.hasVarargs();
}
/**
* Reports whether this function has a residual keyword arguments parameter, {@code def
* f(**kwargs)}.
*/
public boolean hasKwargs() {
return rfn.hasKwargs();
}
/** Returns the location of the function's defining identifier. */
@Override
public Location getLocation() {
return rfn.getLocation();
}
/**
* Returns the name of the function, or "lambda" if anonymous. Implicit functions (those not
* created by a def statement), may have names such as "<toplevel>" or "<expr>".
*/
@Override
public String getName() {
return rfn.getName();
}
/**
* Returns the value denoted by the function's doc string literal (trimmed if necessary), or null
* if absent.
*/
@Nullable
public String getDocumentation() {
String documentation = rfn.getDocumentation();
return documentation != null ? Starlark.trimDocString(documentation) : null;
}
public Module getModule() {
return module;
}
@Override
public Object fastcall(StarlarkThread thread, Object[] positional, Object[] named)
throws EvalException, InterruptedException {
if (!thread.isRecursionAllowed() && thread.isRecursiveCall(this)) {
throw Starlark.errorf("function '%s' called recursively", getName());
}
// Compute the effective parameter values
// and update the corresponding variables.
StarlarkThread.Frame fr = thread.frame(0);
fr.locals = processArgs(thread.mutability(), positional, named);
// Spill indicated locals to cells.
for (int index : rfn.getCellIndices()) {
fr.locals[index] = new Cell(fr.locals[index]);
}
return Eval.execFunctionBody(fr, rfn.getBody());
}
Cell getFreeVar(int index) {
return (Cell) freevars.get(index);
}
@Override
public void repr(Printer printer) {
// TODO(adonovan): use the file name instead. But that's a breaking Bazel change.
Object clientData = module.getClientData();
printer.append("<function " + getName());
if (clientData != null) {
printer.append(" from " + clientData);
}
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. The
// returned array has size of locals and is directly pushed to the stack.
// Newly allocated values (e.g. a **kwargs dict) use the Mutability mu.
//
// If the function has optional parameters, their default values are supplied by getDefaultValue.
private Object[] processArgs(Mutability mu, Object[] positional, Object[] named)
throws EvalException {
// This is the general schema of a function:
//
// def f(p1, p2=dp2, p3=dp3, *args, k1, k2=dk2, k3, **kwargs)
//
// The p parameters are non-kwonly, and may be specified positionally.
// The k parameters are kwonly, and must be specified by name.
// The defaults tuple is (dp2, dp3, MANDATORY, dk2, MANDATORY).
// The missing prefix (p1) is assumed to be all MANDATORY.
//
// Arguments are processed as follows:
// - positional arguments are bound to a prefix of [p1, p2, p3].
// - surplus positional arguments are bound to *args.
// - keyword arguments are bound to any of {p1, p2, p3, k1, k2, k3};
// duplicate bindings are rejected.
// - surplus keyword arguments are bound to **kwargs.
// - defaults are bound to each parameter from p2 to k3 if no value was set.
// default values come from the tuple above.
// It is an error if the defaults tuple entry for an unset parameter is MANDATORY.
ImmutableList<String> names = rfn.getParameterNames();
Object[] locals = new Object[rfn.getLocals().size()];
// nparams is the number of ordinary parameters.
int nparams =
rfn.getParameters().size() - (rfn.hasKwargs() ? 1 : 0) - (rfn.hasVarargs() ? 1 : 0);
// numPositionalParams is the number of non-kwonly parameters.
int numPositionalParams = nparams - rfn.numKeywordOnlyParams();
// Too many positional args?
int n = positional.length;
if (n > numPositionalParams) {
if (!rfn.hasVarargs()) {
if (numPositionalParams > 0) {
throw Starlark.errorf(
"%s() accepts no more than %d positional argument%s but got %d",
getName(), numPositionalParams, plural(numPositionalParams), n);
} else {
throw Starlark.errorf(
"%s() does not accept positional arguments, but got %d", getName(), n);
}
}
n = numPositionalParams;
}
// Inv: n is number of positional arguments that are not surplus.
// Bind positional arguments to non-kwonly parameters.
for (int i = 0; i < n; i++) {
locals[i] = positional[i];
}
// Bind surplus positional arguments to *args parameter.
if (rfn.hasVarargs()) {
locals[nparams] = Tuple.wrap(Arrays.copyOfRange(positional, n, positional.length));
}
List<String> unexpected = null;
// Named arguments.
LinkedHashMap<String, Object> kwargs = null;
if (rfn.hasKwargs()) {
// To avoid Dict overhead, we populate a LinkedHashMap and then pass it to Dict.wrap()
// afterwards. (The contract of Dict.wrap prohibits us from modifying the map once the Dict is
// created.)
kwargs = Maps.newLinkedHashMapWithExpectedSize(1);
}
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 < nparams) {
// keyword is the name of a named parameter
if (locals[pos] != null) {
throw Starlark.errorf("%s() got multiple values for parameter '%s'", getName(), keyword);
}
locals[pos] = value;
} else if (kwargs != null) {
// residual keyword argument
if (kwargs.put(keyword, value) != null) {
throw Starlark.errorf(
"%s() got multiple values for keyword argument '%s'", getName(), keyword);
}
} else {
// unexpected keyword argument
if (unexpected == null) {
unexpected = new ArrayList<>();
}
unexpected.add(keyword);
}
}
if (unexpected != 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(unexpected.size()),
Joiner.on(", ").join(unexpected),
unexpected.size() == 1
? SpellChecker.didYouMean(unexpected.get(0), names.subList(0, nparams))
: "");
}
if (kwargs != null) {
locals[rfn.getParameters().size() - 1] = Dict.wrap(mu, kwargs);
}
// Apply defaults and report errors for missing required arguments.
int m = nparams - defaultValues.size(); // first default
List<String> missingPositional = null;
List<String> missingKwonly = null;
for (int i = n; i < nparams; i++) {
// provided?
if (locals[i] != null) {
continue;
}
// optional?
if (i >= m) {
Object dflt = defaultValues.get(i - m);
if (dflt != MANDATORY) {
locals[i] = dflt;
continue;
}
}
// missing
if (i < numPositionalParams) {
if (missingPositional == null) {
missingPositional = new ArrayList<>();
}
missingPositional.add(names.get(i));
} else {
if (missingKwonly == null) {
missingKwonly = new ArrayList<>();
}
missingKwonly.add(names.get(i));
}
}
if (missingPositional != null) {
throw Starlark.errorf(
"%s() missing %d required positional argument%s: %s",
getName(),
missingPositional.size(),
plural(missingPositional.size()),
Joiner.on(", ").join(missingPositional));
}
if (missingKwonly != null) {
throw Starlark.errorf(
"%s() missing %d required keyword-only argument%s: %s",
getName(),
missingKwonly.size(),
plural(missingKwonly.size()),
Joiner.on(", ").join(missingKwonly));
}
return locals;
}
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 = "";
// TODO(adonovan): include *, ** tokens.
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;
}
// The MANDATORY sentinel indicates a slot in the defaultValues
// tuple corresponding to a required parameter.
// It is not visible to Java or Starlark code.
static final Object MANDATORY = new Mandatory();
private static class Mandatory implements StarlarkValue {}
// A Cell is a local variable shared between an inner and an outer function.
// It is a StarlarkValue because it is a stack operand and a Tuple element,
// but it is not visible to Java or Starlark code.
static final class Cell implements StarlarkValue {
Object x;
Cell(Object x) {
this.x = x;
}
}
}