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