blob: 537f8aeaa09159c8b8834bc02809f40bfca68de3 [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.Preconditions;
import com.google.common.base.Throwables;
import com.google.common.collect.Lists;
import com.google.devtools.build.lib.events.Location;
import com.google.devtools.build.lib.profiler.Profiler;
import com.google.devtools.build.lib.profiler.ProfilerTask;
import com.google.devtools.build.lib.profiler.SilentCloseable;
import com.google.devtools.build.lib.skylarkinterface.SkylarkPrinter;
import com.google.devtools.build.lib.syntax.StarlarkThread.LexicalFrame;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import java.util.NoSuchElementException;
import javax.annotation.Nullable;
/**
* A class for Skylark functions provided as builtins by the Skylark implementation. Instances of
* this class do not need to be serializable because they should effectively be treated as
* constants.
*/
public abstract class BuiltinFunction extends BaseFunction {
// Builtins cannot create or modify variable bindings. So it's sufficient to use a shared
// instance.
private static final LexicalFrame SHARED_LEXICAL_FRAME_FOR_BUILTIN_FUNCTION_CALLS =
LexicalFrame.create(Mutability.IMMUTABLE);
// The underlying invoke() method.
@Nullable private Method invokeMethod;
// Classes of extra arguments required beside signature,
// computed by configure from parameter types of invoke method.
// TODO(adonovan): eliminate Location, FuncallExpression when they can be derived from the thread.
private Class<?>[] extraParams; // ordered subset of {Location,FuncallExpression,StarlarkThread}
// The returnType of the method.
private Class<?> returnType;
/** Creates a BuiltinFunction with the given signature. */
protected BuiltinFunction(FunctionSignature signature) {
super(signature);
initialize();
}
@Override
protected final int getArgArraySize() {
return invokeMethod.getParameterCount();
}
@Override
@Nullable
public Object call(Object[] args, @Nullable FuncallExpression ast, StarlarkThread thread)
throws EvalException, InterruptedException {
Preconditions.checkNotNull(thread);
// ast is null when called from Java (as there's no Skylark call site).
Location loc = ast == null ? Location.BUILTIN : ast.getLocation();
// Add extra arguments as needed.
{
int i = args.length - extraParams.length;
for (Class<?> cls : extraParams) {
if (cls == Location.class) {
args[i] = loc;
} else if (cls == FuncallExpression.class) {
args[i] = ast;
} else if (cls == StarlarkThread.class) {
args[i] = thread;
} else {
throw new IllegalStateException("invalid extra argument: " + cls);
}
i++;
}
}
// Last but not least, actually make an inner call to the function with the resolved arguments.
try (SilentCloseable c =
Profiler.instance().profile(ProfilerTask.STARLARK_BUILTIN_FN, getName())) {
thread.enterScope(
this, SHARED_LEXICAL_FRAME_FOR_BUILTIN_FUNCTION_CALLS, ast, thread.getGlobals());
return invokeMethod.invoke(this, args);
} catch (InvocationTargetException x) {
Throwable e = x.getCause();
if (e instanceof EvalException) {
throw ((EvalException) e).ensureLocation(loc);
} else if (e instanceof IllegalArgumentException) {
throw new EvalException(loc, "illegal argument in call to " + getName(), e);
}
Throwables.throwIfInstanceOf(e, InterruptedException.class);
Throwables.throwIfUnchecked(e);
throw badCallException(loc, e, args);
} catch (IllegalArgumentException e) {
// Either this was thrown by Java itself, or it's a bug
// To cover the first case, let's manually check the arguments.
final int len = args.length - extraParams.length;
final Class<?>[] types = invokeMethod.getParameterTypes();
for (int i = 0; i < args.length; i++) {
if (args[i] != null && !types[i].isAssignableFrom(args[i].getClass())) {
String paramName =
i < len ? getSignature().getParameterNames().get(i) : extraParams[i - len].getName();
throw new EvalException(
loc,
String.format(
"argument '%s' has type '%s', but should be '%s'\nin call to %s",
paramName,
EvalUtils.getDataTypeName(args[i]),
EvalUtils.getDataTypeNameFromClass(types[i]),
getShortSignature()));
}
}
throw badCallException(loc, e, args);
} catch (IllegalAccessException e) {
throw badCallException(loc, e, args);
} finally {
thread.exitScope();
}
}
private static String stacktraceToString(StackTraceElement[] elts) {
StringBuilder b = new StringBuilder();
for (StackTraceElement e : elts) {
b.append(e);
b.append("\n");
}
return b.toString();
}
private IllegalStateException badCallException(Location loc, Throwable e, Object... args) {
// If this happens, it's a bug in our code.
return new IllegalStateException(
String.format(
"%s%s (%s)\n" + "while calling %s with args %s\n" + "Java parameter types: %s",
(loc == null) ? "" : loc + ": ",
Arrays.asList(args),
e.getClass().getName(),
stacktraceToString(e.getStackTrace()),
this,
Arrays.asList(invokeMethod.getParameterTypes())),
e);
}
// Configures the reflection mechanism.
private final void initialize() {
this.invokeMethod = findMethod(this.getClass(), "invoke");
Class<?>[] parameterTypes = invokeMethod.getParameterTypes();
int numParameters = getSignature().numParameters();
this.extraParams = extraParams(numParameters, parameterTypes);
if (returnType != null) {
Class<?> type = returnType;
Class<?> methodReturnType = invokeMethod.getReturnType();
Preconditions.checkArgument(
type == methodReturnType,
"signature for function %s says it returns %s but its invoke method returns %s",
getName(),
returnType,
methodReturnType);
}
}
/**
* Returns the signature as "[className.]methodName(name1: paramType1, name2: paramType2, ...)"
*/
private String getShortSignature() {
StringBuilder builder = new StringBuilder();
builder.append(getName()).append("(");
getSignature().toStringBuilder(builder, /*defaultValuePrinter=*/ null);
builder.append(")");
return builder.toString();
}
// Returns the list of extra parameters beyond those in the signature.
private Class<?>[] extraParams(int i, Class<?>[] parameterTypes) {
List<Class<?>> extra = Lists.newArrayList();
for (Class<?> cls : EXTRA_PARAM_CLASSES) {
if (i < parameterTypes.length && parameterTypes[i] == cls) {
extra.add(cls);
i++;
}
}
if (i != parameterTypes.length) {
throw new IllegalStateException(
String.format(
"bad argument count for %s: method has %s arguments, type list has %s",
getName(), i, parameterTypes.length));
}
return extra.toArray(new Class<?>[0]);
}
private static final Class<?>[] EXTRA_PARAM_CLASSES = {
Location.class, FuncallExpression.class, StarlarkThread.class
};
// finds the method and makes it accessible (which is needed to find it, and later to use it)
private static Method findMethod(Class<?> cls, String name) {
Method found = null;
for (Method method : cls.getDeclaredMethods()) {
method.setAccessible(true);
if (name.equals(method.getName())) {
if (found != null) {
throw new IllegalArgumentException(
String.format("class %s has more than one method named %s", cls.getName(), name));
}
found = method;
}
}
if (found == null) {
throw new NoSuchElementException(
String.format("class %s doesn't have a method named %s", cls.getName(), name));
}
return found;
}
@Override
public void repr(SkylarkPrinter printer) {
printer.append("<built-in function " + getName() + ">");
}
}