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}