浏览代码

Switch over dynamic method calls, loads and stores to invokedynamic.
Remove performance hack for accessing a document's fields, its not needed.
Add support for accessing is-getter methods like List.isEmpty() as .empty

Closes #18201

Robert Muir 9 年之前
父节点
当前提交
ba2fe156e8
共有 21 个文件被更改,包括 1174 次插入644 次删除
  1. 0 32
      docs/reference/modules/scripting/painless.asciidoc
  2. 1 4
      modules/lang-painless/src/main/java/org/elasticsearch/painless/AnalyzerUtility.java
  3. 2 9
      modules/lang-painless/src/main/java/org/elasticsearch/painless/Compiler.java
  4. 239 308
      modules/lang-painless/src/main/java/org/elasticsearch/painless/Def.java
  5. 88 25
      modules/lang-painless/src/main/java/org/elasticsearch/painless/Definition.java
  6. 151 0
      modules/lang-painless/src/main/java/org/elasticsearch/painless/DynamicCallSite.java
  7. 1 0
      modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessError.java
  8. 1 13
      modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessScriptEngineService.java
  9. 14 8
      modules/lang-painless/src/main/java/org/elasticsearch/painless/WriterConstants.java
  10. 27 57
      modules/lang-painless/src/main/java/org/elasticsearch/painless/WriterExternal.java
  11. 0 1
      modules/lang-painless/src/main/java/org/elasticsearch/painless/WriterStatement.java
  12. 30 0
      modules/lang-painless/src/test/java/org/elasticsearch/painless/BasicAPITests.java
  13. 1 0
      modules/lang-painless/src/test/java/org/elasticsearch/painless/BasicStatementTests.java
  14. 97 0
      modules/lang-painless/src/test/java/org/elasticsearch/painless/DynamicCallSiteTests.java
  15. 0 109
      modules/lang-painless/src/test/java/org/elasticsearch/painless/FieldTests.java
  16. 1 0
      modules/lang-painless/src/test/java/org/elasticsearch/painless/NoSemiColonTests.java
  17. 76 78
      modules/lang-painless/src/test/java/org/elasticsearch/painless/WhenThingsGoWrongTests.java
  18. 59 0
      modules/lang-painless/src/test/resources/rest-api-spec/test/plan_a/15_update.yaml
  19. 54 0
      modules/lang-painless/src/test/resources/rest-api-spec/test/plan_a/16_update2.yaml
  20. 63 0
      modules/lang-painless/src/test/resources/rest-api-spec/test/plan_a/25_script_upsert.yaml
  21. 269 0
      modules/lang-painless/src/test/resources/rest-api-spec/test/plan_a/30_search.yaml

+ 0 - 32
docs/reference/modules/scripting/painless.asciidoc

@@ -199,38 +199,6 @@ POST hockey/player/1/_update
 ----------------------------------------------------------------
 // CONSOLE
 
-[float]
-=== Writing Type-Safe Scripts to Improve Performance
-
-If you explicitly specify types, the compiler doesn't have to perform type lookups at runtime, which can significantly
-improve performance. For example, the following script performs the same first name, last name sort we showed before,
-but it's fully type-safe.
-
-[source,js]
-----------------------------------------------------------------
-GET hockey/_search
-{
-  "query": {
-    "match_all": {}
-  },
-  "script_fields": {
-    "full_name_dynamic": {
-      "script": {
-        "lang": "painless",
-        "inline": "def first = input.doc['first'].value; def last = input.doc['last'].value; return first + ' ' + last;"
-      }
-    },
-    "full_name_static": {
-      "script": {
-        "lang": "painless",
-        "inline": "String first = (String)((List)((Map)input.get('doc')).get('first')).get(0); String last = (String)((List)((Map)input.get('doc')).get('last')).get(0); return first + ' ' + last;"
-      }
-    }
-  }
-}
-----------------------------------------------------------------
-// CONSOLE
-
 [[painless-api]]
 [float]
 == Painless API

+ 1 - 4
modules/lang-painless/src/main/java/org/elasticsearch/painless/AnalyzerUtility.java

@@ -22,7 +22,6 @@ package org.elasticsearch.painless;
 import org.antlr.v4.runtime.ParserRuleContext;
 import org.antlr.v4.runtime.tree.ParseTree;
 import org.elasticsearch.painless.Definition.Type;
-import org.elasticsearch.painless.Metadata.ExtNodeMetadata;
 import org.elasticsearch.painless.PainlessParser.ExpressionContext;
 import org.elasticsearch.painless.PainlessParser.IdentifierContext;
 import org.elasticsearch.painless.PainlessParser.PrecedenceContext;
@@ -109,15 +108,13 @@ class AnalyzerUtility {
         return source;
     }
 
