Update Painless to Use New Script Contexts (#25015)

*  All public methods starting with get will be added as local variables
to the execute method.
* The execute method on a ScriptContext must be both public and
abstract.  This method will be implemented by the Painless compiler.
* A static list of parameter names for the execute method must be
provided since the names will be eliminated at runtime.
* The uses$ methods will still be implemented as before.
* A single constructor may be provided by the ScriptContext.  This
constructor will be overridden by the Painless compiler to include the
exact same arguments.  This allows instances of a Painless script to
potentially contain state.  If a constructor is not provided it is
assumed the default constructor with no arguments will be used.
This commit is contained in:
Jack Conradson 2017-06-02 13:36:45 -07:00 committed by GitHub
parent 4ed0abe72d
commit a926ace2e1
11 changed files with 264 additions and 166 deletions

View File

@ -133,15 +133,15 @@ final class Compiler {
* @param settings The CompilerSettings to be used during the compilation.
* @return An executable script that implements both a specified interface and is a subclass of {@link PainlessScript}
*/
Constructor<? extends PainlessScript> compile(Loader loader, String name, String source, CompilerSettings settings) {
Constructor<?> compile(Loader loader, String name, String source, 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.");
}
ScriptInterface scriptInterface = new ScriptInterface(definition, base);
SSource root = Walker.buildPainlessTree(scriptInterface, name, source, settings, definition,
ScriptClassInfo scriptClassInfo = new ScriptClassInfo(definition, base);
SSource root = Walker.buildPainlessTree(scriptClassInfo, name, source, settings, definition,
null);
root.analyze(definition);
root.write();
@ -153,7 +153,7 @@ final class Compiler {
clazz.getField("$STATEMENTS").set(null, root.getStatements());
clazz.getField("$DEFINITION").set(null, definition);
return clazz.getConstructor();
return clazz.getConstructors()[0];
} catch (Exception exception) { // Catch everything to let the user know this is something caused internally.
throw new IllegalStateException("An internal error occurred attempting to define the script [" + name + "].", exception);
}
@ -172,8 +172,8 @@ final class Compiler {
" plugin if a script longer than this length is a requirement.");
}
ScriptInterface scriptInterface = new ScriptInterface(definition, base);
SSource root = Walker.buildPainlessTree(scriptInterface, name, source, settings, definition,
ScriptClassInfo scriptClassInfo = new ScriptClassInfo(definition, base);
SSource root = Walker.buildPainlessTree(scriptClassInfo, name, source, settings, definition,
debugStream);
root.analyze(definition);
root.write();

View File

@ -26,10 +26,14 @@ import java.util.Map;
/**
* Generic script interface that Painless implements for all Elasticsearch scripts.
*/
public interface GenericElasticsearchScript {
String[] ARGUMENTS = new String[] {"params", "_score", "doc", "_value", "ctx"};
Object execute(Map<String, Object> params, double _score, Map<String, ScriptDocValues<?>> doc, Object _value, Map<?, ?> ctx);
public abstract class GenericElasticsearchScript {
boolean uses$_score();
boolean uses$ctx();
public GenericElasticsearchScript() {}
public static final String[] PARAMETERS = new String[] {"params", "_score", "doc", "_value", "ctx"};
public abstract Object execute(
Map<String, Object> params, double _score, Map<String, ScriptDocValues<?>> doc, Object _value, Map<?, ?> ctx);
public abstract boolean uses$_score();
public abstract boolean uses$ctx();
}

View File

@ -22,7 +22,7 @@ package org.elasticsearch.painless;
import org.elasticsearch.painless.Definition.Method;
import org.elasticsearch.painless.Definition.MethodKey;
import org.elasticsearch.painless.Definition.Type;
import org.elasticsearch.painless.ScriptInterface.MethodArgument;
import org.elasticsearch.painless.ScriptClassInfo.MethodArgument;
import java.util.Arrays;
import java.util.Collection;
@ -91,14 +91,14 @@ public final class Locals {
}
/** Creates a new main method scope */
public static Locals newMainMethodScope(ScriptInterface scriptInterface, Locals programScope, int maxLoopCounter) {
public static Locals newMainMethodScope(ScriptClassInfo scriptClassInfo, Locals programScope, int maxLoopCounter) {
Locals locals = new Locals(programScope, programScope.definition,
scriptInterface.getExecuteMethodReturnType(), KEYWORDS);
scriptClassInfo.getExecuteMethodReturnType(), KEYWORDS);
// This reference. Internal use only.
locals.defineVariable(null, programScope.definition.getType("Object"), THIS, true);
// Method arguments
for (MethodArgument arg : scriptInterface.getExecuteArguments()) {
for (MethodArgument arg : scriptClassInfo.getExecuteArguments()) {
locals.defineVariable(null, arg.getType(), arg.getName(), true);
}
@ -186,7 +186,7 @@ public final class Locals {
///// private impl
/** Whitelist against which thhis script is being compiled. */
/** Whitelist against which this script is being compiled. */
private final Definition definition;
// parent scope
private final Locals parent;
@ -275,6 +275,7 @@ public final class Locals {
public final Type type;
public final boolean readonly;
private final int slot;
private boolean used;
public Variable(Location location, String name, Type type, int slot, boolean readonly) {
this.location = location;

View File

@ -136,7 +136,7 @@ public final class PainlessScriptEngine extends AbstractComponent implements Scr
throw new IllegalArgumentException("painless does not know how to handle context [" + context.name + "]");
}
PainlessScript compile(Compiler compiler, String scriptName, final String scriptSource, final Map<String, String> params) {
Object compile(Compiler compiler, String scriptName, String source, Map<String, String> params, Object... args) {
final CompilerSettings compilerSettings;
if (params.isEmpty()) {
@ -189,14 +189,14 @@ public final class PainlessScriptEngine extends AbstractComponent implements Scr
try {
// Drop all permissions to actually compile the code itself.
return AccessController.doPrivileged(new PrivilegedAction<PainlessScript>() {
return AccessController.doPrivileged(new PrivilegedAction<Object>() {
@Override
public PainlessScript run() {
public Object run() {
String name = scriptName == null ? INLINE_NAME : scriptName;
Constructor<? extends PainlessScript> constructor = compiler.compile(loader, name, scriptSource, compilerSettings);
Constructor<?> constructor = compiler.compile(loader, name, source, compilerSettings);
try {
return constructor.newInstance();
return constructor.newInstance(args);
} catch (Exception exception) { // Catch everything to let the user know this is something caused internally.
throw new IllegalStateException(
"An internal error occurred attempting to define the script [" + name + "].", exception);
@ -205,7 +205,7 @@ public final class PainlessScriptEngine extends AbstractComponent implements Scr
}, COMPILATION_CONTEXT);
// Note that it is safe to catch any of the following errors since Painless is stateless.
} catch (OutOfMemoryError | StackOverflowError | VerifyError | Exception e) {
throw convertToScriptException(scriptName == null ? scriptSource : scriptName, scriptSource, e);
throw convertToScriptException(scriptName == null ? source : scriptName, source, e);
}
}

View File

@ -21,6 +21,7 @@ package org.elasticsearch.painless;
import java.lang.invoke.MethodType;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
@ -33,20 +34,25 @@ import static org.elasticsearch.painless.WriterConstants.USES_PARAMETER_METHOD_T
/**
* Information about the interface being implemented by the painless script.
*/
public class ScriptInterface {
private final Class<?> iface;
public class ScriptClassInfo {
private final Class<?> baseClass;
private final org.objectweb.asm.commons.Method executeMethod;
private final Definition.Type executeMethodReturnType;
private final List<MethodArgument> executeArguments;
private final List<org.objectweb.asm.commons.Method> usesMethods;
private final List<org.objectweb.asm.commons.Method> getMethods;
private final List<Definition.Type> getReturns;
public ScriptInterface(Definition definition, Class<?> iface) {
this.iface = iface;
public ScriptClassInfo(Definition definition, Class<?> baseClass) {
this.baseClass = baseClass;
// Find the main method and the uses$argName methods
java.lang.reflect.Method executeMethod = null;
List<org.objectweb.asm.commons.Method> usesMethods = new ArrayList<>();
for (java.lang.reflect.Method m : iface.getMethods()) {
List<org.objectweb.asm.commons.Method> getMethods = new ArrayList<>();
List<Definition.Type> getReturns = new ArrayList<>();
for (java.lang.reflect.Method m : baseClass.getMethods()) {
if (m.isDefault()) {
continue;
}
@ -55,40 +61,45 @@ public class ScriptInterface {
executeMethod = m;
} else {
throw new IllegalArgumentException(
"Painless can only implement interfaces that have a single method named [execute] but [" + iface.getName()
"Painless can only implement interfaces that have a single method named [execute] but [" + baseClass.getName()
+ "] has more than one.");
}
continue;
}
if (m.getName().startsWith("uses$")) {
if (false == m.getReturnType().equals(boolean.class)) {
throw new IllegalArgumentException("Painless can only implement uses$ methods that return boolean but ["
+ iface.getName() + "#" + m.getName() + "] returns [" + m.getReturnType().getName() + "].");
+ baseClass.getName() + "#" + m.getName() + "] returns [" + m.getReturnType().getName() + "].");
}
if (m.getParameterTypes().length > 0) {
throw new IllegalArgumentException("Painless can only implement uses$ methods that do not take parameters but ["
+ iface.getName() + "#" + m.getName() + "] does.");
+ baseClass.getName() + "#" + m.getName() + "] does.");
}
usesMethods.add(new org.objectweb.asm.commons.Method(m.getName(), USES_PARAMETER_METHOD_TYPE.toMethodDescriptorString()));
continue;
}
throw new IllegalArgumentException("Painless can only implement methods named [execute] and [uses$argName] but ["
+ iface.getName() + "] contains a method named [" + m.getName() + "]");
if (m.getName().startsWith("get") && m.getName().equals("getClass") == false && Modifier.isStatic(m.getModifiers()) == false) {
getReturns.add(
definitionTypeForClass(definition, m.getReturnType(), componentType -> "[" + m.getName() + "] has unknown return type ["
+ componentType.getName() + "]. Painless can only support getters with return types that are whitelisted."));
getMethods.add(new org.objectweb.asm.commons.Method(m.getName(),
MethodType.methodType(m.getReturnType()).toMethodDescriptorString()));
}
}
MethodType methodType = MethodType.methodType(executeMethod.getReturnType(), executeMethod.getParameterTypes());
this.executeMethod = new org.objectweb.asm.commons.Method(executeMethod.getName(), methodType.toMethodDescriptorString());
executeMethodReturnType = definitionTypeForClass(definition, executeMethod.getReturnType(),
componentType -> "Painless can only implement execute methods returning a whitelisted type but [" + iface.getName()
componentType -> "Painless can only implement execute methods returning a whitelisted type but [" + baseClass.getName()
+ "#execute] returns [" + componentType.getName() + "] which isn't whitelisted.");
// Look up the argument names
Set<String> argumentNames = new LinkedHashSet<>();
List<MethodArgument> arguments = new ArrayList<>();
String[] argumentNamesConstant = readArgumentNamesConstant(iface);
String[] argumentNamesConstant = readArgumentNamesConstant(baseClass);
Class<?>[] types = executeMethod.getParameterTypes();
if (argumentNamesConstant.length != types.length) {
throw new IllegalArgumentException("[" + iface.getName() + "#ARGUMENTS] has length [2] but ["
+ iface.getName() + "#execute] takes [1] argument.");
throw new IllegalArgumentException("[" + baseClass.getName() + "#ARGUMENTS] has length [2] but ["
+ baseClass.getName() + "#execute] takes [1] argument.");
}
for (int arg = 0; arg < types.length; arg++) {
arguments.add(methodArgument(definition, types[arg], argumentNamesConstant[arg]));
@ -100,17 +111,19 @@ public class ScriptInterface {
for (org.objectweb.asm.commons.Method usesMethod : usesMethods) {
if (false == argumentNames.contains(usesMethod.getName().substring("uses$".length()))) {
throw new IllegalArgumentException("Painless can only implement uses$ methods that match a parameter name but ["
+ iface.getName() + "#" + usesMethod.getName() + "] doesn't match any of " + argumentNames + ".");
+ baseClass.getName() + "#" + usesMethod.getName() + "] doesn't match any of " + argumentNames + ".");
}
}
this.usesMethods = unmodifiableList(usesMethods);
this.getMethods = unmodifiableList(getMethods);
this.getReturns = unmodifiableList(getReturns);
}
/**
* The interface that the Painless script should implement.
*/
public Class<?> getInterface() {
return iface;
public Class<?> getBaseClass() {
return baseClass;
}
/**
@ -143,6 +156,20 @@ public class ScriptInterface {
return usesMethods;
}
/**
* The {@code getVarName} methods that must be implemented by Painless to complete implementing the interface.
*/
public List<org.objectweb.asm.commons.Method> getGetMethods() {
return getMethods;
}
/**
* The {@code getVarName} methods return types.
*/
public List<Definition.Type> getGetReturns() {
return getReturns;
}
/**
* Painless {@link Definition.Type}s and name of the argument to the {@code execute} method.
*/
@ -194,13 +221,13 @@ public class ScriptInterface {
private static String[] readArgumentNamesConstant(Class<?> iface) {
Field argumentNamesField;
try {
argumentNamesField = iface.getField("ARGUMENTS");
argumentNamesField = iface.getField("PARAMETERS");
} catch (NoSuchFieldException e) {
throw new IllegalArgumentException("Painless needs a constant [String[] ARGUMENTS] on all interfaces it implements with the "
throw new IllegalArgumentException("Painless needs a constant [String[] PARAMETERS] on all interfaces it implements with the "
+ "names of the method arguments but [" + iface.getName() + "] doesn't have one.", e);
}
if (false == argumentNamesField.getType().equals(String[].class)) {
throw new IllegalArgumentException("Painless needs a constant [String[] ARGUMENTS] on all interfaces it implements with the "
throw new IllegalArgumentException("Painless needs a constant [String[] PARAMETERS] on all interfaces it implements with the "
+ "names of the method arguments but [" + iface.getName() + "] doesn't have one.");
}
try {

View File

@ -56,7 +56,6 @@ public final class WriterConstants {
public static final String CTOR_METHOD_NAME = "<init>";
public static final Method CONSTRUCTOR = getAsmMethod(void.class, CTOR_METHOD_NAME);
public static final Method CLINIT = getAsmMethod(void.class, "<clinit>");
public static final String GET_NAME_NAME = "getName";

View File

@ -33,7 +33,7 @@ import org.elasticsearch.painless.Definition;
import org.elasticsearch.painless.Globals;
import org.elasticsearch.painless.Location;
import org.elasticsearch.painless.Operation;
import org.elasticsearch.painless.ScriptInterface;
import org.elasticsearch.painless.ScriptClassInfo;
import org.elasticsearch.painless.antlr.PainlessParser.AfterthoughtContext;
import org.elasticsearch.painless.antlr.PainlessParser.ArgumentContext;
import org.elasticsearch.painless.antlr.PainlessParser.ArgumentsContext;
@ -174,14 +174,14 @@ import java.util.List;
*/
public final class Walker extends PainlessParserBaseVisitor<ANode> {
public static SSource buildPainlessTree(ScriptInterface mainMethod, String sourceName,
String sourceText, CompilerSettings settings, Definition definition,
Printer debugStream) {
public static SSource buildPainlessTree(ScriptClassInfo mainMethod, String sourceName,
String sourceText, CompilerSettings settings, Definition definition,
Printer debugStream) {
return new Walker(mainMethod, sourceName, sourceText, settings, definition,
debugStream).source;
}
private final ScriptInterface scriptInterface;
private final ScriptClassInfo scriptClassInfo;
private final SSource source;
private final CompilerSettings settings;
private final Printer debugStream;
@ -193,9 +193,9 @@ public final class Walker extends PainlessParserBaseVisitor<ANode> {
private final Globals globals;
private int syntheticCounter = 0;
private Walker(ScriptInterface scriptInterface, String sourceName, String sourceText,
CompilerSettings settings, Definition definition, Printer debugStream) {
this.scriptInterface = scriptInterface;
private Walker(ScriptClassInfo scriptClassInfo, String sourceName, String sourceText,
CompilerSettings settings, Definition definition, Printer debugStream) {
this.scriptClassInfo = scriptClassInfo;
this.debugStream = debugStream;
this.settings = settings;
this.sourceName = Location.computeSourceName(sourceName, sourceText);
@ -266,7 +266,7 @@ public final class Walker extends PainlessParserBaseVisitor<ANode> {
statements.add((AStatement)visit(statement));
}
return new SSource(scriptInterface, settings, sourceName, sourceText, debugStream, (MainMethodReserved)reserved.pop(),
return new SSource(scriptClassInfo, settings, sourceName, sourceText, debugStream, (MainMethodReserved)reserved.pop(),
location(ctx), functions, globals, statements);
}

View File

@ -29,7 +29,7 @@ import org.elasticsearch.painless.Locals;
import org.elasticsearch.painless.Locals.Variable;
import org.elasticsearch.painless.Location;
import org.elasticsearch.painless.MethodWriter;
import org.elasticsearch.painless.ScriptInterface;
import org.elasticsearch.painless.ScriptClassInfo;
import org.elasticsearch.painless.SimpleChecksAdapter;
import org.elasticsearch.painless.WriterConstants;
import org.objectweb.asm.ClassVisitor;
@ -40,6 +40,7 @@ import org.objectweb.asm.Type;
import org.objectweb.asm.util.Printer;
import org.objectweb.asm.util.TraceClassVisitor;
import java.lang.invoke.MethodType;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.Collection;
@ -58,7 +59,6 @@ import static org.elasticsearch.painless.WriterConstants.BITSET_TYPE;
import static org.elasticsearch.painless.WriterConstants.BOOTSTRAP_METHOD_ERROR_TYPE;
import static org.elasticsearch.painless.WriterConstants.CLASS_TYPE;
import static org.elasticsearch.painless.WriterConstants.COLLECTIONS_TYPE;
import static org.elasticsearch.painless.WriterConstants.CONSTRUCTOR;
import static org.elasticsearch.painless.WriterConstants.CONVERT_TO_SCRIPT_EXCEPTION_METHOD;
import static org.elasticsearch.painless.WriterConstants.DEFINITION_TYPE;
import static org.elasticsearch.painless.WriterConstants.DEF_BOOTSTRAP_DELEGATE_METHOD;
@ -69,7 +69,6 @@ import static org.elasticsearch.painless.WriterConstants.EXCEPTION_TYPE;
import static org.elasticsearch.painless.WriterConstants.GET_NAME_METHOD;
import static org.elasticsearch.painless.WriterConstants.GET_SOURCE_METHOD;
import static org.elasticsearch.painless.WriterConstants.GET_STATEMENTS_METHOD;
import static org.elasticsearch.painless.WriterConstants.OBJECT_TYPE;
import static org.elasticsearch.painless.WriterConstants.OUT_OF_MEMORY_ERROR_TYPE;
import static org.elasticsearch.painless.WriterConstants.PAINLESS_ERROR_TYPE;
import static org.elasticsearch.painless.WriterConstants.PAINLESS_EXPLAIN_ERROR_GET_HEADERS_METHOD;
@ -119,7 +118,7 @@ public final class SSource extends AStatement {
}
}
private final ScriptInterface scriptInterface;
private final ScriptClassInfo scriptClassInfo;
private final CompilerSettings settings;
private final String name;
private final String source;
@ -130,13 +129,14 @@ public final class SSource extends AStatement {
private final List<AStatement> statements;
private Locals mainMethod;
private final List<org.objectweb.asm.commons.Method> getMethods;
private byte[] bytes;
public SSource(ScriptInterface scriptInterface, CompilerSettings settings, String name, String source, Printer debugStream,
public SSource(ScriptClassInfo scriptClassInfo, CompilerSettings settings, String name, String source, Printer debugStream,
MainMethodReserved reserved, Location location,
List<SFunction> functions, Globals globals, List<AStatement> statements) {
super(location);
this.scriptInterface = Objects.requireNonNull(scriptInterface);
this.scriptClassInfo = Objects.requireNonNull(scriptClassInfo);
this.settings = Objects.requireNonNull(settings);
this.name = Objects.requireNonNull(name);
this.source = Objects.requireNonNull(source);
@ -148,6 +148,8 @@ public final class SSource extends AStatement {
this.functions = Collections.unmodifiableList(functions);
this.statements = Collections.unmodifiableList(statements);
this.globals = globals;
this.getMethods = new ArrayList<>();
}
@Override
@ -184,7 +186,19 @@ public final class SSource extends AStatement {
throw createError(new IllegalArgumentException("Cannot generate an empty script."));
}
mainMethod = Locals.newMainMethodScope(scriptInterface, program, reserved.getMaxLoopCounter());
mainMethod = Locals.newMainMethodScope(scriptClassInfo, program, reserved.getMaxLoopCounter());
for (int get = 0; get < scriptClassInfo.getGetMethods().size(); ++get) {
org.objectweb.asm.commons.Method method = scriptClassInfo.getGetMethods().get(get);
String name = method.getName().substring(3);
name = Character.toLowerCase(name.charAt(0)) + name.substring(1);
if (reserved.getUsedVariables().contains(name)) {
Definition.Type rtn = scriptClassInfo.getGetReturns().get(get);
mainMethod.addVariable(new Location("getter [" + name + "]", 0), rtn, name, true);
getMethods.add(method);
}
}
AStatement last = statements.get(statements.size() - 1);
@ -211,7 +225,7 @@ public final class SSource extends AStatement {
int classAccess = Opcodes.ACC_PUBLIC | Opcodes.ACC_SUPER | Opcodes.ACC_FINAL;
String interfaceBase = BASE_INTERFACE_TYPE.getInternalName();
String className = CLASS_TYPE.getInternalName();
String classInterfaces[] = new String[] { interfaceBase, Type.getType(scriptInterface.getInterface()).getInternalName() };
String classInterfaces[] = new String[] { interfaceBase };
ClassWriter writer = new ClassWriter(classFrames);
ClassVisitor visitor = writer;
@ -224,7 +238,8 @@ public final class SSource extends AStatement {
if (debugStream != null) {
visitor = new TraceClassVisitor(visitor, debugStream, null);
}
visitor.visit(WriterConstants.CLASS_VERSION, classAccess, className, null, OBJECT_TYPE.getInternalName(), classInterfaces);
visitor.visit(WriterConstants.CLASS_VERSION, classAccess, className, null,
Type.getType(scriptClassInfo.getBaseClass()).getInternalName(), classInterfaces);
visitor.visitSource(Location.computeSourceName(name, source), null);
// Write the a method to bootstrap def calls
@ -245,12 +260,21 @@ public final class SSource extends AStatement {
// Write the static variable used by the method to bootstrap def calls
visitor.visitField(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC, "$DEFINITION", DEFINITION_TYPE.getDescriptor(), null, null).visitEnd();
org.objectweb.asm.commons.Method init;
if (scriptClassInfo.getBaseClass().getConstructors().length == 0) {
init = new org.objectweb.asm.commons.Method("<init>", MethodType.methodType(void.class).toMethodDescriptorString());
} else {
init = new org.objectweb.asm.commons.Method("<init>", MethodType.methodType(void.class,
scriptClassInfo.getBaseClass().getConstructors()[0].getParameterTypes()).toMethodDescriptorString());
}
// Write the constructor:
MethodWriter constructor = new MethodWriter(Opcodes.ACC_PUBLIC, CONSTRUCTOR, visitor, globals.getStatements(), settings);
MethodWriter constructor = new MethodWriter(Opcodes.ACC_PUBLIC, init, visitor, globals.getStatements(), settings);
constructor.visitCode();
constructor.loadThis();
constructor.loadArgs();
constructor.invokeConstructor(OBJECT_TYPE, CONSTRUCTOR);
constructor.invokeConstructor(Type.getType(scriptClassInfo.getBaseClass()), init);
constructor.returnValue();
constructor.endMethod();
@ -277,7 +301,7 @@ public final class SSource extends AStatement {
statementsMethod.endMethod();
// Write the method defined in the interface:
MethodWriter executeMethod = new MethodWriter(Opcodes.ACC_PUBLIC, scriptInterface.getExecuteMethod(), visitor,
MethodWriter executeMethod = new MethodWriter(Opcodes.ACC_PUBLIC, scriptClassInfo.getExecuteMethod(), visitor,
globals.getStatements(), settings);
executeMethod.visitCode();
write(executeMethod, globals);
@ -324,7 +348,7 @@ public final class SSource extends AStatement {
}
// Write any uses$varName methods for used variables
for (org.objectweb.asm.commons.Method usesMethod : scriptInterface.getUsesMethods()) {
for (org.objectweb.asm.commons.Method usesMethod : scriptClassInfo.getUsesMethods()) {
MethodWriter ifaceMethod = new MethodWriter(Opcodes.ACC_PUBLIC, usesMethod, visitor, globals.getStatements(), settings);
ifaceMethod.visitCode();
ifaceMethod.push(reserved.getUsedVariables().contains(usesMethod.getName().substring("uses$".length())));
@ -358,12 +382,22 @@ public final class SSource extends AStatement {
writer.visitVarInsn(Opcodes.ISTORE, loop.getSlot());
}
for (org.objectweb.asm.commons.Method method : getMethods) {
String name = method.getName().substring(3);
name = Character.toLowerCase(name.charAt(0)) + name.substring(1);
Variable variable = mainMethod.getVariable(null, name);
writer.loadThis();
writer.invokeVirtual(Type.getType(scriptClassInfo.getBaseClass()), method);
writer.visitVarInsn(method.getReturnType().getOpcode(Opcodes.ISTORE), variable.getSlot());
}
for (AStatement statement : statements) {
statement.write(writer, globals);
}
if (!methodEscape) {
switch (scriptInterface.getExecuteMethod().getReturnType().getSort()) {
switch (scriptClassInfo.getExecuteMethod().getReturnType().getSort()) {
case org.objectweb.asm.Type.VOID: break;
case org.objectweb.asm.Type.BOOLEAN: writer.push(false); break;
case org.objectweb.asm.Type.BYTE: writer.push(0); break;

View File

@ -22,6 +22,7 @@ package org.elasticsearch.painless;
import org.elasticsearch.script.ScriptContext;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
@ -34,10 +35,53 @@ import static org.hamcrest.Matchers.startsWith;
/**
* Tests for Painless implementing different interfaces.
*/
public class ImplementInterfacesTests extends ScriptTestCase {
public interface NoArgs {
String[] ARGUMENTS = new String[] {};
Object execute();
public class BaseClassTests extends ScriptTestCase {
public abstract static class Gets {
private final String testString;
private final int testInt;
private final Map<String, Object> testMap;
public Gets(String testString, int testInt, Map<String, Object> testMap) {
this.testString = testString;
this.testInt = testInt;
this.testMap = testMap;
}
public static final String[] PARAMETERS = new String[] {};
public abstract Object execute();
public String getTestString() {
return testString;
}
public int getTestInt() {
return Math.abs(testInt);
}
public Map<String, Object> getTestMap() {
return testMap == null ? new HashMap<>() : testMap;
}
}
public void testGets() {
Compiler compiler = new Compiler(Gets.class, Definition.BUILTINS);
Map<String, Object> map = new HashMap<>();
map.put("s", 1);
assertEquals(1, ((Gets)scriptEngine.compile(compiler, null, "testInt", emptyMap(), "s", -1, null)).execute());
assertEquals(Collections.emptyMap(), ((Gets)scriptEngine.compile(compiler, null, "testMap", emptyMap(), "s", -1, null)).execute());
assertEquals(Collections.singletonMap("1", "1"),
((Gets)scriptEngine.compile(compiler, null, "testMap", emptyMap(), "s", -1, Collections.singletonMap("1", "1"))).execute());
assertEquals("s", ((Gets)scriptEngine.compile(compiler, null, "testString", emptyMap(), "s", -1, null)).execute());
assertEquals(map,
((Gets)scriptEngine.compile(compiler, null, "testMap.put(testString, testInt); testMap", emptyMap(), "s", -1, null)).execute());
}
public abstract static class NoArgs {
public static final String[] PARAMETERS = new String[] {};
public abstract Object execute();
}
public void testNoArgs() {
Compiler compiler = new Compiler(NoArgs.class, Definition.BUILTINS);
@ -59,9 +103,9 @@ public class ImplementInterfacesTests extends ScriptTestCase {
assertThat(debug, containsString("ARETURN"));
}
public interface OneArg {
String[] ARGUMENTS = new String[] {"arg"};
Object execute(Object arg);
public abstract static class OneArg {
public static final String[] PARAMETERS = new String[] {"arg"};
public abstract Object execute(Object arg);
}
public void testOneArg() {
Compiler compiler = new Compiler(OneArg.class, Definition.BUILTINS);
@ -80,9 +124,9 @@ public class ImplementInterfacesTests extends ScriptTestCase {
assertEquals("Variable [_score] is not defined.", e.getMessage());
}
public interface ArrayArg {
String[] ARGUMENTS = new String[] {"arg"};
Object execute(String[] arg);
public abstract static class ArrayArg {
public static final String[] PARAMETERS = new String[] {"arg"};
public abstract Object execute(String[] arg);
}
public void testArrayArg() {
Compiler compiler = new Compiler(ArrayArg.class, Definition.BUILTINS);
@ -90,9 +134,9 @@ public class ImplementInterfacesTests extends ScriptTestCase {
assertEquals(rando, ((ArrayArg)scriptEngine.compile(compiler, null, "arg[0]", emptyMap())).execute(new String[] {rando, "foo"}));
}
public interface PrimitiveArrayArg {
String[] ARGUMENTS = new String[] {"arg"};
Object execute(int[] arg);
public abstract static class PrimitiveArrayArg {
public static final String[] PARAMETERS = new String[] {"arg"};
public abstract Object execute(int[] arg);
}
public void testPrimitiveArrayArg() {
Compiler compiler = new Compiler(PrimitiveArrayArg.class, Definition.BUILTINS);
@ -100,9 +144,9 @@ public class ImplementInterfacesTests extends ScriptTestCase {
assertEquals(rando, ((PrimitiveArrayArg)scriptEngine.compile(compiler, null, "arg[0]", emptyMap())).execute(new int[] {rando, 10}));
}
public interface DefArrayArg {
String[] ARGUMENTS = new String[] {"arg"};
Object execute(Object[] arg);
public abstract static class DefArrayArg {
public static final String[] PARAMETERS = new String[] {"arg"};
public abstract Object execute(Object[] arg);
}
public void testDefArrayArg() {
Compiler compiler = new Compiler(DefArrayArg.class, Definition.BUILTINS);
@ -114,13 +158,13 @@ public class ImplementInterfacesTests extends ScriptTestCase {
((DefArrayArg)scriptEngine.compile(compiler, null, "arg[0].length()", emptyMap())).execute(new Object[] {rando, 10}));
}
public interface ManyArgs {
String[] ARGUMENTS = new String[] {"a", "b", "c", "d"};
Object execute(int a, int b, int c, int d);
boolean uses$a();
boolean uses$b();
boolean uses$c();
boolean uses$d();
public abstract static class ManyArgs {
public static final String[] PARAMETERS = new String[] {"a", "b", "c", "d"};
public abstract Object execute(int a, int b, int c, int d);
public abstract boolean uses$a();
public abstract boolean uses$b();
public abstract boolean uses$c();
public abstract boolean uses$d();
}
public void testManyArgs() {
Compiler compiler = new Compiler(ManyArgs.class, Definition.BUILTINS);
@ -146,9 +190,9 @@ public class ImplementInterfacesTests extends ScriptTestCase {
assertTrue(script.uses$d());
}
public interface VarargTest {
String[] ARGUMENTS = new String[] {"arg"};
Object execute(String... arg);
public abstract static class VarargTest {
public static final String[] PARAMETERS = new String[] {"arg"};
public abstract Object execute(String... arg);
}
public void testVararg() {
Compiler compiler = new Compiler(VarargTest.class, Definition.BUILTINS);
@ -156,13 +200,13 @@ public class ImplementInterfacesTests extends ScriptTestCase {
.execute("foo", "bar", "baz"));
}
public interface DefaultMethods {
String[] ARGUMENTS = new String[] {"a", "b", "c", "d"};
Object execute(int a, int b, int c, int d);
default Object executeWithOne() {
public abstract static class DefaultMethods {
public static final String[] PARAMETERS = new String[] {"a", "b", "c", "d"};
public abstract Object execute(int a, int b, int c, int d);
public Object executeWithOne() {
return execute(1, 1, 1, 1);
}
default Object executeWithASingleOne(int a, int b, int c) {
public Object executeWithASingleOne(int a, int b, int c) {
return execute(a, b, c, 1);
}
}
@ -176,9 +220,9 @@ public class ImplementInterfacesTests extends ScriptTestCase {
assertEquals(7, ((DefaultMethods)scriptEngine.compile(compiler, null, "a + b + c + d", emptyMap())).executeWithASingleOne(1, 2, 3));
}
public interface ReturnsVoid {
String[] ARGUMENTS = new String[] {"map"};
void execute(Map<String, Object> map);
public abstract static class ReturnsVoid {
public static final String[] PARAMETERS = new String[] {"map"};
public abstract void execute(Map<String, Object> map);
}
public void testReturnsVoid() {
Compiler compiler = new Compiler(ReturnsVoid.class, Definition.BUILTINS);
@ -195,9 +239,9 @@ public class ImplementInterfacesTests extends ScriptTestCase {
assertThat(debug, not(containsString("ACONST_NULL")));
}
public interface ReturnsPrimitiveBoolean {
String[] ARGUMENTS = new String[] {};
boolean execute();
public abstract static class ReturnsPrimitiveBoolean {
public static final String[] PARAMETERS = new String[] {};
public abstract boolean execute();
}
public void testReturnsPrimitiveBoolean() {
Compiler compiler = new Compiler(ReturnsPrimitiveBoolean.class, Definition.BUILTINS);
@ -237,9 +281,9 @@ public class ImplementInterfacesTests extends ScriptTestCase {
assertEquals(false, ((ReturnsPrimitiveBoolean)scriptEngine.compile(compiler, null, "int i = 0", emptyMap())).execute());
}
public interface ReturnsPrimitiveInt {
String[] ARGUMENTS = new String[] {};
int execute();
public abstract static class ReturnsPrimitiveInt {
public static final String[] PARAMETERS = new String[] {};
public abstract int execute();
}
public void testReturnsPrimitiveInt() {
Compiler compiler = new Compiler(ReturnsPrimitiveInt.class, Definition.BUILTINS);
@ -279,9 +323,9 @@ public class ImplementInterfacesTests extends ScriptTestCase {
assertEquals(0, ((ReturnsPrimitiveInt)scriptEngine.compile(compiler, null, "int i = 0", emptyMap())).execute());
}
public interface ReturnsPrimitiveFloat {
String[] ARGUMENTS = new String[] {};
float execute();
public abstract static class ReturnsPrimitiveFloat {
public static final String[] PARAMETERS = new String[] {};
public abstract float execute();
}
public void testReturnsPrimitiveFloat() {
Compiler compiler = new Compiler(ReturnsPrimitiveFloat.class, Definition.BUILTINS);
@ -310,9 +354,9 @@ public class ImplementInterfacesTests extends ScriptTestCase {
assertEquals(0.0f, ((ReturnsPrimitiveFloat)scriptEngine.compile(compiler, null, "int i = 0", emptyMap())).execute(), 0);
}
public interface ReturnsPrimitiveDouble {
String[] ARGUMENTS = new String[] {};
double execute();
public abstract static class ReturnsPrimitiveDouble {
public static final String[] PARAMETERS = new String[] {};
public abstract double execute();
}
public void testReturnsPrimitiveDouble() {
Compiler compiler = new Compiler(ReturnsPrimitiveDouble.class, Definition.BUILTINS);
@ -345,32 +389,34 @@ public class ImplementInterfacesTests extends ScriptTestCase {
assertEquals(0.0, ((ReturnsPrimitiveDouble)scriptEngine.compile(compiler, null, "int i = 0", emptyMap())).execute(), 0);
}
public interface NoArgumentsConstant {
Object execute(String foo);
public abstract static class NoArgumentsConstant {
public abstract Object execute(String foo);
}
public void testNoArgumentsConstant() {
Compiler compiler = new Compiler(NoArgumentsConstant.class, Definition.BUILTINS);
Exception e = expectScriptThrows(IllegalArgumentException.class, false, () ->
scriptEngine.compile(compiler, null, "1", emptyMap()));
assertThat(e.getMessage(), startsWith("Painless needs a constant [String[] ARGUMENTS] on all interfaces it implements with the "
assertThat(e.getMessage(), startsWith(
"Painless needs a constant [String[] PARAMETERS] on all interfaces it implements with the "
+ "names of the method arguments but [" + NoArgumentsConstant.class.getName() + "] doesn't have one."));
}
public interface WrongArgumentsConstant {
boolean[] ARGUMENTS = new boolean[] {false};
Object execute(String foo);
public abstract static class WrongArgumentsConstant {
boolean[] PARAMETERS = new boolean[] {false};
public abstract Object execute(String foo);
}
public void testWrongArgumentsConstant() {
Compiler compiler = new Compiler(WrongArgumentsConstant.class, Definition.BUILTINS);
Exception e = expectScriptThrows(IllegalArgumentException.class, false, () ->
scriptEngine.compile(compiler, null, "1", emptyMap()));
assertThat(e.getMessage(), startsWith("Painless needs a constant [String[] ARGUMENTS] on all interfaces it implements with the "
assertThat(e.getMessage(), startsWith(
"Painless needs a constant [String[] PARAMETERS] on all interfaces it implements with the "
+ "names of the method arguments but [" + WrongArgumentsConstant.class.getName() + "] doesn't have one."));
}
public interface WrongLengthOfArgumentConstant {
String[] ARGUMENTS = new String[] {"foo", "bar"};
Object execute(String foo);
public abstract static class WrongLengthOfArgumentConstant {
public static final String[] PARAMETERS = new String[] {"foo", "bar"};
public abstract Object execute(String foo);
}
public void testWrongLengthOfArgumentConstant() {
Compiler compiler = new Compiler(WrongLengthOfArgumentConstant.class, Definition.BUILTINS);
@ -380,9 +426,9 @@ public class ImplementInterfacesTests extends ScriptTestCase {
+ WrongLengthOfArgumentConstant.class.getName() + "#execute] takes [1] argument."));
}
public interface UnknownArgType {
String[] ARGUMENTS = new String[] {"foo"};
Object execute(UnknownArgType foo);
public abstract static class UnknownArgType {
public static final String[] PARAMETERS = new String[] {"foo"};
public abstract Object execute(UnknownArgType foo);
}
public void testUnknownArgType() {
Compiler compiler = new Compiler(UnknownArgType.class, Definition.BUILTINS);
@ -392,9 +438,9 @@ public class ImplementInterfacesTests extends ScriptTestCase {
+ "that are of whitelisted types.", e.getMessage());
}
public interface UnknownReturnType {
String[] ARGUMENTS = new String[] {"foo"};
UnknownReturnType execute(String foo);
public abstract static class UnknownReturnType {
public static final String[] PARAMETERS = new String[] {"foo"};
public abstract UnknownReturnType execute(String foo);
}
public void testUnknownReturnType() {
Compiler compiler = new Compiler(UnknownReturnType.class, Definition.BUILTINS);
@ -404,9 +450,9 @@ public class ImplementInterfacesTests extends ScriptTestCase {
+ "#execute] returns [" + UnknownReturnType.class.getName() + "] which isn't whitelisted.", e.getMessage());
}
public interface UnknownArgTypeInArray {
String[] ARGUMENTS = new String[] {"foo"};
Object execute(UnknownArgTypeInArray[] foo);
public abstract static class UnknownArgTypeInArray {
public static final String[] PARAMETERS = new String[] {"foo"};
public abstract Object execute(UnknownArgTypeInArray[] foo);
}
public void testUnknownArgTypeInArray() {
Compiler compiler = new Compiler(UnknownArgTypeInArray.class, Definition.BUILTINS);
@ -416,9 +462,9 @@ public class ImplementInterfacesTests extends ScriptTestCase {
+ "arguments that are of whitelisted types.", e.getMessage());
}
public interface TwoExecuteMethods {
Object execute();
Object execute(boolean foo);
public abstract static class TwoExecuteMethods {
public abstract Object execute();
public abstract Object execute(boolean foo);
}
public void testTwoExecuteMethods() {
Compiler compiler = new Compiler(TwoExecuteMethods.class, Definition.BUILTINS);
@ -428,21 +474,10 @@ public class ImplementInterfacesTests extends ScriptTestCase {
+ TwoExecuteMethods.class.getName() + "] has more than one.", e.getMessage());
}
public interface BadMethod {
Object something();
}
public void testBadMethod() {
Compiler compiler = new Compiler(BadMethod.class, Definition.BUILTINS);
Exception e = expectScriptThrows(IllegalArgumentException.class, false, () ->
scriptEngine.compile(compiler, null, "null", emptyMap()));
assertEquals("Painless can only implement methods named [execute] and [uses$argName] but [" + BadMethod.class.getName()
+ "] contains a method named [something]", e.getMessage());
}
public interface BadUsesReturn {
String[] ARGUMENTS = new String[] {"foo"};
Object execute(String foo);
Object uses$foo();
public abstract static class BadUsesReturn {
public static final String[] PARAMETERS = new String[] {"foo"};
public abstract Object execute(String foo);
public abstract Object uses$foo();
}
public void testBadUsesReturn() {
Compiler compiler = new Compiler(BadUsesReturn.class, Definition.BUILTINS);
@ -452,10 +487,10 @@ public class ImplementInterfacesTests extends ScriptTestCase {
+ "#uses$foo] returns [java.lang.Object].", e.getMessage());
}
public interface BadUsesParameter {
String[] ARGUMENTS = new String[] {"foo", "bar"};
Object execute(String foo, String bar);
boolean uses$bar(boolean foo);
public abstract static class BadUsesParameter {
public static final String[] PARAMETERS = new String[] {"foo", "bar"};
public abstract Object execute(String foo, String bar);
public abstract boolean uses$bar(boolean foo);
}
public void testBadUsesParameter() {
Compiler compiler = new Compiler(BadUsesParameter.class, Definition.BUILTINS);
@ -465,10 +500,10 @@ public class ImplementInterfacesTests extends ScriptTestCase {
+ "#uses$bar] does.", e.getMessage());
}
public interface BadUsesName {
String[] ARGUMENTS = new String[] {"foo", "bar"};
Object execute(String foo, String bar);
boolean uses$baz();
public abstract static class BadUsesName {
public static final String[] PARAMETERS = new String[] {"foo", "bar"};
public abstract Object execute(String foo, String bar);
public abstract boolean uses$baz();
}
public void testBadUsesName() {
Compiler compiler = new Compiler(BadUsesName.class, Definition.BUILTINS);

View File

@ -34,9 +34,7 @@ import org.junit.Before;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.hamcrest.Matchers.hasSize;
@ -94,11 +92,11 @@ public abstract class ScriptTestCase extends ESTestCase {
// test for ambiguity errors before running the actual script if picky is true
if (picky) {
Definition definition = Definition.BUILTINS;
ScriptInterface scriptInterface = new ScriptInterface(definition, GenericElasticsearchScript.class);
ScriptClassInfo scriptClassInfo = new ScriptClassInfo(definition, GenericElasticsearchScript.class);
CompilerSettings pickySettings = new CompilerSettings();
pickySettings.setPicky(true);
pickySettings.setRegexesEnabled(CompilerSettings.REGEX_ENABLED.get(scriptEngineSettings()));
Walker.buildPainlessTree(scriptInterface, getTestName(), script, pickySettings,
Walker.buildPainlessTree(scriptClassInfo, getTestName(), script, pickySettings,
definition, null);
}
// test actual script execution

View File

@ -31,7 +31,7 @@ import org.elasticsearch.painless.FeatureTest;
import org.elasticsearch.painless.GenericElasticsearchScript;
import org.elasticsearch.painless.Locals.Variable;
import org.elasticsearch.painless.Location;
import org.elasticsearch.painless.ScriptInterface;
import org.elasticsearch.painless.ScriptClassInfo;
import org.elasticsearch.painless.Operation;
import org.elasticsearch.painless.antlr.Walker;
import org.elasticsearch.test.ESTestCase;
@ -898,11 +898,11 @@ public class NodeToStringTests extends ESTestCase {
}
private SSource walk(String code) {
ScriptInterface scriptInterface = new ScriptInterface(definition, GenericElasticsearchScript.class);
ScriptClassInfo scriptClassInfo = new ScriptClassInfo(definition, GenericElasticsearchScript.class);
CompilerSettings compilerSettings = new CompilerSettings();
compilerSettings.setRegexesEnabled(true);
try {
return Walker.buildPainlessTree(scriptInterface, getTestName(), code, compilerSettings,
return Walker.buildPainlessTree(scriptClassInfo, getTestName(), code, compilerSettings,
definition, null);
} catch (Exception e) {
throw new AssertionError("Failed to compile: " + code, e);