Allow painless to implement more interfaces (#22983)

Generalizes three previously hard coded things in painless into
generic concepts:

1. The "main method" is no longer hardcoded to:
```
public abstract Object execute(Map<String, Object> params,
        Scorer scorer, LeafDocLookup doc, Object value);
```
Instead Painless's compiler takes an interface and implements it. It looks like:
```
public interface SomeScript {
    // Argument names we expose to Painless scripts
    String[] ARGUMENTS = new String[] {"a", "b"};
    // Method implemented by Painless script. Must be named execute but can have any parameters or return any value.
    Object execute(String a, int b);
    // Is the "a" argument used by the script?
    boolean uses$a();
}
SomeScript script = scriptEngine.compile(SomeScript.class, null, "the_script_here", emptyMap());
Object result = script.execute("a", 1);
```

`PainlessScriptEngine` now compiles all scripts to the new
`GenericElasticsearchScript` interface by default for compatibility
with the rest of Elasticsearch until it is able to use this new
ability.

2. `_score` and `ctx` are no longer hardcoded to be extracted from
`#score` and `params` respectively. Instead Painless's default
implementation of Elasticsearch scripts uses the `uses$_score` and
`uses$ctx` methods to determine if it is used and gives them
dummy values if they are not used.

3. Throwing the `ScriptException` is now handled by the Painless
script itself. That way Painless doesn't have to leak the metadata
that is required to build the fancy stack trace. And all painless scripts
get the fancy stack trace.
This commit is contained in:
Nik Everett 2017-02-21 14:08:57 -05:00 committed by GitHub
parent fac2d954e3
commit 9105672969
18 changed files with 796 additions and 313 deletions

View File

@ -43,7 +43,7 @@ final class Compiler {
/**
* The maximum number of characters allowed in the script source.
*/
static int MAXIMUM_SOURCE_LENGTH = 16384;
static final int MAXIMUM_SOURCE_LENGTH = 16384;
/**
* Define the class with lowest privileges.
@ -77,39 +77,42 @@ final class Compiler {
* Generates a Class object from the generated byte code.
* @param name The name of the class.
* @param bytes The generated byte code.
* @return A Class object extending {@link Executable}.
* @return A Class object extending {@link PainlessScript}.
*/
Class<? extends Executable> define(String name, byte[] bytes) {
return defineClass(name, bytes, 0, bytes.length, CODESOURCE).asSubclass(Executable.class);
Class<? extends PainlessScript> define(String name, byte[] bytes) {
return defineClass(name, bytes, 0, bytes.length, CODESOURCE).asSubclass(PainlessScript.class);
}
}
/**
* Runs the two-pass compiler to generate a Painless script.
* @param <T> the type of the script
* @param loader The ClassLoader used to define the script.
* @param iface Interface the compiled script should implement
* @param name The name of the script.
* @param source The source code for the script.
* @param settings The CompilerSettings to be used during the compilation.
* @return An {@link Executable} Painless script.
* @return An executable script that implements both {@code <T>} and is a subclass of {@link PainlessScript}
*/
static Executable compile(Loader loader, String name, String source, CompilerSettings settings) {
static <T> T compile(Loader loader, Class<T> iface, 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(iface);
SSource root = Walker.buildPainlessTree(name, source, settings, null);
SSource root = Walker.buildPainlessTree(scriptInterface, name, source, settings, null);
root.analyze();
root.write();
try {
Class<? extends Executable> clazz = loader.define(CLASS_NAME, root.getBytes());
java.lang.reflect.Constructor<? extends Executable> constructor =
Class<? extends PainlessScript> clazz = loader.define(CLASS_NAME, root.getBytes());
java.lang.reflect.Constructor<? extends PainlessScript> constructor =
clazz.getConstructor(String.class, String.class, BitSet.class);
return constructor.newInstance(name, source, root.getStatements());
return iface.cast(constructor.newInstance(name, source, root.getStatements()));
} 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);
}
@ -117,18 +120,20 @@ final class Compiler {
/**
* Runs the two-pass compiler to generate a Painless script. (Used by the debugger.)
* @param iface Interface the compiled script should implement
* @param source The source code for the script.
* @param settings The CompilerSettings to be used during the compilation.
* @return The bytes for compilation.
*/
static byte[] compile(String name, String source, CompilerSettings settings, Printer debugStream) {
static byte[] compile(Class<?> iface, String name, String source, CompilerSettings settings, Printer debugStream) {
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(iface);
SSource root = Walker.buildPainlessTree(name, source, settings, debugStream);
SSource root = Walker.buildPainlessTree(scriptInterface, name, source, settings, debugStream);
root.analyze();
root.write();

View File

@ -1,70 +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.apache.lucene.search.Scorer;
import org.elasticsearch.search.lookup.LeafDocLookup;
import java.util.BitSet;
import java.util.Map;
/**
* The superclass used to build all Painless scripts on top of.
*/
public abstract class Executable {
private final String name;
private final String source;
private final BitSet statements;
public Executable(String name, String source, BitSet statements) {
this.name = name;
this.source = source;
this.statements = statements;
}
public String getName() {
return name;
}
public String getSource() {
return source;
}
/**
* Finds the start of the first statement boundary that is
* on or before {@code offset}. If one is not found, {@code -1}
* is returned.
*/
public int getPreviousStatement(int offset) {
return statements.previousSetBit(offset);
}
/**
* Finds the start of the first statement boundary that is
* after {@code offset}. If one is not found, {@code -1}
* is returned.
*/
public int getNextStatement(int offset) {
return statements.nextSetBit(offset+1);
}
public abstract Object execute(Map<String, Object> params, Scorer scorer, LeafDocLookup doc, Object value);
}

View File

@ -16,10 +16,20 @@
* specific language governing permissions and limitations
* under the License.
*/
package org.elasticsearch.painless;
import org.elasticsearch.index.fielddata.ScriptDocValues;
import java.util.Map;
/**
* Marker interface that a generated {@link Executable} uses the {@code _score} value
* Generic script interface that Painless implements for all Elasticsearch scripts.
*/
public interface NeedsScore {
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);
boolean uses$_score();
boolean uses$ctx();
}

View File

@ -22,6 +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 java.util.Arrays;
import java.util.Collection;
@ -37,36 +38,14 @@ import java.util.Set;
*/
public final class Locals {
/** Reserved word: params map parameter */
public static final String PARAMS = "params";
/** Reserved word: Lucene scorer parameter */
public static final String SCORER = "#scorer";
/** Reserved word: _value variable for aggregations */
public static final String VALUE = "_value";
/** Reserved word: _score variable for search scripts */
public static final String SCORE = "_score";
/** Reserved word: ctx map for executable scripts */
public static final String CTX = "ctx";
/** Reserved word: loop counter */
public static final String LOOP = "#loop";
/** Reserved word: unused */
public static final String THIS = "#this";
/** Reserved word: unused */
public static final String DOC = "doc";
/** Map of always reserved keywords for the main scope */
public static final Set<String> MAIN_KEYWORDS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
THIS,PARAMS,SCORER,DOC,VALUE,SCORE,CTX,LOOP
)));
/** Map of always reserved keywords for a function scope */
public static final Set<String> FUNCTION_KEYWORDS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
THIS,LOOP
)));
/** Map of always reserved keywords for a lambda scope */
public static final Set<String> LAMBDA_KEYWORDS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
THIS,LOOP
/** Set of reserved keywords. */
public static final Set<String> KEYWORDS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
THIS, LOOP
)));
/** Creates a new local variable scope (e.g. loop) inside the current scope */
@ -81,7 +60,7 @@ public final class Locals {
*/
public static Locals newLambdaScope(Locals programScope, Type returnType, List<Parameter> parameters,
int captureCount, int maxLoopCounter) {
Locals locals = new Locals(programScope, returnType, LAMBDA_KEYWORDS);
Locals locals = new Locals(programScope, returnType, KEYWORDS);
for (int i = 0; i < parameters.size(); i++) {
Parameter parameter = parameters.get(i);
// TODO: allow non-captures to be r/w:
@ -100,7 +79,7 @@ public final class Locals {
/** Creates a new function scope inside the current scope */
public static Locals newFunctionScope(Locals programScope, Type returnType, List<Parameter> parameters, int maxLoopCounter) {
Locals locals = new Locals(programScope, returnType, FUNCTION_KEYWORDS);
Locals locals = new Locals(programScope, returnType, KEYWORDS);
for (Parameter parameter : parameters) {
locals.addVariable(parameter.location, parameter.type, parameter.name, false);
}
@ -112,33 +91,14 @@ public final class Locals {
}
/** Creates a new main method scope */
public static Locals newMainMethodScope(Locals programScope, boolean usesScore, boolean usesCtx, int maxLoopCounter) {
Locals locals = new Locals(programScope, Definition.OBJECT_TYPE, MAIN_KEYWORDS);
public static Locals newMainMethodScope(ScriptInterface scriptInterface, Locals programScope, int maxLoopCounter) {
Locals locals = new Locals(programScope, Definition.OBJECT_TYPE, KEYWORDS);
// This reference. Internal use only.
locals.defineVariable(null, Definition.getType("Object"), THIS, true);
// Input map of variables passed to the script.
locals.defineVariable(null, Definition.getType("Map"), PARAMS, true);
// Scorer parameter passed to the script. Internal use only.
locals.defineVariable(null, Definition.DEF_TYPE, SCORER, true);
// Doc parameter passed to the script. TODO: Currently working as a Map, we can do better?
locals.defineVariable(null, Definition.getType("Map"), DOC, true);
// Aggregation _value parameter passed to the script.
locals.defineVariable(null, Definition.DEF_TYPE, VALUE, true);
// Shortcut variables.
// Document's score as a read-only double.
if (usesScore) {
locals.defineVariable(null, Definition.DOUBLE_TYPE, SCORE, true);
}
// The ctx map set by executable scripts as a read-only map.
if (usesCtx) {
locals.defineVariable(null, Definition.getType("Map"), CTX, true);
// Method arguments
for (MethodArgument arg : scriptInterface.getArguments()) {
locals.defineVariable(null, arg.getType(), arg.getName(), true);
}
// Loop counter to catch infinite loops. Internal use only.

View File

@ -66,7 +66,7 @@ public final class Location {
return exception;
}
// This maximum length is theoretically 65535 bytes, but as it's CESU-8 encoded we dont know how large it is in bytes, so be safe
// This maximum length is theoretically 65535 bytes, but as it's CESU-8 encoded we don't know how large it is in bytes, so be safe
private static final int MAX_NAME_LENGTH = 256;
/** Computes the file name (mostly important for stacktraces) */

View File

@ -46,7 +46,7 @@ public class PainlessExplainError extends Error {
/**
* Headers to be added to the {@link ScriptException} for structured rendering.
*/
Map<String, List<String>> getHeaders() {
public Map<String, List<String>> getHeaders() {
Map<String, List<String>> headers = new TreeMap<>();
String toString = "null";
String javaClassName = null;

View File

@ -0,0 +1,128 @@
/*
* 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.elasticsearch.script.ScriptException;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.List;
import java.util.Map;
/**
* Abstract superclass on top of which all Painless scripts are built.
*/
public abstract class PainlessScript {
/**
* Name of the script set at compile time.
*/
private final String name;
/**
* Source of the script.
*/
private final String source;
/**
* Character number of the start of each statement.
*/
private final BitSet statements;
protected PainlessScript(String name, String source, BitSet statements) {
this.name = name;
this.source = source;
this.statements = statements;
}
/**
* Adds stack trace and other useful information to exceptions thrown
* from a Painless script.
* @param t The throwable to build an exception around.
* @return The generated ScriptException.
*/
protected final ScriptException convertToScriptException(Throwable t, Map<String, List<String>> extraMetadata) {
// create a script stack: this is just the script portion
List<String> scriptStack = new ArrayList<>();
for (StackTraceElement element : t.getStackTrace()) {
if (WriterConstants.CLASS_NAME.equals(element.getClassName())) {
// found the script portion
int offset = element.getLineNumber();
if (offset == -1) {
scriptStack.add("<<< unknown portion of script >>>");
} else {
offset--; // offset is 1 based, line numbers must be!
int startOffset = getPreviousStatement(offset);
if (startOffset == -1) {
assert false; // should never happen unless we hit exc in ctor prologue...
startOffset = 0;
}
int endOffset = getNextStatement(startOffset);
if (endOffset == -1) {
endOffset = source.length();
}
// TODO: if this is still too long, truncate and use ellipses
String snippet = source.substring(startOffset, endOffset);
scriptStack.add(snippet);
StringBuilder pointer = new StringBuilder();
for (int i = startOffset; i < offset; i++) {
pointer.append(' ');
}
pointer.append("^---- HERE");
scriptStack.add(pointer.toString());
}
break;
// but filter our own internal stacks (e.g. indy bootstrap)
} else if (!shouldFilter(element)) {
scriptStack.add(element.toString());
}
}
// build a name for the script:
final String name;
if (PainlessScriptEngineService.INLINE_NAME.equals(this.name)) {
name = source;
} else {
name = this.name;
}
ScriptException scriptException = new ScriptException("runtime error", t, scriptStack, name, PainlessScriptEngineService.NAME);
for (Map.Entry<String, List<String>> entry : extraMetadata.entrySet()) {
scriptException.addMetadata(entry.getKey(), entry.getValue());
}
return scriptException;
}
/** returns true for methods that are part of the runtime */
private static boolean shouldFilter(StackTraceElement element) {
return element.getClassName().startsWith("org.elasticsearch.painless.") ||
element.getClassName().startsWith("java.lang.invoke.") ||
element.getClassName().startsWith("sun.invoke.");
}
/**
* Finds the start of the first statement boundary that is on or before {@code offset}. If one is not found, {@code -1} is returned.
*/
private int getPreviousStatement(int offset) {
return statements.previousSetBit(offset);
}
/**
* Finds the start of the first statement boundary that is after {@code offset}. If one is not found, {@code -1} is returned.
*/
private int getNextStatement(int offset) {
return statements.nextSetBit(offset + 1);
}
}

View File

@ -109,6 +109,10 @@ public final class PainlessScriptEngineService extends AbstractComponent impleme
@Override
public Object compile(String scriptName, final String scriptSource, final Map<String, String> params) {
return compile(GenericElasticsearchScript.class, scriptName, scriptSource, params);
}
<T> T compile(Class<T> iface, String scriptName, final String scriptSource, final Map<String, String> params) {
final CompilerSettings compilerSettings;
if (params.isEmpty()) {
@ -161,10 +165,11 @@ public final class PainlessScriptEngineService extends AbstractComponent impleme
try {
// Drop all permissions to actually compile the code itself.
return AccessController.doPrivileged(new PrivilegedAction<Executable>() {
return AccessController.doPrivileged(new PrivilegedAction<T>() {
@Override
public Executable run() {
return Compiler.compile(loader, scriptName == null ? INLINE_NAME : scriptName, scriptSource, compilerSettings);
public T run() {
String name = scriptName == null ? INLINE_NAME : scriptName;
return Compiler.compile(loader, iface, name, scriptSource, compilerSettings);
}
}, COMPILATION_CONTEXT);
// Note that it is safe to catch any of the following errors since Painless is stateless.
@ -181,7 +186,7 @@ public final class PainlessScriptEngineService extends AbstractComponent impleme
*/
@Override
public ExecutableScript executable(final CompiledScript compiledScript, final Map<String, Object> vars) {
return new ScriptImpl((Executable)compiledScript.compiled(), vars, null);
return new ScriptImpl((GenericElasticsearchScript) compiledScript.compiled(), vars, null);
}
/**
@ -201,7 +206,7 @@ public final class PainlessScriptEngineService extends AbstractComponent impleme
*/
@Override
public LeafSearchScript getLeafSearchScript(final LeafReaderContext context) throws IOException {
return new ScriptImpl((Executable)compiledScript.compiled(), vars, lookup.getLeafSearchLookup(context));
return new ScriptImpl((GenericElasticsearchScript) compiledScript.compiled(), vars, lookup.getLeafSearchLookup(context));
}
/**
@ -209,7 +214,7 @@ public final class PainlessScriptEngineService extends AbstractComponent impleme
*/
@Override
public boolean needsScores() {
return compiledScript.compiled() instanceof NeedsScore;
return ((GenericElasticsearchScript) compiledScript.compiled()).uses$_score();
}
};
}

View File

@ -20,18 +20,16 @@
package org.elasticsearch.painless;
import org.apache.lucene.search.Scorer;
import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.script.ExecutableScript;
import org.elasticsearch.script.LeafSearchScript;
import org.elasticsearch.script.ScriptException;
import org.elasticsearch.search.lookup.LeafDocLookup;
import org.elasticsearch.search.lookup.LeafSearchLookup;
import java.util.ArrayList;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static java.util.Collections.emptyMap;
import java.util.function.Function;
/**
* ScriptImpl can be used as either an {@link ExecutableScript} or a {@link LeafSearchScript}
@ -40,9 +38,9 @@ import static java.util.Collections.emptyMap;
final class ScriptImpl implements ExecutableScript, LeafSearchScript {
/**
* The Painless Executable script that can be run.
* The Painless script that can be run.
*/
private final Executable executable;
private final GenericElasticsearchScript script;
/**
* A map that can be used to access input parameters at run-time.
@ -59,6 +57,16 @@ final class ScriptImpl implements ExecutableScript, LeafSearchScript {
*/
private final LeafDocLookup doc;
/**
* Looks up the {@code _score} from {@link #scorer} if {@code _score} is used, otherwise returns {@code 0.0}.
*/
private final ScoreLookup scoreLookup;
/**
* Looks up the {@code ctx} from the {@link #variables} if {@code ctx} is used, otherwise return {@code null}.
*/
private final Function<Map<String, Object>, Map<?, ?>> ctxLookup;
/**
* Current scorer being used
* @see #setScorer(Scorer)
@ -73,12 +81,12 @@ final class ScriptImpl implements ExecutableScript, LeafSearchScript {
/**
* Creates a ScriptImpl for the a previously compiled Painless script.
* @param executable The previously compiled Painless script.
* @param script The previously compiled Painless script.
* @param vars The initial variables to run the script with.
* @param lookup The lookup to allow search fields to be available if this is run as a search script.
*/
ScriptImpl(final Executable executable, final Map<String, Object> vars, final LeafSearchLookup lookup) {
this.executable = executable;
ScriptImpl(final GenericElasticsearchScript script, final Map<String, Object> vars, final LeafSearchLookup lookup) {
this.script = script;
this.lookup = lookup;
this.variables = new HashMap<>();
@ -92,6 +100,9 @@ final class ScriptImpl implements ExecutableScript, LeafSearchScript {
} else {
doc = null;
}
scoreLookup = script.uses$_score() ? ScriptImpl::getScore : scorer -> 0.0;
ctxLookup = script.uses$ctx() ? variables -> (Map<?, ?>) variables.get("ctx") : variables -> null;
}
/**
@ -119,77 +130,7 @@ final class ScriptImpl implements ExecutableScript, LeafSearchScript {
*/
@Override
public Object run() {
try {
return executable.execute(variables, scorer, doc, aggregationValue);
} catch (PainlessExplainError e) {
throw convertToScriptException(e, e.getHeaders());
// Note that it is safe to catch any of the following errors since Painless is stateless.
} catch (PainlessError | BootstrapMethodError | OutOfMemoryError | StackOverflowError | Exception e) {
throw convertToScriptException(e, emptyMap());
}
}
/**
* Adds stack trace and other useful information to exceptions thrown
* from a Painless script.
* @param t The throwable to build an exception around.
* @return The generated ScriptException.
*/
private ScriptException convertToScriptException(Throwable t, Map<String, List<String>> metadata) {
// create a script stack: this is just the script portion
List<String> scriptStack = new ArrayList<>();
for (StackTraceElement element : t.getStackTrace()) {
if (WriterConstants.CLASS_NAME.equals(element.getClassName())) {
// found the script portion
int offset = element.getLineNumber();
if (offset == -1) {
scriptStack.add("<<< unknown portion of script >>>");
} else {
offset--; // offset is 1 based, line numbers must be!
int startOffset = executable.getPreviousStatement(offset);
if (startOffset == -1) {
assert false; // should never happen unless we hit exc in ctor prologue...
startOffset = 0;
}
int endOffset = executable.getNextStatement(startOffset);
if (endOffset == -1) {
endOffset = executable.getSource().length();
}
// TODO: if this is still too long, truncate and use ellipses
String snippet = executable.getSource().substring(startOffset, endOffset);
scriptStack.add(snippet);
StringBuilder pointer = new StringBuilder();
for (int i = startOffset; i < offset; i++) {
pointer.append(' ');
}
pointer.append("^---- HERE");
scriptStack.add(pointer.toString());
}
break;
// but filter our own internal stacks (e.g. indy bootstrap)
} else if (!shouldFilter(element)) {
scriptStack.add(element.toString());
}
}
// build a name for the script:
final String name;
if (PainlessScriptEngineService.INLINE_NAME.equals(executable.getName())) {
name = executable.getSource();
} else {
name = executable.getName();
}
ScriptException scriptException = new ScriptException("runtime error", t, scriptStack, name, PainlessScriptEngineService.NAME);
for (Map.Entry<String, List<String>> entry : metadata.entrySet()) {
scriptException.addMetadata(entry.getKey(), entry.getValue());
}
return scriptException;
}
/** returns true for methods that are part of the runtime */
private static boolean shouldFilter(StackTraceElement element) {
return element.getClassName().startsWith("org.elasticsearch.painless.") ||
element.getClassName().startsWith("java.lang.invoke.") ||
element.getClassName().startsWith("sun.invoke.");
return script.execute(variables, scoreLookup.apply(scorer), doc, aggregationValue, ctxLookup.apply(variables));
}
/**
@ -240,4 +181,16 @@ final class ScriptImpl implements ExecutableScript, LeafSearchScript {
lookup.source().setSource(source);
}
}
private static double getScore(Scorer scorer) {
try {
return scorer.score();
} catch (IOException e) {
throw new ElasticsearchException("couldn't lookup score", e);
}
}
interface ScoreLookup {
double apply(Scorer scorer);
}
}

View File

@ -0,0 +1,176 @@
/*
* 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 java.lang.invoke.MethodType;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import static java.util.Collections.unmodifiableList;
import static org.elasticsearch.painless.WriterConstants.USES_PARAMETER_METHOD_TYPE;
/**
* Information about the interface being implemented by the painless script.
*/
public class ScriptInterface {
private final Class<?> iface;
private final org.objectweb.asm.commons.Method executeMethod;
private final List<MethodArgument> arguments;
private final List<org.objectweb.asm.commons.Method> usesMethods;
public ScriptInterface(Class<?> iface) {
this.iface = iface;
// 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()) {
if (m.isDefault()) {
continue;
}
if (m.getName().equals("execute")) {
if (executeMethod == null) {
executeMethod = m;
} else {
throw new IllegalArgumentException(
"Painless can only implement interfaces that have a single method named [execute] but [" + iface.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() + "].");
}
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.");
}
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() + "]");
}
MethodType methodType = MethodType.methodType(executeMethod.getReturnType(), executeMethod.getParameterTypes());
this.executeMethod = new org.objectweb.asm.commons.Method(executeMethod.getName(), methodType.toMethodDescriptorString());
// Look up the argument names
Set<String> argumentNames = new LinkedHashSet<>();
List<MethodArgument> arguments = new ArrayList<>();
String[] argumentNamesConstant = readArgumentNamesConstant(iface);
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.");
}
for (int arg = 0; arg < types.length; arg++) {
arguments.add(new MethodArgument(argType(argumentNamesConstant[arg], types[arg]), argumentNamesConstant[arg]));
argumentNames.add(argumentNamesConstant[arg]);
}
this.arguments = unmodifiableList(arguments);
// Validate that the uses$argName methods reference argument names
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 + ".");
}
}
this.usesMethods = unmodifiableList(usesMethods);
}
public Class<?> getInterface() {
return iface;
}
public org.objectweb.asm.commons.Method getExecuteMethod() {
return executeMethod;
}
public List<MethodArgument> getArguments() {
return arguments;
}
public List<org.objectweb.asm.commons.Method> getUsesMethods() {
return usesMethods;
}
public static class MethodArgument {
private final Definition.Type type;
private final String name;
public MethodArgument(Definition.Type type, String name) {
this.type = type;
this.name = name;
}
public Definition.Type getType() {
return type;
}
public String getName() {
return name;
}
}
private static Definition.Type argType(String argName, Class<?> type) {
int dimensions = 0;
while (type.isArray()) {
dimensions++;
type = type.getComponentType();
}
Definition.Struct struct;
if (type.equals(Object.class)) {
struct = Definition.DEF_TYPE.struct;
} else {
Definition.RuntimeClass runtimeClass = Definition.getRuntimeClass(type);
if (runtimeClass == null) {
throw new IllegalArgumentException("[" + argName + "] is of unknown type [" + type.getName()
+ ". Painless interfaces can only accept arguments that are of whitelisted types.");
}
struct = runtimeClass.getStruct();
}
return Definition.getType(struct, dimensions);
}
private static String[] readArgumentNamesConstant(Class<?> iface) {
Field argumentNamesField;
try {
argumentNamesField = iface.getField("ARGUMENTS");
} catch (NoSuchFieldException e) {
throw new IllegalArgumentException("Painless needs a constant [String[] ARGUMENTS] 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 "
+ "names of the method arguments but [" + iface.getName() + "] doesn't have one.");
}
try {
return (String[]) argumentNamesField.get(null);
} catch (IllegalArgumentException | IllegalAccessException e) {
throw new IllegalArgumentException("Error trying to read [" + iface.getName() + "#ARGUMENTS]", e);
}
}
}

View File

@ -19,9 +19,8 @@
package org.elasticsearch.painless;
import org.apache.lucene.search.Scorer;
import org.elasticsearch.painless.api.Augmentation;
import org.elasticsearch.search.lookup.LeafDocLookup;
import org.elasticsearch.script.ScriptException;
import org.objectweb.asm.Handle;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
@ -34,6 +33,7 @@ import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.util.BitSet;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.Map;
import java.util.Objects;
@ -47,22 +47,30 @@ public final class WriterConstants {
public static final int CLASS_VERSION = Opcodes.V1_8;
public static final int ASM_VERSION = Opcodes.ASM5;
public static final String BASE_CLASS_NAME = Executable.class.getName();
public static final Type BASE_CLASS_TYPE = Type.getType(Executable.class);
public static final String BASE_CLASS_NAME = PainlessScript.class.getName();
public static final Type BASE_CLASS_TYPE = Type.getType(PainlessScript.class);
public static final Method CONVERT_TO_SCRIPT_EXCEPTION_METHOD = getAsmMethod(ScriptException.class, "convertToScriptException",
Throwable.class, Map.class);
public static final String CLASS_NAME = BASE_CLASS_NAME + "$Script";
public static final Type CLASS_TYPE = Type.getObjectType(CLASS_NAME.replace('.', '/'));
public static final Method CONSTRUCTOR = getAsmMethod(void.class, "<init>", String.class, String.class, BitSet.class);
public static final Method CLINIT = getAsmMethod(void.class, "<clinit>");
public static final Method EXECUTE =
getAsmMethod(Object.class, "execute", Map.class, Scorer.class, LeafDocLookup.class, Object.class);
// All of these types are caught by the main method and rethrown as ScriptException
public static final Type PAINLESS_ERROR_TYPE = Type.getType(PainlessError.class);
public static final Type BOOTSTRAP_METHOD_ERROR_TYPE = Type.getType(BootstrapMethodError.class);
public static final Type OUT_OF_MEMORY_ERROR_TYPE = Type.getType(OutOfMemoryError.class);
public static final Type STACK_OVERFLOW_ERROR_TYPE = Type.getType(StackOverflowError.class);
public static final Type EXCEPTION_TYPE = Type.getType(Exception.class);
public static final Type PAINLESS_EXPLAIN_ERROR_TYPE = Type.getType(PainlessExplainError.class);
public static final Method PAINLESS_EXPLAIN_ERROR_GET_HEADERS_METHOD = getAsmMethod(Map.class, "getHeaders");
public static final Type NEEDS_SCORE_TYPE = Type.getType(NeedsScore.class);
public static final Type SCORER_TYPE = Type.getType(Scorer.class);
public static final Method SCORER_SCORE = getAsmMethod(float.class, "score");
public static final Type COLLECTIONS_TYPE = Type.getType(Collections.class);
public static final Method EMPTY_MAP_METHOD = getAsmMethod(Map.class, "emptyMap");
public static final MethodType USES_PARAMETER_METHOD_TYPE = MethodType.methodType(boolean.class);
public static final Type MAP_TYPE = Type.getType(Map.class);
public static final Method MAP_GET = getAsmMethod(Object.class, "get", Object.class);

View File

@ -31,6 +31,7 @@ import org.antlr.v4.runtime.tree.TerminalNode;
import org.elasticsearch.painless.CompilerSettings;
import org.elasticsearch.painless.Globals;
import org.elasticsearch.painless.Location;
import org.elasticsearch.painless.ScriptInterface;
import org.elasticsearch.painless.Operation;
import org.elasticsearch.painless.antlr.PainlessParser.AfterthoughtContext;
import org.elasticsearch.painless.antlr.PainlessParser.ArgumentContext;
@ -172,10 +173,12 @@ import java.util.List;
*/
public final class Walker extends PainlessParserBaseVisitor<ANode> {
public static SSource buildPainlessTree(String sourceName, String sourceText, CompilerSettings settings, Printer debugStream) {
return new Walker(sourceName, sourceText, settings, debugStream).source;
public static SSource buildPainlessTree(ScriptInterface mainMethod, String sourceName, String sourceText, CompilerSettings settings,
Printer debugStream) {
return new Walker(mainMethod, sourceName, sourceText, settings, debugStream).source;
}
private final ScriptInterface scriptInterface;
private final SSource source;
private final CompilerSettings settings;
private final Printer debugStream;
@ -186,7 +189,8 @@ public final class Walker extends PainlessParserBaseVisitor<ANode> {
private final Globals globals;
private int syntheticCounter = 0;
private Walker(String sourceName, String sourceText, CompilerSettings settings, Printer debugStream) {
private Walker(ScriptInterface scriptInterface, String sourceName, String sourceText, CompilerSettings settings, Printer debugStream) {
this.scriptInterface = scriptInterface;
this.debugStream = debugStream;
this.settings = settings;
this.sourceName = Location.computeSourceName(sourceName, sourceText);
@ -256,7 +260,7 @@ public final class Walker extends PainlessParserBaseVisitor<ANode> {
statements.add((AStatement)visit(statement));
}
return new SSource(settings, sourceName, sourceText, debugStream, (MainMethodReserved)reserved.pop(),
return new SSource(scriptInterface, settings, sourceName, sourceText, debugStream, (MainMethodReserved)reserved.pop(),
location(ctx), functions, globals, statements);
}
@ -850,7 +854,7 @@ public final class Walker extends PainlessParserBaseVisitor<ANode> {
@Override
public ANode visitVariable(VariableContext ctx) {
String name = ctx.ID().getText();
reserved.peek().markReserved(name);
reserved.peek().markUsedVariable(name);
return new EVariable(location(ctx), name);
}

View File

@ -56,14 +56,11 @@ public final class SFunction extends AStatement {
public static final class FunctionReserved implements Reserved {
private int maxLoopCounter = 0;
public void markReserved(String name) {
@Override
public void markUsedVariable(String name) {
// Do nothing.
}
public boolean isReserved(String name) {
return Locals.FUNCTION_KEYWORDS.contains(name);
}
@Override
public void setMaxLoopCounter(int max) {
maxLoopCounter = max;

View File

@ -23,17 +23,19 @@ import org.elasticsearch.painless.CompilerSettings;
import org.elasticsearch.painless.Constant;
import org.elasticsearch.painless.Definition.Method;
import org.elasticsearch.painless.Definition.MethodKey;
import org.elasticsearch.painless.Executable;
import org.elasticsearch.painless.Globals;
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.SimpleChecksAdapter;
import org.elasticsearch.painless.WriterConstants;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Label;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.util.Printer;
import org.objectweb.asm.util.TraceClassVisitor;
@ -42,18 +44,27 @@ import java.util.BitSet;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import static java.util.Collections.emptyList;
import static java.util.Collections.unmodifiableSet;
import static org.elasticsearch.painless.WriterConstants.BASE_CLASS_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.EXECUTE;
import static org.elasticsearch.painless.WriterConstants.MAP_GET;
import static org.elasticsearch.painless.WriterConstants.MAP_TYPE;
import static org.elasticsearch.painless.WriterConstants.CONVERT_TO_SCRIPT_EXCEPTION_METHOD;
import static org.elasticsearch.painless.WriterConstants.EMPTY_MAP_METHOD;
import static org.elasticsearch.painless.WriterConstants.EXCEPTION_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;
import static org.elasticsearch.painless.WriterConstants.PAINLESS_EXPLAIN_ERROR_TYPE;
import static org.elasticsearch.painless.WriterConstants.STACK_OVERFLOW_ERROR_TYPE;
/**
* The root of all Painless trees. Contains a series of statements.
@ -61,44 +72,25 @@ import static org.elasticsearch.painless.WriterConstants.MAP_TYPE;
public final class SSource extends AStatement {
/**
* Tracks reserved variables. Must be given to any source of input
* Tracks derived arguments and the loop counter. Must be given to any source of input
* prior to beginning the analysis phase so that reserved variables
* are known ahead of time to assign appropriate slots without
* being wasteful.
*/
public interface Reserved {
void markReserved(String name);
boolean isReserved(String name);
void markUsedVariable(String name);
void setMaxLoopCounter(int max);
int getMaxLoopCounter();
}
public static final class MainMethodReserved implements Reserved {
private boolean score = false;
private boolean ctx = false;
private final Set<String> usedVariables = new HashSet<>();
private int maxLoopCounter = 0;
@Override
public void markReserved(String name) {
if (Locals.SCORE.equals(name)) {
score = true;
} else if (Locals.CTX.equals(name)) {
ctx = true;
}
}
@Override
public boolean isReserved(String name) {
return Locals.MAIN_KEYWORDS.contains(name);
}
public boolean usesScore() {
return score;
}
public boolean usesCtx() {
return ctx;
public void markUsedVariable(String name) {
usedVariables.add(name);
}
@Override
@ -110,8 +102,13 @@ public final class SSource extends AStatement {
public int getMaxLoopCounter() {
return maxLoopCounter;
}
public Set<String> getUsedVariables() {
return unmodifiableSet(usedVariables);
}
}
private final ScriptInterface scriptInterface;
private final CompilerSettings settings;
private final String name;
private final String source;
@ -124,10 +121,11 @@ public final class SSource extends AStatement {
private Locals mainMethod;
private byte[] bytes;
public SSource(CompilerSettings settings, String name, String source, Printer debugStream,
public SSource(ScriptInterface scriptInterface, 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.settings = Objects.requireNonNull(settings);
this.name = Objects.requireNonNull(name);
this.source = Objects.requireNonNull(source);
@ -175,7 +173,7 @@ public final class SSource extends AStatement {
throw createError(new IllegalArgumentException("Cannot generate an empty script."));
}
mainMethod = Locals.newMainMethodScope(program, reserved.usesScore(), reserved.usesCtx(), reserved.getMaxLoopCounter());
mainMethod = Locals.newMainMethodScope(scriptInterface, program, reserved.getMaxLoopCounter());
AStatement last = statements.get(statements.size() - 1);
@ -202,7 +200,7 @@ public final class SSource extends AStatement {
int classAccess = Opcodes.ACC_PUBLIC | Opcodes.ACC_SUPER | Opcodes.ACC_FINAL;
String classBase = BASE_CLASS_TYPE.getInternalName();
String className = CLASS_TYPE.getInternalName();
String classInterfaces[] = reserved.usesScore() ? new String[] { WriterConstants.NEEDS_SCORE_TYPE.getInternalName() } : null;
String classInterfaces[] = new String[] { Type.getType(scriptInterface.getInterface()).getInternalName() };
ClassWriter writer = new ClassWriter(classFrames);
ClassVisitor visitor = writer;
@ -223,15 +221,16 @@ public final class SSource extends AStatement {
constructor.visitCode();
constructor.loadThis();
constructor.loadArgs();
constructor.invokeConstructor(org.objectweb.asm.Type.getType(Executable.class), CONSTRUCTOR);
constructor.invokeConstructor(BASE_CLASS_TYPE, CONSTRUCTOR);
constructor.returnValue();
constructor.endMethod();
// Write the execute method:
MethodWriter execute = new MethodWriter(Opcodes.ACC_PUBLIC, EXECUTE, visitor, globals.getStatements(), settings);
execute.visitCode();
write(execute, globals);
execute.endMethod();
// Write the method defined in the interface:
MethodWriter executeMethod = new MethodWriter(Opcodes.ACC_PUBLIC, scriptInterface.getExecuteMethod(), visitor,
globals.getStatements(), settings);
executeMethod.visitCode();
write(executeMethod, globals);
executeMethod.endMethod();
// Write all functions:
for (SFunction function : functions) {
@ -273,6 +272,15 @@ public final class SSource extends AStatement {
clinit.endMethod();
}
// Write any uses$varName methods for used variables
for (org.objectweb.asm.commons.Method usesMethod : scriptInterface.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())));
ifaceMethod.returnValue();
ifaceMethod.endMethod();
}
// End writing the class and store the generated bytes.
visitor.visitEnd();
@ -281,30 +289,13 @@ public final class SSource extends AStatement {
@Override
void write(MethodWriter writer, Globals globals) {
if (reserved.usesScore()) {
// if the _score value is used, we do this once:
// final double _score = scorer.score();
Variable scorer = mainMethod.getVariable(null, Locals.SCORER);
Variable score = mainMethod.getVariable(null, Locals.SCORE);
writer.visitVarInsn(Opcodes.ALOAD, scorer.getSlot());
writer.invokeVirtual(WriterConstants.SCORER_TYPE, WriterConstants.SCORER_SCORE);
writer.visitInsn(Opcodes.F2D);
writer.visitVarInsn(Opcodes.DSTORE, score.getSlot());
}
if (reserved.usesCtx()) {
// if the _ctx value is used, we do this once:
// final Map<String,Object> ctx = input.get("ctx");
Variable input = mainMethod.getVariable(null, Locals.PARAMS);
Variable ctx = mainMethod.getVariable(null, Locals.CTX);
writer.visitVarInsn(Opcodes.ALOAD, input.getSlot());
writer.push(Locals.CTX);
writer.invokeInterface(MAP_TYPE, MAP_GET);
writer.visitVarInsn(Opcodes.ASTORE, ctx.getSlot());
}
// We wrap the whole method in a few try/catches to handle and/or convert other exceptions to ScriptException
Label startTry = new Label();
Label endTry = new Label();
Label startExplainCatch = new Label();
Label startOtherCatch = new Label();
Label endCatch = new Label();
writer.mark(startTry);
if (reserved.getMaxLoopCounter() > 0) {
// if there is infinite loop protection, we do this once:
@ -324,6 +315,38 @@ public final class SSource extends AStatement {
writer.visitInsn(Opcodes.ACONST_NULL);
writer.returnValue();
}
writer.mark(endTry);
writer.goTo(endCatch);
// This looks like:
// } catch (PainlessExplainError e) {
// throw this.convertToScriptException(e, e.getHeaders())
// }
writer.visitTryCatchBlock(startTry, endTry, startExplainCatch, PAINLESS_EXPLAIN_ERROR_TYPE.getInternalName());
writer.mark(startExplainCatch);
writer.loadThis();
writer.swap();
writer.dup();
writer.invokeVirtual(PAINLESS_EXPLAIN_ERROR_TYPE, PAINLESS_EXPLAIN_ERROR_GET_HEADERS_METHOD);
writer.invokeVirtual(BASE_CLASS_TYPE, CONVERT_TO_SCRIPT_EXCEPTION_METHOD);
writer.throwException();
// This looks like:
// } catch (PainlessError | BootstrapMethodError | OutOfMemoryError | StackOverflowError | Exception e) {
// throw this.convertToScriptException(e, e.getHeaders())
// }
// We *think* it is ok to catch OutOfMemoryError and StackOverflowError because Painless is stateless
writer.visitTryCatchBlock(startTry, endTry, startOtherCatch, PAINLESS_ERROR_TYPE.getInternalName());
writer.visitTryCatchBlock(startTry, endTry, startOtherCatch, BOOTSTRAP_METHOD_ERROR_TYPE.getInternalName());
writer.visitTryCatchBlock(startTry, endTry, startOtherCatch, OUT_OF_MEMORY_ERROR_TYPE.getInternalName());
writer.visitTryCatchBlock(startTry, endTry, startOtherCatch, STACK_OVERFLOW_ERROR_TYPE.getInternalName());
writer.visitTryCatchBlock(startTry, endTry, startOtherCatch, EXCEPTION_TYPE.getInternalName());
writer.mark(startOtherCatch);
writer.loadThis();
writer.swap();
writer.invokeStatic(COLLECTIONS_TYPE, EMPTY_MAP_METHOD);
writer.invokeVirtual(BASE_CLASS_TYPE, CONVERT_TO_SCRIPT_EXCEPTION_METHOD);
writer.throwException();
writer.mark(endCatch);
}
public BitSet getStatements() {

View File

@ -30,16 +30,16 @@ final class Debugger {
/** compiles source to bytecode, and returns debugging output */
static String toString(final String source) {
return toString(source, new CompilerSettings());
return toString(GenericElasticsearchScript.class, source, new CompilerSettings());
}
/** compiles to bytecode, and returns debugging output */
static String toString(String source, CompilerSettings settings) {
static String toString(Class<?> iface, String source, CompilerSettings settings) {
StringWriter output = new StringWriter();
PrintWriter outputWriter = new PrintWriter(output);
Textifier textifier = new Textifier();
try {
Compiler.compile("<debugging>", source, settings, textifier);
Compiler.compile(iface, "<debugging>", source, settings, textifier);
} catch (Exception e) {
textifier.print(outputWriter);
e.addSuppressed(new Exception("current bytecode: \n" + output));

View File

@ -0,0 +1,280 @@
/*
* 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 java.util.HashMap;
import java.util.Map;
import static java.util.Collections.emptyMap;
import static java.util.Collections.singletonMap;
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 void testNoArgs() {
assertEquals(1, scriptEngine.compile(NoArgs.class, null, "1", emptyMap()).execute());
assertEquals("foo", scriptEngine.compile(NoArgs.class, null, "'foo'", emptyMap()).execute());
Exception e = expectScriptThrows(IllegalArgumentException.class, () ->
scriptEngine.compile(NoArgs.class, null, "doc", emptyMap()));
assertEquals("Variable [doc] is not defined.", e.getMessage());
// _score was once embedded into painless by deep magic
e = expectScriptThrows(IllegalArgumentException.class, () ->
scriptEngine.compile(NoArgs.class, null, "_score", emptyMap()));
assertEquals("Variable [_score] is not defined.", e.getMessage());
}
public interface OneArg {
String[] ARGUMENTS = new String[] {"arg"};
Object execute(Object arg);
}
public void testOneArg() {
Object rando = randomInt();
assertEquals(rando, scriptEngine.compile(OneArg.class, null, "arg", emptyMap()).execute(rando));
rando = randomAsciiOfLength(5);
assertEquals(rando, scriptEngine.compile(OneArg.class, null, "arg", emptyMap()).execute(rando));
Exception e = expectScriptThrows(IllegalArgumentException.class, () ->
scriptEngine.compile(NoArgs.class, null, "doc", emptyMap()));
assertEquals("Variable [doc] is not defined.", e.getMessage());
// _score was once embedded into painless by deep magic
e = expectScriptThrows(IllegalArgumentException.class, () ->
scriptEngine.compile(NoArgs.class, null, "_score", emptyMap()));
assertEquals("Variable [_score] is not defined.", e.getMessage());
}
public interface ArrayArg {
String[] ARGUMENTS = new String[] {"arg"};
Object execute(String[] arg);
}
public void testArrayArg() {
String rando = randomAsciiOfLength(5);
assertEquals(rando, scriptEngine.compile(ArrayArg.class, null, "arg[0]", emptyMap()).execute(new String[] {rando, "foo"}));
}
public interface PrimitiveArrayArg {
String[] ARGUMENTS = new String[] {"arg"};
Object execute(int[] arg);
}
public void testPrimitiveArrayArg() {
int rando = randomInt();
assertEquals(rando, scriptEngine.compile(PrimitiveArrayArg.class, null, "arg[0]", emptyMap()).execute(new int[] {rando, 10}));
}
public interface DefArrayArg {
String[] ARGUMENTS = new String[] {"arg"};
Object execute(Object[] arg);
}
public void testDefArrayArg() {
Object rando = randomInt();
assertEquals(rando, scriptEngine.compile(DefArrayArg.class, null, "arg[0]", emptyMap()).execute(new Object[] {rando, 10}));
rando = randomAsciiOfLength(5);
assertEquals(rando, scriptEngine.compile(DefArrayArg.class, null, "arg[0]", emptyMap()).execute(new Object[] {rando, 10}));
assertEquals(5, scriptEngine.compile(DefArrayArg.class, 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 void testManyArgs() {
int rando = randomInt();
assertEquals(rando, scriptEngine.compile(ManyArgs.class, null, "a", emptyMap()).execute(rando, 0, 0, 0));
assertEquals(10, scriptEngine.compile(ManyArgs.class, null, "a + b + c + d", emptyMap()).execute(1, 2, 3, 4));
// While we're here we can verify that painless correctly finds used variables
ManyArgs script = scriptEngine.compile(ManyArgs.class, null, "a", emptyMap());
assertTrue(script.uses$a());
assertFalse(script.uses$b());
assertFalse(script.uses$c());
assertFalse(script.uses$d());
script = scriptEngine.compile(ManyArgs.class, null, "a + b + c", emptyMap());
assertTrue(script.uses$a());
assertTrue(script.uses$b());
assertTrue(script.uses$c());
assertFalse(script.uses$d());
script = scriptEngine.compile(ManyArgs.class, null, "a + b + c + d", emptyMap());
assertTrue(script.uses$a());
assertTrue(script.uses$b());
assertTrue(script.uses$c());
assertTrue(script.uses$d());
}
public interface VarargTest {
String[] ARGUMENTS = new String[] {"arg"};
Object execute(String... arg);
}
public void testVararg() {
assertEquals("foo bar baz", scriptEngine.compile(VarargTest.class, null, "String.join(' ', Arrays.asList(arg))", emptyMap())
.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() {
return execute(1, 1, 1, 1);
}
default Object executeWithASingleOne(int a, int b, int c) {
return execute(a, b, c, 1);
}
}
public void testDefaultMethods() {
int rando = randomInt();
assertEquals(rando, scriptEngine.compile(DefaultMethods.class, null, "a", emptyMap()).execute(rando, 0, 0, 0));
assertEquals(rando, scriptEngine.compile(DefaultMethods.class, null, "a", emptyMap()).executeWithASingleOne(rando, 0, 0));
assertEquals(10, scriptEngine.compile(DefaultMethods.class, null, "a + b + c + d", emptyMap()).execute(1, 2, 3, 4));
assertEquals(4, scriptEngine.compile(DefaultMethods.class, null, "a + b + c + d", emptyMap()).executeWithOne());
assertEquals(7, scriptEngine.compile(DefaultMethods.class, 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 void testReturnsVoid() {
Map<String, Object> map = new HashMap<>();
scriptEngine.compile(ReturnsVoid.class, null, "map.a = 'foo'", emptyMap()).execute(map);
assertEquals(singletonMap("a", "foo"), map);
scriptEngine.compile(ReturnsVoid.class, null, "map.remove('a')", emptyMap()).execute(map);
assertEquals(emptyMap(), map);
}
public interface NoArgumentsConstant {
Object execute(String foo);
}
public void testNoArgumentsConstant() {
Exception e = expectScriptThrows(IllegalArgumentException.class, () ->
scriptEngine.compile(NoArgumentsConstant.class, null, "1", emptyMap()));
assertThat(e.getMessage(), startsWith("Painless needs a constant [String[] ARGUMENTS] 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 void testWrongArgumentsConstant() {
Exception e = expectScriptThrows(IllegalArgumentException.class, () ->
scriptEngine.compile(WrongArgumentsConstant.class, null, "1", emptyMap()));
assertThat(e.getMessage(), startsWith("Painless needs a constant [String[] ARGUMENTS] 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 void testWrongLengthOfArgumentConstant() {
Exception e = expectScriptThrows(IllegalArgumentException.class, () ->
scriptEngine.compile(WrongLengthOfArgumentConstant.class, null, "1", emptyMap()));
assertThat(e.getMessage(), startsWith("[" + WrongLengthOfArgumentConstant.class.getName() + "#ARGUMENTS] has length [2] but ["
+ WrongLengthOfArgumentConstant.class.getName() + "#execute] takes [1] argument."));
}
public interface UnknownArgType {
String[] ARGUMENTS = new String[] {"foo"};
Object execute(UnknownArgType foo);
}
public void testUnknownArgType() {
Exception e = expectScriptThrows(IllegalArgumentException.class, () ->
scriptEngine.compile(UnknownArgType.class, null, "1", emptyMap()));
assertEquals("[foo] is of unknown type [" + UnknownArgType.class.getName() + ". Painless interfaces can only accept arguments "
+ "that are of whitelisted types.", e.getMessage());
}
public interface UnknownArgTypeInArray {
String[] ARGUMENTS = new String[] {"foo"};
Object execute(UnknownArgTypeInArray[] foo);
}
public void testUnknownArgTypeInArray() {
Exception e = expectScriptThrows(IllegalArgumentException.class, () ->
scriptEngine.compile(UnknownArgTypeInArray.class, null, "1", emptyMap()));
assertEquals("[foo] is of unknown type [" + UnknownArgTypeInArray.class.getName() + ". Painless interfaces can only accept "
+ "arguments that are of whitelisted types.", e.getMessage());
}
public interface TwoExecuteMethods {
Object execute();
Object execute(boolean foo);
}
public void testTwoExecuteMethods() {
Exception e = expectScriptThrows(IllegalArgumentException.class, () ->
scriptEngine.compile(TwoExecuteMethods.class, null, "null", emptyMap()));
assertEquals("Painless can only implement interfaces that have a single method named [execute] but ["
+ TwoExecuteMethods.class.getName() + "] has more than one.", e.getMessage());
}
public interface BadMethod {
Object something();
}
public void testBadMethod() {
Exception e = expectScriptThrows(IllegalArgumentException.class, () ->
scriptEngine.compile(BadMethod.class, 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 void testBadUsesReturn() {
Exception e = expectScriptThrows(IllegalArgumentException.class, () ->
scriptEngine.compile(BadUsesReturn.class, null, "null", emptyMap()));
assertEquals("Painless can only implement uses$ methods that return boolean but [" + BadUsesReturn.class.getName()
+ "#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 void testBadUsesParameter() {
Exception e = expectScriptThrows(IllegalArgumentException.class, () ->
scriptEngine.compile(BadUsesParameter.class, null, "null", emptyMap()));
assertEquals("Painless can only implement uses$ methods that do not take parameters but [" + BadUsesParameter.class.getName()
+ "#uses$bar] does.", e.getMessage());
}
public interface BadUsesName {
String[] ARGUMENTS = new String[] {"foo", "bar"};
Object execute(String foo, String bar);
boolean uses$baz();
}
public void testBadUsesName() {
Exception e = expectScriptThrows(IllegalArgumentException.class, () ->
scriptEngine.compile(BadUsesName.class, null, "null", emptyMap()));
assertEquals("Painless can only implement uses$ methods that match a parameter name but [" + BadUsesName.class.getName()
+ "#uses$baz] doesn't match any of [foo, bar].", e.getMessage());
}
}

View File

@ -19,6 +19,8 @@
package org.elasticsearch.painless;
import junit.framework.AssertionFailedError;
import org.apache.lucene.search.Scorer;
import org.elasticsearch.common.lucene.ScorerAware;
import org.elasticsearch.common.settings.Settings;
@ -30,8 +32,6 @@ import org.elasticsearch.script.ScriptType;
import org.elasticsearch.test.ESTestCase;
import org.junit.Before;
import junit.framework.AssertionFailedError;
import java.util.HashMap;
import java.util.Map;
@ -76,10 +76,11 @@ public abstract class ScriptTestCase extends ESTestCase {
public Object exec(String script, Map<String, Object> vars, Map<String,String> compileParams, Scorer scorer, boolean picky) {
// test for ambiguity errors before running the actual script if picky is true
if (picky) {
ScriptInterface scriptInterface = new ScriptInterface(GenericElasticsearchScript.class);
CompilerSettings pickySettings = new CompilerSettings();
pickySettings.setPicky(true);
pickySettings.setRegexesEnabled(CompilerSettings.REGEX_ENABLED.get(scriptEngineSettings()));
Walker.buildPainlessTree(getTestName(), script, pickySettings, null);
Walker.buildPainlessTree(scriptInterface, getTestName(), script, pickySettings, null);
}
// test actual script execution
Object object = scriptEngine.compile(null, script, compileParams);

View File

@ -28,8 +28,10 @@ import org.elasticsearch.painless.Definition.MethodKey;
import org.elasticsearch.painless.Definition.RuntimeClass;
import org.elasticsearch.painless.Definition.Struct;
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.Operation;
import org.elasticsearch.painless.antlr.Walker;
import org.elasticsearch.test.ESTestCase;
@ -894,10 +896,11 @@ public class NodeToStringTests extends ESTestCase {
}
private SSource walk(String code) {
ScriptInterface scriptInterface = new ScriptInterface(GenericElasticsearchScript.class);
CompilerSettings compilerSettings = new CompilerSettings();
compilerSettings.setRegexesEnabled(true);
try {
return Walker.buildPainlessTree(getTestName(), code, compilerSettings, null);
return Walker.buildPainlessTree(scriptInterface, getTestName(), code, compilerSettings, null);
} catch (Exception e) {
throw new AssertionError("Failed to compile: " + code, e);
}