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