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.DynamicObject;
014
015import java.lang.invoke.*;
016import java.lang.invoke.MethodHandles.Lookup;
017import java.util.Arrays;
018import java.util.HashSet;
019import java.util.WeakHashMap;
020
021import static java.lang.invoke.MethodHandles.*;
022import static java.lang.invoke.MethodType.methodType;
023
024
025public final class MethodInvocationSupport {
026
027  private MethodInvocationSupport() {
028    throw new UnsupportedOperationException("Don't instantiate invokedynamic bootstrap class");
029  }
030
031  /*
032   * This code is heavily inspired from the inline cache construction from
033   * Remi Forax's JSR292 cookbooks.
034   */
035
036  static final class InlineCache extends MutableCallSite {
037
038    static final int MEGAMORPHIC_THRESHOLD = 5;
039
040    final Lookup callerLookup;
041    final String name;
042    final boolean nullSafeGuarded;
043    final String[] argumentNames;
044
045    int depth = 0;
046    MethodHandle resetFallback;
047    WeakHashMap<Class<?>, MethodHandle> vtable;
048
049    InlineCache(Lookup callerLookup, String name, MethodType type, boolean nullSafeGuarded, String... argumentNames) {
050      super(type);
051      this.callerLookup = callerLookup;
052      this.name = name;
053      this.nullSafeGuarded = nullSafeGuarded;
054      this.argumentNames = argumentNames;
055    }
056
057    boolean isMegaMorphic() {
058      return depth > MEGAMORPHIC_THRESHOLD;
059    }
060
061    public MethodInvocation toMethodInvocation(Object[] args) {
062      return new MethodInvocation(name, type(), args, argumentNames);
063    }
064  }
065
066  private static final MethodHandle CLASS_GUARD;
067  private static final MethodHandle FALLBACK;
068  private static final MethodHandle RESET_FALLBACK;
069  private static final MethodHandle VTABLE_LOOKUP;
070
071  private static final MethodHandle OVERLOADED_GUARD_GENERIC;
072  private static final MethodHandle OVERLOADED_GUARD_1;
073  private static final MethodHandle OVERLOADED_GUARD_2;
074  private static final MethodHandle OVERLOADED_GUARD_3;
075  private static final MethodHandle OVERLOADED_GUARD_4;
076
077
078  private static final HashSet<String> DYNAMIC_OBJECT_RESERVED_METHOD_NAMES = new HashSet<String>() {
079    {
080      add("get");
081      add("define");
082      add("undefine");
083      add("mixin");
084      add("copy");
085      add("freeze");
086      add("properties");
087      add("invoker");
088      add("hasMethod");
089      add("fallback");
090      add("hasKind");
091      add("sameKind");
092      add("isFrozen");
093    }
094  };
095
096  static {
097    try {
098      Lookup lookup = MethodHandles.lookup();
099
100      CLASS_GUARD = lookup.findStatic(
101          MethodInvocationSupport.class,
102          "classGuard",
103          methodType(boolean.class, Class.class, Object.class));
104
105      FALLBACK = lookup.findStatic(
106          MethodInvocationSupport.class,
107          "fallback",
108          methodType(Object.class, InlineCache.class, Object[].class));
109
110      RESET_FALLBACK = lookup.findStatic(
111          MethodInvocationSupport.class,
112          "resetFallback",
113          methodType(Object.class, InlineCache.class, Object[].class));
114
115      VTABLE_LOOKUP = lookup.findStatic(
116          MethodInvocationSupport.class,
117          "vtableLookup",
118          methodType(MethodHandle.class, InlineCache.class, Object[].class));
119
120      OVERLOADED_GUARD_GENERIC = lookup.findStatic(
121          MethodInvocationSupport.class,
122          "overloadedGuard_generic",
123          methodType(boolean.class, Class[].class, Object[].class));
124
125      OVERLOADED_GUARD_1 = lookup.findStatic(
126          MethodInvocationSupport.class,
127          "overloadedGuard_1",
128          methodType(boolean.class, Class.class, Class.class, Object.class, Object.class));
129
130      OVERLOADED_GUARD_2 = lookup.findStatic(
131          MethodInvocationSupport.class,
132          "overloadedGuard_2",
133          methodType(boolean.class, Class.class, Class.class, Class.class, Object.class, Object.class, Object.class));
134
135      OVERLOADED_GUARD_3 = lookup.findStatic(
136          MethodInvocationSupport.class,
137          "overloadedGuard_3",
138          methodType(boolean.class, Class.class, Class.class, Class.class, Class.class, Object.class, Object.class, Object.class, Object.class));
139
140      OVERLOADED_GUARD_4 = lookup.findStatic(
141          MethodInvocationSupport.class,
142          "overloadedGuard_4",
143          methodType(boolean.class, Class.class, Class.class, Class.class, Class.class, Class.class, Object.class, Object.class, Object.class, Object.class, Object.class));
144
145    } catch (NoSuchMethodException | IllegalAccessException e) {
146      throw new Error("Could not bootstrap the required method handles", e);
147    }
148  }
149
150  public static CallSite bootstrap(Lookup caller, String name, MethodType type, Object... bsmArgs) {
151    boolean nullSafeGuarded = ((int) bsmArgs[0]) == 1;
152    String[] argumentNames = new String[bsmArgs.length - 1];
153    for (int i = 0; i < bsmArgs.length - 1; i++) {
154      argumentNames[i] = (String) bsmArgs[i + 1];
155    }
156    InlineCache callSite = new InlineCache(caller, name, type, nullSafeGuarded, argumentNames);
157    MethodHandle fallbackHandle = FALLBACK
158        .bindTo(callSite)
159        .asCollector(Object[].class, type.parameterCount())
160        .asType(type);
161    callSite.resetFallback = RESET_FALLBACK
162        .bindTo(callSite)
163        .asCollector(Object[].class, type.parameterCount())
164        .asType(type);
165    callSite.setTarget(fallbackHandle);
166    return callSite;
167  }
168
169  public static boolean classGuard(Class<?> expected, Object receiver) {
170    return receiver.getClass() == expected;
171  }
172
173  public static boolean overloadedGuard_generic(Class<?>[] types, Object[] arguments) {
174    if (arguments[0].getClass() != types[0]) {
175      return false;
176    }
177    for (int i = 1; i < types.length; i++) {
178      if ((arguments[i] != null) && (arguments[i].getClass() != types[i])) {
179        return false;
180      }
181    }
182    return true;
183  }
184
185  public static boolean overloadedGuard_1(Class<?> t1, Class<?> t2, Object receiver, Object arg) {
186    return receiver.getClass() == t1
187      && (arg == null || arg.getClass() == t2);
188  }
189
190  public static boolean overloadedGuard_2(Class<?> t1, Class<?> t2, Class<?> t3, Object receiver, Object arg1, Object arg2) {
191    return receiver.getClass() == t1
192      && (arg1 == null || arg1.getClass() == t2)
193      && (arg2 == null || arg2.getClass() == t3);
194  }
195
196  public static boolean overloadedGuard_3(Class<?> t1, Class<?> t2, Class<?> t3, Class<?> t4, Object receiver, Object arg1, Object arg2, Object arg3) {
197    return receiver.getClass() == t1
198      && (arg1 == null || arg1.getClass() == t2)
199      && (arg2 == null || arg2.getClass() == t3)
200      && (arg3 == null || arg3.getClass() == t4);
201  }
202
203  public static boolean overloadedGuard_4(Class<?> t1, Class<?> t2, Class<?> t3, Class<?> t4, Class<?> t5, Object receiver, Object arg1, Object arg2, Object arg3, Object arg4) {
204    return receiver.getClass() == t1
205      && (arg1 == null || arg1.getClass() == t2)
206      && (arg2 == null || arg2.getClass() == t3)
207      && (arg3 == null || arg3.getClass() == t4)
208      && (arg4 == null || arg4.getClass() == t5);
209  }
210
211  public static MethodHandle vtableLookup(InlineCache inlineCache, Object[] args) {
212    Class<?> receiverClass = args[0].getClass();
213    return inlineCache.vtable.computeIfAbsent(receiverClass, k -> lookupTarget(receiverClass, inlineCache, args));
214  }
215
216  private static MethodHandle lookupTarget(Class<?> receiverClass, InlineCache inlineCache, Object[] args) {
217    MethodInvocation invocation = inlineCache.toMethodInvocation(args);
218    if (receiverClass.isArray()) {
219      return new ArrayMethodFinder(invocation, inlineCache.callerLookup).find();
220    }
221    if (isCallOnDynamicObject(inlineCache, args[0])) {
222      DynamicObject dynamicObject = (DynamicObject) args[0];
223      return dynamicObject.invoker(inlineCache.name, inlineCache.type());
224    } else {
225      return findTarget(invocation, inlineCache);
226    }
227  }
228
229  public static Object resetFallback(InlineCache inlineCache, Object[] args) throws Throwable {
230    inlineCache.depth = 0;
231    return fallback(inlineCache, args);
232  }
233
234  public static Object fallback(InlineCache inlineCache, Object[] args) throws Throwable {
235
236    if (inlineCache.isMegaMorphic()) {
237      return installVTableDispatch(inlineCache, args);
238    }
239
240    if (args[0] == null) {
241      if (shouldReturnNull(inlineCache, args[0])) {
242        return null;
243      } else {
244        throw new NullPointerException("On method: "
245            + inlineCache.name + " " + inlineCache.type().dropParameterTypes(0, 1));
246      }
247    }
248
249    Class<?> receiverClass = args[0].getClass();
250    MethodHandle target = lookupTarget(receiverClass, inlineCache, args);
251
252    if (target == null) {
253      // TODO: extract method to look for a `fallback` method on the receiver
254      InlineCache fallbackCallSite = new InlineCache(
255          inlineCache.callerLookup,
256          "fallback",
257          methodType(Object.class, Object.class, Object.class, Object[].class),
258          false);
259      Object[] fallbackArgs = {
260        args[0],
261        inlineCache.name,
262        Arrays.copyOfRange(args, 1, args.length)
263      };
264      target = lookupTarget(receiverClass, fallbackCallSite, fallbackArgs);
265      if (target != null) {
266        return fallback(fallbackCallSite, fallbackArgs);
267      } else {
268        throw new NoSuchMethodError(receiverClass + "::" + inlineCache.name);
269      }
270    }
271
272    MethodHandle guard = CLASS_GUARD.bindTo(receiverClass);
273    MethodHandle fallback = inlineCache.getTarget();
274    MethodHandle root = guardWithTest(guard, target, fallback);
275    if (inlineCache.nullSafeGuarded) {
276      root = makeNullSafeGuarded(root);
277    }
278    inlineCache.setTarget(root);
279    inlineCache.depth += 1;
280    return target.invokeWithArguments(args);
281  }
282
283  private static MethodHandle makeNullSafeGuarded(MethodHandle root) {
284    MethodHandle catchThenNull = dropArguments(constant(Object.class, null), 0, NullPointerException.class);
285    root = catchException(root, NullPointerException.class, catchThenNull);
286    return root;
287  }
288
289  private static boolean shouldReturnNull(InlineCache inlineCache, Object arg) {
290    return (arg == null) && inlineCache.nullSafeGuarded;
291  }
292
293  private static Object installVTableDispatch(InlineCache inlineCache, Object[] args) throws Throwable {
294    if (inlineCache.vtable == null) {
295      inlineCache.vtable = new WeakHashMap<>();
296    }
297    MethodHandle lookup = VTABLE_LOOKUP
298        .bindTo(inlineCache)
299        .asCollector(Object[].class, args.length);
300    MethodHandle exactInvoker = exactInvoker(inlineCache.type());
301    MethodHandle vtableTarget = foldArguments(exactInvoker, lookup);
302    if (inlineCache.nullSafeGuarded) {
303      vtableTarget = makeNullSafeGuarded(vtableTarget);
304    }
305    inlineCache.setTarget(vtableTarget);
306    if (shouldReturnNull(inlineCache, args[0])) {
307      return null;
308    }
309    return vtableTarget.invokeWithArguments(args);
310  }
311
312  private static boolean isCallOnDynamicObject(InlineCache inlineCache, Object arg) {
313    return (arg instanceof DynamicObject)
314      && !DYNAMIC_OBJECT_RESERVED_METHOD_NAMES.contains(inlineCache.name)
315      && (!"toString".equals(inlineCache.name) || ((DynamicObject) arg).hasMethod("toString"));
316  }
317
318  private static MethodHandle guardOnOverloaded(MethodHandle target, MethodInvocation invocation, MethodHandle reset) {
319    Object[] args = invocation.arguments();
320    Class<?>[] types = new Class<?>[args.length];
321    for (int i = 0; i < types.length; i++) {
322      types[i] = (args[i] == null) ? Object.class : args[i].getClass();
323    }
324    MethodHandle guard;
325    switch (args.length) {
326      case 2:
327        guard = insertArguments(OVERLOADED_GUARD_1, 0, types[0], types[1]);
328        break;
329      case 3:
330        guard = insertArguments(OVERLOADED_GUARD_2, 0, types[0], types[1], types[2]);
331        break;
332      case 4:
333        guard = insertArguments(OVERLOADED_GUARD_3, 0, types[0], types[1], types[2], types[3]);
334        break;
335      case 5:
336        guard = insertArguments(OVERLOADED_GUARD_4, 0, types[0], types[1], types[2], types[3], types[4]);
337        break;
338      default:
339        guard = OVERLOADED_GUARD_GENERIC.bindTo(types).asCollector(Object[].class, types.length);
340    }
341    return guardWithTest(guard, target, reset);
342  }
343
344  private static MethodHandle findTarget(MethodInvocation invocation, InlineCache inlineCache) {
345    MethodHandle target;
346    Lookup lookup = inlineCache.callerLookup;
347
348    RegularMethodFinder regularMethodFinder = new RegularMethodFinder(invocation, lookup);
349    target = regularMethodFinder.find();
350    if (target != null) {
351      if (regularMethodFinder.isOverloaded()) {
352        return guardOnOverloaded(target, invocation, inlineCache.resetFallback);
353      }
354      return target;
355    }
356
357    target = new PropertyMethodFinder(invocation, lookup).find();
358    if (target != null) {
359      return target;
360    }
361
362    target = new AugmentationMethodFinder(invocation, lookup).find();
363    if (target != null) {
364      return target;
365    }
366    return null;
367  }
368}