blob: cce162e740ed43555c7acbf3b254c1b906892aae [file] [log] [blame]
Damien Martin-Guillerezf88f4d82015-09-25 13:56:55 +00001// Copyright 2014 The Bazel Authors. All rights reserved.
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +00002//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14package com.google.devtools.build.lib.syntax;
15
Florian Weikertbf267622016-06-08 11:02:28 +000016import com.google.common.base.Throwables;
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +000017import com.google.devtools.build.lib.events.Location;
Googler768cbc42015-08-28 12:52:14 +000018import com.google.devtools.build.lib.profiler.Profiler;
19import com.google.devtools.build.lib.profiler.ProfilerTask;
vladmos3feea742017-07-06 12:16:18 -040020import com.google.devtools.build.lib.skylarkinterface.SkylarkPrinter;
John Field585d1a02015-12-16 16:03:52 +000021import com.google.devtools.build.lib.skylarkinterface.SkylarkSignature;
Francois-Rene Rideau4a8da552015-04-10 18:56:21 +000022import com.google.devtools.build.lib.syntax.SkylarkType.SkylarkFunctionType;
Mark Schaller6df81792015-12-10 18:47:47 +000023import com.google.devtools.build.lib.util.Preconditions;
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +000024import java.lang.reflect.InvocationTargetException;
25import java.lang.reflect.Method;
26import java.util.ArrayList;
27import java.util.Arrays;
28import java.util.NoSuchElementException;
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +000029import javax.annotation.Nullable;
30
31/**
32 * A class for Skylark functions provided as builtins by the Skylark implementation
33 */
34public class BuiltinFunction extends BaseFunction {
35
36 /** ExtraArgKind so you can tweek your function's own calling convention */
brandjonc06e7462017-07-11 20:54:58 +020037 public enum ExtraArgKind {
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +000038 LOCATION,
39 SYNTAX_TREE,
40 ENVIRONMENT;
41 }
42 // Predefined system add-ons to function signatures
43 public static final ExtraArgKind[] USE_LOC =
44 new ExtraArgKind[] {ExtraArgKind.LOCATION};
45 public static final ExtraArgKind[] USE_LOC_ENV =
46 new ExtraArgKind[] {ExtraArgKind.LOCATION, ExtraArgKind.ENVIRONMENT};
47 public static final ExtraArgKind[] USE_AST =
48 new ExtraArgKind[] {ExtraArgKind.SYNTAX_TREE};
49 public static final ExtraArgKind[] USE_AST_ENV =
50 new ExtraArgKind[] {ExtraArgKind.SYNTAX_TREE, ExtraArgKind.ENVIRONMENT};
51
52
53 // The underlying invoke() method.
54 @Nullable private Method invokeMethod;
55
56 // extra arguments required beside signature.
57 @Nullable private ExtraArgKind[] extraArgs;
58
59 // The count of arguments in the inner invoke method,
60 // to be used as size of argument array by the outer call method.
61 private int innerArgumentCount;
62
Francois-Rene Rideaua3ac2022015-04-20 18:35:05 +000063 // The returnType of the method.
64 private Class<?> returnType;
65
vladmos3feea742017-07-06 12:16:18 -040066 // True if the function is a rule class
67 private boolean isRule;
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +000068
69 /** Create unconfigured function from its name */
70 public BuiltinFunction(String name) {
71 super(name);
72 }
73
74 /** Creates an unconfigured BuiltinFunction with the given name and defaultValues */
75 public BuiltinFunction(String name, Iterable<Object> defaultValues) {
76 super(name, defaultValues);
77 }
78
79 /** Creates a BuiltinFunction with the given name and signature */
80 public BuiltinFunction(String name, FunctionSignature signature) {
81 super(name, signature);
82 configure();
83 }
84
85 /** Creates a BuiltinFunction with the given name and signature with values */
86 public BuiltinFunction(String name,
87 FunctionSignature.WithValues<Object, SkylarkType> signature) {
88 super(name, signature);
89 configure();
90 }
91
92 /** Creates a BuiltinFunction with the given name and signature and extra arguments */
93 public BuiltinFunction(String name, FunctionSignature signature, ExtraArgKind[] extraArgs) {
94 super(name, signature);
95 this.extraArgs = extraArgs;
96 configure();
97 }
98
vladmos3feea742017-07-06 12:16:18 -040099 /** Creates a BuiltinFunction with the given name, signature, extra arguments, and a rule flag */
100 public BuiltinFunction(
101 String name, FunctionSignature signature, ExtraArgKind[] extraArgs, boolean isRule) {
102 super(name, signature);
103 this.extraArgs = extraArgs;
104 this.isRule = isRule;
105 configure();
106 }
107
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +0000108 /** Creates a BuiltinFunction with the given name, signature with values, and extra arguments */
109 public BuiltinFunction(String name,
110 FunctionSignature.WithValues<Object, SkylarkType> signature, ExtraArgKind[] extraArgs) {
111 super(name, signature);
112 this.extraArgs = extraArgs;
113 configure();
114 }
115
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +0000116 /** Creates a BuiltinFunction from the given name and a Factory */
117 public BuiltinFunction(String name, Factory factory) {
118 super(name);
119 configure(factory);
120 }
121
122 @Override
123 protected int getArgArraySize () {
124 return innerArgumentCount;
125 }
126
127 protected ExtraArgKind[] getExtraArgs () {
128 return extraArgs;
129 }
130
131 @Override
132 @Nullable
133 public Object call(Object[] args,
Francois-Rene Rideau89312fb2015-09-10 18:53:03 +0000134 FuncallExpression ast, Environment env)
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +0000135 throws EvalException, InterruptedException {
Francois-Rene Rideau89312fb2015-09-10 18:53:03 +0000136 Preconditions.checkNotNull(env);
Vladimir Moskva658a8ea2016-09-02 15:39:17 +0000137
138 // ast is null when called from Java (as there's no Skylark call site).
139 Location loc = ast == null ? Location.BUILTIN : ast.getLocation();
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +0000140
141 // Add extra arguments, if needed
142 if (extraArgs != null) {
143 int i = args.length - extraArgs.length;
144 for (BuiltinFunction.ExtraArgKind extraArg : extraArgs) {
145 switch(extraArg) {
146 case LOCATION:
147 args[i] = loc;
148 break;
149
150 case SYNTAX_TREE:
151 args[i] = ast;
152 break;
153
154 case ENVIRONMENT:
155 args[i] = env;
156 break;
157 }
158 i++;
159 }
160 }
161
Michajlo Matijkiw4e4e0e12016-07-13 15:12:05 +0000162 Profiler.instance().startTask(ProfilerTask.SKYLARK_BUILTIN_FN, getName());
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +0000163 // Last but not least, actually make an inner call to the function with the resolved arguments.
164 try {
Francois-Rene Rideau89312fb2015-09-10 18:53:03 +0000165 env.enterScope(this, ast, env.getGlobals());
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +0000166 return invokeMethod.invoke(this, args);
167 } catch (InvocationTargetException x) {
168 Throwable e = x.getCause();
Florian Weikertbf267622016-06-08 11:02:28 +0000169
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +0000170 if (e instanceof EvalException) {
Francois-Rene Rideaub6038e02015-06-11 19:51:16 +0000171 throw ((EvalException) e).ensureLocation(loc);
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +0000172 } else if (e instanceof IllegalArgumentException) {
Laurent Le Brunc31f3512016-12-29 21:41:33 +0000173 throw new EvalException(loc, "illegal argument in call to " + getName(), e);
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +0000174 }
Florian Weikertbf267622016-06-08 11:02:28 +0000175 // TODO(bazel-team): replace with Throwables.throwIfInstanceOf once Guava 20 is released.
176 Throwables.propagateIfInstanceOf(e, InterruptedException.class);
177 // TODO(bazel-team): replace with Throwables.throwIfUnchecked once Guava 20 is released.
178 Throwables.propagateIfPossible(e);
179 throw badCallException(loc, e, args);
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +0000180 } catch (IllegalArgumentException e) {
Florian Weikertee5e5e12015-08-11 16:47:31 +0000181 // Either this was thrown by Java itself, or it's a bug
182 // To cover the first case, let's manually check the arguments.
183 final int len = args.length - ((extraArgs == null) ? 0 : extraArgs.length);
184 final Class<?>[] types = invokeMethod.getParameterTypes();
185 for (int i = 0; i < args.length; i++) {
186 if (args[i] != null && !types[i].isAssignableFrom(args[i].getClass())) {
187 String paramName =
188 i < len ? signature.getSignature().getNames().get(i) : extraArgs[i - len].name();
Florian Weikertee5e5e12015-08-11 16:47:31 +0000189 throw new EvalException(
190 loc,
191 String.format(
laurentlb9e540882017-07-07 06:58:45 -0400192 "argument '%s' has type '%s', but should be '%s'\nin call to builtin %s %s",
Laurent Le Brunc31f3512016-12-29 21:41:33 +0000193 paramName,
194 EvalUtils.getDataTypeName(args[i]),
laurentlb9e540882017-07-07 06:58:45 -0400195 EvalUtils.getDataTypeNameFromClass(types[i]),
196 hasSelfArgument() ? "method" : "function",
197 getShortSignature()));
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +0000198 }
Florian Weikertee5e5e12015-08-11 16:47:31 +0000199 }
200 throw badCallException(loc, e, args);
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +0000201 } catch (IllegalAccessException e) {
202 throw badCallException(loc, e, args);
Googler768cbc42015-08-28 12:52:14 +0000203 } finally {
Klaas Boeschef441c192015-09-09 16:03:55 +0000204 Profiler.instance().completeTask(ProfilerTask.SKYLARK_BUILTIN_FN);
Francois-Rene Rideau89312fb2015-09-10 18:53:03 +0000205 env.exitScope();
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +0000206 }
207 }
208
Han-Wen Nienhuys612d2212016-01-21 17:00:47 +0000209 private static String stacktraceToString(StackTraceElement[] elts) {
210 StringBuilder b = new StringBuilder();
211 for (StackTraceElement e : elts) {
Laurent Le Brune51a4d22016-10-11 18:04:16 +0000212 b.append(e);
Han-Wen Nienhuys612d2212016-01-21 17:00:47 +0000213 b.append("\n");
214 }
215 return b.toString();
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +0000216 }
217
Han-Wen Nienhuys612d2212016-01-21 17:00:47 +0000218 private IllegalStateException badCallException(Location loc, Throwable e, Object... args) {
219 // If this happens, it's a bug in our code.
220 return new IllegalStateException(
221 String.format(
222 "%s%s (%s)\n"
223 + "while calling %s with args %s\n"
224 + "Java parameter types: %s\nSkylark type checks: %s",
225 (loc == null) ? "" : loc + ": ",
226 Arrays.asList(args),
227 e.getClass().getName(),
228 stacktraceToString(e.getStackTrace()),
229 this,
230 Arrays.asList(invokeMethod.getParameterTypes()),
231 signature.getTypes()),
232 e);
233 }
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +0000234
235 /** Configure the reflection mechanism */
236 @Override
237 public void configure(SkylarkSignature annotation) {
238 Preconditions.checkState(!isConfigured()); // must not be configured yet
239 enforcedArgumentTypes = new ArrayList<>();
240 this.extraArgs = SkylarkSignatureProcessor.getExtraArgs(annotation);
Francois-Rene Rideaua3ac2022015-04-20 18:35:05 +0000241 this.returnType = annotation.returnType();
Francois-Rene Rideau537a90b2015-04-22 06:47:31 +0000242 super.configure(annotation);
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +0000243 }
244
245 // finds the method and makes it accessible (which is needed to find it, and later to use it)
246 protected Method findMethod(final String name) {
247 Method found = null;
248 for (Method method : this.getClass().getDeclaredMethods()) {
249 method.setAccessible(true);
250 if (name.equals(method.getName())) {
Francois-Rene Rideau0b6fdcd2015-03-31 21:47:03 +0000251 if (found != null) {
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +0000252 throw new IllegalArgumentException(String.format(
253 "function %s has more than one method named %s", getName(), name));
254 }
255 found = method;
256 }
257 }
258 if (found == null) {
259 throw new NoSuchElementException(String.format(
260 "function %s doesn't have a method named %s", getName(), name));
261 }
262 return found;
263 }
264
265 /** Configure the reflection mechanism */
266 @Override
267 protected void configure() {
268 invokeMethod = findMethod("invoke");
269
270 int arguments = signature.getSignature().getShape().getArguments();
271 innerArgumentCount = arguments + (extraArgs == null ? 0 : extraArgs.length);
272 Class<?>[] parameterTypes = invokeMethod.getParameterTypes();
Janak Ramakrishnanba395072016-01-19 15:46:42 +0000273 if (innerArgumentCount != parameterTypes.length) {
274 // Guard message construction by check to avoid autoboxing two integers.
275 throw new IllegalStateException(
276 String.format(
277 "bad argument count for %s: method has %s arguments, type list has %s",
278 getName(),
279 innerArgumentCount,
280 parameterTypes.length));
281 }
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +0000282
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +0000283 if (enforcedArgumentTypes != null) {
284 for (int i = 0; i < arguments; i++) {
285 SkylarkType enforcedType = enforcedArgumentTypes.get(i);
286 if (enforcedType != null) {
287 Class<?> parameterType = parameterTypes[i];
Francois-Rene Rideau4a8da552015-04-10 18:56:21 +0000288 String msg = String.format(
289 "fun %s(%s), param %s, enforcedType: %s (%s); parameterType: %s",
290 getName(), signature, signature.getSignature().getNames().get(i),
Francois-Rene Rideau76023b92015-04-17 15:31:59 +0000291 enforcedType, enforcedType.getType(), parameterType);
Francois-Rene Rideau4a8da552015-04-10 18:56:21 +0000292 if (enforcedType instanceof SkylarkType.Simple
293 || enforcedType instanceof SkylarkFunctionType) {
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +0000294 Preconditions.checkArgument(
Francois-Rene Rideau76023b92015-04-17 15:31:59 +0000295 enforcedType.getType() == parameterType, msg);
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +0000296 // No need to enforce Simple types on the Skylark side, the JVM will do it for us.
297 enforcedArgumentTypes.set(i, null);
298 } else if (enforcedType instanceof SkylarkType.Combination) {
Francois-Rene Rideaua3ac2022015-04-20 18:35:05 +0000299 Preconditions.checkArgument(enforcedType.getType() == parameterType, msg);
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +0000300 } else {
301 Preconditions.checkArgument(
302 parameterType == Object.class || parameterType == null, msg);
303 }
304 }
305 }
306 }
307 // No need for the enforcedArgumentTypes List if all the types were Simple
brandjonc06e7462017-07-11 20:54:58 +0200308 enforcedArgumentTypes = FunctionSignature.valueListOrNull(enforcedArgumentTypes);
Francois-Rene Rideaua3ac2022015-04-20 18:35:05 +0000309
310 if (returnType != null) {
311 Class<?> type = returnType;
Francois-Rene Rideau537a90b2015-04-22 06:47:31 +0000312 Class<?> methodReturnType = invokeMethod.getReturnType();
313 Preconditions.checkArgument(type == methodReturnType,
314 "signature for function %s says it returns %s but its invoke method returns %s",
315 getName(), returnType, methodReturnType);
Francois-Rene Rideaua3ac2022015-04-20 18:35:05 +0000316 }
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +0000317 }
318
319 /** Configure by copying another function's configuration */
320 // Alternatively, we could have an extension BuiltinFunctionSignature of FunctionSignature,
321 // and use *that* instead of a Factory.
322 public void configure(BuiltinFunction.Factory factory) {
323 // this function must not be configured yet, but the factory must be
324 Preconditions.checkState(!isConfigured());
325 Preconditions.checkState(factory.isConfigured(),
326 "function factory is not configured for %s", getName());
327
328 this.paramDoc = factory.getParamDoc();
329 this.signature = factory.getSignature();
330 this.extraArgs = factory.getExtraArgs();
331 this.objectType = factory.getObjectType();
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +0000332 configure();
333 }
334
335 /**
336 * A Factory allows for a @SkylarkSignature annotation to be provided and processed in advance
337 * for a function that will be defined later as a closure (see e.g. in PackageFactory).
338 *
339 * <p>Each instance of this class must define a method create that closes over some (final)
340 * variables and returns a BuiltinFunction.
341 */
342 public abstract static class Factory extends BuiltinFunction {
343 @Nullable private Method createMethod;
344
345 /** Create unconfigured function Factory from its name */
346 public Factory(String name) {
347 super(name);
348 }
349
350 /** Creates an unconfigured function Factory with the given name and defaultValues */
351 public Factory(String name, Iterable<Object> defaultValues) {
352 super(name, defaultValues);
353 }
354
355 @Override
356 public void configure() {
357 if (createMethod != null) {
358 return;
359 }
360 createMethod = findMethod("create");
361 }
362
363 @Override
364 public Object call(Object[] args, @Nullable FuncallExpression ast, @Nullable Environment env)
365 throws EvalException {
Laurent Le Brunc31f3512016-12-29 21:41:33 +0000366 throw new EvalException(null, "tried to invoke a Factory for function " + this);
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +0000367 }
368
369 /** Instantiate the Factory
370 * @param args arguments to pass to the create method
371 * @return a new BuiltinFunction that closes over the arguments
372 */
373 public BuiltinFunction apply(Object... args) {
374 try {
375 return (BuiltinFunction) createMethod.invoke(this, args);
376 } catch (InvocationTargetException | IllegalArgumentException | IllegalAccessException e) {
377 throw new RuntimeException(String.format(
378 "Exception while applying BuiltinFunction.Factory %s: %s",
379 this, e.getMessage()), e);
380 }
381 }
382 }
vladmos3feea742017-07-06 12:16:18 -0400383
384 @Override
385 public void repr(SkylarkPrinter printer) {
386 if (isRule) {
387 printer.append("<built-in rule " + getName() + ">");
388 } else {
389 printer.append("<built-in function " + getName() + ">");
390 }
391 }
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +0000392}