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 gololang;
011
012import java.lang.invoke.MethodHandle;
013import java.lang.invoke.MethodHandles;
014import java.lang.invoke.MethodType;
015import java.util.HashMap;
016import java.util.Map;
017import java.util.Set;
018import java.util.List;
019import java.util.LinkedList;
020import java.util.Objects;
021
022import static java.lang.System.arraycopy;
023import static java.lang.invoke.MethodType.genericMethodType;
024import static java.lang.invoke.MethodType.methodType;
025import static gololang.Predefined.isClosure;
026
027/**
028 * A dynamic object is an object whose properties can be dynamically added, changed and removed. Properties can be any
029 * object value or a function reference.
030 * <p>
031 * The methods <code>plug</code> and <code>propertyMissing</code> are left undocumented. They are being used
032 * by the Golo runtime to dispatch method invocations on dynamic objects.
033 */
034public final class DynamicObject {
035
036  private final Object kind;
037  private final HashMap<String, Object> properties = new HashMap<>();
038  private boolean frozen = false;
039
040
041  public DynamicObject() {
042    this("DynamicObject");
043  }
044
045  public DynamicObject(Object kind) {
046    this.kind = kind;
047  }
048
049  public boolean hasKind(Object k) {
050    return Objects.equals(kind, k);
051  }
052
053  public boolean sameKind(DynamicObject other) {
054    return Objects.equals(kind, other.kind);
055  }
056
057  @Override
058  public String toString() {
059    List<String> props = new LinkedList<>();
060    for (Map.Entry<String, Object> prop : properties.entrySet()) {
061      if (!isClosure(prop.getValue())) {
062        props.add(String.format("%s=%s", prop.getKey(), prop.getValue().toString()));
063      }
064    }
065    return String.format("%s{%s}", kind, String.join(", ", props));
066  }
067
068  /**
069   * Defines a property.
070   *
071   * @param name  the property name.
072   * @param value the property value.
073   * @return the same dynamic object.
074   * @throws IllegalStateException if the dynamic object is frozen.
075   */
076  public DynamicObject define(String name, Object value) {
077    frozenMutationCheck();
078    properties.put(name, value);
079    return this;
080  }
081
082  /**
083   * @return a view of all properties.
084   */
085  public Set<Map.Entry<String, Object>> properties() {
086    return properties.entrySet();
087  }
088
089  /**
090   * @param name the property name.
091   * @return the property value.
092   */
093  public Object get(String name) {
094    return properties.get(name);
095  }
096
097  /**
098   * Removes a property.
099   *
100   * @param name the property name.
101   * @return the same dynamic object.
102   */
103  public DynamicObject undefine(String name) {
104    properties.remove(name);
105    return this;
106  }
107
108  /**
109   * @return a new dynamic object whose properties point to the same objects.
110   */
111  public DynamicObject copy() {
112    DynamicObject copy = new DynamicObject(this.kind);
113    for (Map.Entry<String, Object> entry : properties.entrySet()) {
114      copy.properties.put(entry.getKey(), entry.getValue());
115    }
116    return copy;
117  }
118
119  /**
120   * Mixes all properties from another dynamic object into this one, overwriting existing properties.
121   *
122   * @param other the dynamic object to mix the properties from.
123   * @return the same dynamic object.
124   */
125  public DynamicObject mixin(DynamicObject other) {
126    frozenMutationCheck();
127    for (Map.Entry<String, Object> entry : other.properties.entrySet()) {
128      properties.put(entry.getKey(), entry.getValue());
129    }
130    return this;
131  }
132
133  /**
134   * Freezes a dynamic object, meaning that its properties cannot be added, updated and removed anymore.
135   *
136   * @return the same dynamic object.
137   */
138  public DynamicObject freeze() {
139    this.frozen = true;
140    return this;
141  }
142
143  /**
144   * Tells whether the dynamic object is frozen or not.
145   *
146   * @return {@code true} if frozen, {@code false} otherwise.
147   */
148  public boolean isFrozen() {
149    return frozen;
150  }
151
152  /**
153   * Dispatch dynamic object "methods". The receiver dynamic object is expected to be the first element of {@code args}.
154   *
155   * @param property the method property in the dynamic object.
156   * @param args     the arguments.
157   * @return the return value.
158   * @throws Throwable in case everything is wrong.
159   */
160  public static Object dispatchCall(String property, Object... args) throws Throwable {
161    DynamicObject obj = (DynamicObject) args[0];
162    Object value = obj.properties.get(property);
163    if (value != null) {
164      if (value instanceof FunctionReference) {
165        FunctionReference funRef = (FunctionReference) value;
166        if (funRef.isVarargsCollector() && args[args.length - 1] instanceof Object[]) {
167          return funRef.spread(args);
168        }
169        return funRef.invoke(args);
170      } else {
171        throw new UnsupportedOperationException("There is no dynamic object method defined for " + property);
172      }
173    }
174    if (obj.hasFallback()) {
175      FunctionReference handle = (FunctionReference) obj.properties.get("fallback");
176      Object[] fallback_args = new Object[args.length + 1];
177      fallback_args[0] = obj;
178      fallback_args[1] = property;
179      arraycopy(args, 1, fallback_args, 2, args.length - 1);
180      return handle.invoke(fallback_args);
181    }
182    throw new UnsupportedOperationException("There is neither a dynamic object method defined for " + property + " nor a 'fallback' method");
183  }
184
185  /**
186   * Dispatches getter-style dynamic object methods, i.e., methods with a receiver and no argument.
187   *
188   * @param property the method property in the dynamic object.
189   * @param object   the receiver object.
190   * @return the return value.
191   * @throws Throwable in case everything is wrong.
192   */
193  public static Object dispatchGetterStyle(String property, DynamicObject object) throws Throwable {
194    Object value = object.get(property);
195    if (value != null || object.properties.containsKey(property)) {
196      if (value instanceof FunctionReference) {
197        FunctionReference funRef = (FunctionReference) value;
198        if (funRef.acceptArity(1)) {
199          return funRef.invoke(object);
200        }
201      }
202      return value;
203    }
204    if (object.hasFallback()) {
205      FunctionReference funRef = (FunctionReference) object.properties.get("fallback");
206      return funRef.invoke(object, property);
207    }
208    return null;
209  }
210
211  /**
212   * Dispatches setter-style dynamic object methods, i.e., methods with a receiver and exactly 1 argument.
213   *
214   * @param property the method property in the dynamic object.
215   * @param object   the receiver object.
216   * @param arg      the arguments.
217   * @return the return value.
218   * @throws Throwable in case everything is wrong.
219   */
220  public static Object dispatchSetterStyle(String property, DynamicObject object, Object arg) throws Throwable {
221    Object value = object.get(property);
222    if (value != null || object.properties.containsKey(property)) {
223      if (value instanceof FunctionReference) {
224        FunctionReference funRef = (FunctionReference) value;
225        if (funRef.arity() == 2) {
226          if (funRef.isVarargsCollector() && arg instanceof Object[]) {
227            return funRef.handle().invokeExact((Object) object, (Object[]) arg);
228          }
229          return funRef.invoke(object, arg);
230        }
231      }
232    }
233    // XXX: should we try the fallback method here ?
234    return object.define(property, arg);
235  }
236
237  /**
238   * Dispatches on another dynamic object (fallback helper).
239   *
240   * @param deleguee the object to delegate to.
241   * @param receiver the receiver object.
242   * @param property the method property in the dynamic object.
243   * @param args     the arguments.
244   * @return the return value.
245   * @throws Throwable in case everything is wrong.
246   */
247  public static Object dispatchDelegate(DynamicObject deleguee, DynamicObject receiver, String property, Object... args) throws Throwable {
248    return deleguee
249      .invoker(property, genericMethodType(args.length + 1))
250      .bindTo(deleguee)
251      .invokeWithArguments(args);
252  }
253
254  /**
255   * Creates a function suitable for the {@code fallback} property delegating to the given dynamic object.
256   *
257   * Example:
258   * <pre class="listing"><code class="lang-golo" data-lang="golo">
259   * let d = DynamicObject(): name("Zaphod")
260   * let o = DynamicObject(): fallback(delegate(d))
261   * </code></pre>
262   *
263   * @param deleguee the object to delegate to.
264   * @return a function delegating to {@code deleguee}
265   */
266  public static FunctionReference delegate(DynamicObject deleguee) {
267    return new FunctionReference(
268        DISPATCH_DELEGATE.bindTo(deleguee).asVarargsCollector(Object[].class), //.asType(genericMethodType(2, true)),
269        new String[]{"this", "name", "args"});
270  }
271
272  /**
273   * Gives an invoker method handle for a given property.
274   * <p>
275   * While this method may be useful in itself, it is mostly relevant for the Golo runtime internals so as
276   * to allow calling "methods" on dynamic objects, as in:
277   * <pre class="listing"><code class="lang-golo" data-lang="golo">
278   * # obj is some dynamic object...
279   * obj: foo("bar")
280   * println(foo: bar())
281   *
282   * obj: define("plop", |this| -> "Plop!")
283   * println(obj: plop())
284   * </code></pre>
285   *
286   * @param property the name of a property.
287   * @param type     the expected invoker type with at least one parameter (the dynamic object as a receiver).
288   * @return a method handle.
289   */
290  public MethodHandle invoker(String property, MethodType type) {
291    switch (type.parameterCount()) {
292      case 0:
293        throw new IllegalArgumentException("A dynamic object invoker type needs at least 1 argument (the receiver)");
294      case 1:
295        return DISPATCH_GET.bindTo(property).asType(genericMethodType(1));
296      case 2:
297        return DISPATCH_SET.bindTo(property).asType(genericMethodType(2));
298      default:
299        return DISPATCH_CALL.bindTo(property).asCollector(Object[].class, type.parameterCount());
300    }
301  }
302
303  /**
304   * Verify if a method is defined for the dynamic object.
305   *
306   * @param method the method name.
307   * @return {@code true} if method is defined, {@code false} otherwise.
308   */
309  public boolean hasMethod(String method) {
310    Object obj = properties.get(method);
311    if (obj != null) {
312      return isClosure(obj);
313    }
314    return false;
315  }
316
317  /**
318   * Let the user define a fallback behavior.
319   *
320   * @param value the fallback value
321   * @return the current object
322   */
323  public DynamicObject fallback(Object value) {
324    return define("fallback", value);
325  }
326
327  /**
328   * Verify a fallback property exists.
329   *
330   * @return {@code true} if a fallback behavior is defined, {@code false} otherwise.
331   */
332  private boolean hasFallback() {
333    return properties.containsKey("fallback");
334  }
335
336  public static final MethodHandle DISPATCH_CALL;
337  public static final MethodHandle DISPATCH_GET;
338  public static final MethodHandle DISPATCH_SET;
339  public static final MethodHandle DISPATCH_DELEGATE;
340
341  static {
342    MethodHandles.Lookup lookup = MethodHandles.lookup();
343    try {
344      DISPATCH_DELEGATE = lookup.findStatic(DynamicObject.class, "dispatchDelegate",
345          methodType(Object.class, DynamicObject.class, DynamicObject.class, String.class, Object[].class));
346      DISPATCH_CALL = lookup.findStatic(DynamicObject.class, "dispatchCall", methodType(Object.class, String.class, Object[].class));
347      DISPATCH_GET = lookup.findStatic(DynamicObject.class, "dispatchGetterStyle", methodType(Object.class, String.class, DynamicObject.class));
348      DISPATCH_SET = lookup.findStatic(DynamicObject.class, "dispatchSetterStyle", methodType(Object.class, String.class, DynamicObject.class, Object.class));
349    } catch (NoSuchMethodException | IllegalAccessException e) {
350      e.printStackTrace();
351      throw new Error("Could not bootstrap the required method handles");
352    }
353  }
354
355  private void frozenMutationCheck() {
356    if (frozen) {
357      throw new IllegalStateException("the object is frozen");
358    }
359  }
360}