001/*
002 * Copyright (c) 2012-2018 Institut National des Sciences Appliquées de Lyon (INSA Lyon) and others
003 *
004 * This program and the accompanying materials are made available under the
005 * terms of the Eclipse Public License 2.0 which is available at
006 * http://www.eclipse.org/legal/epl-2.0.
007 *
008 * SPDX-License-Identifier: EPL-2.0
009 */
010
011package org.eclipse.golo.runtime;
012
013import gololang.FunctionReference;
014
015import java.lang.invoke.*;
016import java.lang.invoke.MethodHandles.Lookup;
017import java.lang.reflect.*;
018import java.util.Optional;
019
020import static java.lang.invoke.MethodType.methodType;
021import static java.lang.reflect.Modifier.isPrivate;
022import static java.lang.reflect.Modifier.isStatic;
023import static org.eclipse.golo.runtime.DecoratorsHelper.getDecoratedMethodHandle;
024import static org.eclipse.golo.runtime.DecoratorsHelper.isMethodDecorated;
025import static gololang.Messages.message;
026import static gololang.Messages.info;
027import static org.eclipse.golo.runtime.Extractors.checkDeprecation;
028import static org.eclipse.golo.runtime.NamedArgumentsHelper.*;
029
030public final class FunctionCallSupport {
031  private static final boolean DEBUG = Boolean.getBoolean("golo.debug.function-resolution");
032
033
034  private static void debug(String message, Object... args) {
035    if (DEBUG || gololang.Runtime.debugMode()) {
036      info("Function resolution: " + String.format(message, args));
037    }
038  }
039
040  private FunctionCallSupport() {
041    throw new UnsupportedOperationException("Don't instantiate invokedynamic bootstrap class");
042  }
043
044  public static class FunctionCallSite extends MutableCallSite {
045
046    final Lookup callerLookup;
047    final String name;
048    final boolean constant;
049    final String[] argumentNames;
050
051    FunctionCallSite(Lookup callerLookup, String name, MethodType type, boolean constant, String... argumentNames) {
052      super(type);
053      this.callerLookup = callerLookup;
054      this.name = name;
055      this.constant = constant;
056      this.argumentNames = argumentNames;
057    }
058  }
059
060  private static final MethodHandle FALLBACK;
061  private static final MethodHandle SAM_FILTER;
062  private static final MethodHandle FUNCTIONAL_INTERFACE_FILTER;
063
064  static {
065    try {
066      Lookup lookup = MethodHandles.lookup();
067      FALLBACK = lookup.findStatic(
068          FunctionCallSupport.class,
069          "fallback",
070          methodType(Object.class, FunctionCallSite.class, Object[].class));
071      SAM_FILTER = lookup.findStatic(
072          FunctionCallSupport.class,
073          "samFilter",
074          methodType(Object.class, Class.class, Object.class));
075      FUNCTIONAL_INTERFACE_FILTER = lookup.findStatic(
076          FunctionCallSupport.class,
077          "functionalInterfaceFilter",
078          methodType(Object.class, Lookup.class, Class.class, Object.class));
079    } catch (NoSuchMethodException | IllegalAccessException e) {
080      throw new Error("Could not bootstrap the required method handles", e);
081    }
082  }
083
084  public static Object samFilter(Class<?> type, Object value) {
085    if (value instanceof FunctionReference) {
086      return MethodHandleProxies.asInterfaceInstance(type, ((FunctionReference) value).handle());
087    }
088    return value;
089  }
090
091  public static Object functionalInterfaceFilter(Lookup caller, Class<?> type, Object value) throws Throwable {
092    if (value instanceof FunctionReference) {
093      return asFunctionalInterface(caller, type, ((FunctionReference) value).handle());
094    }
095    return value;
096  }
097
098  public static Object asFunctionalInterface(Lookup caller, Class<?> type, MethodHandle handle) throws Throwable {
099    for (Method method : type.getMethods()) {
100      if (!method.isDefault() && !isStatic(method.getModifiers())) {
101        MethodType lambdaType = methodType(method.getReturnType(), method.getParameterTypes());
102        CallSite callSite = LambdaMetafactory.metafactory(
103            caller,
104            method.getName(),
105            methodType(type),
106            lambdaType,
107            handle,
108            lambdaType);
109        return callSite.dynamicInvoker().invoke();
110      }
111    }
112    throw new RuntimeException(message("handle_conversion_failed", handle, type));
113  }
114
115  public static CallSite bootstrap(Lookup caller, String name, MethodType type, Object... bsmArgs) throws IllegalAccessException, ClassNotFoundException {
116    boolean constant = ((int) bsmArgs[0]) == 1;
117    String[] argumentNames = new String[bsmArgs.length - 1];
118    for (int i = 0; i < bsmArgs.length - 1; i++) {
119      argumentNames[i] = (String) bsmArgs[i + 1];
120    }
121    FunctionCallSite callSite = new FunctionCallSite(
122        caller,
123        name.replaceAll("#", "\\."),
124        type,
125        constant,
126        argumentNames);
127    MethodHandle fallbackHandle = FALLBACK
128        .bindTo(callSite)
129        .asCollector(Object[].class, type.parameterCount())
130        .asType(type);
131    callSite.setTarget(fallbackHandle);
132    return callSite;
133  }
134
135  public static Object fallback(FunctionCallSite callSite, Object[] args) throws Throwable {
136    String functionName = callSite.name;
137    MethodType type = callSite.type();
138    Lookup caller = callSite.callerLookup;
139    Class<?> callerClass = caller.lookupClass();
140    String[] argumentNames = callSite.argumentNames;
141
142    MethodHandle handle = null;
143    AccessibleObject result = null;
144    if (!functionName.contains(".")) {
145      result = findStaticMethodOrField(callerClass, callerClass, functionName, args);
146    }
147    if (result == null) {
148      result = findClassWithStaticMethodOrField(callerClass, functionName, args);
149    }
150    if (result == null) {
151      result = findClassWithStaticMethodOrFieldFromImports(callerClass, functionName, args);
152    }
153    if (result == null) {
154      result = findClassWithConstructor(callerClass, functionName, args);
155    }
156    if (result == null) {
157      result = findClassWithConstructorFromImports(callerClass, functionName, args);
158    }
159    if (result == null) {
160      throw new NoSuchMethodError(functionName + type.toMethodDescriptorString());
161    }
162
163    Class<?>[] types = null;
164    if (result instanceof Method) {
165      Method method = (Method) result;
166      checkLocalFunctionCallFromSameModuleAugmentation(method, callerClass.getName());
167      if (isMethodDecorated(method)) {
168        handle = getDecoratedMethodHandle(caller, method, type.parameterCount());
169      } else {
170        types = method.getParameterTypes();
171        handle = caller.unreflect(method);
172        if (method.isAnnotationPresent(WithCaller.class)) {
173          handle = handle.bindTo(callerClass);
174        }
175        //TODO: improve varargs support on named arguments. Matching the last param type + according argument
176        if (isVarargsWithNames(method, types, args, argumentNames)) {
177          handle = handle.asFixedArity().asType(type);
178        } else {
179          handle = handle.asType(type);
180        }
181      }
182      handle = reorderArguments(method, handle, argumentNames);
183    } else if (result instanceof Constructor) {
184      Constructor<?> constructor = (Constructor<?>) result;
185      types = constructor.getParameterTypes();
186      if (constructor.isVarArgs() && TypeMatching.isLastArgumentAnArray(types.length, args)) {
187        handle = caller.unreflectConstructor(constructor).asFixedArity().asType(type);
188      } else {
189        handle = caller.unreflectConstructor(constructor).asType(type);
190      }
191    } else {
192      Field field = (Field) result;
193      handle = caller.unreflectGetter(field).asType(type);
194    }
195    handle = insertSAMFilter(handle, callSite.callerLookup, types, 0);
196
197    if (callSite.constant) {
198      Object constantValue = handle.invokeWithArguments(args);
199      MethodHandle constant;
200      if (constantValue == null) {
201        constant = MethodHandles.constant(Object.class, null);
202      } else {
203        constant = MethodHandles.constant(constantValue.getClass(), constantValue);
204      }
205      constant = MethodHandles.dropArguments(constant, 0, type.parameterArray());
206      callSite.setTarget(constant.asType(type));
207      return constantValue;
208    } else {
209      callSite.setTarget(handle);
210      return handle.invokeWithArguments(args);
211    }
212  }
213
214  private static boolean isVarargsWithNames(Method method, Class<?>[] types, Object[] args, String[] argumentNames) {
215    return method.isVarArgs()
216      && (
217          TypeMatching.isLastArgumentAnArray(types.length, args)
218          || argumentNames.length > 0);
219  }
220
221  public static MethodHandle reorderArguments(Method method, MethodHandle handle, String[] argumentNames) {
222    return NamedArgumentsHelper.reorderArguments(
223        method.getName(),
224        getParameterNames(method),
225        handle,
226        argumentNames, 0, 0);
227  }
228
229  public static MethodHandle insertSAMFilter(MethodHandle handle, Lookup caller, Class<?>[] types, int startIndex) {
230    if (types != null) {
231      for (int i = 0; i < types.length; i++) {
232        if (TypeMatching.isSAM(types[i])) {
233          handle = MethodHandles.filterArguments(handle, startIndex + i, SAM_FILTER.bindTo(types[i]));
234        } else if (TypeMatching.isFunctionalInterface(types[i])) {
235          handle = MethodHandles.filterArguments(
236              handle,
237              startIndex + i,
238              FUNCTIONAL_INTERFACE_FILTER.bindTo(caller).bindTo(types[i]));
239        }
240      }
241    }
242    return handle;
243  }
244
245  private static void checkLocalFunctionCallFromSameModuleAugmentation(Method method, String callerClassName) {
246    if (isPrivate(method.getModifiers()) && callerClassName.contains("$")) {
247      String prefix = callerClassName.substring(0, callerClassName.indexOf("$"));
248      if (method.getDeclaringClass().getName().equals(prefix)) {
249        method.setAccessible(true);
250      }
251    }
252  }
253
254  private static AccessibleObject findClassWithConstructorFromImports(Class<?> callerClass, String classname, Object[] args) {
255    String[] imports = Module.imports(callerClass);
256    for (String imported : imports) {
257      AccessibleObject result = findClassWithConstructor(
258          callerClass,
259          mergeImportAndCall(imported, classname),
260          args);
261      if (result != null) {
262        return result;
263      }
264    }
265    return null;
266  }
267
268  private static AccessibleObject findClassWithConstructor(Class<?> callerClass, String classname, Object[] args) {
269    debug("looking for constructor for `%s`", classname);
270    try {
271      Class<?> targetClass = Class.forName(classname, true, callerClass.getClassLoader());
272      for (Constructor<?> constructor : targetClass.getConstructors()) {
273        if (TypeMatching.argumentsMatch(constructor, args)) {
274          debug("constructor found");
275          return checkDeprecation(callerClass, constructor);
276        }
277      }
278    } catch (ClassNotFoundException ignored) {
279      // ignored to try the next strategy
280    }
281    return null;
282  }
283
284  static String mergeImportAndCall(String importName, String functionName) {
285    if (importName == null || importName.isEmpty()) {
286      return functionName;
287    }
288    String[] importParts = importName.split("\\.");
289    String[] functionParts = functionName.split("\\.");
290    StringBuilder merged = new StringBuilder();
291    int fidx = 0;
292    for (String imp : importParts) {
293      if (imp.equals(functionParts[fidx])) {
294        fidx++;
295      } else if (fidx > 0) {
296        return importName + '.' + functionName;
297      }
298      if (merged.length() != 0) {
299        merged.append('.');
300      }
301      merged.append(imp);
302    }
303    while (fidx < functionParts.length - 1) {
304      merged.append('.').append(functionParts[fidx]);
305      fidx++;
306    }
307    if (fidx == functionParts.length - 1) {
308      merged.append('.').append(functionParts[fidx]);
309    }
310    return merged.toString();
311  }
312
313  private static AccessibleObject findClassWithStaticMethodOrFieldFromImports(Class<?> callerClass, String functionName, Object[] args) {
314    AccessibleObject result = null;
315    if (functionName.contains(".")) {
316      result = findClassWithStaticMethodOrField(
317          callerClass,
318          mergeImportAndCall(callerClass.getCanonicalName(), functionName),
319          args);
320      if (result != null) {
321        return result;
322      }
323    }
324    String[] imports = Module.imports(callerClass);
325    for (String importedClassName : imports) {
326      result = findClassWithStaticMethodOrField(
327          callerClass,
328          mergeImportAndCall(importedClassName, functionName),
329          args);
330      if (result != null) {
331        return result;
332      }
333    }
334    return null;
335  }
336
337  private static AccessibleObject findClassWithStaticMethodOrField(Class<?> callerClass, String functionName, Object[] args) {
338    int methodClassSeparatorIndex = functionName.lastIndexOf(".");
339    if (methodClassSeparatorIndex >= 0) {
340      String className = functionName.substring(0, methodClassSeparatorIndex);
341      String methodName = functionName.substring(methodClassSeparatorIndex + 1);
342      debug("looking for function `%s` in named `%s`", methodName, className);
343      try {
344        Class<?> targetClass = Class.forName(className, true, callerClass.getClassLoader());
345        return findStaticMethodOrField(callerClass, targetClass, methodName, args);
346      } catch (ClassNotFoundException ignored) {
347        // ignored to try the next strategy
348        Warnings.unavailableClass(className, callerClass.getName());
349      }
350    }
351    return null;
352  }
353
354  private static AccessibleObject findStaticMethodOrField(Class<?> caller, Class<?> klass, String name, Object[] arguments) {
355    debug("looking for function `%s` in loaded class `%s`", name, klass.getCanonicalName());
356    Optional<Method> meth = Extractors.getMethods(klass)
357      .filter(m -> methodMatches(caller, name, arguments, m, m.isVarArgs()))
358      .map(m -> checkDeprecation(caller, m))
359      .findFirst();
360    if (meth.isPresent()) {
361      debug("method found");
362      return meth.get();
363    }
364    if (arguments.length == 0) {
365      Optional<Field> f = Extractors.getFields(klass)
366        .filter(o -> fieldMatches(name, o))
367        .map(o -> checkDeprecation(caller, o))
368        .findFirst();
369      return f.orElse(null);
370    }
371    return null;
372  }
373
374  private static boolean methodMatches(Class<?> caller, String name, Object[] arguments, Method method, boolean varargs) {
375    return methodMatches(caller, name, arguments, method, varargs, true);
376  }
377
378  private static boolean methodMatches(Class<?> caller, String name, Object[] arguments, Method method, boolean varargs, boolean tryCaller) {
379    if (!method.getName().equals(name) || !isStatic(method.getModifiers())) { return false; }
380    if (isMethodDecorated(method)) { return true; }
381    if (TypeMatching.argumentsMatch(method, arguments, varargs)) { return true; }
382    if (method.isAnnotationPresent(WithCaller.class) && tryCaller) {
383      Object[] argsWithCaller = new Object[arguments.length + 1];
384      argsWithCaller[0] = caller;
385      System.arraycopy(arguments, 0, argsWithCaller, 1, arguments.length);
386      return methodMatches(caller, name, argsWithCaller, method, varargs, false);
387    }
388    return false;
389  }
390
391  private static boolean fieldMatches(String name, Field field) {
392    return field.getName().equals(name) && isStatic(field.getModifiers());
393  }
394}