blob: b050b2ec4893a7888cd7092ce203c4d4a9534dbe [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;
John Field585d1a02015-12-16 16:03:52 +000020import com.google.devtools.build.lib.skylarkinterface.SkylarkSignature;
Francois-Rene Rideau4a8da552015-04-10 18:56:21 +000021import com.google.devtools.build.lib.syntax.SkylarkType.SkylarkFunctionType;
Mark Schaller6df81792015-12-10 18:47:47 +000022import com.google.devtools.build.lib.util.Preconditions;
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +000023import java.lang.reflect.InvocationTargetException;
24import java.lang.reflect.Method;
25import java.util.ArrayList;
26import java.util.Arrays;
27import java.util.NoSuchElementException;
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +000028import javax.annotation.Nullable;
29
30/**
31 * A class for Skylark functions provided as builtins by the Skylark implementation
32 */
33public class BuiltinFunction extends BaseFunction {
34
35 /** ExtraArgKind so you can tweek your function's own calling convention */
36 public static enum ExtraArgKind {
37 LOCATION,
38 SYNTAX_TREE,
39 ENVIRONMENT;
40 }
41 // Predefined system add-ons to function signatures
42 public static final ExtraArgKind[] USE_LOC =
43 new ExtraArgKind[] {ExtraArgKind.LOCATION};
44 public static final ExtraArgKind[] USE_LOC_ENV =
45 new ExtraArgKind[] {ExtraArgKind.LOCATION, ExtraArgKind.ENVIRONMENT};
46 public static final ExtraArgKind[] USE_AST =
47 new ExtraArgKind[] {ExtraArgKind.SYNTAX_TREE};
48 public static final ExtraArgKind[] USE_AST_ENV =
49 new ExtraArgKind[] {ExtraArgKind.SYNTAX_TREE, ExtraArgKind.ENVIRONMENT};
50
51
52 // The underlying invoke() method.
53 @Nullable private Method invokeMethod;
54
55 // extra arguments required beside signature.
56 @Nullable private ExtraArgKind[] extraArgs;
57
58 // The count of arguments in the inner invoke method,
59 // to be used as size of argument array by the outer call method.
60 private int innerArgumentCount;
61
Francois-Rene Rideaua3ac2022015-04-20 18:35:05 +000062 // The returnType of the method.
63 private Class<?> returnType;
64
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +000065
66 /** Create unconfigured function from its name */
67 public BuiltinFunction(String name) {
68 super(name);
69 }
70
71 /** Creates an unconfigured BuiltinFunction with the given name and defaultValues */
72 public BuiltinFunction(String name, Iterable<Object> defaultValues) {
73 super(name, defaultValues);
74 }
75
76 /** Creates a BuiltinFunction with the given name and signature */
77 public BuiltinFunction(String name, FunctionSignature signature) {
78 super(name, signature);
79 configure();
80 }
81
82 /** Creates a BuiltinFunction with the given name and signature with values */
83 public BuiltinFunction(String name,
84 FunctionSignature.WithValues<Object, SkylarkType> signature) {
85 super(name, signature);
86 configure();
87 }
88
89 /** Creates a BuiltinFunction with the given name and signature and extra arguments */
90 public BuiltinFunction(String name, FunctionSignature signature, ExtraArgKind[] extraArgs) {
91 super(name, signature);
92 this.extraArgs = extraArgs;
93 configure();
94 }
95
96 /** Creates a BuiltinFunction with the given name, signature with values, and extra arguments */
97 public BuiltinFunction(String name,
98 FunctionSignature.WithValues<Object, SkylarkType> signature, ExtraArgKind[] extraArgs) {
99 super(name, signature);
100 this.extraArgs = extraArgs;
101 configure();
102 }
103
104
105 /** Creates a BuiltinFunction from the given name and a Factory */
106 public BuiltinFunction(String name, Factory factory) {
107 super(name);
108 configure(factory);
109 }
110
111 @Override
112 protected int getArgArraySize () {
113 return innerArgumentCount;
114 }
115
116 protected ExtraArgKind[] getExtraArgs () {
117 return extraArgs;
118 }
119
120 @Override
121 @Nullable
122 public Object call(Object[] args,
Francois-Rene Rideau89312fb2015-09-10 18:53:03 +0000123 FuncallExpression ast, Environment env)
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +0000124 throws EvalException, InterruptedException {
Francois-Rene Rideau89312fb2015-09-10 18:53:03 +0000125 Preconditions.checkNotNull(env);
Vladimir Moskva658a8ea2016-09-02 15:39:17 +0000126
127 // ast is null when called from Java (as there's no Skylark call site).
128 Location loc = ast == null ? Location.BUILTIN : ast.getLocation();
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +0000129
130 // Add extra arguments, if needed
131 if (extraArgs != null) {
132 int i = args.length - extraArgs.length;
133 for (BuiltinFunction.ExtraArgKind extraArg : extraArgs) {
134 switch(extraArg) {
135 case LOCATION:
136 args[i] = loc;
137 break;
138
139 case SYNTAX_TREE:
140 args[i] = ast;
141 break;
142
143 case ENVIRONMENT:
144 args[i] = env;
145 break;
146 }
147 i++;
148 }
149 }
150
Michajlo Matijkiw4e4e0e12016-07-13 15:12:05 +0000151 Profiler.instance().startTask(ProfilerTask.SKYLARK_BUILTIN_FN, getName());
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +0000152 // Last but not least, actually make an inner call to the function with the resolved arguments.
153 try {
Francois-Rene Rideau89312fb2015-09-10 18:53:03 +0000154 env.enterScope(this, ast, env.getGlobals());
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +0000155 return invokeMethod.invoke(this, args);
156 } catch (InvocationTargetException x) {
157 Throwable e = x.getCause();
Florian Weikertbf267622016-06-08 11:02:28 +0000158
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +0000159 if (e instanceof EvalException) {
Francois-Rene Rideaub6038e02015-06-11 19:51:16 +0000160 throw ((EvalException) e).ensureLocation(loc);
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +0000161 } else if (e instanceof IllegalArgumentException) {
Francois-Rene Rideau0b6fdcd2015-03-31 21:47:03 +0000162 throw new EvalException(loc, "Illegal argument in call to " + getName(), e);
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +0000163 }
Florian Weikertbf267622016-06-08 11:02:28 +0000164 // TODO(bazel-team): replace with Throwables.throwIfInstanceOf once Guava 20 is released.
165 Throwables.propagateIfInstanceOf(e, InterruptedException.class);
166 // TODO(bazel-team): replace with Throwables.throwIfUnchecked once Guava 20 is released.
167 Throwables.propagateIfPossible(e);
168 throw badCallException(loc, e, args);
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +0000169 } catch (IllegalArgumentException e) {
Florian Weikertee5e5e12015-08-11 16:47:31 +0000170 // Either this was thrown by Java itself, or it's a bug
171 // To cover the first case, let's manually check the arguments.
172 final int len = args.length - ((extraArgs == null) ? 0 : extraArgs.length);
173 final Class<?>[] types = invokeMethod.getParameterTypes();
174 for (int i = 0; i < args.length; i++) {
175 if (args[i] != null && !types[i].isAssignableFrom(args[i].getClass())) {
176 String paramName =
177 i < len ? signature.getSignature().getNames().get(i) : extraArgs[i - len].name();
178 int extraArgsCount = (extraArgs == null) ? 0 : extraArgs.length;
179 throw new EvalException(
180 loc,
181 String.format(
182 "Method %s is not applicable for arguments %s: '%s' is %s, but should be %s",
Florian Weikert3f610e82015-08-18 14:37:46 +0000183 getShortSignature(true), printTypeString(args, args.length - extraArgsCount),
Florian Weikertee5e5e12015-08-11 16:47:31 +0000184 paramName, EvalUtils.getDataTypeName(args[i]),
185 EvalUtils.getDataTypeNameFromClass(types[i])));
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +0000186 }
Florian Weikertee5e5e12015-08-11 16:47:31 +0000187 }
188 throw badCallException(loc, e, args);
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +0000189 } catch (IllegalAccessException e) {
190 throw badCallException(loc, e, args);
Googler768cbc42015-08-28 12:52:14 +0000191 } finally {
Klaas Boeschef441c192015-09-09 16:03:55 +0000192 Profiler.instance().completeTask(ProfilerTask.SKYLARK_BUILTIN_FN);
Francois-Rene Rideau89312fb2015-09-10 18:53:03 +0000193 env.exitScope();
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +0000194 }
195 }
196
Han-Wen Nienhuys612d2212016-01-21 17:00:47 +0000197 private static String stacktraceToString(StackTraceElement[] elts) {
198 StringBuilder b = new StringBuilder();
199 for (StackTraceElement e : elts) {
Laurent Le Brune51a4d22016-10-11 18:04:16 +0000200 b.append(e);
Han-Wen Nienhuys612d2212016-01-21 17:00:47 +0000201 b.append("\n");
202 }
203 return b.toString();
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +0000204 }
205
Han-Wen Nienhuys612d2212016-01-21 17:00:47 +0000206 private IllegalStateException badCallException(Location loc, Throwable e, Object... args) {
207 // If this happens, it's a bug in our code.
208 return new IllegalStateException(
209 String.format(
210 "%s%s (%s)\n"
211 + "while calling %s with args %s\n"
212 + "Java parameter types: %s\nSkylark type checks: %s",
213 (loc == null) ? "" : loc + ": ",
214 Arrays.asList(args),
215 e.getClass().getName(),
216 stacktraceToString(e.getStackTrace()),
217 this,
218 Arrays.asList(invokeMethod.getParameterTypes()),
219 signature.getTypes()),
220 e);
221 }
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +0000222
223 /** Configure the reflection mechanism */
224 @Override
225 public void configure(SkylarkSignature annotation) {
226 Preconditions.checkState(!isConfigured()); // must not be configured yet
227 enforcedArgumentTypes = new ArrayList<>();
228 this.extraArgs = SkylarkSignatureProcessor.getExtraArgs(annotation);
Francois-Rene Rideaua3ac2022015-04-20 18:35:05 +0000229 this.returnType = annotation.returnType();
Francois-Rene Rideau537a90b2015-04-22 06:47:31 +0000230 super.configure(annotation);
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +0000231 }
232
233 // finds the method and makes it accessible (which is needed to find it, and later to use it)
234 protected Method findMethod(final String name) {
235 Method found = null;
236 for (Method method : this.getClass().getDeclaredMethods()) {
237 method.setAccessible(true);
238 if (name.equals(method.getName())) {
Francois-Rene Rideau0b6fdcd2015-03-31 21:47:03 +0000239 if (found != null) {
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +0000240 throw new IllegalArgumentException(String.format(
241 "function %s has more than one method named %s", getName(), name));
242 }
243 found = method;
244 }
245 }
246 if (found == null) {
247 throw new NoSuchElementException(String.format(
248 "function %s doesn't have a method named %s", getName(), name));
249 }
250 return found;
251 }
252
253 /** Configure the reflection mechanism */
254 @Override
255 protected void configure() {
256 invokeMethod = findMethod("invoke");
257
258 int arguments = signature.getSignature().getShape().getArguments();
259 innerArgumentCount = arguments + (extraArgs == null ? 0 : extraArgs.length);
260 Class<?>[] parameterTypes = invokeMethod.getParameterTypes();
Janak Ramakrishnanba395072016-01-19 15:46:42 +0000261 if (innerArgumentCount != parameterTypes.length) {
262 // Guard message construction by check to avoid autoboxing two integers.
263 throw new IllegalStateException(
264 String.format(
265 "bad argument count for %s: method has %s arguments, type list has %s",
266 getName(),
267 innerArgumentCount,
268 parameterTypes.length));
269 }
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +0000270
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +0000271 if (enforcedArgumentTypes != null) {
272 for (int i = 0; i < arguments; i++) {
273 SkylarkType enforcedType = enforcedArgumentTypes.get(i);
274 if (enforcedType != null) {
275 Class<?> parameterType = parameterTypes[i];
Francois-Rene Rideau4a8da552015-04-10 18:56:21 +0000276 String msg = String.format(
277 "fun %s(%s), param %s, enforcedType: %s (%s); parameterType: %s",
278 getName(), signature, signature.getSignature().getNames().get(i),
Francois-Rene Rideau76023b92015-04-17 15:31:59 +0000279 enforcedType, enforcedType.getType(), parameterType);
Francois-Rene Rideau4a8da552015-04-10 18:56:21 +0000280 if (enforcedType instanceof SkylarkType.Simple
281 || enforcedType instanceof SkylarkFunctionType) {
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +0000282 Preconditions.checkArgument(
Francois-Rene Rideau76023b92015-04-17 15:31:59 +0000283 enforcedType.getType() == parameterType, msg);
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +0000284 // No need to enforce Simple types on the Skylark side, the JVM will do it for us.
285 enforcedArgumentTypes.set(i, null);
286 } else if (enforcedType instanceof SkylarkType.Combination) {
Francois-Rene Rideaua3ac2022015-04-20 18:35:05 +0000287 Preconditions.checkArgument(enforcedType.getType() == parameterType, msg);
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +0000288 } else {
289 Preconditions.checkArgument(
290 parameterType == Object.class || parameterType == null, msg);
291 }
292 }
293 }
294 }
295 // No need for the enforcedArgumentTypes List if all the types were Simple
296 enforcedArgumentTypes = FunctionSignature.<SkylarkType>valueListOrNull(enforcedArgumentTypes);
Francois-Rene Rideaua3ac2022015-04-20 18:35:05 +0000297
298 if (returnType != null) {
299 Class<?> type = returnType;
Francois-Rene Rideau537a90b2015-04-22 06:47:31 +0000300 Class<?> methodReturnType = invokeMethod.getReturnType();
301 Preconditions.checkArgument(type == methodReturnType,
302 "signature for function %s says it returns %s but its invoke method returns %s",
303 getName(), returnType, methodReturnType);
Francois-Rene Rideaua3ac2022015-04-20 18:35:05 +0000304 }
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +0000305 }
306
307 /** Configure by copying another function's configuration */
308 // Alternatively, we could have an extension BuiltinFunctionSignature of FunctionSignature,
309 // and use *that* instead of a Factory.
310 public void configure(BuiltinFunction.Factory factory) {
311 // this function must not be configured yet, but the factory must be
312 Preconditions.checkState(!isConfigured());
313 Preconditions.checkState(factory.isConfigured(),
314 "function factory is not configured for %s", getName());
315
316 this.paramDoc = factory.getParamDoc();
317 this.signature = factory.getSignature();
318 this.extraArgs = factory.getExtraArgs();
319 this.objectType = factory.getObjectType();
Francois-Rene Rideau4feb1602015-03-18 19:49:13 +0000320 configure();
321 }
322
323 /**
324 * A Factory allows for a @SkylarkSignature annotation to be provided and processed in advance
325 * for a function that will be defined later as a closure (see e.g. in PackageFactory).
326 *
327 * <p>Each instance of this class must define a method create that closes over some (final)
328 * variables and returns a BuiltinFunction.
329 */
330 public abstract static class Factory extends BuiltinFunction {
331 @Nullable private Method createMethod;
332
333 /** Create unconfigured function Factory from its name */
334 public Factory(String name) {
335 super(name);
336 }
337
338 /** Creates an unconfigured function Factory with the given name and defaultValues */
339 public Factory(String name, Iterable<Object> defaultValues) {
340 super(name, defaultValues);
341 }
342
343 @Override
344 public void configure() {
345 if (createMethod != null) {
346 return;
347 }
348 createMethod = findMethod("create");
349 }
350
351 @Override
352 public Object call(Object[] args, @Nullable FuncallExpression ast, @Nullable Environment env)
353 throws EvalException {
354 throw new EvalException(null, "Tried to invoke a Factory for function " + this);
355 }
356
357 /** Instantiate the Factory
358 * @param args arguments to pass to the create method
359 * @return a new BuiltinFunction that closes over the arguments
360 */
361 public BuiltinFunction apply(Object... args) {
362 try {
363 return (BuiltinFunction) createMethod.invoke(this, args);
364 } catch (InvocationTargetException | IllegalArgumentException | IllegalAccessException e) {
365 throw new RuntimeException(String.format(
366 "Exception while applying BuiltinFunction.Factory %s: %s",
367 this, e.getMessage()), e);
368 }
369 }
370 }
371}