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:
Robert Muir 2016-05-09 21:44:32 -04:00
parent 5a7edf992c
commit ba2fe156e8
21 changed files with 1182 additions and 652 deletions

View File

@ -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

View File

@ -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() {

View File

@ -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);

View File

@ -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) {

View File

@ -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() + "].");
}

View File

@ -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);
}
}
}

View File

@ -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.

View File

@ -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);
}

View File

@ -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);

View File

@ -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 });
}
}

View File

@ -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;

View File

@ -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);"));
}
}

View File

@ -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;"));

View File

@ -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);
}
}

View File

@ -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\""));
}
}

View File

@ -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"));

View File

@ -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');");
});
}
}

View File

@ -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 }

View File

@ -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" }

View File

@ -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 }

View File

@ -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 }