Skip to content

Commit 5752340

Browse files
authored
Scripting: Converters can adapt return values (#60937)
Add ability to explicitly coerce the return value from a script. Runtime fields wants to avoid returning `Object` from the execute method in each context. Instead, they will return an array of primitives, such as `long[]`. However, it's convenient to allow a user to return a single primitive type rather than allocating a length-one array. To achieve this, an implementer can add explicit conversion functions to a context with the signature: `public static <context-return-value> convertFrom<Suffix>(<any type>)` When a user returns a type other than the context return value, at compile-time, painless will insert a call to their `convertFrom` method. This commit is Phase 1 of this work. It handles explicit converters for all painless types EXCEPT def type. Refs: #59647
1 parent bfda26e commit 5752340

File tree

5 files changed

+364
-81
lines changed

5 files changed

+364
-81
lines changed

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

+75-72
Original file line numberDiff line numberDiff line change
@@ -157,81 +157,84 @@ public void writeLoopCounter(int slot, Location location) {
157157
}
158158

159159
public void writeCast(PainlessCast cast) {
160-
if (cast != null) {
161-
if (cast.originalType == char.class && cast.targetType == String.class) {
162-
invokeStatic(UTILITY_TYPE, CHAR_TO_STRING);
163-
} else if (cast.originalType == String.class && cast.targetType == char.class) {
164-
invokeStatic(UTILITY_TYPE, STRING_TO_CHAR);
165-
// TODO: remove this when the transition from Joda to Java datetimes is completed
166-
} else if (cast.originalType == JodaCompatibleZonedDateTime.class && cast.targetType == ZonedDateTime.class) {
167-
invokeStatic(UTILITY_TYPE, JCZDT_TO_ZONEDDATETIME);
168-
} else if (cast.unboxOriginalType != null && cast.boxTargetType != null) {
169-
unbox(getType(cast.unboxOriginalType));
170-
writeCast(cast.unboxOriginalType, cast.boxTargetType);
171-
box(getType(cast.boxTargetType));
172-
} else if (cast.unboxOriginalType != null) {
173-
unbox(getType(cast.unboxOriginalType));
174-
writeCast(cast.originalType, cast.targetType);
175-
} else if (cast.unboxTargetType != null) {
176-
writeCast(cast.originalType, cast.targetType);
177-
unbox(getType(cast.unboxTargetType));
178-
} else if (cast.boxOriginalType != null) {
179-
box(getType(cast.boxOriginalType));
180-
writeCast(cast.originalType, cast.targetType);
181-
} else if (cast.boxTargetType != null) {
182-
writeCast(cast.originalType, cast.targetType);
183-
box(getType(cast.boxTargetType));
184-
} else if (cast.originalType == def.class) {
185-
if (cast.explicitCast) {
186-
if (cast.targetType == boolean.class) invokeStatic(DEF_UTIL_TYPE, DEF_TO_P_BOOLEAN);
187-
else if (cast.targetType == byte.class) invokeStatic(DEF_UTIL_TYPE, DEF_TO_P_BYTE_EXPLICIT);
188-
else if (cast.targetType == short.class) invokeStatic(DEF_UTIL_TYPE, DEF_TO_P_SHORT_EXPLICIT);
189-
else if (cast.targetType == char.class) invokeStatic(DEF_UTIL_TYPE, DEF_TO_P_CHAR_EXPLICIT);
190-
else if (cast.targetType == int.class) invokeStatic(DEF_UTIL_TYPE, DEF_TO_P_INT_EXPLICIT);
191-
else if (cast.targetType == long.class) invokeStatic(DEF_UTIL_TYPE, DEF_TO_P_LONG_EXPLICIT);
192-
else if (cast.targetType == float.class) invokeStatic(DEF_UTIL_TYPE, DEF_TO_P_FLOAT_EXPLICIT);
193-
else if (cast.targetType == double.class) invokeStatic(DEF_UTIL_TYPE, DEF_TO_P_DOUBLE_EXPLICIT);
194-
else if (cast.targetType == Boolean.class) invokeStatic(DEF_UTIL_TYPE, DEF_TO_B_BOOLEAN);
195-
else if (cast.targetType == Byte.class) invokeStatic(DEF_UTIL_TYPE, DEF_TO_B_BYTE_EXPLICIT);
196-
else if (cast.targetType == Short.class) invokeStatic(DEF_UTIL_TYPE, DEF_TO_B_SHORT_EXPLICIT);
197-
else if (cast.targetType == Character.class) invokeStatic(DEF_UTIL_TYPE, DEF_TO_B_CHARACTER_EXPLICIT);
198-
else if (cast.targetType == Integer.class) invokeStatic(DEF_UTIL_TYPE, DEF_TO_B_INTEGER_EXPLICIT);
199-
else if (cast.targetType == Long.class) invokeStatic(DEF_UTIL_TYPE, DEF_TO_B_LONG_EXPLICIT);
200-
else if (cast.targetType == Float.class) invokeStatic(DEF_UTIL_TYPE, DEF_TO_B_FLOAT_EXPLICIT);
201-
else if (cast.targetType == Double.class) invokeStatic(DEF_UTIL_TYPE, DEF_TO_B_DOUBLE_EXPLICIT);
202-
else if (cast.targetType == String.class) invokeStatic(DEF_UTIL_TYPE, DEF_TO_STRING_EXPLICIT);
203-
// TODO: remove this when the transition from Joda to Java datetimes is completed
204-
else if (cast.targetType == ZonedDateTime.class) invokeStatic(DEF_UTIL_TYPE, DEF_TO_ZONEDDATETIME);
205-
else {
206-
writeCast(cast.originalType, cast.targetType);
207-
}
208-
} else {
209-
if (cast.targetType == boolean.class) invokeStatic(DEF_UTIL_TYPE, DEF_TO_P_BOOLEAN);
210-
else if (cast.targetType == byte.class) invokeStatic(DEF_UTIL_TYPE, DEF_TO_P_BYTE_IMPLICIT);
211-
else if (cast.targetType == short.class) invokeStatic(DEF_UTIL_TYPE, DEF_TO_P_SHORT_IMPLICIT);
212-
else if (cast.targetType == char.class) invokeStatic(DEF_UTIL_TYPE, DEF_TO_P_CHAR_IMPLICIT);
213-
else if (cast.targetType == int.class) invokeStatic(DEF_UTIL_TYPE, DEF_TO_P_INT_IMPLICIT);
214-
else if (cast.targetType == long.class) invokeStatic(DEF_UTIL_TYPE, DEF_TO_P_LONG_IMPLICIT);
215-
else if (cast.targetType == float.class) invokeStatic(DEF_UTIL_TYPE, DEF_TO_P_FLOAT_IMPLICIT);
216-
else if (cast.targetType == double.class) invokeStatic(DEF_UTIL_TYPE, DEF_TO_P_DOUBLE_IMPLICIT);
217-
else if (cast.targetType == Boolean.class) invokeStatic(DEF_UTIL_TYPE, DEF_TO_B_BOOLEAN);
218-
else if (cast.targetType == Byte.class) invokeStatic(DEF_UTIL_TYPE, DEF_TO_B_BYTE_IMPLICIT);
219-
else if (cast.targetType == Short.class) invokeStatic(DEF_UTIL_TYPE, DEF_TO_B_SHORT_IMPLICIT);
220-
else if (cast.targetType == Character.class) invokeStatic(DEF_UTIL_TYPE, DEF_TO_B_CHARACTER_IMPLICIT);
221-
else if (cast.targetType == Integer.class) invokeStatic(DEF_UTIL_TYPE, DEF_TO_B_INTEGER_IMPLICIT);
222-
else if (cast.targetType == Long.class) invokeStatic(DEF_UTIL_TYPE, DEF_TO_B_LONG_IMPLICIT);
223-
else if (cast.targetType == Float.class) invokeStatic(DEF_UTIL_TYPE, DEF_TO_B_FLOAT_IMPLICIT);
224-
else if (cast.targetType == Double.class) invokeStatic(DEF_UTIL_TYPE, DEF_TO_B_DOUBLE_IMPLICIT);
225-
else if (cast.targetType == String.class) invokeStatic(DEF_UTIL_TYPE, DEF_TO_STRING_IMPLICIT);
226-
// TODO: remove this when the transition from Joda to Java datetimes is completed
227-
else if (cast.targetType == ZonedDateTime.class) invokeStatic(DEF_UTIL_TYPE, DEF_TO_ZONEDDATETIME);
228-
else {
229-
writeCast(cast.originalType, cast.targetType);
230-
}
160+
if (cast == null) {
161+
return;
162+
}
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) {
166+
invokeStatic(UTILITY_TYPE, CHAR_TO_STRING);
167+
} else if (cast.originalType == String.class && cast.targetType == char.class) {
168+
invokeStatic(UTILITY_TYPE, STRING_TO_CHAR);
169+
// TODO: remove this when the transition from Joda to Java datetimes is completed
170+
} else if (cast.originalType == JodaCompatibleZonedDateTime.class && cast.targetType == ZonedDateTime.class) {
171+
invokeStatic(UTILITY_TYPE, JCZDT_TO_ZONEDDATETIME);
172+
} else if (cast.unboxOriginalType != null && cast.boxTargetType != null) {
173+
unbox(getType(cast.unboxOriginalType));
174+
writeCast(cast.unboxOriginalType, cast.boxTargetType);
175+
box(getType(cast.boxTargetType));
176+
} else if (cast.unboxOriginalType != null) {
177+
unbox(getType(cast.unboxOriginalType));
178+
writeCast(cast.originalType, cast.targetType);
179+
} else if (cast.unboxTargetType != null) {
180+
writeCast(cast.originalType, cast.targetType);
181+
unbox(getType(cast.unboxTargetType));
182+
} else if (cast.boxOriginalType != null) {
183+
box(getType(cast.boxOriginalType));
184+
writeCast(cast.originalType, cast.targetType);
185+
} else if (cast.boxTargetType != null) {
186+
writeCast(cast.originalType, cast.targetType);
187+
box(getType(cast.boxTargetType));
188+
} else if (cast.originalType == def.class) {
189+
if (cast.explicitCast) {
190+
if (cast.targetType == boolean.class) invokeStatic(DEF_UTIL_TYPE, DEF_TO_P_BOOLEAN);
191+
else if (cast.targetType == byte.class) invokeStatic(DEF_UTIL_TYPE, DEF_TO_P_BYTE_EXPLICIT);
192+
else if (cast.targetType == short.class) invokeStatic(DEF_UTIL_TYPE, DEF_TO_P_SHORT_EXPLICIT);
193+
else if (cast.targetType == char.class) invokeStatic(DEF_UTIL_TYPE, DEF_TO_P_CHAR_EXPLICIT);
194+
else if (cast.targetType == int.class) invokeStatic(DEF_UTIL_TYPE, DEF_TO_P_INT_EXPLICIT);
195+
else if (cast.targetType == long.class) invokeStatic(DEF_UTIL_TYPE, DEF_TO_P_LONG_EXPLICIT);
196+
else if (cast.targetType == float.class) invokeStatic(DEF_UTIL_TYPE, DEF_TO_P_FLOAT_EXPLICIT);
197+
else if (cast.targetType == double.class) invokeStatic(DEF_UTIL_TYPE, DEF_TO_P_DOUBLE_EXPLICIT);
198+
else if (cast.targetType == Boolean.class) invokeStatic(DEF_UTIL_TYPE, DEF_TO_B_BOOLEAN);
199+
else if (cast.targetType == Byte.class) invokeStatic(DEF_UTIL_TYPE, DEF_TO_B_BYTE_EXPLICIT);
200+
else if (cast.targetType == Short.class) invokeStatic(DEF_UTIL_TYPE, DEF_TO_B_SHORT_EXPLICIT);
201+
else if (cast.targetType == Character.class) invokeStatic(DEF_UTIL_TYPE, DEF_TO_B_CHARACTER_EXPLICIT);
202+
else if (cast.targetType == Integer.class) invokeStatic(DEF_UTIL_TYPE, DEF_TO_B_INTEGER_EXPLICIT);
203+
else if (cast.targetType == Long.class) invokeStatic(DEF_UTIL_TYPE, DEF_TO_B_LONG_EXPLICIT);
204+
else if (cast.targetType == Float.class) invokeStatic(DEF_UTIL_TYPE, DEF_TO_B_FLOAT_EXPLICIT);
205+
else if (cast.targetType == Double.class) invokeStatic(DEF_UTIL_TYPE, DEF_TO_B_DOUBLE_EXPLICIT);
206+
else if (cast.targetType == String.class) invokeStatic(DEF_UTIL_TYPE, DEF_TO_STRING_EXPLICIT);
207+
// TODO: remove this when the transition from Joda to Java datetimes is completed
208+
else if (cast.targetType == ZonedDateTime.class) invokeStatic(DEF_UTIL_TYPE, DEF_TO_ZONEDDATETIME);
209+
else {
210+
writeCast(cast.originalType, cast.targetType);
231211
}
232212
} else {
233-
writeCast(cast.originalType, cast.targetType);
213+
if (cast.targetType == boolean.class) invokeStatic(DEF_UTIL_TYPE, DEF_TO_P_BOOLEAN);
214+
else if (cast.targetType == byte.class) invokeStatic(DEF_UTIL_TYPE, DEF_TO_P_BYTE_IMPLICIT);
215+
else if (cast.targetType == short.class) invokeStatic(DEF_UTIL_TYPE, DEF_TO_P_SHORT_IMPLICIT);
216+
else if (cast.targetType == char.class) invokeStatic(DEF_UTIL_TYPE, DEF_TO_P_CHAR_IMPLICIT);
217+
else if (cast.targetType == int.class) invokeStatic(DEF_UTIL_TYPE, DEF_TO_P_INT_IMPLICIT);
218+
else if (cast.targetType == long.class) invokeStatic(DEF_UTIL_TYPE, DEF_TO_P_LONG_IMPLICIT);
219+
else if (cast.targetType == float.class) invokeStatic(DEF_UTIL_TYPE, DEF_TO_P_FLOAT_IMPLICIT);
220+
else if (cast.targetType == double.class) invokeStatic(DEF_UTIL_TYPE, DEF_TO_P_DOUBLE_IMPLICIT);
221+
else if (cast.targetType == Boolean.class) invokeStatic(DEF_UTIL_TYPE, DEF_TO_B_BOOLEAN);
222+
else if (cast.targetType == Byte.class) invokeStatic(DEF_UTIL_TYPE, DEF_TO_B_BYTE_IMPLICIT);
223+
else if (cast.targetType == Short.class) invokeStatic(DEF_UTIL_TYPE, DEF_TO_B_SHORT_IMPLICIT);
224+
else if (cast.targetType == Character.class) invokeStatic(DEF_UTIL_TYPE, DEF_TO_B_CHARACTER_IMPLICIT);
225+
else if (cast.targetType == Integer.class) invokeStatic(DEF_UTIL_TYPE, DEF_TO_B_INTEGER_IMPLICIT);
226+
else if (cast.targetType == Long.class) invokeStatic(DEF_UTIL_TYPE, DEF_TO_B_LONG_IMPLICIT);
227+
else if (cast.targetType == Float.class) invokeStatic(DEF_UTIL_TYPE, DEF_TO_B_FLOAT_IMPLICIT);
228+
else if (cast.targetType == Double.class) invokeStatic(DEF_UTIL_TYPE, DEF_TO_B_DOUBLE_IMPLICIT);
229+
else if (cast.targetType == String.class) invokeStatic(DEF_UTIL_TYPE, DEF_TO_STRING_IMPLICIT);
230+
// TODO: remove this when the transition from Joda to Java datetimes is completed
231+
else if (cast.targetType == ZonedDateTime.class) invokeStatic(DEF_UTIL_TYPE, DEF_TO_ZONEDDATETIME);
232+
else {
233+
writeCast(cast.originalType, cast.targetType);
234+
}
234235
}
236+
} else {
237+
writeCast(cast.originalType, cast.targetType);
235238
}
236239
}
237240

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

+46-4
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525

2626
import java.lang.invoke.MethodType;
2727
import java.lang.reflect.Field;
28+
import java.lang.reflect.Method;
2829
import java.lang.reflect.Modifier;
2930
import java.util.ArrayList;
3031
import java.util.List;
@@ -45,6 +46,7 @@ public class ScriptClassInfo {
4546
private final List<org.objectweb.asm.commons.Method> needsMethods;
4647
private final List<org.objectweb.asm.commons.Method> getMethods;
4748
private final List<Class<?>> getReturns;
49+
private final List<ConverterSignature> converterSignatures;
4850

4951
public ScriptClassInfo(PainlessLookup painlessLookup, Class<?> baseClass) {
5052
this.baseClass = baseClass;
@@ -54,23 +56,28 @@ public ScriptClassInfo(PainlessLookup painlessLookup, Class<?> baseClass) {
5456
List<org.objectweb.asm.commons.Method> needsMethods = new ArrayList<>();
5557
List<org.objectweb.asm.commons.Method> getMethods = new ArrayList<>();
5658
List<Class<?>> getReturns = new ArrayList<>();
59+
60+
Class<?> returnType = null;
5761
for (java.lang.reflect.Method m : baseClass.getMethods()) {
5862
if (m.isDefault()) {
5963
continue;
6064
}
6165
if (m.getName().equals("execute")) {
6266
if (executeMethod == null) {
6367
executeMethod = m;
68+
returnType = m.getReturnType();
6469
} else {
6570
throw new IllegalArgumentException(
6671
"Painless can only implement interfaces that have a single method named [execute] but [" + baseClass.getName()
6772
+ "] has more than one.");
6873
}
69-
}
70-
if (m.getName().startsWith("needs") && m.getReturnType() == boolean.class && m.getParameterTypes().length == 0) {
74+
} else if (m.getName().startsWith("needs") &&
75+
m.getReturnType() == boolean.class &&
76+
m.getParameterTypes().length == 0) {
7177
needsMethods.add(new org.objectweb.asm.commons.Method(m.getName(), NEEDS_PARAMETER_METHOD_TYPE.toMethodDescriptorString()));
72-
}
73-
if (m.getName().startsWith("get") && m.getName().equals("getClass") == false && Modifier.isStatic(m.getModifiers()) == false) {
78+
} else if (m.getName().startsWith("get") &&
79+
m.getName().equals("getClass") == false &&
80+
Modifier.isStatic(m.getModifiers()) == false) {
7481
getReturns.add(
7582
definitionTypeForClass(painlessLookup, m.getReturnType(), componentType -> "[" + m.getName() + "] has unknown return " +
7683
"type [" + componentType.getName() + "]. Painless can only support getters with return types that are " +
@@ -81,6 +88,22 @@ public ScriptClassInfo(PainlessLookup painlessLookup, Class<?> baseClass) {
8188

8289
}
8390
}
91+
92+
if (executeMethod == null) {
93+
throw new IllegalStateException("no execute method found");
94+
}
95+
ArrayList<ConverterSignature> converterSignatures = new ArrayList<>();
96+
for (java.lang.reflect.Method m : baseClass.getMethods()) {
97+
if (m.getName().startsWith("convertFrom") &&
98+
m.getParameterTypes().length == 1 &&
99+
m.getReturnType() == returnType &&
100+
Modifier.isStatic(m.getModifiers())) {
101+
102+
converterSignatures.add(new ConverterSignature(m));
103+
}
104+
}
105+
this.converterSignatures = unmodifiableList(converterSignatures);
106+
84107
MethodType methodType = MethodType.methodType(executeMethod.getReturnType(), executeMethod.getParameterTypes());
85108
this.executeMethod = new org.objectweb.asm.commons.Method(executeMethod.getName(), methodType.toMethodDescriptorString());
86109
executeMethodReturnType = definitionTypeForClass(painlessLookup, executeMethod.getReturnType(),
@@ -216,4 +239,23 @@ private static String[] readArgumentNamesConstant(Class<?> iface) {
216239
throw new IllegalArgumentException("Error trying to read [" + iface.getName() + "#ARGUMENTS]", e);
217240
}
218241
}
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+
}
219261
}

0 commit comments

Comments
 (0)