Skip to content
This repository was archived by the owner on Jul 17, 2024. It is now read-only.

Commit 1c7d902

Browse files
feat: add support for Decimal and Decimal score types (#110)
- Decimal maps mostly to BigDecimal, although its floating point concepts are ignored (Python does not have an infinite precision MathContext, so it acts more like a dynamic range floating point with an adjustable precision. The precision used is shared in a thread local object that can be changed using decimal.setcontext. - Added `str` constructors to `float` and `int` - Added sanity tests for all variants of penalize/reward/impact and score types
1 parent e1dac95 commit 1c7d902

36 files changed

+4198
-191
lines changed

jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/PythonClassTranslator.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,8 @@ public class PythonClassTranslator {
6666
// $ is illegal in variables/methods in Python
6767
public static final String TYPE_FIELD_NAME = "$TYPE";
6868
public static final String CPYTHON_TYPE_FIELD_NAME = "$CPYTHON_TYPE";
69-
private static final String JAVA_METHOD_PREFIX = "$method$";
70-
private static final String PYTHON_JAVA_TYPE_MAPPING_PREFIX = "$pythonJavaTypeMapping";
69+
public static final String JAVA_METHOD_PREFIX = "$method$";
70+
public static final String PYTHON_JAVA_TYPE_MAPPING_PREFIX = "$pythonJavaTypeMapping";
7171

7272
public record PreparedClassInfo(PythonLikeType type, String className, String classInternalName) {
7373
}

jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/implementors/JavaPythonTypeConversionImplementor.java

+104-83
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package ai.timefold.jpyinterpreter.implementors;
22

33
import java.lang.reflect.Field;
4+
import java.math.BigDecimal;
45
import java.math.BigInteger;
56
import java.util.IdentityHashMap;
67
import java.util.Iterator;
@@ -31,6 +32,7 @@
3132
import ai.timefold.jpyinterpreter.types.collections.PythonLikeTuple;
3233
import ai.timefold.jpyinterpreter.types.errors.TypeError;
3334
import ai.timefold.jpyinterpreter.types.numeric.PythonBoolean;
35+
import ai.timefold.jpyinterpreter.types.numeric.PythonDecimal;
3436
import ai.timefold.jpyinterpreter.types.numeric.PythonFloat;
3537
import ai.timefold.jpyinterpreter.types.numeric.PythonInteger;
3638
import ai.timefold.jpyinterpreter.types.numeric.PythonNumber;
@@ -65,76 +67,78 @@ public static PythonLikeObject wrapJavaObject(Object object, Map<Object, PythonL
6567
return existingObject;
6668
}
6769

68-
if (object instanceof OpaqueJavaReference) {
69-
return ((OpaqueJavaReference) object).proxy();
70+
if (object instanceof OpaqueJavaReference opaqueJavaReference) {
71+
return opaqueJavaReference.proxy();
7072
}
7173

72-
if (object instanceof PythonLikeObject) {
74+
if (object instanceof PythonLikeObject instance) {
7375
// Object already a PythonLikeObject; need to do nothing
74-
return (PythonLikeObject) object;
76+
return instance;
7577
}
7678

7779
if (object instanceof Byte || object instanceof Short || object instanceof Integer || object instanceof Long) {
7880
return PythonInteger.valueOf(((Number) object).longValue());
7981
}
8082

81-
if (object instanceof BigInteger) {
82-
return PythonInteger.valueOf((BigInteger) object);
83+
if (object instanceof BigInteger integer) {
84+
return PythonInteger.valueOf(integer);
85+
}
86+
87+
if (object instanceof BigDecimal decimal) {
88+
return new PythonDecimal(decimal);
8389
}
8490

8591
if (object instanceof Float || object instanceof Double) {
8692
return PythonFloat.valueOf(((Number) object).doubleValue());
8793
}
8894

89-
if (object instanceof Boolean) {
90-
return PythonBoolean.valueOf((Boolean) object);
95+
if (object instanceof Boolean booleanValue) {
96+
return PythonBoolean.valueOf(booleanValue);
9197
}
9298

93-
if (object instanceof String) {
94-
return PythonString.valueOf((String) object);
99+
if (object instanceof String string) {
100+
return PythonString.valueOf(string);
95101
}
96102

97-
if (object instanceof Iterator) {
98-
return new DelegatePythonIterator<>((Iterator) object);
103+
if (object instanceof Iterator<?> iterator) {
104+
return new DelegatePythonIterator<>(iterator);
99105
}
100106

101-
if (object instanceof List) {
102-
PythonLikeList out = new PythonLikeList();
107+
if (object instanceof List<?> list) {
108+
PythonLikeList<?> out = new PythonLikeList<>();
103109
createdObjectMap.put(object, out);
104-
for (Object item : (List) object) {
110+
for (Object item : list) {
105111
out.add(wrapJavaObject(item));
106112
}
107113
return out;
108114
}
109115

110-
if (object instanceof Set) {
111-
PythonLikeSet out = new PythonLikeSet();
116+
if (object instanceof Set<?> set) {
117+
PythonLikeSet<?> out = new PythonLikeSet<>();
112118
createdObjectMap.put(object, out);
113-
for (Object item : (Set) object) {
119+
for (Object item : set) {
114120
out.add(wrapJavaObject(item));
115121
}
116122
return out;
117123
}
118124

119-
if (object instanceof Map) {
120-
PythonLikeDict out = new PythonLikeDict();
125+
if (object instanceof Map<?, ?> map) {
126+
PythonLikeDict<?, ?> out = new PythonLikeDict<>();
121127
createdObjectMap.put(object, out);
122-
Set<Map.Entry<?, ?>> entrySet = ((Map) object).entrySet();
123-
for (Map.Entry<?, ?> entry : entrySet) {
128+
var entrySet = map.entrySet();
129+
for (var entry : entrySet) {
124130
out.put(wrapJavaObject(entry.getKey()), wrapJavaObject(entry.getValue()));
125131
}
126132
return out;
127133
}
128134

129-
if (object instanceof Class) {
130-
Class<?> maybeFunctionClass = (Class<?>) object;
131-
if (Set.of(maybeFunctionClass.getInterfaces()).contains(PythonLikeFunction.class)) {
132-
return new PythonCode((Class<? extends PythonLikeFunction>) maybeFunctionClass);
133-
}
135+
if (object instanceof Class<?> maybeFunctionClass &&
136+
Set.of(maybeFunctionClass.getInterfaces()).contains(PythonLikeFunction.class)) {
137+
return new PythonCode((Class<? extends PythonLikeFunction>) maybeFunctionClass);
134138
}
135139

136-
if (object instanceof OpaquePythonReference) {
137-
return new PythonObjectWrapper((OpaquePythonReference) object);
140+
if (object instanceof OpaquePythonReference opaquePythonReference) {
141+
return new PythonObjectWrapper(opaquePythonReference);
138142
}
139143

140144
// Default: return a JavaObjectWrapper
@@ -161,6 +165,10 @@ public static PythonLikeType getPythonLikeType(Class<?> javaClass) {
161165
return BuiltinTypes.INT_TYPE;
162166
}
163167

168+
if (BigDecimal.class.equals(javaClass) || PythonDecimal.class.equals(javaClass)) {
169+
return BuiltinTypes.DECIMAL_TYPE;
170+
}
171+
164172
if (float.class.equals(javaClass) || double.class.equals(javaClass) ||
165173
Float.class.equals(javaClass) || Double.class.equals(javaClass) ||
166174
PythonFloat.class.equals(javaClass)) {
@@ -254,8 +262,7 @@ public static <T> T convertPythonObjectToJavaType(Class<? extends T> type, Pytho
254262
return null;
255263
}
256264

257-
if (object instanceof JavaObjectWrapper) {
258-
JavaObjectWrapper wrappedObject = (JavaObjectWrapper) object;
265+
if (object instanceof JavaObjectWrapper wrappedObject) {
259266
Object javaObject = wrappedObject.getWrappedObject();
260267
if (!type.isAssignableFrom(javaObject.getClass())) {
261268
throw new TypeError("Cannot convert from (" + getPythonLikeType(javaObject.getClass()) + ") to ("
@@ -266,14 +273,13 @@ public static <T> T convertPythonObjectToJavaType(Class<? extends T> type, Pytho
266273

267274
if (type.equals(byte.class) || type.equals(short.class) || type.equals(int.class) || type.equals(long.class) ||
268275
type.equals(float.class) || type.equals(double.class) || Number.class.isAssignableFrom(type)) {
269-
if (!(object instanceof PythonNumber)) {
276+
if (!(object instanceof PythonNumber pythonNumber)) {
270277
throw new TypeError("Cannot convert from (" + getPythonLikeType(object.getClass()) + ") to ("
271278
+ getPythonLikeType(type) + ").");
272279
}
273-
PythonNumber pythonNumber = (PythonNumber) object;
274280
Number value = pythonNumber.getValue();
275281

276-
if (type.equals(BigInteger.class)) {
282+
if (type.equals(BigInteger.class) || type.equals(BigDecimal.class)) {
277283
return (T) value;
278284
}
279285

@@ -303,11 +309,10 @@ public static <T> T convertPythonObjectToJavaType(Class<? extends T> type, Pytho
303309
}
304310

305311
if (type.equals(boolean.class) || type.equals(Boolean.class)) {
306-
if (!(object instanceof PythonBoolean)) {
312+
if (!(object instanceof PythonBoolean pythonBoolean)) {
307313
throw new TypeError("Cannot convert from (" + getPythonLikeType(object.getClass()) + ") to ("
308314
+ getPythonLikeType(type) + ").");
309315
}
310-
PythonBoolean pythonBoolean = (PythonBoolean) object;
311316
return (T) (Boolean) pythonBoolean.getBooleanValue();
312317
}
313318

@@ -335,6 +340,53 @@ public static void loadName(MethodVisitor methodVisitor, String name) {
335340
false);
336341
}
337342

343+
private record ReturnValueOpDescriptor(
344+
String wrapperClassName,
345+
String methodName,
346+
String methodDescriptor,
347+
int opcode,
348+
boolean noConversionNeeded) {
349+
public static ReturnValueOpDescriptor noConversion() {
350+
return new ReturnValueOpDescriptor("", "", "",
351+
Opcodes.ARETURN, true);
352+
}
353+
354+
public static ReturnValueOpDescriptor forNumeric(String methodName,
355+
String methodDescriptor,
356+
int opcode) {
357+
return new ReturnValueOpDescriptor(Type.getInternalName(Number.class), methodName, methodDescriptor, opcode,
358+
false);
359+
}
360+
}
361+
362+
private static final Map<Type, ReturnValueOpDescriptor> numericReturnValueOpDescriptorMap = Map.of(
363+
Type.BYTE_TYPE, ReturnValueOpDescriptor.forNumeric(
364+
"byteValue",
365+
Type.getMethodDescriptor(Type.BYTE_TYPE),
366+
Opcodes.IRETURN),
367+
Type.SHORT_TYPE, ReturnValueOpDescriptor.forNumeric(
368+
"shortValue",
369+
Type.getMethodDescriptor(Type.SHORT_TYPE),
370+
Opcodes.IRETURN),
371+
Type.INT_TYPE, ReturnValueOpDescriptor.forNumeric(
372+
"intValue",
373+
Type.getMethodDescriptor(Type.INT_TYPE),
374+
Opcodes.IRETURN),
375+
Type.LONG_TYPE, ReturnValueOpDescriptor.forNumeric(
376+
"longValue",
377+
Type.getMethodDescriptor(Type.LONG_TYPE),
378+
Opcodes.LRETURN),
379+
Type.FLOAT_TYPE, ReturnValueOpDescriptor.forNumeric(
380+
"floatValue",
381+
Type.getMethodDescriptor(Type.FLOAT_TYPE),
382+
Opcodes.FRETURN),
383+
Type.DOUBLE_TYPE, ReturnValueOpDescriptor.forNumeric(
384+
"doubleValue",
385+
Type.getMethodDescriptor(Type.DOUBLE_TYPE),
386+
Opcodes.DRETURN),
387+
Type.getType(BigInteger.class), ReturnValueOpDescriptor.noConversion(),
388+
Type.getType(BigDecimal.class), ReturnValueOpDescriptor.noConversion());
389+
338390
/**
339391
* If {@code method} return type is not void, convert TOS into its Java equivalent and return it.
340392
* If {@code method} return type is void, immediately return.
@@ -344,67 +396,36 @@ public static void loadName(MethodVisitor methodVisitor, String name) {
344396
public static void returnValue(MethodVisitor methodVisitor, MethodDescriptor method, StackMetadata stackMetadata) {
345397
Type returnAsmType = method.getReturnType();
346398

399+
if (Type.CHAR_TYPE.equals(returnAsmType)) {
400+
throw new IllegalStateException("Unhandled case for primitive type (char).");
401+
}
402+
347403
if (Type.VOID_TYPE.equals(returnAsmType)) {
348404
methodVisitor.visitInsn(Opcodes.RETURN);
349405
return;
350406
}
351407

352-
if (Type.BYTE_TYPE.equals(returnAsmType) ||
353-
Type.CHAR_TYPE.equals(returnAsmType) ||
354-
Type.SHORT_TYPE.equals(returnAsmType) ||
355-
Type.INT_TYPE.equals(returnAsmType) ||
356-
Type.LONG_TYPE.equals(returnAsmType) ||
357-
Type.FLOAT_TYPE.equals(returnAsmType) ||
358-
Type.DOUBLE_TYPE.equals(returnAsmType)) {
408+
if (numericReturnValueOpDescriptorMap.containsKey(returnAsmType)) {
409+
var returnValueOpDescriptor = numericReturnValueOpDescriptorMap.get(returnAsmType);
359410
methodVisitor.visitTypeInsn(Opcodes.CHECKCAST, Type.getInternalName(PythonNumber.class));
360411
methodVisitor.visitMethodInsn(Opcodes.INVOKEINTERFACE,
361412
Type.getInternalName(PythonNumber.class),
362413
"getValue",
363414
Type.getMethodDescriptor(Type.getType(Number.class)),
364415
true);
365-
String wrapperClassName = null;
366-
String methodName = null;
367-
String methodDescriptor = null;
368-
int returnOpcode = 0;
369-
370-
if (Type.BYTE_TYPE.equals(returnAsmType)) {
371-
wrapperClassName = Type.getInternalName(Number.class);
372-
methodName = "byteValue";
373-
methodDescriptor = Type.getMethodDescriptor(Type.BYTE_TYPE);
374-
returnOpcode = Opcodes.IRETURN;
375-
} else if (Type.CHAR_TYPE.equals(returnAsmType)) {
376-
throw new IllegalStateException("Unhandled case for primitive type (char).");
377-
// returnOpcode = Opcodes.IRETURN;
378-
} else if (Type.SHORT_TYPE.equals(returnAsmType)) {
379-
wrapperClassName = Type.getInternalName(Number.class);
380-
methodName = "shortValue";
381-
methodDescriptor = Type.getMethodDescriptor(Type.SHORT_TYPE);
382-
returnOpcode = Opcodes.IRETURN;
383-
} else if (Type.INT_TYPE.equals(returnAsmType)) {
384-
wrapperClassName = Type.getInternalName(Number.class);
385-
methodName = "intValue";
386-
methodDescriptor = Type.getMethodDescriptor(Type.INT_TYPE);
387-
returnOpcode = Opcodes.IRETURN;
388-
} else if (Type.FLOAT_TYPE.equals(returnAsmType)) {
389-
wrapperClassName = Type.getInternalName(Number.class);
390-
methodName = "floatValue";
391-
methodDescriptor = Type.getMethodDescriptor(Type.FLOAT_TYPE);
392-
returnOpcode = Opcodes.FRETURN;
393-
} else if (Type.LONG_TYPE.equals(returnAsmType)) {
394-
wrapperClassName = Type.getInternalName(Number.class);
395-
methodName = "longValue";
396-
methodDescriptor = Type.getMethodDescriptor(Type.LONG_TYPE);
397-
returnOpcode = Opcodes.LRETURN;
398-
} else if (Type.DOUBLE_TYPE.equals(returnAsmType)) {
399-
wrapperClassName = Type.getInternalName(Number.class);
400-
methodName = "doubleValue";
401-
methodDescriptor = Type.getMethodDescriptor(Type.DOUBLE_TYPE);
402-
returnOpcode = Opcodes.DRETURN;
416+
417+
if (returnValueOpDescriptor.noConversionNeeded) {
418+
methodVisitor.visitTypeInsn(Opcodes.CHECKCAST, returnAsmType.getInternalName());
419+
methodVisitor.visitInsn(Opcodes.ARETURN);
420+
return;
403421
}
422+
404423
methodVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
405-
wrapperClassName, methodName, methodDescriptor,
424+
returnValueOpDescriptor.wrapperClassName,
425+
returnValueOpDescriptor.methodName,
426+
returnValueOpDescriptor.methodDescriptor,
406427
false);
407-
methodVisitor.visitInsn(returnOpcode);
428+
methodVisitor.visitInsn(returnValueOpDescriptor.opcode);
408429
return;
409430
}
410431

jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/types/BuiltinTypes.java

+2
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import ai.timefold.jpyinterpreter.types.collections.view.DictValueView;
2424
import ai.timefold.jpyinterpreter.types.numeric.PythonBoolean;
2525
import ai.timefold.jpyinterpreter.types.numeric.PythonComplex;
26+
import ai.timefold.jpyinterpreter.types.numeric.PythonDecimal;
2627
import ai.timefold.jpyinterpreter.types.numeric.PythonFloat;
2728
import ai.timefold.jpyinterpreter.types.numeric.PythonInteger;
2829
import ai.timefold.jpyinterpreter.types.numeric.PythonNumber;
@@ -60,6 +61,7 @@ public class BuiltinTypes {
6061
public static final PythonLikeType BOOLEAN_TYPE = new PythonLikeType("bool", PythonBoolean.class, List.of(INT_TYPE));
6162
public static final PythonLikeType FLOAT_TYPE = new PythonLikeType("float", PythonFloat.class, List.of(NUMBER_TYPE));
6263
public final static PythonLikeType COMPLEX_TYPE = new PythonLikeType("complex", PythonComplex.class, List.of(NUMBER_TYPE));
64+
public final static PythonLikeType DECIMAL_TYPE = new PythonLikeType("Decimal", PythonDecimal.class, List.of(NUMBER_TYPE));
6365

6466
public static final PythonLikeType STRING_TYPE = new PythonLikeType("str", PythonString.class, List.of(BASE_TYPE));
6567
public static final PythonLikeType BYTES_TYPE = new PythonLikeType("bytes", PythonBytes.class, List.of(BASE_TYPE));

jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/types/collections/PythonLikeTuple.java

+4-4
Original file line numberDiff line numberDiff line change
@@ -113,14 +113,14 @@ public PythonLikeTuple<T> createNewInstance() {
113113
return new PythonLikeTuple<>();
114114
}
115115

116-
public static PythonLikeTuple fromItems(PythonLikeObject... items) {
117-
PythonLikeTuple result = new PythonLikeTuple();
116+
public static <T extends PythonLikeObject> PythonLikeTuple<T> fromItems(T... items) {
117+
PythonLikeTuple<T> result = new PythonLikeTuple<>();
118118
Collections.addAll(result, items);
119119
return result;
120120
}
121121

122-
public static PythonLikeTuple fromList(List<PythonLikeObject> other) {
123-
PythonLikeTuple result = new PythonLikeTuple();
122+
public static <T extends PythonLikeObject> PythonLikeTuple<T> fromList(List<T> other) {
123+
PythonLikeTuple<T> result = new PythonLikeTuple<>();
124124
result.addAll(other);
125125
return result;
126126
}

0 commit comments

Comments
 (0)