-    private final Metadata metadata;
     private final Definition definition;
 
     private final Deque<Integer> scopes = new ArrayDeque<>();
     private final Deque<Variable> variables = new ArrayDeque<>();
 
     AnalyzerUtility(final Metadata metadata) {
-        this.metadata = metadata;
-        definition = metadata.definition;
+        this.definition = metadata.definition;
     }
 
     void incrementScope() {

+ 2 - 9
modules/lang-painless/src/main/java/org/elasticsearch/painless/Compiler.java

@@ -42,12 +42,6 @@ final class Compiler {
      */
     static int MAXIMUM_SOURCE_LENGTH = 16384;
 
-    /**
-     * The default language API to be used with Painless.  The second construction is used
-     * to finalize all the variables, so there is no mistake of modification afterwards.
-     */
-    private static Definition DEFAULT_DEFINITION = new Definition(new Definition());
-
     /**
      * Define the class with lowest privileges.
      */
@@ -95,15 +89,14 @@ final class Compiler {
      * @param settings The CompilerSettings to be used during the compilation.
      * @return An {@link Executable} Painless script.
      */
-    static Executable compile(final Loader loader, final String name, final String source,
-                              final Definition custom, final CompilerSettings settings) {
+    static Executable compile(final Loader loader, final String name, final String source, final CompilerSettings settings) {
         if (source.length() > MAXIMUM_SOURCE_LENGTH) {
             throw new IllegalArgumentException("Scripts may be no longer than " + MAXIMUM_SOURCE_LENGTH +
                 " characters.  The passed in script is " + source.length() + " characters.  Consider using a" +
                 " plugin if a script longer than this length is a requirement.");
         }
 
-        final Definition definition = custom != null ? new Definition(custom) : DEFAULT_DEFINITION;
+        final Definition definition = Definition.INSTANCE;
         final ParserRuleContext root = createParseTree(source);
         final Metadata metadata = new Metadata(definition, source, root, settings);
         Analyzer.analyze(metadata);

+ 239 - 308
modules/lang-painless/src/main/java/org/elasticsearch/painless/Def.java

@@ -19,371 +19,302 @@
 
 package org.elasticsearch.painless;
 
-import org.elasticsearch.index.fielddata.ScriptDocValues;
-import org.elasticsearch.painless.Definition.Cast;
-import org.elasticsearch.painless.Definition.Field;
 import org.elasticsearch.painless.Definition.Method;
-import org.elasticsearch.painless.Definition.Struct;
-import org.elasticsearch.painless.Definition.Transform;
-import org.elasticsearch.painless.Definition.Type;
+import org.elasticsearch.painless.Definition.RuntimeClass;
 
 import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.MethodType;
+import java.lang.invoke.MethodHandles.Lookup;
 import java.lang.reflect.Array;
 import java.util.List;
 import java.util.Map;
 
+/**
+ * Support for dynamic type (def).
+ * <p>
+ * Dynamic types can invoke methods, load/store fields, and be passed as parameters to operators without 
+ * compile-time type information. 
+ * <p>
+ * Dynamic methods, loads, and stores involve locating the appropriate field or method depending
+ * on the receiver's class. For these, we emit an {@code invokedynamic} instruction that, for each new 
+ * type encountered will query a corresponding {@code lookupXXX} method to retrieve the appropriate method.
+ * In most cases, the {@code lookupXXX} methods here will only be called once for a given call site, because 
+ * caching ({@link DynamicCallSite}) generally works: usually all objects at any call site will be consistently 
+ * the same type (or just a few types).  In extreme cases, if there is type explosion, they may be called every 
+ * single time, but simplicity is still more valuable than performance in this code.
+ * <p>
+ * Dynamic array loads and stores and operator functions (e.g. {@code +}) are called directly
+ * with {@code invokestatic}. Because these features cannot be overloaded in painless, they are hardcoded 
+ * decision trees based on the only types that are possible. This keeps overhead low, and seems to be as fast
+ * on average as the more adaptive methodhandle caching. 
+ */
 public class Def {
-    public static Object methodCall(final Object owner, final String name, final Definition definition,
-                                    final Object[] arguments, final boolean[] typesafe) {
-        final Method method = getMethod(owner, name, definition);
-
-        if (method == null) {
-            throw new IllegalArgumentException("Unable to find dynamic method [" + name + "] " +
-                    "for class [" + owner.getClass().getCanonicalName() + "].");
-        }
 
-        final MethodHandle handle = method.handle;
-        final List<Type> types = method.arguments;
-        final Object[] parameters = new Object[arguments.length + 1];
+    /** 
+     * Looks up handle for a dynamic method call.
+     * <p>
+     * A dynamic method call for variable {@code x} of type {@code def} looks like:
+     * {@code x.method(args...)}
+     * <p>
+     * This method traverses {@code recieverClass}'s class hierarchy (including interfaces) 
+     * until it finds a matching whitelisted method. If one is not found, it throws an exception. 
+     * Otherwise it returns a handle to the matching method.
+     * <p>
+     * @param receiverClass Class of the object to invoke the method on.
+     * @param name Name of the method.
+     * @param definition Whitelist to check.
+     * @return pointer to matching method to invoke. never returns null.
+     * @throws IllegalArgumentException if no matching whitelisted method was found.
+     */
+    static MethodHandle lookupMethod(Class<?> receiverClass, String name, Definition definition) {
+        // check whitelist for matching method
+        for (Class<?> clazz = receiverClass; clazz != null; clazz = clazz.getSuperclass()) {
+            RuntimeClass struct = definition.runtimeMap.get(clazz);
 
-        parameters[0] = owner;
+            if (struct != null) {
+                Method method = struct.methods.get(name);
+                if (method != null) {
+                    return method.handle;
+                }
+            }
 
-        if (types.size() != arguments.length) {
-            throw new IllegalArgumentException("When dynamically calling [" + name + "] from class " +
-                    "[" + owner.getClass() + "] expected [" + types.size() + "] arguments," +
-                    " but found [" + arguments.length + "].");
-        }
+            for (Class<?> iface : clazz.getInterfaces()) {
+                struct = definition.runtimeMap.get(iface);
 
-        try {
-            for (int count = 0; count < arguments.length; ++count) {
-                if (typesafe[count]) {
-                    parameters[count + 1] = arguments[count];
-                } else {
-                    final Transform transform = getTransform(arguments[count].getClass(), types.get(count).clazz, definition);
-                    parameters[count + 1] = transform == null ? arguments[count] : transform.method.handle.invoke(arguments[count]);
+                if (struct != null) {
+                    Method method = struct.methods.get(name);
+                    if (method != null) {
+                        return method.handle;
+                    }
                 }
             }
-
-            return handle.invokeWithArguments(parameters);
-        } catch (Throwable throwable) {
-            throw new IllegalArgumentException("Error invoking method [" + name + "] " +
-                    "with owner class [" + owner.getClass().getCanonicalName() + "].", throwable);
         }
-    }
 
-    @SuppressWarnings({ "unchecked", "rawtypes" })
-    public static void fieldStore(final Object owner, Object value, final String name,
-                                  final Definition definition, final boolean typesafe) {
-        final Field field = getField(owner, name, definition);
-        MethodHandle handle = null;
+        // no matching methods in whitelist found
+        throw new IllegalArgumentException("Unable to find dynamic method [" + name + "] " +
+                                           "for class [" + receiverClass.getCanonicalName() + "].");
+    }
 
-        if (field == null) {
-            final String set = "set" + Character.toUpperCase(name.charAt(0)) + name.substring(1);
-            final Method method = getMethod(owner, set, definition);
+    /** pointer to Array.getLength(Object) */
+    private static final MethodHandle ARRAY_LENGTH;
+    /** pointer to Map.get(Object) */
+    private static final MethodHandle MAP_GET;
+    /** pointer to Map.put(Object,Object) */
+    private static final MethodHandle MAP_PUT;
+    /** pointer to List.get(int) */
+    private static final MethodHandle LIST_GET;
+    /** pointer to List.set(int,Object) */
+    private static final MethodHandle LIST_SET;
+    static {
+        Lookup lookup = MethodHandles.publicLookup();
+        try {
+            // TODO: maybe specialize handles for different array types. this may be slower, but simple :)
+            ARRAY_LENGTH = lookup.findStatic(Array.class, "getLength",
+                                             MethodType.methodType(int.class, Object.class));
+            MAP_GET      = lookup.findVirtual(Map.class, "get",
+                                             MethodType.methodType(Object.class, Object.class));
+            MAP_PUT      = lookup.findVirtual(Map.class, "put",
+                                             MethodType.methodType(Object.class, Object.class, Object.class));
+            LIST_GET     = lookup.findVirtual(List.class, "get",
+                                             MethodType.methodType(Object.class, int.class));
+            LIST_SET     = lookup.findVirtual(List.class, "set",
+                                             MethodType.methodType(Object.class, int.class, Object.class));
+        } catch (ReflectiveOperationException e) {
+            throw new AssertionError(e);
+        }
+    }
+    
+    /** 
+     * Looks up handle for a dynamic field getter (field load)
+     * <p>
+     * A dynamic field load for variable {@code x} of type {@code def} looks like:
+     * {@code y = x.field}
+     * <p>
+     * The following field loads are allowed:
+     * <ul>
+     *   <li>Whitelisted {@code field} from receiver's class or any superclasses.
+     *   <li>Whitelisted method named {@code getField()} from receiver's class/superclasses/interfaces.
+     *   <li>Whitelisted method named {@code isField()} from receiver's class/superclasses/interfaces.
+     *   <li>The {@code length} field of an array.
+     *   <li>The value corresponding to a map key named {@code field} when the receiver is a Map.
+     *   <li>The value in a list at element {@code field} (integer) when the receiver is a List.
+     * </ul>
+     * <p>
+     * This method traverses {@code recieverClass}'s class hierarchy (including interfaces) 
+     * until it finds a matching whitelisted getter. If one is not found, it throws an exception. 
+     * Otherwise it returns a handle to the matching getter.
+     * <p>
+     * @param receiverClass Class of the object to retrieve the field from.
+     * @param name Name of the field.
+     * @param definition Whitelist to check.
+     * @return pointer to matching field. never returns null.
+     * @throws IllegalArgumentException if no matching whitelisted field was found.
+     */
+    static MethodHandle lookupGetter(Class<?> receiverClass, String name, Definition definition) {
+        // first try whitelist
+        for (Class<?> clazz = receiverClass; clazz != null; clazz = clazz.getSuperclass()) {
+            RuntimeClass struct = definition.runtimeMap.get(clazz);
 
-            if (method != null) {
-                handle = method.handle;
+            if (struct != null) {
+                MethodHandle handle = struct.getters.get(name);
+                if (handle != null) {
+                    return handle;
+                }
             }
-        } else {
-            handle = field.setter;
-        }
 
-        if (handle != null) {
-            try {
-                if (!typesafe) {
-                    final Transform transform = getTransform(value.getClass(), handle.type().parameterType(1), definition);
+            for (final Class<?> iface : clazz.getInterfaces()) {
+                struct = definition.runtimeMap.get(iface);
 
-                    if (transform != null) {
-                        value = transform.method.handle.invoke(value);
+                if (struct != null) {
+                    MethodHandle handle = struct.getters.get(name);
+                    if (handle != null) {
+                        return handle;
                     }
                 }
-
-                handle.invoke(owner, value);
-            } catch (Throwable throwable) {
-                throw new IllegalArgumentException("Error storing value [" + value + "] " +
-                        "in field [" + name + "] with owner class [" + owner.getClass() + "].", throwable);
             }
-        } else if (owner instanceof Map) {
-            ((Map)owner).put(name, value);
-        } else if (owner instanceof List) {
+        }
+        // special case: arrays, maps, and lists
+        if (receiverClass.isArray() && "length".equals(name)) {
+            // arrays expose .length as a read-only getter
+            return ARRAY_LENGTH;
+        } else if (Map.class.isAssignableFrom(receiverClass)) {
+            // maps allow access like mymap.key
+            // wire 'key' as a parameter, its a constant in painless
+            return MethodHandles.insertArguments(MAP_GET, 1, name);
+        } else if (List.class.isAssignableFrom(receiverClass)) {
+            // lists allow access like mylist.0
+            // wire '0' (index) as a parameter, its a constant. this also avoids
+            // parsing the same integer millions of times!
             try {
-                final int index = Integer.parseInt(name);
-                ((List)owner).set(index, value);
+                int index = Integer.parseInt(name);
+                return MethodHandles.insertArguments(LIST_GET, 1, index);            
             } catch (NumberFormatException exception) {
                 throw new IllegalArgumentException( "Illegal list shortcut value [" + name + "].");
             }
-        } else {
-            throw new IllegalArgumentException("Unable to find dynamic field [" + name + "] " +
-                    "for class [" + owner.getClass().getCanonicalName() + "].");
         }
+        
+        throw new IllegalArgumentException("Unable to find dynamic field [" + name + "] " +
+                                           "for class [" + receiverClass.getCanonicalName() + "].");
     }
+    
+    /** 
+     * Looks up handle for a dynamic field setter (field store)
+     * <p>
+     * A dynamic field store for variable {@code x} of type {@code def} looks like:
+     * {@code x.field = y}
+     * <p>
+     * The following field stores are allowed:
+     * <ul>
+     *   <li>Whitelisted {@code field} from receiver's class or any superclasses.
+     *   <li>Whitelisted method named {@code setField()} from receiver's class/superclasses/interfaces.
+     *   <li>The value corresponding to a map key named {@code field} when the receiver is a Map.
+     *   <li>The value in a list at element {@code field} (integer) when the receiver is a List.
+     * </ul>
+     * <p>
+     * This method traverses {@code recieverClass}'s class hierarchy (including interfaces) 
+     * until it finds a matching whitelisted setter. If one is not found, it throws an exception. 
+     * Otherwise it returns a handle to the matching setter.
+     * <p>
+     * @param receiverClass Class of the object to retrieve the field from.
+     * @param name Name of the field.
+     * @param definition Whitelist to check.
+     * @return pointer to matching field. never returns null.
+     * @throws IllegalArgumentException if no matching whitelisted field was found.
+     */
+    static MethodHandle lookupSetter(Class<?> receiverClass, String name, Definition definition) {
+        // first try whitelist
+        for (Class<?> clazz = receiverClass; clazz != null; clazz = clazz.getSuperclass()) {
+            RuntimeClass struct = definition.runtimeMap.get(clazz);
 
-    @SuppressWarnings("rawtypes")
-    public static Object fieldLoad(final Object owner, final String name, final Definition definition) {
-        final Class<?> clazz = owner.getClass();
-        if (clazz.isArray() && "length".equals(name)) {
-            return Array.getLength(owner);
-        } else {
-            // TODO: remove this fast-path, once we speed up dynamics some more
-            if ("value".equals(name) && owner instanceof ScriptDocValues) {
-                if (clazz == ScriptDocValues.Doubles.class) {
-                    return ((ScriptDocValues.Doubles)owner).getValue();
-                } else if (clazz == ScriptDocValues.Longs.class) {
-                    return ((ScriptDocValues.Longs)owner).getValue();
-                } else if (clazz == ScriptDocValues.Strings.class) {
-                    return ((ScriptDocValues.Strings)owner).getValue();
-                } else if (clazz == ScriptDocValues.GeoPoints.class) {
-                    return ((ScriptDocValues.GeoPoints)owner).getValue();
+            if (struct != null) {
+                MethodHandle handle = struct.setters.get(name);
+                if (handle != null) {
+                    return handle;
                 }
             }
-            final Field field = getField(owner, name, definition);
-            MethodHandle handle;
 
-            if (field == null) {
-                final String get = "get" + Character.toUpperCase(name.charAt(0)) + name.substring(1);
-                final Method method = getMethod(owner, get, definition);
+            for (final Class<?> iface : clazz.getInterfaces()) {
+                struct = definition.runtimeMap.get(iface);
 
-                if (method != null) {
-                    handle = method.handle;
-                } else if (owner instanceof Map) {
-                    return ((Map)owner).get(name);
-                } else if (owner instanceof List) {
-                    try {
-                        final int index = Integer.parseInt(name);
-
-                        return ((List)owner).get(index);
-                    } catch (NumberFormatException exception) {
-                        throw new IllegalArgumentException( "Illegal list shortcut value [" + name + "].");
+                if (struct != null) {
+                    MethodHandle handle = struct.setters.get(name);
+                    if (handle != null) {
+                        return handle;
                     }
-                } else {
-                    throw new IllegalArgumentException("Unable to find dynamic field [" + name + "] " +
-                            "for class [" + clazz.getCanonicalName() + "].");
                 }
-            } else {
-                handle = field.getter;
             }
-
-            if (handle == null) {
-                throw new IllegalArgumentException(
-                        "Unable to read from field [" + name + "] with owner class [" + clazz + "].");
-            } else {
-                try {
-                    return handle.invoke(owner);
-                } catch (final Throwable throwable) {
-                    throw new IllegalArgumentException("Error loading value from " +
-                            "field [" + name + "] with owner class [" + clazz + "].", throwable);
-                }
+        }
+        // special case: maps, and lists
+        if (Map.class.isAssignableFrom(receiverClass)) {
+            // maps allow access like mymap.key
+            // wire 'key' as a parameter, its a constant in painless
+            return MethodHandles.insertArguments(MAP_PUT, 1, name);
+        } else if (List.class.isAssignableFrom(receiverClass)) {
+            // lists allow access like mylist.0
+            // wire '0' (index) as a parameter, its a constant. this also avoids
+            // parsing the same integer millions of times!
+            try {
+                int index = Integer.parseInt(name);
+                return MethodHandles.insertArguments(LIST_SET, 1, index);            
+            } catch (NumberFormatException exception) {
+                throw new IllegalArgumentException( "Illegal list shortcut value [" + name + "].");
             }
         }
+        
+        throw new IllegalArgumentException("Unable to find dynamic field [" + name + "] " +
+                                           "for class [" + receiverClass.getCanonicalName() + "].");
     }
 
+    // NOTE: below methods are not cached, instead invoked directly because they are performant.
+
+    /**
+     * Performs an actual array store.
+     * @param array array object
+     * @param index map key, array index (integer), or list index (integer)
+     * @param value value to store in the array.
+     */
     @SuppressWarnings({ "unchecked", "rawtypes" })
-    public static void arrayStore(final Object array, Object index, Object value, final Definition definition,
-                                  final boolean indexsafe, final boolean valuesafe) {
+    public static void arrayStore(final Object array, Object index, Object value) {
         if (array instanceof Map) {
             ((Map)array).put(index, value);
-        } else {
+        } else if (array.getClass().isArray()) {
             try {
-                if (!indexsafe) {
-                    final Transform transform = getTransform(index.getClass(), Integer.class, definition);
-
-                    if (transform != null) {
-                        index = transform.method.handle.invoke(index);
-                    }
-                }
+                Array.set(array, (int)index, value);
             } catch (final Throwable throwable) {
-                throw new IllegalArgumentException(
-                        "Error storing value [" + value + "] in list using index [" + index + "].", throwable);
-            }
-
-            if (array.getClass().isArray()) {
-                try {
-                    if (!valuesafe) {
-                        final Transform transform = getTransform(value.getClass(), array.getClass().getComponentType(), definition);
-
-                        if (transform != null) {
-                            value = transform.method.handle.invoke(value);
-                        }
-                    }
-
-                    Array.set(array, (int)index, value);
-                } catch (final Throwable throwable) {
-                    throw new IllegalArgumentException("Error storing value [" + value + "] " +
-                            "in array class [" + array.getClass().getCanonicalName() + "].", throwable);
-                }
-            } else if (array instanceof List) {
-                ((List)array).set((int)index, value);
-            } else {
-                throw new IllegalArgumentException("Attempting to address a non-array type " +
-                        "[" + array.getClass().getCanonicalName() + "] as an array.");
+                throw new IllegalArgumentException("Error storing value [" + value + "] " +
+                                                   "in array class [" + array.getClass().getCanonicalName() + "].", throwable);
             }
+        } else if (array instanceof List) {
+            ((List)array).set((int)index, value);
+        } else {
+            throw new IllegalArgumentException("Attempting to address a non-array type " +
+                                               "[" + array.getClass().getCanonicalName() + "] as an array.");
         }
     }
-
+    
+    /**
+     * Performs an actual array load.
+     * @param array array object
+     * @param index map key, array index (integer), or list index (integer)
+     */
     @SuppressWarnings("rawtypes")
-    public static Object arrayLoad(final Object array, Object index,
-                                   final Definition definition, final boolean indexsafe) {
+    public static Object arrayLoad(final Object array, Object index) {
         if (array instanceof Map) {
             return ((Map)array).get(index);
-        } else {
+        } else if (array.getClass().isArray()) {
             try {
-                if (!indexsafe) {
-                    final Transform transform = getTransform(index.getClass(), Integer.class, definition);
-
-                    if (transform != null) {
-                        index = transform.method.handle.invoke(index);
-                    }
-                }
+                return Array.get(array, (int)index);
             } catch (final Throwable throwable) {
-                throw new IllegalArgumentException(
-                        "Error loading value using index [" + index + "].", throwable);
-            }
-
-            if (array.getClass().isArray()) {
-                try {
-                    return Array.get(array, (int)index);
-                } catch (final Throwable throwable) {
-                    throw new IllegalArgumentException("Error loading value from " +
-                            "array class [" + array.getClass().getCanonicalName() + "].", throwable);
-                }
-            } else if (array instanceof List) {
-                return ((List)array).get((int)index);
-            } else {
-                throw new IllegalArgumentException("Attempting to address a non-array type " +
-                        "[" + array.getClass().getCanonicalName() + "] as an array.");
-            }
-        }
-    }
-
-    /** Method lookup for owner.name(), returns null if no matching method was found */ 
-    private static Method getMethod(final Object owner, final String name, final Definition definition) {
-        Class<?> clazz = owner.getClass();
-
-        while (clazz != null) {
-            Struct struct = definition.classes.get(clazz);
-
-            if (struct != null) {
-                Method method = struct.methods.get(name);
-
-                if (method != null) {
-                    return method;
-                }
-            }
-
-            for (final Class<?> iface : clazz.getInterfaces()) {
-                struct = definition.classes.get(iface);
-
-                if (struct != null) {
-                    Method method = struct.methods.get(name);
-
-                    if (method != null) {
-                        return method;
-                    }
-                }
-            }
-
-            clazz = clazz.getSuperclass();
-        }
-
-        return null;
-    }
-
-    /** Field lookup for owner.name, returns null if no matching field was found */ 
-    private static Field getField(final Object owner, final String name, final Definition definition) {
-        Class<?> clazz = owner.getClass();
-
-        while (clazz != null) {
-            Struct struct = definition.classes.get(clazz);
-
-            if (struct != null) {
-                Field field = struct.members.get(name);
-
-                if (field != null) {
-                    return field;
-                }
-            }
-
-            for (final Class<?> iface : clazz.getInterfaces()) {
-                struct = definition.classes.get(iface);
-
-                if (struct != null) {
-                    Field field = struct.members.get(name);
-
-                    if (field != null) {
-                        return field;
-                    }
-                }
+                throw new IllegalArgumentException("Error loading value from " +
+                                                   "array class [" + array.getClass().getCanonicalName() + "].", throwable);
             }
-
-            clazz = clazz.getSuperclass();
-        }
-
-        return null;
-    }
-
-    public static Transform getTransform(Class<?> fromClass, Class<?> toClass, final Definition definition) {
-        Struct fromStruct = null;
-        Struct toStruct = null;
-
-        if (fromClass.equals(toClass)) {
-            return null;
-        }
-
-        while (fromClass != null) {
-            fromStruct = definition.classes.get(fromClass);
-
-            if (fromStruct != null) {
-                break;
-            }
-
-            for (final Class<?> iface : fromClass.getInterfaces()) {
-                fromStruct = definition.classes.get(iface);
-
-                if (fromStruct != null) {
-                    break;
-                }
-            }
-
-            if (fromStruct != null) {
-                break;
-            }
-
-            fromClass = fromClass.getSuperclass();
-        }
-
-        if (fromStruct != null) {
-            while (toClass != null) {
-                toStruct = definition.classes.get(toClass);
-
-                if (toStruct != null) {
-                    break;
-                }
-
-                for (final Class<?> iface : toClass.getInterfaces()) {
-                    toStruct = definition.classes.get(iface);
-
-                    if (toStruct != null) {
-                        break;
-                    }
-                }
-
-                if (toStruct != null) {
-                    break;
-                }
-
-                toClass = toClass.getSuperclass();
-            }
-        }
-
-        if (toStruct != null) {
-            final Type fromType = definition.getType(fromStruct.name);
-            final Type toType = definition.getType(toStruct.name);
-            final Cast cast = new Cast(fromType, toType);
-
-            return definition.transforms.get(cast);
+        } else if (array instanceof List) {
+            return ((List)array).get((int)index);
+        } else {
+            throw new IllegalArgumentException("Attempting to address a non-array type " +
+                                               "[" + array.getClass().getCanonicalName() + "] as an array.");
         }
-
-        return null;
     }
 
     public static Object not(final Object unary) {

+ 88 - 25
modules/lang-painless/src/main/java/org/elasticsearch/painless/Definition.java

@@ -21,7 +21,6 @@ package org.elasticsearch.painless;
 
 import java.lang.invoke.MethodHandle;
 import java.lang.invoke.MethodHandles;
-import java.lang.invoke.MethodType;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -37,6 +36,12 @@ import org.elasticsearch.common.geo.GeoPoint;
 import org.elasticsearch.index.fielddata.ScriptDocValues;
 
 class Definition {
+    /**
+     * The default language API to be used with Painless.  The second construction is used
+     * to finalize all the variables, so there is no mistake of modification afterwards.
+     */
+    static Definition INSTANCE = new Definition(new Definition());
+
     enum Sort {
         VOID(       void.class      , 0 , true  , false , false , false ),
         BOOL(       boolean.class   , 1 , true  , true  , false , true  ),
@@ -323,11 +328,24 @@ class Definition {
             this.downcast = downcast;
         }
     }
+    
+    static class RuntimeClass {
+        final Map<String, Method> methods;
+        final Map<String, MethodHandle> getters;
+        final Map<String, MethodHandle> setters;
+        
+        private RuntimeClass(Map<String, Method> methods, Map<String, MethodHandle> getters, Map<String, MethodHandle> setters) {
+            this.methods = methods;
+            this.getters = getters;
+            this.setters = setters;
+        }
+    }
 
     final Map<String, Struct> structs;
     final Map<Class<?>, Struct> classes;
     final Map<Cast, Transform> transforms;
     final Map<Pair, Type> bounds;
+    final Map<Class<?>, RuntimeClass> runtimeMap; 
 
     final Type voidType;
     final Type booleanType;
@@ -405,11 +423,12 @@ class Definition {
     final Type doublesType;
     final Type geoPointsType;
 
-    public Definition() {
+    private Definition() {
         structs = new HashMap<>();
         classes = new HashMap<>();
         transforms = new HashMap<>();
         bounds = new HashMap<>();
+        runtimeMap = new HashMap<>();
 
         addDefaultStructs();
         addDefaultClasses();
@@ -492,9 +511,64 @@ class Definition {
         copyDefaultStructs();
         addDefaultTransforms();
         addDefaultBounds();
+        computeRuntimeClasses();
+    }
+    
+    // precompute a more efficient structure for dynamic method/field access:
+    void computeRuntimeClasses() {
+        this.runtimeMap.clear();
+        for (Class<?> clazz : classes.keySet()) {
+            runtimeMap.put(clazz, computeRuntimeClass(clazz));
+        }
+    }
+    
+    RuntimeClass computeRuntimeClass(Class<?> clazz) {
+        Struct struct = classes.get(clazz);
+        Map<String, Method> methods = struct.methods;
+        Map<String, MethodHandle> getters = new HashMap<>();
+        Map<String, MethodHandle> setters = new HashMap<>();
+        // add all members
+        for (Map.Entry<String,Field> member : struct.members.entrySet()) {
+            getters.put(member.getKey(), member.getValue().getter);
+            setters.put(member.getKey(), member.getValue().setter);
+        }
+        // add all getters/setters
+        for (Map.Entry<String,Method> method : methods.entrySet()) {
+            String name = method.getKey();
+            Method m = method.getValue();
+            
+            if (m.arguments.size() == 0 &&
+                name.startsWith("get") &&
+                name.length() > 3 &&
+                Character.isUpperCase(name.charAt(3))) {
+              StringBuilder newName = new StringBuilder();
+              newName.append(Character.toLowerCase(name.charAt(3)));
+              newName.append(name.substring(4));
+              getters.putIfAbsent(newName.toString(), m.handle);
+            } else if (m.arguments.size() == 0 &&
+                       name.startsWith("is") &&
+                       name.length() > 2 && 
+                       Character.isUpperCase(name.charAt(2))) {
+              StringBuilder newName = new StringBuilder();
+              newName.append(Character.toLowerCase(name.charAt(2)));
+              newName.append(name.substring(3));
+              getters.putIfAbsent(newName.toString(), m.handle);
+            }
+            
+            if (m.arguments.size() == 1 &&
+                name.startsWith("set") &&
+                name.length() > 3 &&
+                Character.isUpperCase(name.charAt(3))) {
+              StringBuilder newName = new StringBuilder();
+              newName.append(Character.toLowerCase(name.charAt(3)));
+              newName.append(name.substring(4));
+              setters.putIfAbsent(newName.toString(), m.handle);
+            }
+        }
+        return new RuntimeClass(methods, getters, setters);
     }
 
-    Definition(final Definition definition) {
+    private Definition(final Definition definition) {
         final Map<String, Struct> structs = new HashMap<>();
 
         for (final Struct struct : definition.structs.values()) {
@@ -513,6 +587,7 @@ class Definition {
 
         transforms = Collections.unmodifiableMap(definition.transforms);
         bounds = Collections.unmodifiableMap(definition.bounds);
+        this.runtimeMap = Collections.unmodifiableMap(definition.runtimeMap);
 
         voidType = definition.voidType;
         booleanType = definition.booleanType;
@@ -1815,14 +1890,8 @@ class Definition {
         MethodHandle handle;
 
         try {
-            if (statik) {
-                handle = MethodHandles.publicLookup().in(owner.clazz).findStatic(
-                    owner.clazz, alias == null ? name : alias, MethodType.methodType(rtn.clazz, classes));
-            } else {
-                handle = MethodHandles.publicLookup().in(owner.clazz).findVirtual(
-                    owner.clazz, alias == null ? name : alias, MethodType.methodType(rtn.clazz, classes));
-            }
-        } catch (NoSuchMethodException | IllegalAccessException exception) {
+            handle = MethodHandles.publicLookup().in(owner.clazz).unreflect(reflect);
+        } catch (IllegalAccessException exception) {
             throw new IllegalArgumentException("Method [" + (alias == null ? name : alias) + "]" +
                 " not found for class [" + owner.clazz.getName() + "]" +
                 " with arguments " + Arrays.toString(classes) + ".");
@@ -1907,12 +1976,10 @@ class Definition {
 
         try {
             if (!statik) {
-                getter = MethodHandles.publicLookup().in(owner.clazz).findGetter(
-                    owner.clazz, alias == null ? name : alias, type.clazz);
-                setter = MethodHandles.publicLookup().in(owner.clazz).findSetter(
-                    owner.clazz, alias == null ? name : alias, type.clazz);
+                getter = MethodHandles.publicLookup().unreflectGetter(reflect);
+                setter = MethodHandles.publicLookup().unreflectSetter(reflect);
             }
-        } catch (NoSuchFieldException | IllegalAccessException exception) {
+        } catch (IllegalAccessException exception) {
             throw new IllegalArgumentException("Getter/Setter [" + (alias == null ? name : alias) + "]" +
                 " not found for class [" + owner.clazz.getName() + "].");
         }
@@ -1982,10 +2049,8 @@ class Definition {
                     }
 
                     try {
-                        handle = MethodHandles.publicLookup().in(owner.clazz).findVirtual(
-                            owner.clazz, method.method.getName(),
-                            MethodType.methodType(method.reflect.getReturnType(), method.reflect.getParameterTypes()));
-                    } catch (NoSuchMethodException | IllegalAccessException exception) {
+                        handle = MethodHandles.publicLookup().in(owner.clazz).unreflect(reflect);
+                    } catch (IllegalAccessException exception) {
                         throw new IllegalArgumentException("Method [" + method.method.getName() + "] not found for" +
                             " class [" + owner.clazz.getName() + "] with arguments " +
                             Arrays.toString(method.reflect.getParameterTypes()) + ".");
@@ -2010,11 +2075,9 @@ class Definition {
                     }
 
                     try {
-                        getter = MethodHandles.publicLookup().in(owner.clazz).findGetter(
-                            owner.clazz, field.name, field.type.clazz);
-                        setter = MethodHandles.publicLookup().in(owner.clazz).findSetter(
-                            owner.clazz, field.name, field.type.clazz);
-                    } catch (NoSuchFieldException | IllegalAccessException exception) {
+                        getter = MethodHandles.publicLookup().unreflectGetter(reflect);
+                        setter = MethodHandles.publicLookup().unreflectSetter(reflect);
+                    } catch (IllegalAccessException exception) {
                         throw new IllegalArgumentException("Getter/Setter [" + field.name + "]" +
                             " not found for class [" + owner.clazz.getName() + "].");
                     }

+ 151 - 0
modules/lang-painless/src/main/java/org/elasticsearch/painless/DynamicCallSite.java

@@ -0,0 +1,151 @@
+package org.elasticsearch.painless;
+
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import java.lang.invoke.CallSite;
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.MethodHandles.Lookup;
+import java.lang.invoke.MethodType;
+import java.lang.invoke.MutableCallSite;
+
+/**
+ * Painless invokedynamic call site.
+ * <p>
+ * Has 3 flavors (passed as static bootstrap parameters): dynamic method call,
+ * dynamic field load (getter), and dynamic field store (setter).
+ * <p>
+ * When a new type is encountered at the call site, we lookup from the appropriate
+ * whitelist, and cache with a guard. If we encounter too many types, we stop caching.
+ * <p>
+ * Based on the cascaded inlining cache from the JSR 292 cookbook 
+ * (https://code.google.com/archive/p/jsr292-cookbook/, BSD license)
+ */
+// NOTE: this class must be public, because generated painless classes are in a different package,
+// and it needs to be accessible by that code.
+public final class DynamicCallSite {
+    // NOTE: these must be primitive types, see https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html#jvms-6.5.invokedynamic
+    /** static bootstrap parameter indicating a dynamic method call, e.g. foo.bar(...) */
+    static final int METHOD_CALL = 0;
+    /** static bootstrap parameter indicating a dynamic load (getter), e.g. baz = foo.bar */
+    static final int LOAD = 1;
+    /** static bootstrap parameter indicating a dynamic store (setter), e.g. foo.bar = baz */
+    static final int STORE = 2;
+    
+    static class InliningCacheCallSite extends MutableCallSite {
+        /** maximum number of types before we go megamorphic */
+        static final int MAX_DEPTH = 5;
+        
+        final Lookup lookup;
+        final String name;
+        final int flavor;
+        int depth;
+        
+        InliningCacheCallSite(Lookup lookup, String name, MethodType type, int flavor) {
+            super(type);
+            this.lookup = lookup;
+            this.name = name;
+            this.flavor = flavor;
+        }
+    }
+    
+    /** 
+     * invokeDynamic bootstrap method
+     * <p>
+     * In addition to ordinary parameters, we also take a static parameter {@code flavor} which
+     * tells us what type of dynamic call it is (and which part of whitelist to look at).
+     * <p>
+     * see https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html#jvms-6.5.invokedynamic
+     */
+    public static CallSite bootstrap(Lookup lookup, String name, MethodType type, int flavor) {
+        InliningCacheCallSite callSite = new InliningCacheCallSite(lookup, name, type, flavor);
+        
+        MethodHandle fallback = FALLBACK.bindTo(callSite);
+        fallback = fallback.asCollector(Object[].class, type.parameterCount());
+        fallback = fallback.asType(type);
+        
+        callSite.setTarget(fallback);
+        return callSite;
+    }
+    
+    /** 
+     * guard method for inline caching: checks the receiver's class is the same
+     * as the cached class
+     */
+    static boolean checkClass(Class<?> clazz, Object receiver) {
+        return receiver.getClass() == clazz;
+    }
+    
+    /**
+     * Does a slow lookup against the whitelist.
+     */
+    private static MethodHandle lookup(int flavor, Class<?> clazz, String name) {
+        switch(flavor) {
+            case METHOD_CALL: 
+                return Def.lookupMethod(clazz, name, Definition.INSTANCE);
+            case LOAD: 
+                return Def.lookupGetter(clazz, name, Definition.INSTANCE);
+            case STORE:
+                return Def.lookupSetter(clazz, name, Definition.INSTANCE);
+            default: throw new AssertionError();
+        }
+    }
+    
+    /**
+     * Called when a new type is encountered (or, when we have encountered more than {@code MAX_DEPTH}
+     * types at this call site and given up on caching). 
+     */
+    static Object fallback(InliningCacheCallSite callSite, Object[] args) throws Throwable {
+        MethodType type = callSite.type();
+        Object receiver = args[0];
+        Class<?> receiverClass = receiver.getClass();
+        MethodHandle target = lookup(callSite.flavor, receiverClass, callSite.name);
+        target = target.asType(type);
+        
+        if (callSite.depth >= InliningCacheCallSite.MAX_DEPTH) {
+            // revert to a vtable call
+            callSite.setTarget(target);
+            return target.invokeWithArguments(args);
+        }
+        
+        MethodHandle test = CHECK_CLASS.bindTo(receiverClass);
+        test = test.asType(test.type().changeParameterType(0, type.parameterType(0)));
+        
+        MethodHandle guard = MethodHandles.guardWithTest(test, target, callSite.getTarget());
+        callSite.depth++;
+        
+        callSite.setTarget(guard);
+        return target.invokeWithArguments(args);
+    }
+    
+    private static final MethodHandle CHECK_CLASS;
+    private static final MethodHandle FALLBACK;
+    static {
+        Lookup lookup = MethodHandles.lookup();
+        try {
+            CHECK_CLASS = lookup.findStatic(DynamicCallSite.class, "checkClass",
+                                            MethodType.methodType(boolean.class, Class.class, Object.class));
+            FALLBACK = lookup.findStatic(DynamicCallSite.class, "fallback",
+                                         MethodType.methodType(Object.class, InliningCacheCallSite.class, Object[].class));
+        } catch (ReflectiveOperationException e) {
+            throw new AssertionError(e);
+        }
+    }
+}

+ 1 - 0
modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessError.java

@@ -26,6 +26,7 @@ package org.elasticsearch.painless;
  * something hazardous.  The alternative was extending {@link Throwable}, but that seemed worse than using
  * an {@link Error} in this case.
  */
+@SuppressWarnings("serial")
 public class PainlessError extends Error {
     /**
      * Constructor.

+ 1 - 13
modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessScriptEngineService.java

@@ -88,18 +88,6 @@ public class PainlessScriptEngineService extends AbstractComponent implements Sc
         });
     }
 
-    /**
-     * Used only for testing.
-     */
-    private Definition definition = null;
-
-    /**
-     * Used only for testing.
-     */
-    void setDefinition(final Definition definition) {
-        this.definition = definition;
-    }
-
     /**
      * Constructor.
      * @param settings The settings to initialize the engine with.
@@ -189,7 +177,7 @@ public class PainlessScriptEngineService extends AbstractComponent implements Sc
         return AccessController.doPrivileged(new PrivilegedAction<Executable>() {
             @Override
             public Executable run() {
-                return Compiler.compile(loader, "unknown", script, definition, compilerSettings);
+                return Compiler.compile(loader, "unknown", script, compilerSettings);
             }
         }, COMPILATION_CONTEXT);
     }

+ 14 - 8
modules/lang-painless/src/main/java/org/elasticsearch/painless/WriterConstants.java

@@ -20,9 +20,13 @@
 package org.elasticsearch.painless;
 
 import org.elasticsearch.script.ScoreAccessor;
+import org.objectweb.asm.Handle;
+import org.objectweb.asm.Opcodes;
 import org.objectweb.asm.Type;
 import org.objectweb.asm.commons.Method;
 
+import java.lang.invoke.CallSite;
+import java.lang.invoke.MethodHandles;
 import java.lang.invoke.MethodType;
 import java.util.Map;
 
@@ -40,22 +44,24 @@ class WriterConstants {
 
     final static Type DEFINITION_TYPE = Type.getType(Definition.class);
 
+    final static Type OBJECT_TYPE = Type.getType(Object.class);
+
     final static Type MAP_TYPE  = Type.getType(Map.class);
     final static Method MAP_GET = getAsmMethod(Object.class, "get", Object.class);
 
     final static Type SCORE_ACCESSOR_TYPE    = Type.getType(ScoreAccessor.class);
     final static Method SCORE_ACCESSOR_FLOAT = getAsmMethod(float.class, "floatValue");
 
-    final static Method DEF_METHOD_CALL = getAsmMethod(
-        Object.class, "methodCall", Object.class, String.class, Definition.class, Object[].class, boolean[].class);
+    /** dynamic callsite bootstrap signature */
+    final static MethodType DEF_BOOTSTRAP_TYPE = MethodType.methodType(CallSite.class, MethodHandles.Lookup.class, 
+                                                                       String.class, MethodType.class, int.class);
+    final static Handle DEF_BOOTSTRAP_HANDLE = new Handle(Opcodes.H_INVOKESTATIC, Type.getInternalName(DynamicCallSite.class),
+                                                          "bootstrap", WriterConstants.DEF_BOOTSTRAP_TYPE.toMethodDescriptorString());
+
     final static Method DEF_ARRAY_STORE = getAsmMethod(
-        void.class, "arrayStore", Object.class, Object.class, Object.class, Definition.class, boolean.class, boolean.class);
+        void.class, "arrayStore", Object.class, Object.class, Object.class);
     final static Method DEF_ARRAY_LOAD = getAsmMethod(
-        Object.class, "arrayLoad", Object.class, Object.class, Definition.class, boolean.class);
-    final static Method DEF_FIELD_STORE = getAsmMethod(
-        void.class, "fieldStore", Object.class, Object.class, String.class, Definition.class, boolean.class);
-    final static Method DEF_FIELD_LOAD = getAsmMethod(
-        Object.class, "fieldLoad", Object.class, String.class, Definition.class);
+        Object.class, "arrayLoad", Object.class, Object.class);
 
     final static Method DEF_NOT_CALL = getAsmMethod(Object.class, "not", Object.class);
     final static Method DEF_NEG_CALL = getAsmMethod(Object.class, "neg", Object.class);

+ 27 - 57
modules/lang-painless/src/main/java/org/elasticsearch/painless/WriterExternal.java

@@ -49,13 +49,8 @@ import static org.elasticsearch.painless.PainlessParser.DIV;
 import static org.elasticsearch.painless.PainlessParser.MUL;
 import static org.elasticsearch.painless.PainlessParser.REM;
 import static org.elasticsearch.painless.PainlessParser.SUB;
-import static org.elasticsearch.painless.WriterConstants.CLASS_TYPE;
-import static org.elasticsearch.painless.WriterConstants.DEFINITION_TYPE;
 import static org.elasticsearch.painless.WriterConstants.DEF_ARRAY_LOAD;
 import static org.elasticsearch.painless.WriterConstants.DEF_ARRAY_STORE;
-import static org.elasticsearch.painless.WriterConstants.DEF_FIELD_LOAD;
-import static org.elasticsearch.painless.WriterConstants.DEF_FIELD_STORE;
-import static org.elasticsearch.painless.WriterConstants.DEF_METHOD_CALL;
 import static org.elasticsearch.painless.WriterConstants.TOBYTEEXACT_INT;
 import static org.elasticsearch.painless.WriterConstants.TOBYTEEXACT_LONG;
 import static org.elasticsearch.painless.WriterConstants.TOBYTEWOOVERFLOW_DOUBLE;
@@ -473,20 +468,11 @@ class WriterExternal {
 
     private void writeLoadStoreField(final ParserRuleContext source, final boolean store, final String name) {
         if (store) {
-            final ExtNodeMetadata sourceemd = metadata.getExtNodeMetadata(source);
-            final ExternalMetadata parentemd = metadata.getExternalMetadata(sourceemd.parent);
-            final ExpressionMetadata expremd = metadata.getExpressionMetadata(parentemd.storeExpr);
-
-            execute.push(name);
-            execute.loadThis();
-            execute.getField(CLASS_TYPE, "definition", DEFINITION_TYPE);
-            execute.push(parentemd.token == 0 && expremd.typesafe);
-            execute.invokeStatic(definition.defobjType.type, DEF_FIELD_STORE);
+            execute.visitInvokeDynamicInsn(name, "(Ljava/lang/Object;Ljava/lang/Object;)V", 
+                                           WriterConstants.DEF_BOOTSTRAP_HANDLE, new Object[] { DynamicCallSite.STORE });
         } else {
-            execute.push(name);
-            execute.loadThis();
-            execute.getField(CLASS_TYPE, "definition", DEFINITION_TYPE);
-            execute.invokeStatic(definition.defobjType.type, DEF_FIELD_LOAD);
+            execute.visitInvokeDynamicInsn(name, "(Ljava/lang/Object;)Ljava/lang/Object;", 
+                                           WriterConstants.DEF_BOOTSTRAP_HANDLE, new Object[] { DynamicCallSite.LOAD });
         }
     }
 
@@ -496,23 +482,9 @@ class WriterExternal {
         }
 
         if (type.sort == Sort.DEF) {
-            final ExtbraceContext bracectx = (ExtbraceContext)source;
-            final ExpressionMetadata expremd0 = metadata.getExpressionMetadata(bracectx.expression());
-
             if (store) {
-                final ExtNodeMetadata braceenmd = metadata.getExtNodeMetadata(bracectx);
-                final ExternalMetadata parentemd = metadata.getExternalMetadata(braceenmd.parent);
-                final ExpressionMetadata expremd1 = metadata.getExpressionMetadata(parentemd.storeExpr);
-
-                execute.loadThis();
-                execute.getField(CLASS_TYPE, "definition", DEFINITION_TYPE);
-                execute.push(expremd0.typesafe);
-                execute.push(parentemd.token == 0 && expremd1.typesafe);
                 execute.invokeStatic(definition.defobjType.type, DEF_ARRAY_STORE);
             } else {
-                execute.loadThis();
-                execute.getField(CLASS_TYPE, "definition", DEFINITION_TYPE);
-                execute.push(expremd0.typesafe);
                 execute.invokeStatic(definition.defobjType.type, DEF_ARRAY_LOAD);
             }
         } else {
@@ -729,31 +701,29 @@ class WriterExternal {
                 execute.checkCast(target.rtn.type);
             }
         } else {
-            execute.push((String)sourceenmd.target);
-            execute.loadThis();
-            execute.getField(CLASS_TYPE, "definition", DEFINITION_TYPE);
-
-            execute.push(arguments.size());
-            execute.newArray(definition.defType.type);
-
-            for (int argument = 0; argument < arguments.size(); ++argument) {
-                execute.dup();
-                execute.push(argument);
-                writer.visit(arguments.get(argument));
-                execute.arrayStore(definition.defType.type);
-            }
-
-            execute.push(arguments.size());
-            execute.newArray(definition.booleanType.type);
-
-            for (int argument = 0; argument < arguments.size(); ++argument) {
-                execute.dup();
-                execute.push(argument);
-                execute.push(metadata.getExpressionMetadata(arguments.get(argument)).typesafe);
-                execute.arrayStore(definition.booleanType.type);
-            }
-
-            execute.invokeStatic(definition.defobjType.type, DEF_METHOD_CALL);
+            writeDynamicCallExternal(source);
+        }
+    }
+    
+    private void writeDynamicCallExternal(final ExtcallContext source) {
+        final ExtNodeMetadata sourceenmd = metadata.getExtNodeMetadata(source);
+        final List<ExpressionContext> arguments = source.arguments().expression();
+        
+        StringBuilder signature = new StringBuilder();
+        signature.append('(');
+        // first parameter is the receiver, we never know its type: always Object
+        signature.append(WriterConstants.OBJECT_TYPE.getDescriptor());
+
+        // TODO: remove our explicit conversions and feed more type information for args/return value,
+        // it can avoid some unnecessary boxing etc.
+        for (int i = 0; i < arguments.size(); i++) {
+            signature.append(WriterConstants.OBJECT_TYPE.getDescriptor());
+            writer.visit(arguments.get(i));
         }
+        signature.append(')');
+        // return value
+        signature.append(WriterConstants.OBJECT_TYPE.getDescriptor());
+        execute.visitInvokeDynamicInsn((String)sourceenmd.target, signature.toString(), 
+                                       WriterConstants.DEF_BOOTSTRAP_HANDLE, new Object[] { DynamicCallSite.METHOD_CALL });
     }
 }

+ 0 - 1
modules/lang-painless/src/main/java/org/elasticsearch/painless/WriterStatement.java

@@ -26,7 +26,6 @@ import org.elasticsearch.painless.PainlessParser.AfterthoughtContext;
 import org.elasticsearch.painless.PainlessParser.BlockContext;
 import org.elasticsearch.painless.PainlessParser.DeclContext;
 import org.elasticsearch.painless.PainlessParser.DeclarationContext;
-import org.elasticsearch.painless.PainlessParser.DecltypeContext;
 import org.elasticsearch.painless.PainlessParser.DeclvarContext;
 import org.elasticsearch.painless.PainlessParser.DoContext;
 import org.elasticsearch.painless.PainlessParser.EmptyscopeContext;

+ 30 - 0
modules/lang-painless/src/test/java/org/elasticsearch/painless/BasicAPITests.java

@@ -49,4 +49,34 @@ public class BasicAPITests extends ScriptTestCase {
         assertEquals(3, exec("Map x = new HashMap(); x.put(2, 2); x.put(3, 3); x.put(-2, -2); Iterator y = x.values().iterator(); " +
             "int total = 0; while (y.hasNext()) total += (int)y.next(); return total;"));
     }
+    
+    /** Test loads and stores with a map */
+    public void testMapLoadStore() {
+        assertEquals(5, exec("def x = new HashMap(); x.abc = 5; return x.abc;"));
+        assertEquals(5, exec("def x = new HashMap(); x['abc'] = 5; return x['abc'];"));
+    }
+    
+    /** Test loads and stores with a list */
+    public void testListLoadStore() {
+        assertEquals(5, exec("def x = new ArrayList(); x.add(3); x.0 = 5; return x.0;"));
+        assertEquals(5, exec("def x = new ArrayList(); x.add(3); x[0] = 5; return x[0];"));
+    }
+    
+    /** Test loads and stores with a list */
+    public void testArrayLoadStore() {
+        assertEquals(5, exec("def x = new int[5]; return x.length"));
+        assertEquals(5, exec("def x = new int[4]; x[0] = 5; return x[0];"));
+    }
+    
+    /** Test shortcut for getters with isXXXX */
+    public void testListEmpty() {
+        assertEquals(true, exec("def x = new ArrayList(); return x.empty;"));
+        assertEquals(true, exec("def x = new HashMap(); return x.empty;"));
+    }
+    
+    /** Test list method invocation */
+    public void testListGet() {
+        assertEquals(5, exec("def x = new ArrayList(); x.add(5); return x.get(0);"));
+        assertEquals(5, exec("def x = new ArrayList(); x.add(5); def index = 0; return x.get(index);"));
+    }
 }

+ 1 - 0
modules/lang-painless/src/test/java/org/elasticsearch/painless/BasicStatementTests.java

@@ -168,6 +168,7 @@ public class BasicStatementTests extends ScriptTestCase {
         assertEquals(4, exec("int x = 0, y = 0; while (x < 10) { ++x; if (x == 5) break; ++y; } return y;"));
     }
 
+    @SuppressWarnings("rawtypes")
     public void testReturnStatement() {
         assertEquals(10, exec("return 10;"));
         assertEquals(5, exec("int x = 5; return x;"));

+ 97 - 0
modules/lang-painless/src/test/java/org/elasticsearch/painless/DynamicCallSiteTests.java

@@ -0,0 +1,97 @@
+package org.elasticsearch.painless;
+
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import java.lang.invoke.CallSite;
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.MethodType;
+
+import org.elasticsearch.test.ESTestCase;
+
+public class DynamicCallSiteTests extends ESTestCase {
+    
+    /** calls toString() on integers, twice */
+    public void testOneType() throws Throwable {
+        CallSite site = DynamicCallSite.bootstrap(MethodHandles.publicLookup(), 
+                                                  "toString", 
+                                                  MethodType.methodType(String.class, Object.class), 
+                                                  DynamicCallSite.METHOD_CALL);
+        MethodHandle handle = site.dynamicInvoker();
+        assertDepthEquals(site, 0);
+
+        // invoke with integer, needs lookup
+        assertEquals("5", handle.invoke(Integer.valueOf(5)));
+        assertDepthEquals(site, 1);
+
+        // invoked with integer again: should be cached
+        assertEquals("6", handle.invoke(Integer.valueOf(6)));
+        assertDepthEquals(site, 1);
+    }
+    
+    public void testTwoTypes() throws Throwable {
+        CallSite site = DynamicCallSite.bootstrap(MethodHandles.publicLookup(), 
+                                                  "toString", 
+                                                  MethodType.methodType(String.class, Object.class), 
+                                                  DynamicCallSite.METHOD_CALL);
+        MethodHandle handle = site.dynamicInvoker();
+        assertDepthEquals(site, 0);
+
+        assertEquals("5", handle.invoke(Integer.valueOf(5)));
+        assertDepthEquals(site, 1);
+        assertEquals("1.5", handle.invoke(Float.valueOf(1.5f)));
+        assertDepthEquals(site, 2);
+
+        // both these should be cached
+        assertEquals("6", handle.invoke(Integer.valueOf(6)));
+        assertDepthEquals(site, 2);
+        assertEquals("2.5", handle.invoke(Float.valueOf(2.5f)));
+        assertDepthEquals(site, 2);
+    }
+    
+    public void testTooManyTypes() throws Throwable {
+        // if this changes, test must be rewritten
+        assertEquals(5, DynamicCallSite.InliningCacheCallSite.MAX_DEPTH);
+        CallSite site = DynamicCallSite.bootstrap(MethodHandles.publicLookup(), 
+                                                  "toString", 
+                                                  MethodType.methodType(String.class, Object.class), 
+                                                  DynamicCallSite.METHOD_CALL);
+        MethodHandle handle = site.dynamicInvoker();
+        assertDepthEquals(site, 0);
+
+        assertEquals("5", handle.invoke(Integer.valueOf(5)));
+        assertDepthEquals(site, 1);
+        assertEquals("1.5", handle.invoke(Float.valueOf(1.5f)));
+        assertDepthEquals(site, 2);
+        assertEquals("6", handle.invoke(Long.valueOf(6)));
+        assertDepthEquals(site, 3);
+        assertEquals("3.2", handle.invoke(Double.valueOf(3.2d)));
+        assertDepthEquals(site, 4);
+        assertEquals("foo", handle.invoke(new String("foo")));
+        assertDepthEquals(site, 5);
+        assertEquals("c", handle.invoke(Character.valueOf('c')));
+        assertDepthEquals(site, 5);
+    }
+    
+    static void assertDepthEquals(CallSite site, int expected) {
+        DynamicCallSite.InliningCacheCallSite dsite = (DynamicCallSite.InliningCacheCallSite) site;
+        assertEquals(expected, dsite.depth);
+    }
+}

+ 0 - 109
modules/lang-painless/src/test/java/org/elasticsearch/painless/FieldTests.java

@@ -1,109 +0,0 @@
-/*
- * Licensed to Elasticsearch under one or more contributor
- * license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright
- * ownership. Elasticsearch licenses this file to you under
- * the Apache License, Version 2.0 (the "License"); you may
- * not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *    http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-package org.elasticsearch.painless;
-
-import org.junit.Before;
-
-public class FieldTests extends ScriptTestCase {
-    public static class FieldClass {
-        public boolean z = false;
-        public byte b = 0;
-        public short s = 1;
-        public char c = 'c';
-        public int i = 2;
-        public int si = -1;
-        public long j = 3L;
-        public float f = 4.0f;
-        public double d = 5.0;
-        public String t = "s";
-        public Object l = new Object();
-
-        public float test(float a, float b) {
-            return Math.min(a, b);
-        }
-
-        public int getSi() {
-            return si;
-        }
-
-        public void setSi(final int si) {
-            this.si = si;
-        }
-    }
-
-    public static class FieldDefinition extends Definition {
-        FieldDefinition() {
-            super();
-
-            addStruct("FieldClass", FieldClass.class);
-            addConstructor("FieldClass", "new", new Type[] {}, null);
-            addField("FieldClass", "z", null, false, booleanType, null);
-            addField("FieldClass", "b", null, false, byteType, null);
-            addField("FieldClass", "s", null, false, shortType, null);
-            addField("FieldClass", "c", null, false, charType, null);
-            addField("FieldClass", "i", null, false, intType, null);
-            addField("FieldClass", "j", null, false, longType, null);
-            addField("FieldClass", "f", null, false, floatType, null);
-            addField("FieldClass", "d", null, false, doubleType, null);
-            addField("FieldClass", "t", null, false, stringType, null);
-            addField("FieldClass", "l", null, false, objectType, null);
-            addClass("FieldClass");
-            addMethod("FieldClass", "getSi", null, false, intType, new Type[] {}, null, null);
-            addMethod("FieldClass", "setSi", null, false, voidType, new Type[] {intType}, null, null);
-            addMethod("FieldClass", "test", null, false, floatType, new Type[] {floatType, floatType}, null, null);
-        }
-    }
-
-    @Before
-    public void setDefinition() {
-        scriptEngine.setDefinition(new FieldDefinition());
-    }
-
-    public void testIntField() {
-        assertEquals("s5t42", exec("def fc = new FieldClass() return fc.t += 2 + fc.j + \"t\" + 4 + (3 - 1)"));
-        assertEquals(2.0f, exec("def fc = new FieldClass(); def l = new Double(3) Byte b = new Byte((byte)2) return fc.test(l, b)"));
-        assertEquals(4, exec("def fc = new FieldClass() fc.i = 4 return fc.i"));
-        assertEquals(5,
-                exec("FieldClass fc0 = new FieldClass() FieldClass fc1 = new FieldClass() fc0.i = 7 - fc0.i fc1.i = fc0.i return fc1.i"));
-        assertEquals(8, exec("def fc0 = new FieldClass() def fc1 = new FieldClass() fc0.i += fc1.i fc0.i += fc0.i return fc0.i"));
-    }
-
-    public void testExplicitShortcut() {
-        assertEquals(5, exec("FieldClass fc = new FieldClass() fc.setSi(5) return fc.si"));
-        assertEquals(-1, exec("FieldClass fc = new FieldClass() def x = fc.getSi() x"));
-        assertEquals(5, exec("FieldClass fc = new FieldClass() fc.si = 5 return fc.si"));
-        assertEquals(0, exec("FieldClass fc = new FieldClass() fc.si++ return fc.si"));
-        assertEquals(-1, exec("FieldClass fc = new FieldClass() def x = fc.si++ return x"));
-        assertEquals(0, exec("FieldClass fc = new FieldClass() def x = ++fc.si return x"));
-        assertEquals(-2, exec("FieldClass fc = new FieldClass() fc.si *= 2 fc.si"));
-        assertEquals("-1test", exec("FieldClass fc = new FieldClass() fc.si + \"test\""));
-    }
-
-    public void testImplicitShortcut() {
-        assertEquals(5, exec("def fc = new FieldClass() fc.setSi(5) return fc.si"));
-        assertEquals(-1, exec("def fc = new FieldClass() def x = fc.getSi() x"));
-        assertEquals(5, exec("def fc = new FieldClass() fc.si = 5 return fc.si"));
-        assertEquals(0, exec("def fc = new FieldClass() fc.si++ return fc.si"));
-        assertEquals(-1, exec("def fc = new FieldClass() def x = fc.si++ return x"));
-        assertEquals(0, exec("def fc = new FieldClass() def x = ++fc.si return x"));
-        assertEquals(-2, exec("def fc = new FieldClass() fc.si *= 2 fc.si"));
-        assertEquals("-1test", exec("def fc = new FieldClass() fc.si + \"test\""));
-    }
-}

+ 1 - 0
modules/lang-painless/src/test/java/org/elasticsearch/painless/NoSemiColonTests.java

@@ -168,6 +168,7 @@ public class NoSemiColonTests extends ScriptTestCase {
         assertEquals(4, exec("int x = 0, y = 0 while (x < 10) { ++x if (x == 5) break ++y } return y"));
     }
 
+    @SuppressWarnings("rawtypes")
     public void testReturnStatement() {
         assertEquals(10, exec("return 10"));
         assertEquals(5, exec("int x = 5 return x"));

+ 76 - 78
modules/lang-painless/src/test/java/org/elasticsearch/painless/WhenThingsGoWrongTests.java

@@ -24,126 +24,124 @@ import java.util.Collections;
 
 public class WhenThingsGoWrongTests extends ScriptTestCase {
     public void testNullPointer() {
-        try {
+        expectThrows(NullPointerException.class, () -> {
             exec("int x = (int) ((Map) input).get(\"missing\"); return x;");
-            fail("should have hit npe");
-        } catch (NullPointerException expected) {}
+        });
     }
 
     public void testInvalidShift() {
-        try {
+        expectThrows(ClassCastException.class, () -> {
             exec("float x = 15F; x <<= 2; return x;");
-            fail("should have hit cce");
-        } catch (ClassCastException expected) {}
+        });
 
-        try {
+        expectThrows(ClassCastException.class, () -> {
             exec("double x = 15F; x <<= 2; return x;");
-            fail("should have hit cce");
-        } catch (ClassCastException expected) {}
+        });
     }
 
     public void testBogusParameter() {
-        try {
+        IllegalArgumentException expected = expectThrows(IllegalArgumentException.class, () -> {
             exec("return 5;", null, Collections.singletonMap("bogusParameterKey", "bogusParameterValue"));
-            fail("should have hit IAE");
-        } catch (IllegalArgumentException expected) {
-            assertTrue(expected.getMessage().contains("Unrecognized compile-time parameter"));
-        }
+        });
+        assertTrue(expected.getMessage().contains("Unrecognized compile-time parameter"));
     }
 
     public void testInfiniteLoops() {
-        try {
+        PainlessError expected = expectThrows(PainlessError.class, () -> {
             exec("boolean x = true; while (x) {}");
-            fail("should have hit PainlessError");
-        } catch (PainlessError expected) {
-            assertTrue(expected.getMessage().contains(
-                "The maximum number of statements that can be executed in a loop has been reached."));
-        }
+        });
+        assertTrue(expected.getMessage().contains(
+                   "The maximum number of statements that can be executed in a loop has been reached."));
 
-        try {
+        expected = expectThrows(PainlessError.class, () -> {
             exec("while (true) {int y = 5}");
-            fail("should have hit PainlessError");
-        } catch (PainlessError expected) {
-            assertTrue(expected.getMessage().contains(
-                "The maximum number of statements that can be executed in a loop has been reached."));
-        }
+        });
+        assertTrue(expected.getMessage().contains(
+                   "The maximum number of statements that can be executed in a loop has been reached."));
 
-        try {
+        expected = expectThrows(PainlessError.class, () -> {
             exec("while (true) { boolean x = true; while (x) {} }");
-            fail("should have hit PainlessError");
-        } catch (PainlessError expected) {
-            assertTrue(expected.getMessage().contains(
-                "The maximum number of statements that can be executed in a loop has been reached."));
-        }
+        });
+        assertTrue(expected.getMessage().contains(
+                   "The maximum number of statements that can be executed in a loop has been reached."));
 
-        try {
+        expected = expectThrows(PainlessError.class, () -> {
             exec("while (true) { boolean x = false; while (x) {} }");
             fail("should have hit PainlessError");
-        } catch (PainlessError expected) {
-            assertTrue(expected.getMessage().contains(
-                "The maximum number of statements that can be executed in a loop has been reached."));
-        }
+        });
+        assertTrue(expected.getMessage().contains(
+                   "The maximum number of statements that can be executed in a loop has been reached."));
 
-        try {
+        expected = expectThrows(PainlessError.class, () -> {
             exec("boolean x = true; for (;x;) {}");
             fail("should have hit PainlessError");
-        } catch (PainlessError expected) {
-            assertTrue(expected.getMessage().contains(
-                "The maximum number of statements that can be executed in a loop has been reached."));
-        }
+        });
+        assertTrue(expected.getMessage().contains(
+                   "The maximum number of statements that can be executed in a loop has been reached."));
 
-        try {
+        expected = expectThrows(PainlessError.class, () -> {
             exec("for (;;) {int x = 5}");
             fail("should have hit PainlessError");
-        } catch (PainlessError expected) {
-            assertTrue(expected.getMessage().contains(
-                "The maximum number of statements that can be executed in a loop has been reached."));
-        }
+        });
+        assertTrue(expected.getMessage().contains(
+                   "The maximum number of statements that can be executed in a loop has been reached."));
 
-        try {
+        expected = expectThrows(PainlessError.class, () -> {
             exec("def x = true; do {int y = 5;} while (x)");
             fail("should have hit PainlessError");
-        } catch (PainlessError expected) {
-            assertTrue(expected.getMessage().contains(
-                "The maximum number of statements that can be executed in a loop has been reached."));
-        }
+        });
+        assertTrue(expected.getMessage().contains(
+                   "The maximum number of statements that can be executed in a loop has been reached."));
 
-        try {
+        RuntimeException parseException = expectThrows(RuntimeException.class, () -> {
             exec("try { int x } catch (PainlessError error) {}");
             fail("should have hit ParseException");
-        } catch (RuntimeException expected) {
-            assertTrue(expected.getMessage().contains(
-                "Invalid type [PainlessError]."));
-        }
-
+        });
+        assertTrue(parseException.getMessage().contains("Invalid type [PainlessError]."));
     }
 
     public void testLoopLimits() {
+        // right below limit: ok
         exec("for (int x = 0; x < 9999; ++x) {}");
 
-        try {
+        PainlessError expected = expectThrows(PainlessError.class, () -> {
             exec("for (int x = 0; x < 10000; ++x) {}");
-            fail("should have hit PainlessError");
-        } catch (PainlessError expected) {
-            assertTrue(expected.getMessage().contains(
-                "The maximum number of statements that can be executed in a loop has been reached."));
-        }
+        });
+        assertTrue(expected.getMessage().contains(
+                   "The maximum number of statements that can be executed in a loop has been reached."));
     }
 
     public void testSourceLimits() {
-        char[] chars = new char[Compiler.MAXIMUM_SOURCE_LENGTH + 1];
-        Arrays.fill(chars, '0');
-
-        try {
-            exec(new String(chars));
-            fail("should have hit IllegalArgumentException");
-        } catch (IllegalArgumentException expected) {
-            assertTrue(expected.getMessage().contains("Scripts may be no longer than"));
-        }
-
-        chars = new char[Compiler.MAXIMUM_SOURCE_LENGTH];
-        Arrays.fill(chars, '0');
-
-        assertEquals(0, exec(new String(chars)));
+        final char[] tooManyChars = new char[Compiler.MAXIMUM_SOURCE_LENGTH + 1];
+        Arrays.fill(tooManyChars, '0');
+
+        IllegalArgumentException expected = expectThrows(IllegalArgumentException.class, () -> {
+            exec(new String(tooManyChars));
+        });
+        assertTrue(expected.getMessage().contains("Scripts may be no longer than"));
+
+        final char[] exactlyAtLimit = new char[Compiler.MAXIMUM_SOURCE_LENGTH];
+        Arrays.fill(exactlyAtLimit, '0');
+        // ok
+        assertEquals(0, exec(new String(exactlyAtLimit)));
+    }
+    
+    public void testIllegalDynamicMethod() {
+        IllegalArgumentException expected = expectThrows(IllegalArgumentException.class, () -> {
+            exec("def x = 'test'; return x.getClass().toString()");
+        });
+        assertTrue(expected.getMessage().contains("Unable to find dynamic method"));
+    }
+    
+    public void testDynamicNPE() {
+        expectThrows(NullPointerException.class, () -> {
+            exec("def x = null; return x.toString()");
+        });
+    }
+    
+    public void testDynamicWrongArgs() {
+        expectThrows(ClassCastException.class, () -> {
+            exec("def x = new ArrayList(); return x.get('bogus');");
+        });
     }
 }

+ 59 - 0
modules/lang-painless/src/test/resources/rest-api-spec/test/plan_a/15_update.yaml

@@ -0,0 +1,59 @@
+---
+"Update Script":
+
+  - do:
+      index:
+          index:  test_1
+          type:   test
+          id:     1
+          body:
+              foo:    bar
+              count:  1
+
+  - do:
+      update:
+          index:  test_1
+          type:   test
+          id:     1
+          script: "1"
+          body:
+            lang:   painless
+            script: "input.ctx._source.foo = input.bar"
+            params: { bar: 'xxx' }
+
+  - match: { _index:   test_1 }
+  - match: { _type:    test   }
+  - match: { _id:      "1"    }
+  - match: { _version: 2      }
+
+  - do:
+      get:
+          index:  test_1
+          type:   test
+          id:     1
+
+  - match: { _source.foo:        xxx }
+  - match: { _source.count:      1   }
+
+  - do:
+      update:
+          index:  test_1
+          type:   test
+          id:     1
+          lang:   painless
+          script: "input.ctx._source.foo = 'yyy'"
+
+  - match: { _index:   test_1 }
+  - match: { _type:    test   }
+  - match: { _id:      "1"    }
+  - match: { _version: 3      }
+
+  - do:
+      get:
+          index:  test_1
+          type:   test
+          id:     1
+
+  - match: { _source.foo:        yyy }
+  - match: { _source.count:      1   }
+  

+ 54 - 0
modules/lang-painless/src/test/resources/rest-api-spec/test/plan_a/16_update2.yaml

@@ -0,0 +1,54 @@
+---
+"Indexed script":
+
+  - do:
+      put_script:
+        id: "1"
+        lang: "painless"
+        body: { "script":  "_score * input.doc[\"myParent.weight\"].value" }
+  - match: { acknowledged: true }
+
+  - do:
+     get_script:
+       id: "1"
+       lang: "painless"
+  - match: { found: true }
+  - match: { lang: painless }
+  - match: { _id: "1" }
+  - match: { "script":  "_score * input.doc[\"myParent.weight\"].value" }
+
+  - do:
+     catch: missing
+     get_script:
+       id: "2"
+       lang: "painless"
+  - match: { found: false }
+  - match: { lang: painless }
+  - match: { _id: "2" }
+  - is_false: script
+
+  - do:
+     delete_script:
+       id: "1"
+       lang: "painless"
+  - match: { acknowledged: true }
+
+  - do:
+     catch: missing
+     delete_script:
+       id: "non_existing"
+       lang: "painless"
+
+  - do:
+      catch: request
+      put_script:
+        id: "1"
+        lang: "painless"
+        body: { "script":  "_score * foo bar + input.doc[\"myParent.weight\"].value" }
+
+  - do:
+      catch: /Unable.to.parse.*/
+      put_script:
+        id: "1"
+        lang: "painless"
+        body: { "script":  "_score * foo bar + input.doc[\"myParent.weight\"].value" }

+ 63 - 0
modules/lang-painless/src/test/resources/rest-api-spec/test/plan_a/25_script_upsert.yaml

@@ -0,0 +1,63 @@
+---
+"Script upsert":
+
+  - do:
+      update:
+          index:    test_1
+          type:     test
+          id:       1
+          body:
+            script: "input.ctx._source.foo = input.bar"
+            lang: "painless"
+            params: { bar: 'xxx' }
+            upsert: { foo: baz }
+
+  - do:
+      get:
+          index:  test_1
+          type:   test
+          id:     1
+
+  - match:  { _source.foo: baz }
+
+
+  - do:
+      update:
+          index:    test_1
+          type:     test
+          id:       1
+          body:
+            script: "input.ctx._source.foo = input.bar"
+            lang: "painless"
+            params: { bar: 'xxx' }
+            upsert: { foo: baz }
+
+  - do:
+      get:
+          index:  test_1
+          type:   test
+          id:     1
+
+  - match:  { _source.foo: xxx }
+
+  - do:
+      update:
+          index:    test_1
+          type:     test
+          id:       2
+          body:
+            script: "input.ctx._source.foo = input.bar"
+            lang: "painless"
+            params: { bar: 'xxx' }
+            upsert: { foo: baz }
+            scripted_upsert: true
+
+  - do:
+      get:
+          index:  test_1
+          type:   test
+          id:     2
+
+  - match:  { _source.foo: xxx }
+
+

+ 269 - 0
modules/lang-painless/src/test/resources/rest-api-spec/test/plan_a/30_search.yaml

@@ -94,3 +94,272 @@
     - match: { hits.hits.0.fields.sNum1.0: 1.0 }
     - match: { hits.hits.1.fields.sNum1.0: 2.0 }
     - match: { hits.hits.2.fields.sNum1.0: 3.0 }
+
+---
+
+"Custom Script Boost":
+    - do:
+        index:
+            index: test
+            type: test
+            id: 1
+            body: { "test": "value beck", "num1": 1.0 }
+    - do:
+        index:
+            index: test
+            type: test
+            id: 2
+            body: { "test": "value beck", "num1": 2.0 }
+    - do:
+        indices.refresh: {}
+
+    - do:
+        index: test
+        search:
+            body:
+                query:
+                    function_score:
+                        query:
+                            term:
+                                test: value
+                        "functions": [{
+                            "script_score": {
+                                "script": {
+                                    "lang": "painless",
+                                    "inline": "input.doc['num1'].value"
+                                }
+                            }
+                        }]
+
+    - match: { hits.total: 2 }
+    - match: { hits.hits.0._id: "2" }
+    - match: { hits.hits.1._id: "1" }
+
+    - do:
+        index: test
+        search:
+            body:
+                query:
+                    function_score:
+                        query:
+                            term:
+                                test: value
+                        "functions": [{
+                            "script_score": {
+                                "script": {
+                                    "lang": "painless",
+                                    "inline": "-input.doc['num1'].value"
+                                }
+                            }
+                        }]
+
+    - match: { hits.total: 2 }
+    - match: { hits.hits.0._id: "1" }
+    - match: { hits.hits.1._id: "2" }
+
+    - do:
+        index: test
+        search:
+            body:
+                query:
+                    function_score:
+                        query:
+                            term:
+                                test: value
+                        "functions": [{
+                            "script_score": {
+                                "script": {
+                                    "lang": "painless",
+                                    "inline": "Math.pow(input.doc['num1'].value, 2)"
+                                }
+                            }
+                        }]
+
+    - match: { hits.total: 2 }
+    - match: { hits.hits.0._id: "2" }
+    - match: { hits.hits.1._id: "1" }
+
+    - do:
+        index: test
+        search:
+            body:
+                query:
+                    function_score:
+                        query:
+                            term:
+                                test: value
+                        "functions": [{
+                            "script_score": {
+                                "script": {
+                                    "lang": "painless",
+                                    "inline": "Math.max(input.doc['num1'].value, 1)"
+                                }
+                            }
+                        }]
+
+    - match: { hits.total: 2 }
+    - match: { hits.hits.0._id: "2" }
+    - match: { hits.hits.1._id: "1" }
+
+    - do:
+        index: test
+        search:
+            body:
+                query:
+                    function_score:
+                        query:
+                            term:
+                                test: value
+                        "functions": [{
+                            "script_score": {
+                                "script": {
+                                    "lang": "painless",
+                                    "inline": "input.doc['num1'].value * _score"
+                                }
+                            }
+                        }]
+
+    - match: { hits.total: 2 }
+    - match: { hits.hits.0._id: "2" }
+    - match: { hits.hits.1._id: "1" }
+
+    - do:
+        index: test
+        search:
+            body:
+                query:
+                    function_score:
+                        query:
+                            term:
+                                test: value
+                        "functions": [{
+                            "script_score": {
+                                "script": {
+                                    "lang": "painless",
+                                    "inline": "input.param1 * input.param2 * _score",
+                                    "params": {
+                                        "param1": 2,
+                                        "param2": 2
+
+                                    }
+                                }
+                            }
+                        }]
+
+    - match: { hits.total: 2 }
+
+---
+
+"Scores Nested":
+    - do:
+        index:
+            index: test
+            type: test
+            id: 1
+            body: { "dummy_field": 1 }
+    - do:
+        indices.refresh: {}
+
+    - do:
+        index: test
+        search:
+            body:
+                query:
+                    function_score:
+                        query:
+                            function_score:
+                                "functions": [
+                                    {
+                                        "script_score": {
+                                            "script": {
+                                                "lang": "painless",
+                                                "inline": "1"
+                                            }
+                                        }
+                                    }, {
+                                        "script_score": {
+                                            "script": {
+                                                "lang": "painless",
+                                                "inline": "_score"
+                                            }
+                                        }
+                                    }
+                                ]
+                        "functions": [{
+                            "script_score": {
+                                "script": {
+                                    "lang": "painless",
+                                    "inline": "_score"
+                                }
+                            }
+                        }]
+
+    - match: { hits.total: 1 }
+    - match: { hits.hits.0._score: 1.0 }
+
+
+---
+
+"Scores With Agg":
+    - do:
+        index:
+            index: test
+            type: test
+            id: 1
+            body: { "dummy_field": 1 }
+    - do:
+        indices.refresh: {}
+
+
+    - do:
+        index: test
+        search:
+            body:
+                query:
+                    function_score:
+                        "functions": [{
+                            "script_score": {
+                                "script": {
+                                    "lang": "painless",
+                                    "inline": "_score"
+                                }
+                            }
+                        }]
+                aggs:
+                    score_agg:
+                        terms:
+                            script:
+                                lang: painless
+                                inline: "_score"
+
+    - match: { hits.total: 1 }
+    - match: { hits.hits.0._score: 1.0 }
+    - match: { aggregations.score_agg.buckets.0.key: "1.0" }
+    - match: { aggregations.score_agg.buckets.0.doc_count: 1 }
+
+---
+
+"Use List Size In Scripts":
+    - do:
+        index:
+            index: test
+            type: test
+            id: 1
+            body: { "f": 42 }
+    - do:
+        indices.refresh: {}
+
+
+    - do:
+        index: test
+        search:
+            body:
+                script_fields:
+                    foobar:
+                        script: 
+                            inline: "input.doc['f'].values.size()"
+                            lang: painless
+
+
+    - match: { hits.total: 1 }
+    - match: { hits.hits.0.fields.foobar.0: 1 }