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
This commit is contained in:
parent
5a7edf992c
commit
ba2fe156e8
|
@ -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
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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];
|
||||
|
||||
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.
|
||||
* <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);
|
||||
|
||||
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();
|
||||
/** 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);
|
||||
}
|
||||
}
|
||||
|
||||
while (clazz != null) {
|
||||
Struct struct = definition.classes.get(clazz);
|
||||
/**
|
||||
* 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 (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 + "].");
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
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;
|
||||
/**
|
||||
* 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);
|
||||
|
||||
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 (struct != null) {
|
||||
MethodHandle handle = struct.setters.get(name);
|
||||
if (handle != null) {
|
||||
return handle;
|
||||
}
|
||||
}
|
||||
|
||||
if (fromStruct != null) {
|
||||
break;
|
||||
}
|
||||
for (final Class<?> iface : clazz.getInterfaces()) {
|
||||
struct = definition.runtimeMap.get(iface);
|
||||
|
||||
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 (struct != null) {
|
||||
MethodHandle handle = struct.setters.get(name);
|
||||
if (handle != null) {
|
||||
return handle;
|
||||
}
|
||||
}
|
||||
|
||||
if (toStruct != null) {
|
||||
break;
|
||||
}
|
||||
|
||||
toClass = toClass.getSuperclass();
|
||||
}
|
||||
}
|
||||
// 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 + "].");
|
||||
}
|
||||
}
|
||||
|
||||
if (toStruct != null) {
|
||||
final Type fromType = definition.getType(fromStruct.name);
|
||||
final Type toType = definition.getType(toStruct.name);
|
||||
final Cast cast = new Cast(fromType, toType);
|
||||
throw new IllegalArgumentException("Unable to find dynamic field [" + name + "] " +
|
||||
"for class [" + receiverClass.getCanonicalName() + "].");
|
||||
}
|
||||
|
||||
return definition.transforms.get(cast);
|
||||
// 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) {
|
||||
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);
|
||||
}
|
||||
} 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.");
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
/**
|
||||
* 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.");
|
||||
}
|
||||
}
|
||||
|
||||
public static Object not(final Object unary) {
|
||||
|
|
|
@ -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 ),
|
||||
|
@ -324,10 +329,23 @@ class Definition {
|
|||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
Definition(final Definition definition) {
|
||||
// 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);
|
||||
}
|
||||
|
||||
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() + "].");
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);"));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;"));
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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\""));
|
||||
}
|
||||
}
|
|
@ -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"));
|
||||
|
|
|
@ -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');
|
||||
final char[] exactlyAtLimit = new char[Compiler.MAXIMUM_SOURCE_LENGTH];
|
||||
Arrays.fill(exactlyAtLimit, '0');
|
||||
// ok
|
||||
assertEquals(0, exec(new String(exactlyAtLimit)));
|
||||
}
|
||||
|
||||
assertEquals(0, exec(new String(chars)));
|
||||
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');");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 }
|
||||
|
|
@ -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" }
|
|
@ -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 }
|
||||
|
||||
|
|
@ -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 }
|
||||
|
|
Loading…
Reference in New Issue