Skip to content

Commit bb48a6d

Browse files
authored
Scripting: Converts casting and def support (#61350)
Painless will cast returned values to a converter argument type, if necessary. Painless will also look for a special `convertFromDef` converter which is called to explicitly handle `def` conversions. `convertFromDef` must handle all valid def conversions. Refs: #59647
1 parent 8989a64 commit bb48a6d

File tree

7 files changed

+236
-75
lines changed

7 files changed

+236
-75
lines changed

modules/lang-painless/src/main/java/org/elasticsearch/painless/MethodWriter.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -160,9 +160,7 @@ public void writeCast(PainlessCast cast) {
160160
if (cast == null) {
161161
return;
162162
}
163-
if (cast.converter != null) {
164-
invokeStatic(Type.getType(cast.converter.getDeclaringClass()), Method.getMethod(cast.converter));
165-
} else if (cast.originalType == char.class && cast.targetType == String.class) {
163+
if (cast.originalType == char.class && cast.targetType == String.class) {
166164
invokeStatic(UTILITY_TYPE, CHAR_TO_STRING);
167165
} else if (cast.originalType == String.class && cast.targetType == char.class) {
168166
invokeStatic(UTILITY_TYPE, STRING_TO_CHAR);

modules/lang-painless/src/main/java/org/elasticsearch/painless/ScriptClassInfo.java

Lines changed: 20 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,13 @@
2222
import org.elasticsearch.painless.lookup.PainlessLookup;
2323
import org.elasticsearch.painless.lookup.PainlessLookupUtility;
2424
import org.elasticsearch.painless.lookup.def;
25+
import org.elasticsearch.painless.symbol.FunctionTable;
2526

2627
import java.lang.invoke.MethodType;
2728
import java.lang.reflect.Field;
28-
import java.lang.reflect.Method;
2929
import java.lang.reflect.Modifier;
3030
import java.util.ArrayList;
31+
import java.util.Arrays;
3132
import java.util.List;
3233
import java.util.function.Function;
3334

@@ -46,7 +47,8 @@ public class ScriptClassInfo {
4647
private final List<org.objectweb.asm.commons.Method> needsMethods;
4748
private final List<org.objectweb.asm.commons.Method> getMethods;
4849
private final List<Class<?>> getReturns;
49-
private final List<ConverterSignature> converterSignatures;
50+
public final List<FunctionTable.LocalFunction> converters;
51+
public final FunctionTable.LocalFunction defConverter;
5052

5153
public ScriptClassInfo(PainlessLookup painlessLookup, Class<?> baseClass) {
5254
this.baseClass = baseClass;
@@ -92,17 +94,30 @@ public ScriptClassInfo(PainlessLookup painlessLookup, Class<?> baseClass) {
9294
if (executeMethod == null) {
9395
throw new IllegalStateException("no execute method found");
9496
}
95-
ArrayList<ConverterSignature> converterSignatures = new ArrayList<>();
97+
ArrayList<FunctionTable.LocalFunction> converters = new ArrayList<>();
98+
FunctionTable.LocalFunction defConverter = null;
9699
for (java.lang.reflect.Method m : baseClass.getMethods()) {
97100
if (m.getName().startsWith("convertFrom") &&
98101
m.getParameterTypes().length == 1 &&
99102
m.getReturnType() == returnType &&
100103
Modifier.isStatic(m.getModifiers())) {
101104

102-
converterSignatures.add(new ConverterSignature(m));
105+
if (m.getName().equals("convertFromDef")) {
106+
if (m.getParameterTypes()[0] != Object.class) {
107+
throw new IllegalStateException("convertFromDef must take a single Object as an argument, " +
108+
"not [" + m.getParameterTypes()[0] + "]");
109+
}
110+
defConverter = new FunctionTable.LocalFunction(m.getName(), m.getReturnType(), Arrays.asList(m.getParameterTypes()),
111+
true, true);
112+
} else {
113+
converters.add(
114+
new FunctionTable.LocalFunction(m.getName(), m.getReturnType(), Arrays.asList(m.getParameterTypes()), true, true)
115+
);
116+
}
103117
}
104118
}
105-
this.converterSignatures = unmodifiableList(converterSignatures);
119+
this.defConverter = defConverter;
120+
this.converters = unmodifiableList(converters);
106121

107122
MethodType methodType = MethodType.methodType(executeMethod.getReturnType(), executeMethod.getParameterTypes());
108123
this.executeMethod = new org.objectweb.asm.commons.Method(executeMethod.getName(), methodType.toMethodDescriptorString());
@@ -239,23 +254,4 @@ private static String[] readArgumentNamesConstant(Class<?> iface) {
239254
throw new IllegalArgumentException("Error trying to read [" + iface.getName() + "#ARGUMENTS]", e);
240255
}
241256
}
242-
243-
private static class ConverterSignature {
244-
final Class<?> parameter;
245-
final Method method;
246-
247-
ConverterSignature(Method method) {
248-
this.method = method;
249-
this.parameter = method.getParameterTypes()[0];
250-
}
251-
}
252-
253-
public Method getConverter(Class<?> original) {
254-
for (ConverterSignature converterSignature: converterSignatures) {
255-
if (converterSignature.parameter.isAssignableFrom(original)) {
256-
return converterSignature.method;
257-
}
258-
}
259-
return null;
260-
}
261257
}

modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessCast.java

Lines changed: 9 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919

2020
package org.elasticsearch.painless.lookup;
2121

22-
import java.lang.reflect.Method;
2322
import java.util.Objects;
2423

2524
public class PainlessCast {
@@ -85,41 +84,16 @@ public static PainlessCast unboxOriginalTypeToBoxTargetType(boolean explicitCast
8584
return new PainlessCast(null, null, explicitCast, unboxOriginalType, null, null, boxTargetType);
8685
}
8786

88-
public static PainlessCast convertedReturn(Class<?> originalType, Class<?> targetType, Method converter) {
89-
Objects.requireNonNull(originalType);
90-
Objects.requireNonNull(targetType);
91-
Objects.requireNonNull(converter);
92-
93-
return new PainlessCast(originalType, targetType, false, null, null, null, null, converter);
94-
}
95-
9687
public final Class<?> originalType;
9788
public final Class<?> targetType;
9889
public final boolean explicitCast;
9990
public final Class<?> unboxOriginalType;
10091
public final Class<?> unboxTargetType;
10192
public final Class<?> boxOriginalType;
10293
public final Class<?> boxTargetType;
103-
public final Method converter; // access
104-
105-
private PainlessCast(Class<?> originalType,
106-
Class<?> targetType,
107-
boolean explicitCast,
108-
Class<?> unboxOriginalType,
109-
Class<?> unboxTargetType,
110-
Class<?> boxOriginalType,
111-
Class<?> boxTargetType) {
112-
this(originalType, targetType, explicitCast, unboxOriginalType, unboxTargetType, boxOriginalType, boxTargetType, null);
113-
}
11494

115-
private PainlessCast(Class<?> originalType,
116-
Class<?> targetType,
117-
boolean explicitCast,
118-
Class<?> unboxOriginalType,
119-
Class<?> unboxTargetType,
120-
Class<?> boxOriginalType,
121-
Class<?> boxTargetType,
122-
Method converter) {
95+
private PainlessCast(Class<?> originalType, Class<?> targetType, boolean explicitCast,
96+
Class<?> unboxOriginalType, Class<?> unboxTargetType, Class<?> boxOriginalType, Class<?> boxTargetType) {
12397

12498
this.originalType = originalType;
12599
this.targetType = targetType;
@@ -128,7 +102,6 @@ private PainlessCast(Class<?> originalType,
128102
this.unboxTargetType = unboxTargetType;
129103
this.boxOriginalType = boxOriginalType;
130104
this.boxTargetType = boxTargetType;
131-
this.converter = converter;
132105
}
133106

134107
@Override
@@ -144,18 +117,16 @@ public boolean equals(Object object) {
144117
PainlessCast that = (PainlessCast)object;
145118

146119
return explicitCast == that.explicitCast &&
147-
Objects.equals(originalType, that.originalType) &&
148-
Objects.equals(targetType, that.targetType) &&
149-
Objects.equals(unboxOriginalType, that.unboxOriginalType) &&
150-
Objects.equals(unboxTargetType, that.unboxTargetType) &&
151-
Objects.equals(boxOriginalType, that.boxOriginalType) &&
152-
Objects.equals(boxTargetType, that.boxTargetType) &&
153-
Objects.equals(converter, that.converter);
120+
Objects.equals(originalType, that.originalType) &&
121+
Objects.equals(targetType, that.targetType) &&
122+
Objects.equals(unboxOriginalType, that.unboxOriginalType) &&
123+
Objects.equals(unboxTargetType, that.unboxTargetType) &&
124+
Objects.equals(boxOriginalType, that.boxOriginalType) &&
125+
Objects.equals(boxTargetType, that.boxTargetType);
154126
}
155127

156128
@Override
157129
public int hashCode() {
158-
return Objects.hash(originalType, targetType, explicitCast, unboxOriginalType, unboxTargetType, boxOriginalType, boxTargetType,
159-
converter);
130+
return Objects.hash(originalType, targetType, explicitCast, unboxOriginalType, unboxTargetType, boxOriginalType, boxTargetType);
160131
}
161132
}

modules/lang-painless/src/main/java/org/elasticsearch/painless/phase/PainlessSemanticAnalysisPhase.java

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@
2424
import org.elasticsearch.painless.ScriptClassInfo;
2525
import org.elasticsearch.painless.lookup.PainlessCast;
2626
import org.elasticsearch.painless.lookup.PainlessLookupUtility;
27+
import org.elasticsearch.painless.lookup.def;
2728
import org.elasticsearch.painless.node.AExpression;
29+
import org.elasticsearch.painless.node.AStatement;
2830
import org.elasticsearch.painless.node.SBlock;
2931
import org.elasticsearch.painless.node.SExpression;
3032
import org.elasticsearch.painless.node.SFunction;
@@ -43,7 +45,6 @@
4345
import org.elasticsearch.painless.symbol.SemanticScope;
4446
import org.elasticsearch.painless.symbol.SemanticScope.FunctionScope;
4547

46-
import java.lang.reflect.Method;
4748
import java.util.List;
4849

4950
import static org.elasticsearch.painless.symbol.SemanticScope.newFunctionScope;
@@ -128,7 +129,8 @@ public void visitExpression(SExpression userExpressionNode, SemanticScope semant
128129
semanticScope.putDecoration(userStatementNode, new TargetType(rtnType));
129130
semanticScope.setCondition(userStatementNode, Internal.class);
130131
if ("execute".equals(functionName)) {
131-
decorateWithCast(userStatementNode, semanticScope, semanticScope.getScriptScope().getScriptClassInfo());
132+
decorateWithCastForReturn(userStatementNode, userExpressionNode, semanticScope,
133+
semanticScope.getScriptScope().getScriptClassInfo());
132134
} else {
133135
decorateWithCast(userStatementNode, semanticScope);
134136
}
@@ -162,7 +164,8 @@ public void visitReturn(SReturn userReturnNode, SemanticScope semanticScope) {
162164
semanticScope.setCondition(userValueNode, Internal.class);
163165
checkedVisit(userValueNode, semanticScope);
164166
if ("execute".equals(functionName)) {
165-
decorateWithCast(userValueNode, semanticScope, semanticScope.getScriptScope().getScriptClassInfo());
167+
decorateWithCastForReturn(userValueNode, userReturnNode, semanticScope,
168+
semanticScope.getScriptScope().getScriptClassInfo());
166169
} else {
167170
decorateWithCast(userValueNode, semanticScope);
168171
}
@@ -176,21 +179,40 @@ public void visitReturn(SReturn userReturnNode, SemanticScope semanticScope) {
176179
/**
177180
* Decorates a user expression node with a PainlessCast.
178181
*/
179-
public void decorateWithCast(AExpression userExpressionNode, SemanticScope semanticScope, ScriptClassInfo scriptClassInfo) {
182+
public void decorateWithCastForReturn(
183+
AExpression userExpressionNode,
184+
AStatement parent,
185+
SemanticScope semanticScope,
186+
ScriptClassInfo scriptClassInfo
187+
) {
180188
Location location = userExpressionNode.getLocation();
181189
Class<?> valueType = semanticScope.getDecoration(userExpressionNode, Decorations.ValueType.class).getValueType();
182190
Class<?> targetType = semanticScope.getDecoration(userExpressionNode, TargetType.class).getTargetType();
183-
boolean isExplicitCast = semanticScope.getCondition(userExpressionNode, Decorations.Explicit.class);
184-
boolean isInternalCast = semanticScope.getCondition(userExpressionNode, Internal.class);
185191

186192
PainlessCast painlessCast;
187-
Method converter = scriptClassInfo.getConverter(valueType);
188-
if (converter != null) {
189-
painlessCast = PainlessCast.convertedReturn(valueType, targetType, converter);
193+
if (valueType == def.class) {
194+
if (scriptClassInfo.defConverter != null) {
195+
semanticScope.putDecoration(parent, new Decorations.Converter(scriptClassInfo.defConverter));
196+
return;
197+
}
190198
} else {
191-
painlessCast = AnalyzerCaster.getLegalCast(location, valueType, targetType, isExplicitCast, isInternalCast);
199+
for (LocalFunction converter : scriptClassInfo.converters) {
200+
try {
201+
painlessCast = AnalyzerCaster.getLegalCast(location, valueType, converter.getTypeParameters().get(0), false, true);
202+
if (painlessCast != null) {
203+
semanticScope.putDecoration(userExpressionNode, new ExpressionPainlessCast(painlessCast));
204+
}
205+
semanticScope.putDecoration(parent, new Decorations.Converter(converter));
206+
return;
207+
} catch (ClassCastException e) {
208+
// Do nothing, we're checking all converters
209+
}
210+
}
192211
}
193212

213+
boolean isExplicitCast = semanticScope.getCondition(userExpressionNode, Decorations.Explicit.class);
214+
boolean isInternalCast = semanticScope.getCondition(userExpressionNode, Internal.class);
215+
painlessCast = AnalyzerCaster.getLegalCast(location, valueType, targetType, isExplicitCast, isInternalCast);
194216
if (painlessCast != null) {
195217
semanticScope.putDecoration(userExpressionNode, new ExpressionPainlessCast(painlessCast));
196218
}

modules/lang-painless/src/main/java/org/elasticsearch/painless/phase/PainlessUserTreeToIRTreePhase.java

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import org.elasticsearch.painless.ir.ExpressionNode;
3333
import org.elasticsearch.painless.ir.FieldNode;
3434
import org.elasticsearch.painless.ir.FunctionNode;
35+
import org.elasticsearch.painless.ir.IRNode;
3536
import org.elasticsearch.painless.ir.InvokeCallMemberNode;
3637
import org.elasticsearch.painless.ir.InvokeCallNode;
3738
import org.elasticsearch.painless.ir.LoadFieldMemberNode;
@@ -43,7 +44,11 @@
4344
import org.elasticsearch.painless.ir.TryNode;
4445
import org.elasticsearch.painless.lookup.PainlessLookup;
4546
import org.elasticsearch.painless.lookup.PainlessMethod;
47+
import org.elasticsearch.painless.node.AStatement;
48+
import org.elasticsearch.painless.node.SExpression;
4649
import org.elasticsearch.painless.node.SFunction;
50+
import org.elasticsearch.painless.node.SReturn;
51+
import org.elasticsearch.painless.symbol.Decorations.Converter;
4752
import org.elasticsearch.painless.symbol.Decorations.IRNodeDecoration;
4853
import org.elasticsearch.painless.symbol.Decorations.MethodEscape;
4954
import org.elasticsearch.painless.symbol.FunctionTable.LocalFunction;
@@ -535,4 +540,43 @@ protected void injectSandboxExceptions(FunctionNode irFunctionNode) {
535540
throw new RuntimeException(exception);
536541
}
537542
}
543+
544+
@Override
545+
public void visitExpression(SExpression userExpressionNode, ScriptScope scriptScope) {
546+
// sets IRNodeDecoration with ReturnNode or StatementExpressionNode
547+
super.visitExpression(userExpressionNode, scriptScope);
548+
injectConverter(userExpressionNode, scriptScope);
549+
}
550+
551+
@Override
552+
public void visitReturn(SReturn userReturnNode, ScriptScope scriptScope) {
553+
super.visitReturn(userReturnNode, scriptScope);
554+
injectConverter(userReturnNode, scriptScope);
555+
}
556+
557+
public void injectConverter(AStatement userStatementNode, ScriptScope scriptScope) {
558+
Converter converter = scriptScope.getDecoration(userStatementNode, Converter.class);
559+
if (converter == null) {
560+
return;
561+
}
562+
563+
IRNodeDecoration irNodeDecoration = scriptScope.getDecoration(userStatementNode, IRNodeDecoration.class);
564+
IRNode irNode = irNodeDecoration.getIRNode();
565+
566+
if ((irNode instanceof ReturnNode) == false) {
567+
// Shouldn't have a Converter decoration if StatementExpressionNode, should be ReturnNode if explicit return
568+
throw userStatementNode.createError(new IllegalStateException("illegal tree structure"));
569+
}
570+
571+
ReturnNode returnNode = (ReturnNode) irNode;
572+
573+
// inject converter
574+
InvokeCallMemberNode irInvokeCallMemberNode = new InvokeCallMemberNode();
575+
irInvokeCallMemberNode.setLocation(userStatementNode.getLocation());
576+
irInvokeCallMemberNode.setLocalFunction(converter.getConverter());
577+
ExpressionNode returnExpression = returnNode.getExpressionNode();
578+
returnNode.setExpressionNode(irInvokeCallMemberNode);
579+
irInvokeCallMemberNode.addArgumentNode(returnExpression);
580+
581+
}
538582
}

modules/lang-painless/src/main/java/org/elasticsearch/painless/symbol/Decorations.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -601,6 +601,17 @@ public IRNode getIRNode() {
601601
}
602602
}
603603

604+
public static class Converter implements Decoration {
605+
private final LocalFunction converter;
606+
public Converter(LocalFunction converter) {
607+
this.converter = converter;
608+
}
609+
610+
public LocalFunction getConverter() {
611+
return converter;
612+
}
613+
}
614+
604615
// collect additional information about where doc is used
605616

606617
public interface IsDocument extends Condition {

0 commit comments

Comments
 (0)