blob: 10a14a6631acb6500efa090da85859fdfc7090fb [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
tomlua155b532017-11-08 20:12:47 +010016import com.google.common.base.Preconditions;
Florian Weikertbf267622016-06-08 11:02:28 +000017import com.google.common.base.Throwables;
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +000018import com.google.devtools.build.lib.events.Location;
Googler768cbc42015-08-28 12:52:14 +000019import com.google.devtools.build.lib.profiler.Profiler;
20import com.google.devtools.build.lib.profiler.ProfilerTask;
vladmos3feea742017-07-06 12:16:18 -040021import com.google.devtools.build.lib.skylarkinterface.SkylarkPrinter;
John Field585d1a02015-12-16 16:03:52 +000022import com.google.devtools.build.lib.skylarkinterface.SkylarkSignature;
nharmata68cc06b2018-03-01 10:21:57 -080023import com.google.devtools.build.lib.syntax.Environment.LexicalFrame;
Francois-Rene Rideau4a8da552015-04-10 18:56:21 +000024import com.google.devtools.build.lib.syntax.SkylarkType.SkylarkFunctionType;
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +000025import java.lang.reflect.InvocationTargetException;
26import java.lang.reflect.Method;
27import java.util.ArrayList;
28import java.util.Arrays;
29import java.util.NoSuchElementException;
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +000030import javax.annotation.Nullable;
31
32/**
mjhalupkacfa0bb72018-03-12 12:43:15 -070033 * A class for Skylark functions provided as builtins by the Skylark implementation. Instances of
34 * this class do not need to be serializable because they should effectively be treated as
35 * constants.
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +000036 */
37public class BuiltinFunction extends BaseFunction {
38
39 /** ExtraArgKind so you can tweek your function's own calling convention */
brandjonc06e7462017-07-11 20:54:58 +020040 public enum ExtraArgKind {
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +000041 LOCATION,
42 SYNTAX_TREE,
43 ENVIRONMENT;
44 }
45 // Predefined system add-ons to function signatures
46 public static final ExtraArgKind[] USE_LOC =
47 new ExtraArgKind[] {ExtraArgKind.LOCATION};
48 public static final ExtraArgKind[] USE_LOC_ENV =
49 new ExtraArgKind[] {ExtraArgKind.LOCATION, ExtraArgKind.ENVIRONMENT};
50 public static final ExtraArgKind[] USE_AST =
51 new ExtraArgKind[] {ExtraArgKind.SYNTAX_TREE};
52 public static final ExtraArgKind[] USE_AST_ENV =
53 new ExtraArgKind[] {ExtraArgKind.SYNTAX_TREE, ExtraArgKind.ENVIRONMENT};
54
nharmata68cc06b2018-03-01 10:21:57 -080055 // Builtins cannot create or modify variable bindings. So it's sufficient to use a shared
56 // instance.
57 private static final LexicalFrame SHARED_LEXICAL_FRAME_FOR_BUILTIN_FUNCTION_CALLS =
58 LexicalFrame.create(Mutability.IMMUTABLE);
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +000059
60 // The underlying invoke() method.
61 @Nullable private Method invokeMethod;
62
63 // extra arguments required beside signature.
64 @Nullable private ExtraArgKind[] extraArgs;
65
66 // The count of arguments in the inner invoke method,
67 // to be used as size of argument array by the outer call method.
68 private int innerArgumentCount;
69
Francois-Rene Rideaua3ac2022015-04-20 18:35:05 +000070 // The returnType of the method.
71 private Class<?> returnType;
72
vladmos3feea742017-07-06 12:16:18 -040073 // True if the function is a rule class
74 private boolean isRule;
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +000075
76 /** Create unconfigured function from its name */
77 public BuiltinFunction(String name) {
78 super(name);
79 }
80
81 /** Creates an unconfigured BuiltinFunction with the given name and defaultValues */
82 public BuiltinFunction(String name, Iterable<Object> defaultValues) {
83 super(name, defaultValues);
84 }
85
86 /** Creates a BuiltinFunction with the given name and signature */
87 public BuiltinFunction(String name, FunctionSignature signature) {
88 super(name, signature);
89 configure();
90 }
91
92 /** Creates a BuiltinFunction with the given name and signature with values */
93 public BuiltinFunction(String name,
94 FunctionSignature.WithValues<Object, SkylarkType> signature) {
95 super(name, signature);
96 configure();
97 }
98
99 /** Creates a BuiltinFunction with the given name and signature and extra arguments */
100 public BuiltinFunction(String name, FunctionSignature signature, ExtraArgKind[] extraArgs) {
101 super(name, signature);
102 this.extraArgs = extraArgs;
103 configure();
104 }
105
vladmos3feea742017-07-06 12:16:18 -0400106 /** Creates a BuiltinFunction with the given name, signature, extra arguments, and a rule flag */
107 public BuiltinFunction(
108 String name, FunctionSignature signature, ExtraArgKind[] extraArgs, boolean isRule) {
109 super(name, signature);
110 this.extraArgs = extraArgs;
111 this.isRule = isRule;
112 configure();
113 }
114
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +0000115 /** Creates a BuiltinFunction with the given name, signature with values, and extra arguments */
116 public BuiltinFunction(String name,
117 FunctionSignature.WithValues<Object, SkylarkType> signature, ExtraArgKind[] extraArgs) {
118 super(name, signature);
119 this.extraArgs = extraArgs;
120 configure();
121 }
122
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +0000123 /** Creates a BuiltinFunction from the given name and a Factory */
124 public BuiltinFunction(String name, Factory factory) {
125 super(name);
126 configure(factory);
127 }
128
129 @Override
130 protected int getArgArraySize () {
131 return innerArgumentCount;
132 }
133
134 protected ExtraArgKind[] getExtraArgs () {
135 return extraArgs;
136 }
137
138 @Override
139 @Nullable
140 public Object call(Object[] args,
Francois-Rene Rideau89312fb2015-09-10 18:53:03 +0000141 FuncallExpression ast, Environment env)
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +0000142 throws EvalException, InterruptedException {
Francois-Rene Rideau89312fb2015-09-10 18:53:03 +0000143 Preconditions.checkNotNull(env);
Vladimir Moskva658a8ea2016-09-02 15:39:17 +0000144
145 // ast is null when called from Java (as there's no Skylark call site).
146 Location loc = ast == null ? Location.BUILTIN : ast.getLocation();
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +0000147
148 // Add extra arguments, if needed
149 if (extraArgs != null) {
150 int i = args.length - extraArgs.length;
151 for (BuiltinFunction.ExtraArgKind extraArg : extraArgs) {
152 switch(extraArg) {
153 case LOCATION:
154 args[i] = loc;
155 break;
156
157 case SYNTAX_TREE:
158 args[i] = ast;
159 break;
160
161 case ENVIRONMENT:
162 args[i] = env;
163 break;
164 }
165 i++;
166 }
167 }
168
Michajlo Matijkiw4e4e0e12016-07-13 15:12:05 +0000169 Profiler.instance().startTask(ProfilerTask.SKYLARK_BUILTIN_FN, getName());
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +0000170 // Last but not least, actually make an inner call to the function with the resolved arguments.
171 try {
nharmata68cc06b2018-03-01 10:21:57 -0800172 env.enterScope(this, SHARED_LEXICAL_FRAME_FOR_BUILTIN_FUNCTION_CALLS, ast, env.getGlobals());
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +0000173 return invokeMethod.invoke(this, args);
174 } catch (InvocationTargetException x) {
175 Throwable e = x.getCause();
Florian Weikertbf267622016-06-08 11:02:28 +0000176
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +0000177 if (e instanceof EvalException) {
Francois-Rene Rideaub6038e02015-06-11 19:51:16 +0000178 throw ((EvalException) e).ensureLocation(loc);
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +0000179 } else if (e instanceof IllegalArgumentException) {
Laurent Le Brunc31f3512016-12-29 21:41:33 +0000180 throw new EvalException(loc, "illegal argument in call to " + getName(), e);
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +0000181 }
Florian Weikertbf267622016-06-08 11:02:28 +0000182 // TODO(bazel-team): replace with Throwables.throwIfInstanceOf once Guava 20 is released.
183 Throwables.propagateIfInstanceOf(e, InterruptedException.class);
184 // TODO(bazel-team): replace with Throwables.throwIfUnchecked once Guava 20 is released.
185 Throwables.propagateIfPossible(e);
186 throw badCallException(loc, e, args);
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +0000187 } catch (IllegalArgumentException e) {
Florian Weikertee5e5e12015-08-11 16:47:31 +0000188 // Either this was thrown by Java itself, or it's a bug
189 // To cover the first case, let's manually check the arguments.
190 final int len = args.length - ((extraArgs == null) ? 0 : extraArgs.length);
191 final Class<?>[] types = invokeMethod.getParameterTypes();
192 for (int i = 0; i < args.length; i++) {
193 if (args[i] != null && !types[i].isAssignableFrom(args[i].getClass())) {
194 String paramName =
195 i < len ? signature.getSignature().getNames().get(i) : extraArgs[i - len].name();
Florian Weikertee5e5e12015-08-11 16:47:31 +0000196 throw new EvalException(
197 loc,
198 String.format(
laurentlb9e540882017-07-07 06:58:45 -0400199 "argument '%s' has type '%s', but should be '%s'\nin call to builtin %s %s",
Laurent Le Brunc31f3512016-12-29 21:41:33 +0000200 paramName,
201 EvalUtils.getDataTypeName(args[i]),
laurentlb9e540882017-07-07 06:58:45 -0400202 EvalUtils.getDataTypeNameFromClass(types[i]),
203 hasSelfArgument() ? "method" : "function",
204 getShortSignature()));
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +0000205 }
Florian Weikertee5e5e12015-08-11 16:47:31 +0000206 }
207 throw badCallException(loc, e, args);
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +0000208 } catch (IllegalAccessException e) {
209 throw badCallException(loc, e, args);
Googler768cbc42015-08-28 12:52:14 +0000210 } finally {
Klaas Boeschef441c192015-09-09 16:03:55 +0000211 Profiler.instance().completeTask(ProfilerTask.SKYLARK_BUILTIN_FN);
Francois-Rene Rideau89312fb2015-09-10 18:53:03 +0000212 env.exitScope();
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +0000213 }
214 }
215
Han-Wen Nienhuys612d2212016-01-21 17:00:47 +0000216 private static String stacktraceToString(StackTraceElement[] elts) {
217 StringBuilder b = new StringBuilder();
218 for (StackTraceElement e : elts) {
Laurent Le Brune51a4d22016-10-11 18:04:16 +0000219 b.append(e);
Han-Wen Nienhuys612d2212016-01-21 17:00:47 +0000220 b.append("\n");
221 }
222 return b.toString();
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +0000223 }
224
Han-Wen Nienhuys612d2212016-01-21 17:00:47 +0000225 private IllegalStateException badCallException(Location loc, Throwable e, Object... args) {
226 // If this happens, it's a bug in our code.
227 return new IllegalStateException(
228 String.format(
229 "%s%s (%s)\n"
230 + "while calling %s with args %s\n"
231 + "Java parameter types: %s\nSkylark type checks: %s",
232 (loc == null) ? "" : loc + ": ",
233 Arrays.asList(args),
234 e.getClass().getName(),
235 stacktraceToString(e.getStackTrace()),
236 this,
237 Arrays.asList(invokeMethod.getParameterTypes()),
238 signature.getTypes()),
239 e);
240 }
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +0000241
242 /** Configure the reflection mechanism */
243 @Override
244 public void configure(SkylarkSignature annotation) {
245 Preconditions.checkState(!isConfigured()); // must not be configured yet
246 enforcedArgumentTypes = new ArrayList<>();
247 this.extraArgs = SkylarkSignatureProcessor.getExtraArgs(annotation);
Francois-Rene Rideaua3ac2022015-04-20 18:35:05 +0000248 this.returnType = annotation.returnType();
Francois-Rene Rideau537a90b2015-04-22 06:47:31 +0000249 super.configure(annotation);
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +0000250 }
251
252 // finds the method and makes it accessible (which is needed to find it, and later to use it)
253 protected Method findMethod(final String name) {
254 Method found = null;
255 for (Method method : this.getClass().getDeclaredMethods()) {
256 method.setAccessible(true);
257 if (name.equals(method.getName())) {
Francois-Rene Rideau0b6fdcd2015-03-31 21:47:03 +0000258 if (found != null) {
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +0000259 throw new IllegalArgumentException(String.format(
260 "function %s has more than one method named %s", getName(), name));
261 }
262 found = method;
263 }
264 }
265 if (found == null) {
266 throw new NoSuchElementException(String.format(
267 "function %s doesn't have a method named %s", getName(), name));
268 }
269 return found;
270 }
271
272 /** Configure the reflection mechanism */
273 @Override
274 protected void configure() {
275 invokeMethod = findMethod("invoke");
276
277 int arguments = signature.getSignature().getShape().getArguments();
278 innerArgumentCount = arguments + (extraArgs == null ? 0 : extraArgs.length);
279 Class<?>[] parameterTypes = invokeMethod.getParameterTypes();
Janak Ramakrishnanba395072016-01-19 15:46:42 +0000280 if (innerArgumentCount != parameterTypes.length) {
281 // Guard message construction by check to avoid autoboxing two integers.
282 throw new IllegalStateException(
283 String.format(
284 "bad argument count for %s: method has %s arguments, type list has %s",
285 getName(),
286 innerArgumentCount,
287 parameterTypes.length));
288 }
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +0000289
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +0000290 if (enforcedArgumentTypes != null) {
291 for (int i = 0; i < arguments; i++) {
292 SkylarkType enforcedType = enforcedArgumentTypes.get(i);
293 if (enforcedType != null) {
294 Class<?> parameterType = parameterTypes[i];
Francois-Rene Rideau4a8da552015-04-10 18:56:21 +0000295 String msg = String.format(
296 "fun %s(%s), param %s, enforcedType: %s (%s); parameterType: %s",
297 getName(), signature, signature.getSignature().getNames().get(i),
Francois-Rene Rideau76023b92015-04-17 15:31:59 +0000298 enforcedType, enforcedType.getType(), parameterType);
Francois-Rene Rideau4a8da552015-04-10 18:56:21 +0000299 if (enforcedType instanceof SkylarkType.Simple
300 || enforcedType instanceof SkylarkFunctionType) {
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +0000301 Preconditions.checkArgument(
Francois-Rene Rideau76023b92015-04-17 15:31:59 +0000302 enforcedType.getType() == parameterType, msg);
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +0000303 // No need to enforce Simple types on the Skylark side, the JVM will do it for us.
304 enforcedArgumentTypes.set(i, null);
305 } else if (enforcedType instanceof SkylarkType.Combination) {
Francois-Rene Rideaua3ac2022015-04-20 18:35:05 +0000306 Preconditions.checkArgument(enforcedType.getType() == parameterType, msg);
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +0000307 } else {
308 Preconditions.checkArgument(
309 parameterType == Object.class || parameterType == null, msg);
310 }
311 }
312 }
313 }
314 // No need for the enforcedArgumentTypes List if all the types were Simple
brandjonc06e7462017-07-11 20:54:58 +0200315 enforcedArgumentTypes = FunctionSignature.valueListOrNull(enforcedArgumentTypes);
Francois-Rene Rideaua3ac2022015-04-20 18:35:05 +0000316
317 if (returnType != null) {
318 Class<?> type = returnType;
Francois-Rene Rideau537a90b2015-04-22 06:47:31 +0000319 Class<?> methodReturnType = invokeMethod.getReturnType();
320 Preconditions.checkArgument(type == methodReturnType,
321 "signature for function %s says it returns %s but its invoke method returns %s",
322 getName(), returnType, methodReturnType);
Francois-Rene Rideaua3ac2022015-04-20 18:35:05 +0000323 }
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +0000324 }
325
326 /** Configure by copying another function's configuration */
327 // Alternatively, we could have an extension BuiltinFunctionSignature of FunctionSignature,
328 // and use *that* instead of a Factory.
329 public void configure(BuiltinFunction.Factory factory) {
330 // this function must not be configured yet, but the factory must be
331 Preconditions.checkState(!isConfigured());
332 Preconditions.checkState(factory.isConfigured(),
333 "function factory is not configured for %s", getName());
334
335 this.paramDoc = factory.getParamDoc();
336 this.signature = factory.getSignature();
337 this.extraArgs = factory.getExtraArgs();
338 this.objectType = factory.getObjectType();
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +0000339 configure();
340 }
341
342 /**
343 * A Factory allows for a @SkylarkSignature annotation to be provided and processed in advance
344 * for a function that will be defined later as a closure (see e.g. in PackageFactory).
345 *
346 * <p>Each instance of this class must define a method create that closes over some (final)
347 * variables and returns a BuiltinFunction.
348 */
349 public abstract static class Factory extends BuiltinFunction {
350 @Nullable private Method createMethod;
351
352 /** Create unconfigured function Factory from its name */
353 public Factory(String name) {
354 super(name);
355 }
356
357 /** Creates an unconfigured function Factory with the given name and defaultValues */
358 public Factory(String name, Iterable<Object> defaultValues) {
359 super(name, defaultValues);
360 }
361
362 @Override
363 public void configure() {
364 if (createMethod != null) {
365 return;
366 }
367 createMethod = findMethod("create");
368 }
369
370 @Override
tomlud8a21e32018-04-24 18:49:52 -0700371 public Object call(Object[] args, @Nullable FuncallExpression ast, Environment env)
372 throws EvalException {
Laurent Le Brunc31f3512016-12-29 21:41:33 +0000373 throw new EvalException(null, "tried to invoke a Factory for function " + this);
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +0000374 }
375
376 /** Instantiate the Factory
377 * @param args arguments to pass to the create method
378 * @return a new BuiltinFunction that closes over the arguments
379 */
380 public BuiltinFunction apply(Object... args) {
381 try {
382 return (BuiltinFunction) createMethod.invoke(this, args);
383 } catch (InvocationTargetException | IllegalArgumentException | IllegalAccessException e) {
384 throw new RuntimeException(String.format(
385 "Exception while applying BuiltinFunction.Factory %s: %s",
386 this, e.getMessage()), e);
387 }
388 }
389 }
vladmos3feea742017-07-06 12:16:18 -0400390
391 @Override
392 public void repr(SkylarkPrinter printer) {
393 if (isRule) {
394 printer.append("<built-in rule " + getName() + ">");
395 } else {
396 printer.append("<built-in function " + getName() + ">");
397 }
398 }
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +0000399}