From ba2fe156e814cfa7adefd1eabbe6503b89366f5e Mon Sep 17 00:00:00 2001 From: Robert Muir Date: Mon, 9 May 2016 21:44:32 -0400 Subject: [PATCH] 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 --- .../modules/scripting/painless.asciidoc | 32 - .../painless/AnalyzerUtility.java | 5 +- .../org/elasticsearch/painless/Compiler.java | 11 +- .../java/org/elasticsearch/painless/Def.java | 567 ++++++++---------- .../elasticsearch/painless/Definition.java | 113 +++- .../painless/DynamicCallSite.java | 151 +++++ .../elasticsearch/painless/PainlessError.java | 1 + .../painless/PainlessScriptEngineService.java | 14 +- .../painless/WriterConstants.java | 22 +- .../painless/WriterExternal.java | 84 +-- .../painless/WriterStatement.java | 1 - .../elasticsearch/painless/BasicAPITests.java | 30 + .../painless/BasicStatementTests.java | 1 + .../painless/DynamicCallSiteTests.java | 97 +++ .../elasticsearch/painless/FieldTests.java | 109 ---- .../painless/NoSemiColonTests.java | 1 + .../painless/WhenThingsGoWrongTests.java | 150 +++-- .../rest-api-spec/test/plan_a/15_update.yaml | 59 ++ .../rest-api-spec/test/plan_a/16_update2.yaml | 54 ++ .../test/plan_a/25_script_upsert.yaml | 63 ++ .../rest-api-spec/test/plan_a/30_search.yaml | 269 +++++++++ 21 files changed, 1182 insertions(+), 652 deletions(-) create mode 100644 modules/lang-painless/src/main/java/org/elasticsearch/painless/DynamicCallSite.java create mode 100644 modules/lang-painless/src/test/java/org/elasticsearch/painless/DynamicCallSiteTests.java delete mode 100644 modules/lang-painless/src/test/java/org/elasticsearch/painless/FieldTests.java create mode 100644 modules/lang-painless/src/test/resources/rest-api-spec/test/plan_a/15_update.yaml create mode 100644 modules/lang-painless/src/test/resources/rest-api-spec/test/plan_a/16_update2.yaml create mode 100644 modules/lang-painless/src/test/resources/rest-api-spec/test/plan_a/25_script_upsert.yaml diff --git a/docs/reference/modules/scripting/painless.asciidoc b/docs/reference/modules/scripting/painless.asciidoc index 4f7d36f38f5..b5300751caf 100644 --- a/docs/reference/modules/scripting/painless.asciidoc +++ b/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 diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/AnalyzerUtility.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/AnalyzerUtility.java index af40c58a7d7..0b345431ac5 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/AnalyzerUtility.java +++ b/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 scopes = new ArrayDeque<>(); private final Deque variables = new ArrayDeque<>(); AnalyzerUtility(final Metadata metadata) { - this.metadata = metadata; - definition = metadata.definition; + this.definition = metadata.definition; } void incrementScope() { diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/Compiler.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/Compiler.java index e3300fade78..4cba3cb7923 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/Compiler.java +++ b/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); diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/Def.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/Def.java index 136746026fa..3623368c21e 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/Def.java +++ b/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). + *

+ * Dynamic types can invoke methods, load/store fields, and be passed as parameters to operators without + * compile-time type information. + *

+ * 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. + *

