com.google.devtools.build.lib.syntax.BaseFunction.java Source code

Java tutorial

Introduction

Here is the source code for com.google.devtools.build.lib.syntax.BaseFunction.java

Source

// 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.util.Preconditions;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
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.
 */
// 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<Object, SkylarkType> 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<String> 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<SkylarkType> enforcedArgumentTypes;

    // Defaults to be used when configure(annotation) is called (after the function is constructed).
    @Nullable
    private Iterable<Object> 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<Object, SkylarkType> 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<Object, SkylarkType> 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<Object, SkylarkType> 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.<Object, SkylarkType>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<Object> defaultValues) {
        this(name);
        this.unconfiguredDefaultValues = defaultValues;
    }

    /** Get parameter documentation as a list corresponding to each parameter */
    public List<String> 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<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 Environment env) throws EvalException {

        Object[] arguments = new Object[getArgArraySize()];

        // extract function signature
        FunctionSignature sig = signature.getSignature();
        FunctionSignature.Shape shape = sig.getShape();
        ImmutableList<String> names = sig.getNames();
        List<Object> 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<String, Object> kwArg = hasKwParam ? SkylarkDict.<String, Object>of(env)
                    : SkylarkDict.<String, Object>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 (!hasKwParam) {
                        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, 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<SkylarkType> types = getEnforcedArgumentTypes();

        // Check types, if supplied
        if (types == null) {
            return;
        }
        List<String> 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.
     *
     * <p>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;
    }

    /**
     * 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<Object> args, @Nullable Map<String, Object> 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<SkylarkType> types = signature.getTypes();
        ImmutableList<String> 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, "<function " + getName() + ">");
    }
}