+ * 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 types = method.arguments; - final Object[] parameters = new Object[arguments.length + 1]; - - parameters[0] = owner; - - if (types.size() != arguments.length) { - throw new IllegalArgumentException("When dynamically calling [" + name + "] from class " + - "[" + owner.getClass() + "] expected [" + types.size() + "] arguments," + - " but found [" + arguments.length + "]."); - } - - 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]); - } - } - - 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; - - if (field == null) { - final String set = "set" + Character.toUpperCase(name.charAt(0)) + name.substring(1); - final Method method = getMethod(owner, set, definition); - - if (method != null) { - handle = method.handle; - } - } else { - handle = field.setter; - } - - if (handle != null) { - try { - if (!typesafe) { - final Transform transform = getTransform(value.getClass(), handle.type().parameterType(1), definition); - - if (transform != null) { - value = transform.method.handle.invoke(value); - } - } - - 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) { - try { - final int index = Integer.parseInt(name); - ((List)owner).set(index, value); - } 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() + "]."); - } - } - - @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(); - } - } - 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); - - 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 + "]."); - } - } 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); - } - } - } - } - - @SuppressWarnings({ "unchecked", "rawtypes" }) - public static void arrayStore(final Object array, Object index, Object value, final Definition definition, - final boolean indexsafe, final boolean valuesafe) { - if (array instanceof Map) { - ((Map)array).put(index, value); - } else { - try { - if (!indexsafe) { - final Transform transform = getTransform(index.getClass(), Integer.class, definition); - - if (transform != null) { - index = transform.method.handle.invoke(index); - } - } - } 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."); - } - } - } - - @SuppressWarnings("rawtypes") - public static Object arrayLoad(final Object array, Object index, - final Definition definition, final boolean indexsafe) { - if (array instanceof Map) { - return ((Map)array).get(index); - } else { - try { - if (!indexsafe) { - final Transform transform = getTransform(index.getClass(), Integer.class, definition); - - if (transform != null) { - index = transform.method.handle.invoke(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); + /** + * Looks up handle for a dynamic method call. + *

+ * A dynamic method call for variable {@code x} of type {@code def} looks like: + * {@code x.method(args...)} + *

+ * 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. + *

+ * @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); if (struct != null) { Method method = struct.methods.get(name); - if (method != null) { - return method; + return method.handle; } } - for (final Class iface : clazz.getInterfaces()) { - struct = definition.classes.get(iface); + for (Class iface : clazz.getInterfaces()) { + struct = definition.runtimeMap.get(iface); if (struct != null) { Method method = struct.methods.get(name); - if (method != null) { - return method; + return method.handle; } } } - - clazz = clazz.getSuperclass(); } - return null; + // no matching methods in whitelist found + throw new IllegalArgumentException("Unable to find dynamic method [" + name + "] " + + "for class [" + receiverClass.getCanonicalName() + "]."); } - /** 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); + /** 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) + *

+ * A dynamic field load for variable {@code x} of type {@code def} looks like: + * {@code y = x.field} + *

+ * The following field loads are allowed: + *

    + *
  • Whitelisted {@code field} from receiver's class or any superclasses. + *
  • Whitelisted method named {@code getField()} from receiver's class/superclasses/interfaces. + *
  • Whitelisted method named {@code isField()} from receiver's class/superclasses/interfaces. + *
  • The {@code length} field of an array. + *
  • The value corresponding to a map key named {@code field} when the receiver is a Map. + *
  • The value in a list at element {@code field} (integer) when the receiver is a List. + *
+ *

+ * 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. + *

+ * @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 (struct != null) { - Field field = struct.members.get(name); - - if (field != null) { - return field; + MethodHandle handle = struct.getters.get(name); + if (handle != null) { + return handle; } } for (final Class iface : clazz.getInterfaces()) { - struct = definition.classes.get(iface); + struct = definition.runtimeMap.get(iface); if (struct != null) { - Field field = struct.members.get(name); - - if (field != null) { - return field; + MethodHandle handle = struct.getters.get(name); + if (handle != null) { + return handle; } } } - - clazz = clazz.getSuperclass(); } + // 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 { + int index = Integer.parseInt(name); + return MethodHandles.insertArguments(LIST_GET, 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() + "]."); + } + + /** + * Looks up handle for a dynamic field setter (field store) + *

+ * A dynamic field store for variable {@code x} of type {@code def} looks like: + * {@code x.field = y} + *

+ * The following field stores are allowed: + *

    + *
  • Whitelisted {@code field} from receiver's class or any superclasses. + *
  • Whitelisted method named {@code setField()} from receiver's class/superclasses/interfaces. + *
  • The value corresponding to a map key named {@code field} when the receiver is a Map. + *
  • The value in a list at element {@code field} (integer) when the receiver is a List. + *
+ *

+ * 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. + *

+ * @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); - return null; + if (struct != null) { + MethodHandle handle = struct.setters.get(name); + if (handle != null) { + return handle; + } + } + + for (final Class iface : clazz.getInterfaces()) { + struct = definition.runtimeMap.get(iface); + + if (struct != null) { + MethodHandle handle = struct.setters.get(name); + if (handle != null) { + return handle; + } + } + } + } + // 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() + "]."); } - public static Transform getTransform(Class fromClass, Class toClass, final Definition definition) { - Struct fromStruct = null; - Struct toStruct = null; + // NOTE: below methods are not cached, instead invoked directly because they are performant. - if (fromClass.equals(toClass)) { - return null; - } - - while (fromClass != null) { - fromStruct = definition.classes.get(fromClass); - - if (fromStruct != null) { - break; + /** + * 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) { + if (array instanceof Map) { + ((Map)array).put(index, value); + } else if (array.getClass().isArray()) { + try { + Array.set(array, (int)index, value); + } catch (final Throwable throwable) { + throw new IllegalArgumentException("Error storing value [" + value + "] " + + "in array class [" + array.getClass().getCanonicalName() + "].", throwable); } - - for (final Class iface : fromClass.getInterfaces()) { - fromStruct = definition.classes.get(iface); - - if (fromStruct != null) { - break; - } - } - - if (fromStruct != null) { - break; - } - - fromClass = fromClass.getSuperclass(); + } 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."); } - - 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(); + } + + /** + * 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) { + if (array instanceof Map) { + return ((Map)array).get(index); + } else 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."); } - - 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); - } - - return null; } public static Object not(final Object unary) { diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/Definition.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/Definition.java index 5a0e0c1e636..f418edd2beb 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/Definition.java +++ b/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 methods; + final Map getters; + final Map setters; + + private RuntimeClass(Map methods, Map getters, Map setters) { + this.methods = methods; + this.getters = getters; + this.setters = setters; + } + } final Map structs; final Map, Struct> classes; final Map transforms; final Map bounds; + final Map, 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 methods = struct.methods; + Map getters = new HashMap<>(); + Map setters = new HashMap<>(); + // add all members + for (Map.Entry 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 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 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() + "]."); } diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/DynamicCallSite.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/DynamicCallSite.java new file mode 100644 index 00000000000..b0870c60219 --- /dev/null +++ b/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. + *

+ * Has 3 flavors (passed as static bootstrap parameters): dynamic method call, + * dynamic field load (getter), and dynamic field store (setter). + *

+ * 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. + *

+ * 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 + *

+ * 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). + *

+ * 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); + } + } +} diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessError.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessError.java index 0d6fa915caf..68eac7a260e 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessError.java +++ b/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. diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessScriptEngineService.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessScriptEngineService.java index 34ebddd9fb0..7be3f9832be 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessScriptEngineService.java +++ b/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() { @Override public Executable run() { - return Compiler.compile(loader, "unknown", script, definition, compilerSettings); + return Compiler.compile(loader, "unknown", script, compilerSettings); } }, COMPILATION_CONTEXT); } diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/WriterConstants.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/WriterConstants.java index 33fea094058..a1a594b354e 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/WriterConstants.java +++ b/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); diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/WriterExternal.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/WriterExternal.java index c89e856f858..ab05e1b6e56 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/WriterExternal.java +++ b/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 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 }); + } } diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/WriterStatement.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/WriterStatement.java index d7a964882a4..4cd698a74cd 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/WriterStatement.java +++ b/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; diff --git a/modules/lang-painless/src/test/java/org/elasticsearch/painless/BasicAPITests.java b/modules/lang-painless/src/test/java/org/elasticsearch/painless/BasicAPITests.java index bcfec2343d1..46fc6768c2f 100644 --- a/modules/lang-painless/src/test/java/org/elasticsearch/painless/BasicAPITests.java +++ b/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);")); + } } diff --git a/modules/lang-painless/src/test/java/org/elasticsearch/painless/BasicStatementTests.java b/modules/lang-painless/src/test/java/org/elasticsearch/painless/BasicStatementTests.java index ebdb9021b7b..f0022e6bcf1 100644 --- a/modules/lang-painless/src/test/java/org/elasticsearch/painless/BasicStatementTests.java +++ b/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;")); diff --git a/modules/lang-painless/src/test/java/org/elasticsearch/painless/DynamicCallSiteTests.java b/modules/lang-painless/src/test/java/org/elasticsearch/painless/DynamicCallSiteTests.java new file mode 100644 index 00000000000..de5202236db --- /dev/null +++ b/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); + } +} diff --git a/modules/lang-painless/src/test/java/org/elasticsearch/painless/FieldTests.java b/modules/lang-painless/src/test/java/org/elasticsearch/painless/FieldTests.java deleted file mode 100644 index 04d0b1a64a1..00000000000 --- a/modules/lang-painless/src/test/java/org/elasticsearch/painless/FieldTests.java +++ /dev/null @@ -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\"")); - } -} diff --git a/modules/lang-painless/src/test/java/org/elasticsearch/painless/NoSemiColonTests.java b/modules/lang-painless/src/test/java/org/elasticsearch/painless/NoSemiColonTests.java index e9c399e1eff..5ca98c2e575 100644 --- a/modules/lang-painless/src/test/java/org/elasticsearch/painless/NoSemiColonTests.java +++ b/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")); diff --git a/modules/lang-painless/src/test/java/org/elasticsearch/painless/WhenThingsGoWrongTests.java b/modules/lang-painless/src/test/java/org/elasticsearch/painless/WhenThingsGoWrongTests.java index 088d7cf2fde..3f79ae1974b 100644 --- a/modules/lang-painless/src/test/java/org/elasticsearch/painless/WhenThingsGoWrongTests.java +++ b/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'); + final char[] tooManyChars = new char[Compiler.MAXIMUM_SOURCE_LENGTH + 1]; + Arrays.fill(tooManyChars, '0'); - try { - exec(new String(chars)); - fail("should have hit IllegalArgumentException"); - } catch (IllegalArgumentException expected) { - assertTrue(expected.getMessage().contains("Scripts may be no longer than")); - } + IllegalArgumentException expected = expectThrows(IllegalArgumentException.class, () -> { + exec(new String(tooManyChars)); + }); + 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[] 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');"); + }); } } diff --git a/modules/lang-painless/src/test/resources/rest-api-spec/test/plan_a/15_update.yaml b/modules/lang-painless/src/test/resources/rest-api-spec/test/plan_a/15_update.yaml new file mode 100644 index 00000000000..0cd8b52a2bb --- /dev/null +++ b/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 } + diff --git a/modules/lang-painless/src/test/resources/rest-api-spec/test/plan_a/16_update2.yaml b/modules/lang-painless/src/test/resources/rest-api-spec/test/plan_a/16_update2.yaml new file mode 100644 index 00000000000..bef3250621b --- /dev/null +++ b/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" } diff --git a/modules/lang-painless/src/test/resources/rest-api-spec/test/plan_a/25_script_upsert.yaml b/modules/lang-painless/src/test/resources/rest-api-spec/test/plan_a/25_script_upsert.yaml new file mode 100644 index 00000000000..6a0fef06e37 --- /dev/null +++ b/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 } + + diff --git a/modules/lang-painless/src/test/resources/rest-api-spec/test/plan_a/30_search.yaml b/modules/lang-painless/src/test/resources/rest-api-spec/test/plan_a/30_search.yaml index 4a1ec86a267..db39e6a31b9 100644 --- a/modules/lang-painless/src/test/resources/rest-api-spec/test/plan_a/30_search.yaml +++ b/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 }