diff --git a/core/src/main/java/ai/timefold/solver/core/api/domain/entity/PlanningPinToIndex.java b/core/src/main/java/ai/timefold/solver/core/api/domain/entity/PlanningPinToIndex.java
index 3d46e2154a..1f43a507c1 100644
--- a/core/src/main/java/ai/timefold/solver/core/api/domain/entity/PlanningPinToIndex.java
+++ b/core/src/main/java/ai/timefold/solver/core/api/domain/entity/PlanningPinToIndex.java
@@ -37,7 +37,7 @@
* Example: Assuming a list of values {@code [A, B, C]}:
*
*
- * - 0 or null allows the entire list to be modified.
+ * - 0 allows the entire list to be modified.
* - 1 pins {@code A}; rest of the list may be modified or added to.
* - 2 pins {@code A, B}; rest of the list may be modified or added to.
* - 3 pins {@code A, B, C}; the list can only be added to.
diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/entity/descriptor/EntityDescriptor.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/entity/descriptor/EntityDescriptor.java
index d8cbb61da8..85696a6793 100644
--- a/core/src/main/java/ai/timefold/solver/core/impl/domain/entity/descriptor/EntityDescriptor.java
+++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/entity/descriptor/EntityDescriptor.java
@@ -518,7 +518,8 @@ private void createEffectivePlanningPinIndexReader() {
case 0 -> effectivePlanningPinToIndexReader = null;
case 1 -> {
var memberAccessor = planningPinIndexMemberAccessorList.get(0);
- effectivePlanningPinToIndexReader = (solution, entity) -> (int) memberAccessor.executeGetter(entity);
+ effectivePlanningPinToIndexReader =
+ (solution, entity) -> (int) memberAccessor.executeGetter(entity);
}
default -> throw new IllegalStateException(
"The entityClass (%s) has (%d) @%s-annotated members (%s), where it should only have one."
diff --git a/python/jpyinterpreter/pom.xml b/python/jpyinterpreter/pom.xml
index 05f0539107..46e67b150e 100644
--- a/python/jpyinterpreter/pom.xml
+++ b/python/jpyinterpreter/pom.xml
@@ -61,6 +61,12 @@
runtime
+
+
+ org.jspecify
+ jspecify
+
+
org.junit.jupiter
diff --git a/python/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/AnnotationMetadata.java b/python/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/AnnotationMetadata.java
index f3593da02a..a1e1d65fc2 100644
--- a/python/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/AnnotationMetadata.java
+++ b/python/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/AnnotationMetadata.java
@@ -8,13 +8,17 @@
import java.util.List;
import java.util.Map;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Type;
-public record AnnotationMetadata(Class extends Annotation> annotationType, Map annotationValueMap) {
+public record AnnotationMetadata(@NonNull Class extends Annotation> annotationType,
+ @NonNull Map annotationValueMap,
+ @Nullable Class> fieldTypeOverride) {
public void addAnnotationTo(ClassVisitor classVisitor) {
visitAnnotation(classVisitor.visitAnnotation(Type.getDescriptor(annotationType), true));
}
@@ -30,6 +34,7 @@ public void addAnnotationTo(MethodVisitor methodVisitor) {
public static List getAnnotationListWithoutRepeatable(List metadata) {
List out = new ArrayList<>();
Map, List> repeatableAnnotationMap = new LinkedHashMap<>();
+ Map, Class>> fieldTypeOverrideMap = new LinkedHashMap<>();
for (AnnotationMetadata annotation : metadata) {
Repeatable repeatable = annotation.annotationType().getAnnotation(Repeatable.class);
if (repeatable == null) {
@@ -37,12 +42,14 @@ public static List getAnnotationListWithoutRepeatable(List new ArrayList<>()).add(annotation);
}
for (var entry : repeatableAnnotationMap.entrySet()) {
out.add(new AnnotationMetadata(entry.getKey(),
- Map.of("value", entry.getValue().toArray(AnnotationMetadata[]::new))));
+ Map.of("value", entry.getValue().toArray(AnnotationMetadata[]::new)),
+ fieldTypeOverrideMap.get(entry.getKey())));
}
return out;
}
diff --git a/python/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/InterfaceProxyGenerator.java b/python/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/InterfaceProxyGenerator.java
index 5e6e1ee3ff..f853ccd399 100644
--- a/python/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/InterfaceProxyGenerator.java
+++ b/python/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/InterfaceProxyGenerator.java
@@ -267,7 +267,7 @@ private static void createMethodDelegate(ClassWriter classWriter,
interfaceMethodVisitor.visitInsn(Opcodes.RETURN);
} else {
if (returnType.isPrimitive()) {
- DelegatingInterfaceImplementor.loadBoxedPrimitiveTypeClass(returnType, interfaceMethodVisitor);
+ JavaPythonTypeConversionImplementor.loadTypeClass(returnType, interfaceMethodVisitor);
} else {
interfaceMethodVisitor.visitLdcInsn(Type.getType(returnType));
}
@@ -279,7 +279,7 @@ private static void createMethodDelegate(ClassWriter classWriter,
PythonLikeObject.class)),
false);
if (returnType.isPrimitive()) {
- DelegatingInterfaceImplementor.unboxBoxedPrimitiveType(returnType, interfaceMethodVisitor);
+ JavaPythonTypeConversionImplementor.unboxBoxedPrimitiveType(returnType, interfaceMethodVisitor);
interfaceMethodVisitor.visitInsn(Type.getType(returnType).getOpcode(Opcodes.IRETURN));
} else {
interfaceMethodVisitor.visitTypeInsn(Opcodes.CHECKCAST, Type.getInternalName(returnType));
diff --git a/python/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/PythonClassTranslator.java b/python/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/PythonClassTranslator.java
index fb4a147f6e..573ef946d8 100644
--- a/python/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/PythonClassTranslator.java
+++ b/python/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/PythonClassTranslator.java
@@ -845,6 +845,11 @@ private static void createJavaGetterSetter(ClassWriter classWriter,
private static void createJavaGetter(ClassWriter classWriter, PreparedClassInfo preparedClassInfo,
MatchedMapping matchedMapping, String attributeName,
Type attributeType, Type getterType, String signature, TypeHint typeHint) {
+ var typeOverride = typeHint.getOverrideTypeDescriptor();
+ var isTypeOverridden = typeOverride != null;
+ if (isTypeOverridden) {
+ getterType = Type.getType(typeOverride);
+ }
var getterName = "get" + attributeName.substring(0, 1).toUpperCase() + attributeName.substring(1);
if (signature != null && Objects.equals(attributeType, getterType)) {
signature = "()" + signature;
@@ -858,6 +863,9 @@ private static void createJavaGetter(ClassWriter classWriter, PreparedClassInfo
}
getterVisitor.visitCode();
+ if (isTypeOverridden && !Objects.equals(attributeType, getterType)) {
+ JavaPythonTypeConversionImplementor.loadTypeClass(getterType, getterVisitor);
+ }
getterVisitor.visitVarInsn(Opcodes.ALOAD, 0);
getterVisitor.visitFieldInsn(Opcodes.GETFIELD, preparedClassInfo.classInternalName,
attributeName, attributeType.getDescriptor());
@@ -890,9 +898,22 @@ private static void createJavaGetter(ClassWriter classWriter, PreparedClassInfo
true);
getterVisitor.visitLabel(skipMapping);
}
- getterVisitor.visitTypeInsn(Opcodes.CHECKCAST, getterType.getInternalName());
+ if (isTypeOverridden) {
+ getterVisitor.visitMethodInsn(Opcodes.INVOKESTATIC,
+ Type.getInternalName(JavaPythonTypeConversionImplementor.class),
+ "convertPythonObjectToJavaType",
+ Type.getMethodDescriptor(Type.getType(Object.class),
+ Type.getType(Class.class),
+ Type.getType(PythonLikeObject.class)),
+ false);
+ }
+ if (getterType.getSort() == Type.OBJECT) {
+ getterVisitor.visitTypeInsn(Opcodes.CHECKCAST, getterType.getInternalName());
+ } else {
+ JavaPythonTypeConversionImplementor.unboxBoxedPrimitiveType(getterType, getterVisitor);
+ }
}
- getterVisitor.visitInsn(Opcodes.ARETURN);
+ getterVisitor.visitInsn(getterType.getOpcode(Opcodes.IRETURN));
getterVisitor.visitMaxs(maxStack, 0);
getterVisitor.visitEnd();
}
@@ -901,6 +922,11 @@ private static void createJavaSetter(ClassWriter classWriter, PreparedClassInfo
MatchedMapping matchedMapping, String attributeName,
Type attributeType, Type setterType, String signature, TypeHint typeHint) {
var setterName = "set" + attributeName.substring(0, 1).toUpperCase() + attributeName.substring(1);
+ var typeOverride = typeHint.getOverrideTypeDescriptor();
+ var isTypeOverridden = typeOverride != null;
+ if (isTypeOverridden) {
+ setterType = Type.getType(typeOverride);
+ }
if (signature != null && Objects.equals(attributeType, setterType)) {
signature = "(" + signature + ")V";
}
@@ -910,7 +936,10 @@ private static void createJavaSetter(ClassWriter classWriter, PreparedClassInfo
var maxStack = 2;
setterVisitor.visitCode();
setterVisitor.visitVarInsn(Opcodes.ALOAD, 0);
- setterVisitor.visitVarInsn(Opcodes.ALOAD, 1);
+ setterVisitor.visitVarInsn(setterType.getOpcode(Opcodes.ILOAD), 1);
+ if (setterType.getSort() != Type.OBJECT) {
+ JavaPythonTypeConversionImplementor.boxPrimitiveType(setterType, setterVisitor);
+ }
if (typeHint.type().isInstance(PythonNone.INSTANCE)) {
maxStack = 4;
// We want to replace null with None
@@ -941,6 +970,14 @@ private static void createJavaSetter(ClassWriter classWriter, PreparedClassInfo
true);
setterVisitor.visitLabel(skipMapping);
}
+ if (isTypeOverridden) {
+ setterVisitor.visitMethodInsn(Opcodes.INVOKESTATIC,
+ Type.getInternalName(JavaPythonTypeConversionImplementor.class),
+ "wrapJavaObject",
+ Type.getMethodDescriptor(Type.getType(PythonLikeObject.class),
+ Type.getType(Object.class)),
+ false);
+ }
setterVisitor.visitTypeInsn(Opcodes.CHECKCAST, attributeType.getInternalName());
}
setterVisitor.visitFieldInsn(Opcodes.PUTFIELD, preparedClassInfo.classInternalName,
diff --git a/python/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/TypeHint.java b/python/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/TypeHint.java
index c7341c23a7..0cba221c0b 100644
--- a/python/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/TypeHint.java
+++ b/python/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/TypeHint.java
@@ -8,6 +8,9 @@
import ai.timefold.jpyinterpreter.types.PythonLikeType;
+import org.jspecify.annotations.Nullable;
+import org.objectweb.asm.Type;
+
public record TypeHint(PythonLikeType type, List annotationList, TypeHint[] genericArgs,
PythonLikeType javaGetterType) {
public TypeHint {
@@ -22,6 +25,20 @@ public TypeHint(PythonLikeType type, List annotationList, Py
this(type, annotationList, null, javaGetterType);
}
+ @Nullable
+ public String getOverrideTypeDescriptor() {
+ Class> override = null;
+ for (var annotation : annotationList) {
+ var newOverride = annotation.fieldTypeOverride();
+ if (override != null && !override.equals(newOverride)) {
+ throw new IllegalArgumentException(
+ "Multiple override specified that do not match in annotations (" + annotationList + ").");
+ }
+ override = newOverride;
+ }
+ return (override != null) ? Type.getDescriptor(override) : null;
+ }
+
public TypeHint addAnnotations(List addedAnnotations) {
List combinedAnnotations = new ArrayList<>(annotationList.size() + addedAnnotations.size());
combinedAnnotations.addAll(annotationList);
diff --git a/python/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/implementors/DelegatingInterfaceImplementor.java b/python/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/implementors/DelegatingInterfaceImplementor.java
index f29e8e3a6d..9f944119ac 100644
--- a/python/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/implementors/DelegatingInterfaceImplementor.java
+++ b/python/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/implementors/DelegatingInterfaceImplementor.java
@@ -119,7 +119,7 @@ private void implementMethod(ClassWriter classWriter, PythonCompiledClass compil
interfaceMethodVisitor.visitInsn(Opcodes.RETURN);
} else {
if (returnType.isPrimitive()) {
- loadBoxedPrimitiveTypeClass(returnType, interfaceMethodVisitor);
+ JavaPythonTypeConversionImplementor.loadTypeClass(returnType, interfaceMethodVisitor);
} else {
interfaceMethodVisitor.visitLdcInsn(Type.getType(returnType));
}
@@ -131,7 +131,7 @@ private void implementMethod(ClassWriter classWriter, PythonCompiledClass compil
PythonLikeObject.class)),
false);
if (returnType.isPrimitive()) {
- unboxBoxedPrimitiveType(returnType, interfaceMethodVisitor);
+ JavaPythonTypeConversionImplementor.unboxBoxedPrimitiveType(returnType, interfaceMethodVisitor);
interfaceMethodVisitor.visitInsn(Type.getType(returnType).getOpcode(Opcodes.IRETURN));
} else {
interfaceMethodVisitor.visitTypeInsn(Opcodes.CHECKCAST, Type.getInternalName(returnType));
@@ -156,7 +156,7 @@ public static void prepareParametersForMethodCallFromArgumentSpec(Method interfa
interfaceMethodVisitor.visitVarInsn(Type.getType(parameterType).getOpcode(Opcodes.ILOAD),
i + 1);
if (parameterType.isPrimitive()) {
- convertPrimitiveToObjectType(parameterType, interfaceMethodVisitor);
+ JavaPythonTypeConversionImplementor.boxPrimitiveType(parameterType, interfaceMethodVisitor);
}
interfaceMethodVisitor.visitVarInsn(Opcodes.ALOAD, interfaceMethod.getParameterCount() + 1);
interfaceMethodVisitor.visitMethodInsn(Opcodes.INVOKESTATIC,
@@ -191,93 +191,4 @@ public static void prepareParametersForMethodCallFromArgumentSpec(Method interfa
interfaceMethodVisitor.visitInsn(Opcodes.POP);
}
- public static void convertPrimitiveToObjectType(Class> primitiveType, MethodVisitor methodVisitor) {
- if (primitiveType.equals(boolean.class)) {
- methodVisitor.visitMethodInsn(Opcodes.INVOKESTATIC, Type.getInternalName(Boolean.class),
- "valueOf", Type.getMethodDescriptor(Type.getType(Boolean.class), Type.BOOLEAN_TYPE), false);
- } else if (primitiveType.equals(byte.class)) {
- methodVisitor.visitMethodInsn(Opcodes.INVOKESTATIC, Type.getInternalName(Byte.class),
- "valueOf", Type.getMethodDescriptor(Type.getType(Byte.class), Type.BYTE_TYPE), false);
- } else if (primitiveType.equals(char.class)) {
- methodVisitor.visitMethodInsn(Opcodes.INVOKESTATIC, Type.getInternalName(Character.class),
- "valueOf", Type.getMethodDescriptor(Type.getType(Character.class), Type.CHAR_TYPE), false);
- } else if (primitiveType.equals(short.class)) {
- methodVisitor.visitMethodInsn(Opcodes.INVOKESTATIC, Type.getInternalName(Short.class),
- "valueOf", Type.getMethodDescriptor(Type.getType(Short.class), Type.SHORT_TYPE), false);
- } else if (primitiveType.equals(int.class)) {
- methodVisitor.visitMethodInsn(Opcodes.INVOKESTATIC, Type.getInternalName(Integer.class),
- "valueOf", Type.getMethodDescriptor(Type.getType(Integer.class), Type.INT_TYPE), false);
- } else if (primitiveType.equals(long.class)) {
- methodVisitor.visitMethodInsn(Opcodes.INVOKESTATIC, Type.getInternalName(Long.class),
- "valueOf", Type.getMethodDescriptor(Type.getType(Long.class), Type.LONG_TYPE), false);
- } else if (primitiveType.equals(float.class)) {
- methodVisitor.visitMethodInsn(Opcodes.INVOKESTATIC, Type.getInternalName(Float.class),
- "valueOf", Type.getMethodDescriptor(Type.getType(Float.class), Type.FLOAT_TYPE), false);
- } else if (primitiveType.equals(double.class)) {
- methodVisitor.visitMethodInsn(Opcodes.INVOKESTATIC, Type.getInternalName(Double.class),
- "valueOf", Type.getMethodDescriptor(Type.getType(Double.class), Type.DOUBLE_TYPE), false);
- } else {
- throw new IllegalStateException("Unknown primitive type %s.".formatted(primitiveType));
- }
- }
-
- public static void loadBoxedPrimitiveTypeClass(Class> primitiveType, MethodVisitor methodVisitor) {
- if (primitiveType.equals(boolean.class)) {
- methodVisitor.visitLdcInsn(Type.getType(Boolean.class));
- } else if (primitiveType.equals(byte.class)) {
- methodVisitor.visitLdcInsn(Type.getType(Byte.class));
- } else if (primitiveType.equals(char.class)) {
- methodVisitor.visitLdcInsn(Type.getType(Character.class));
- } else if (primitiveType.equals(short.class)) {
- methodVisitor.visitLdcInsn(Type.getType(Short.class));
- } else if (primitiveType.equals(int.class)) {
- methodVisitor.visitLdcInsn(Type.getType(Integer.class));
- } else if (primitiveType.equals(long.class)) {
- methodVisitor.visitLdcInsn(Type.getType(Long.class));
- } else if (primitiveType.equals(float.class)) {
- methodVisitor.visitLdcInsn(Type.getType(Float.class));
- } else if (primitiveType.equals(double.class)) {
- methodVisitor.visitLdcInsn(Type.getType(Double.class));
- } else {
- throw new IllegalStateException("Unknown primitive type %s.".formatted(primitiveType));
- }
- }
-
- public static void unboxBoxedPrimitiveType(Class> primitiveType, MethodVisitor methodVisitor) {
- if (primitiveType.equals(boolean.class)) {
- methodVisitor.visitTypeInsn(Opcodes.CHECKCAST, Type.getInternalName(Boolean.class));
- methodVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, Type.getInternalName(Boolean.class),
- "booleanValue", Type.getMethodDescriptor(Type.BOOLEAN_TYPE), false);
- } else if (primitiveType.equals(byte.class)) {
- methodVisitor.visitTypeInsn(Opcodes.CHECKCAST, Type.getInternalName(Byte.class));
- methodVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, Type.getInternalName(Byte.class),
- "byteValue", Type.getMethodDescriptor(Type.BYTE_TYPE), false);
- } else if (primitiveType.equals(char.class)) {
- methodVisitor.visitTypeInsn(Opcodes.CHECKCAST, Type.getInternalName(Character.class));
- methodVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, Type.getInternalName(Character.class),
- "charValue", Type.getMethodDescriptor(Type.CHAR_TYPE), false);
- } else if (primitiveType.equals(short.class)) {
- methodVisitor.visitTypeInsn(Opcodes.CHECKCAST, Type.getInternalName(Short.class));
- methodVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, Type.getInternalName(Short.class),
- "shortValue", Type.getMethodDescriptor(Type.SHORT_TYPE), false);
- } else if (primitiveType.equals(int.class)) {
- methodVisitor.visitTypeInsn(Opcodes.CHECKCAST, Type.getInternalName(Integer.class));
- methodVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, Type.getInternalName(Integer.class),
- "intValue", Type.getMethodDescriptor(Type.INT_TYPE), false);
- } else if (primitiveType.equals(long.class)) {
- methodVisitor.visitTypeInsn(Opcodes.CHECKCAST, Type.getInternalName(Long.class));
- methodVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, Type.getInternalName(Long.class),
- "longValue", Type.getMethodDescriptor(Type.LONG_TYPE), false);
- } else if (primitiveType.equals(float.class)) {
- methodVisitor.visitTypeInsn(Opcodes.CHECKCAST, Type.getInternalName(Float.class));
- methodVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, Type.getInternalName(Float.class),
- "floatValue", Type.getMethodDescriptor(Type.FLOAT_TYPE), false);
- } else if (primitiveType.equals(double.class)) {
- methodVisitor.visitTypeInsn(Opcodes.CHECKCAST, Type.getInternalName(Double.class));
- methodVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, Type.getInternalName(Double.class),
- "doubleValue", Type.getMethodDescriptor(Type.DOUBLE_TYPE), false);
- } else {
- throw new IllegalStateException("Unknown primitive type %s.".formatted(primitiveType));
- }
- }
}
diff --git a/python/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/implementors/JavaPythonTypeConversionImplementor.java b/python/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/implementors/JavaPythonTypeConversionImplementor.java
index ba302add4f..7a5d71710b 100644
--- a/python/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/implementors/JavaPythonTypeConversionImplementor.java
+++ b/python/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/implementors/JavaPythonTypeConversionImplementor.java
@@ -340,6 +340,108 @@ public static void loadName(MethodVisitor methodVisitor, String name) {
false);
}
+ public static void unboxBoxedPrimitiveType(Class> primitiveType, MethodVisitor methodVisitor) {
+ unboxBoxedPrimitiveType(Type.getType(primitiveType), methodVisitor);
+ }
+
+ public static void unboxBoxedPrimitiveType(Type primitiveType, MethodVisitor methodVisitor) {
+ if (primitiveType.equals(Type.BOOLEAN_TYPE)) {
+ methodVisitor.visitTypeInsn(Opcodes.CHECKCAST, Type.getInternalName(Boolean.class));
+ methodVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, Type.getInternalName(Boolean.class),
+ "booleanValue", Type.getMethodDescriptor(Type.BOOLEAN_TYPE), false);
+ } else if (primitiveType.equals(Type.BYTE_TYPE)) {
+ methodVisitor.visitTypeInsn(Opcodes.CHECKCAST, Type.getInternalName(Byte.class));
+ methodVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, Type.getInternalName(Byte.class),
+ "byteValue", Type.getMethodDescriptor(Type.BYTE_TYPE), false);
+ } else if (primitiveType.equals(Type.CHAR_TYPE)) {
+ methodVisitor.visitTypeInsn(Opcodes.CHECKCAST, Type.getInternalName(Character.class));
+ methodVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, Type.getInternalName(Character.class),
+ "charValue", Type.getMethodDescriptor(Type.CHAR_TYPE), false);
+ } else if (primitiveType.equals(Type.SHORT_TYPE)) {
+ methodVisitor.visitTypeInsn(Opcodes.CHECKCAST, Type.getInternalName(Short.class));
+ methodVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, Type.getInternalName(Short.class),
+ "shortValue", Type.getMethodDescriptor(Type.SHORT_TYPE), false);
+ } else if (primitiveType.equals(Type.INT_TYPE)) {
+ methodVisitor.visitTypeInsn(Opcodes.CHECKCAST, Type.getInternalName(Integer.class));
+ methodVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, Type.getInternalName(Integer.class),
+ "intValue", Type.getMethodDescriptor(Type.INT_TYPE), false);
+ } else if (primitiveType.equals(Type.LONG_TYPE)) {
+ methodVisitor.visitTypeInsn(Opcodes.CHECKCAST, Type.getInternalName(Long.class));
+ methodVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, Type.getInternalName(Long.class),
+ "longValue", Type.getMethodDescriptor(Type.LONG_TYPE), false);
+ } else if (primitiveType.equals(Type.FLOAT_TYPE)) {
+ methodVisitor.visitTypeInsn(Opcodes.CHECKCAST, Type.getInternalName(Float.class));
+ methodVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, Type.getInternalName(Float.class),
+ "floatValue", Type.getMethodDescriptor(Type.FLOAT_TYPE), false);
+ } else if (primitiveType.equals(Type.DOUBLE_TYPE)) {
+ methodVisitor.visitTypeInsn(Opcodes.CHECKCAST, Type.getInternalName(Double.class));
+ methodVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, Type.getInternalName(Double.class),
+ "doubleValue", Type.getMethodDescriptor(Type.DOUBLE_TYPE), false);
+ } else {
+ throw new IllegalStateException("Unknown primitive type %s.".formatted(primitiveType));
+ }
+ }
+
+ public static void boxPrimitiveType(Class> primitiveType, MethodVisitor methodVisitor) {
+ boxPrimitiveType(Type.getType(primitiveType), methodVisitor);
+ }
+
+ public static void boxPrimitiveType(Type primitiveType, MethodVisitor methodVisitor) {
+ if (primitiveType.equals(Type.BOOLEAN_TYPE)) {
+ methodVisitor.visitMethodInsn(Opcodes.INVOKESTATIC, Type.getInternalName(Boolean.class),
+ "valueOf", Type.getMethodDescriptor(Type.getType(Boolean.class), Type.BOOLEAN_TYPE), false);
+ } else if (primitiveType.equals(Type.BYTE_TYPE)) {
+ methodVisitor.visitMethodInsn(Opcodes.INVOKESTATIC, Type.getInternalName(Byte.class),
+ "valueOf", Type.getMethodDescriptor(Type.getType(Byte.class), Type.BYTE_TYPE), false);
+ } else if (primitiveType.equals(Type.CHAR_TYPE)) {
+ methodVisitor.visitMethodInsn(Opcodes.INVOKESTATIC, Type.getInternalName(Character.class),
+ "valueOf", Type.getMethodDescriptor(Type.getType(Character.class), Type.CHAR_TYPE), false);
+ } else if (primitiveType.equals(Type.SHORT_TYPE)) {
+ methodVisitor.visitMethodInsn(Opcodes.INVOKESTATIC, Type.getInternalName(Short.class),
+ "valueOf", Type.getMethodDescriptor(Type.getType(Short.class), Type.SHORT_TYPE), false);
+ } else if (primitiveType.equals(Type.INT_TYPE)) {
+ methodVisitor.visitMethodInsn(Opcodes.INVOKESTATIC, Type.getInternalName(Integer.class),
+ "valueOf", Type.getMethodDescriptor(Type.getType(Integer.class), Type.INT_TYPE), false);
+ } else if (primitiveType.equals(Type.LONG_TYPE)) {
+ methodVisitor.visitMethodInsn(Opcodes.INVOKESTATIC, Type.getInternalName(Long.class),
+ "valueOf", Type.getMethodDescriptor(Type.getType(Long.class), Type.LONG_TYPE), false);
+ } else if (primitiveType.equals(Type.FLOAT_TYPE)) {
+ methodVisitor.visitMethodInsn(Opcodes.INVOKESTATIC, Type.getInternalName(Float.class),
+ "valueOf", Type.getMethodDescriptor(Type.getType(Float.class), Type.FLOAT_TYPE), false);
+ } else if (primitiveType.equals(Type.DOUBLE_TYPE)) {
+ methodVisitor.visitMethodInsn(Opcodes.INVOKESTATIC, Type.getInternalName(Double.class),
+ "valueOf", Type.getMethodDescriptor(Type.getType(Double.class), Type.DOUBLE_TYPE), false);
+ } else {
+ throw new IllegalStateException("Unknown primitive type %s.".formatted(primitiveType));
+ }
+ }
+
+ public static void loadTypeClass(Class> type, MethodVisitor methodVisitor) {
+ loadTypeClass(Type.getType(type), methodVisitor);
+ }
+
+ public static void loadTypeClass(Type type, MethodVisitor methodVisitor) {
+ if (type.equals(Type.BOOLEAN_TYPE)) {
+ methodVisitor.visitLdcInsn(Type.getType(Boolean.class));
+ } else if (type.equals(Type.BYTE_TYPE)) {
+ methodVisitor.visitLdcInsn(Type.getType(Byte.class));
+ } else if (type.equals(Type.CHAR_TYPE)) {
+ methodVisitor.visitLdcInsn(Type.getType(Character.class));
+ } else if (type.equals(Type.SHORT_TYPE)) {
+ methodVisitor.visitLdcInsn(Type.getType(Short.class));
+ } else if (type.equals(Type.INT_TYPE)) {
+ methodVisitor.visitLdcInsn(Type.getType(Integer.class));
+ } else if (type.equals(Type.LONG_TYPE)) {
+ methodVisitor.visitLdcInsn(Type.getType(Long.class));
+ } else if (type.equals(Type.FLOAT_TYPE)) {
+ methodVisitor.visitLdcInsn(Type.getType(Float.class));
+ } else if (type.equals(Type.DOUBLE_TYPE)) {
+ methodVisitor.visitLdcInsn(Type.getType(Double.class));
+ } else {
+ methodVisitor.visitLdcInsn(type);
+ }
+ }
+
private record ReturnValueOpDescriptor(
String wrapperClassName,
String methodName,
diff --git a/python/jpyinterpreter/src/main/python/annotations.py b/python/jpyinterpreter/src/main/python/annotations.py
index 77fc952460..d61bdba8e9 100644
--- a/python/jpyinterpreter/src/main/python/annotations.py
+++ b/python/jpyinterpreter/src/main/python/annotations.py
@@ -18,6 +18,7 @@ def get_value(self) -> Any:
class JavaAnnotation:
annotation_type: JClass
annotation_values: Dict[str, Any]
+ field_type_override: JClass | None = None
def __hash__(self):
return 0
@@ -191,7 +192,11 @@ def convert_java_annotation(java_annotation: JavaAnnotation):
attribute_name, attribute_value)
if java_attribute_value is not None:
annotation_values.put(attribute_name, java_attribute_value)
- return AnnotationMetadata(java_annotation.annotation_type.class_, annotation_values)
+
+ field_type_override = (java_annotation.field_type_override.class_
+ if java_annotation.field_type_override is not None
+ else None)
+ return AnnotationMetadata(java_annotation.annotation_type.class_, annotation_values, field_type_override)
def convert_annotation_value(annotation_type: JClass, attribute_type: JClass, attribute_name: str, attribute_value: Any):
diff --git a/python/jpyinterpreter/tests/test_classes.py b/python/jpyinterpreter/tests/test_classes.py
index 4bd5382394..7e45b6569d 100644
--- a/python/jpyinterpreter/tests/test_classes.py
+++ b/python/jpyinterpreter/tests/test_classes.py
@@ -1,6 +1,5 @@
-from typing import Type
-
import pytest
+from typing import Type
from .conftest import verifier_for
@@ -942,11 +941,13 @@ def is_red(order: Order):
def test_class_annotations():
from typing import Annotated
- from java.lang import Deprecated
+ from jpype import JInt
+ from java.lang import Deprecated, Integer
from java.lang.annotation import Target, ElementType
from ai.timefold.solver.core.api.domain.variable import PiggybackShadowVariable
from jpyinterpreter import (add_class_annotation, JavaAnnotation, translate_python_class_to_java_class,
- get_java_type_for_python_type)
+ get_java_type_for_python_type, convert_to_java_python_like_object)
+ from ai.timefold.jpyinterpreter.types.numeric import PythonInteger
class B:
some_field: int
@@ -967,6 +968,14 @@ class A:
'shadowEntityClass': B
})
]
+ type_overridden: Annotated[int, JavaAnnotation(Deprecated, {
+ 'forRemoval': True,
+ 'since': '1.0.0'
+ }, field_type_override=Integer)]
+ primitive_type_overridden: Annotated[int, JavaAnnotation(Deprecated, {
+ 'forRemoval': True,
+ 'since': '1.0.0'
+ }, field_type_override=JInt)]
def my_method(self) -> Annotated[str, 'extra', JavaAnnotation(Deprecated, {
'forRemoval': False,
@@ -981,7 +990,9 @@ def my_method(self) -> Annotated[str, 'extra', JavaAnnotation(Deprecated, {
assert annotations[0].forRemoval()
assert annotations[0].since() == '0.0.0'
- annotations = translated_class.getMethod('getMy_field').getAnnotations()
+ my_field_getter = translated_class.getMethod('getMy_field')
+ annotations = my_field_getter.getAnnotations()
+ assert my_field_getter.getReturnType() == PythonInteger.class_
assert len(annotations) == 3
assert isinstance(annotations[0], Deprecated)
assert annotations[0].forRemoval()
@@ -992,12 +1003,29 @@ def my_method(self) -> Annotated[str, 'extra', JavaAnnotation(Deprecated, {
assert annotations[2].shadowVariableName() == 'some_field'
assert annotations[2].shadowEntityClass() == get_java_type_for_python_type(B).getJavaClass()
+ type_overridden_getter = translated_class.getMethod('getType_overridden')
+ assert type_overridden_getter.getReturnType() == Integer.class_
+
+ type_overridden_getter = translated_class.getMethod('getPrimitive_type_overridden')
+ assert type_overridden_getter.getReturnType() == JInt.class_
+
annotations = translated_class.getMethod('$method$my_method').getAnnotations()
assert len(annotations) == 1
assert isinstance(annotations[0], Deprecated)
assert annotations[0].forRemoval() is False
assert annotations[0].since() == '2.0.0'
+ a = A()
+ a.my_field = 1
+ a.type_overridden = 2
+ a.primitive_type_overridden = 3
+
+ converted_a = convert_to_java_python_like_object(a)
+ assert isinstance(converted_a.getMy_field(), PythonInteger)
+ assert converted_a.getMy_field().equals(PythonInteger.valueOf(1))
+ assert converted_a.getType_overridden() == 2
+ assert converted_a.getPrimitive_type_overridden() == 3
+
def test_extra_attributes():
from jpyinterpreter import convert_to_java_python_like_object, unwrap_python_like_object
diff --git a/python/jpyinterpreter/tox.ini b/python/jpyinterpreter/tox.ini
index 46a1cba429..51300fc1e4 100644
--- a/python/jpyinterpreter/tox.ini
+++ b/python/jpyinterpreter/tox.ini
@@ -4,7 +4,7 @@
# and then run "tox" from this directory.
[tox]
-env_list = py310,py311,p312
+env_list = py310,py311,py312
[testenv]
pass_env = * # needed by tox4, to pass JAVA_HOME
diff --git a/python/python-core/src/main/python/domain/_annotations.py b/python/python-core/src/main/python/domain/_annotations.py
index 1c7109be07..6a2a5b3b88 100644
--- a/python/python-core/src/main/python/domain/_annotations.py
+++ b/python/python-core/src/main/python/domain/_annotations.py
@@ -38,7 +38,7 @@ def __init__(self):
super().__init__(JavaPlanningId, {})
-class PlanningPin:
+class PlanningPin(JavaAnnotation):
"""
Specifies that a boolean attribute of a `planning_entity` determines if the planning entity is pinned.
A pinned planning entity is never changed during planning.
@@ -60,8 +60,77 @@ class PlanningPin:
>>> @planning_entity
... class Lesson:
... is_pinned: Annotated[bool, PlanningPin]
+
+ See Also
+ --------
+ PlanningPinToIndex
+ """
+
+ def __init__(self):
+ ensure_init()
+ from ai.timefold.solver.core.api.domain.entity import PlanningPin as JavaPlanningPin
+ from jpype import JBoolean
+ super().__init__(JavaPlanningPin, {}, field_type_override=JBoolean)
+
+
+class PlanningPinToIndex(JavaAnnotation):
"""
- pass
+ Specifies that an int attribute of a `planning_entity` determines how far
+ a PlanningListVariable is pinned.
+
+ This annotation can only be specified on an attribute of the same entity,
+ which also specifies a PlanningListVariable.
+
+ The annotated int field has the following semantics:
+
+ - 0: Pinning is disabled.
+ All the values in the list can be removed,
+ new values may be added anywhere in the list,
+ values in the list may be reordered.
+
+ - Positive int: Values before this index in the list are pinned.
+ No value can be added at those indexes,
+ removed from them, or shuffled between them.
+ Values on or after this index are not pinned
+ and can be added, removed or shuffled freely.
+
+ - Positive int that exceeds the lists size: fail fast.
+
+ - Negative int: fail fast.
+
+ To pin the entire list and disallow any changes, use PlanningPin instead.
+
+ Example: Assuming a list of values [A, B, C]:
+
+ - 0 allows the entire list to be modified.
+ - 1 pins [A] rest of the list may be modified or added to.
+ - 2 pins [A, B] rest of the list may be modified or added to.
+ - 3 pins [A, B, C] the list can only be added to.
+ - 4 fails fast as there is no such index in the list.
+
+ If the same entity also specifies a PlanningPin and the pin is enabled,
+ any value of PlanningPinToIndex is ignored.
+ In other words, enabling PlanningPin pins the entire list without exception.
+
+ Examples
+ --------
+ >>> from timefold.solver.domain import PlanningPinToIndex, planning_entity
+ >>> from typing import Annotated
+ >>>
+ >>> @planning_entity
+ ... class Visit:
+ ... first_unpinned_index: Annotated[int, PlanningPinToIndex]
+
+ See Also
+ --------
+ planning_entity
+ """
+
+ def __init__(self):
+ ensure_init()
+ from ai.timefold.solver.core.api.domain.entity import PlanningPinToIndex as JavaPlanningPinToIndex
+ from jpype import JInt
+ super().__init__(JavaPlanningPinToIndex, {}, field_type_override=JInt)
class PlanningVariableGraphType(Enum):
@@ -716,28 +785,10 @@ def planning_entity(entity_class: Type = None, /, *, pinning_filter: Callable =
def planning_entity_wrapper(entity_class_argument):
from .._timefold_java_interop import _add_to_compilation_queue
from _jpyinterpreter import add_class_annotation
- from typing import get_origin, Annotated
-
- planning_pin_field = None
- for name, type_hint in entity_class_argument.__annotations__.items():
- if get_origin(type_hint) == Annotated:
- for metadata in type_hint.__metadata__:
- if metadata == PlanningPin or isinstance(metadata, PlanningPin):
- if planning_pin_field is not None:
- raise ValueError(f'Only one attribute can be annotated with PlanningPin, '
- f'but found multiple fields ({planning_pin_field} and {name}).')
- planning_pin_field = name
pinning_filter_function = None
if pinning_filter is not None:
- if planning_pin_field is not None:
- pinning_filter_function = lambda solution, entity: (getattr(entity, planning_pin_field, False) or
- pinning_filter(solution, entity))
- else:
- pinning_filter_function = pinning_filter
- else:
- if planning_pin_field is not None:
- pinning_filter_function = lambda solution, entity: getattr(entity, planning_pin_field, False)
+ pinning_filter_function = pinning_filter
out = add_class_annotation(JavaPlanningEntity,
pinningFilter=pinning_filter_function)(entity_class_argument)
@@ -823,9 +874,9 @@ def constraint_configuration(constraint_configuration_class: Type[Solution_]) ->
return out
-__all__ = ['PlanningId', 'PlanningScore', 'PlanningPin', 'PlanningVariable',
- 'PlanningVariableGraphType', 'PlanningListVariable', 'ShadowVariable',
- 'PiggybackShadowVariable', 'CascadingUpdateShadowVariable',
+__all__ = ['PlanningId', 'PlanningScore', 'PlanningPin', 'PlanningPinToIndex',
+ 'PlanningVariable', 'PlanningVariableGraphType', 'PlanningListVariable',
+ 'ShadowVariable', 'PiggybackShadowVariable', 'CascadingUpdateShadowVariable',
'IndexShadowVariable', 'PreviousElementShadowVariable', 'NextElementShadowVariable',
'AnchorShadowVariable', 'InverseRelationShadowVariable',
'ProblemFactProperty', 'ProblemFactCollectionProperty',
diff --git a/python/python-core/tests/test_pinning.py b/python/python-core/tests/test_pinning.py
index a201467b61..b643083e09 100644
--- a/python/python-core/tests/test_pinning.py
+++ b/python/python-core/tests/test_pinning.py
@@ -1,9 +1,8 @@
+from dataclasses import dataclass, field
from timefold.solver import *
-from timefold.solver.domain import *
from timefold.solver.config import *
+from timefold.solver.domain import *
from timefold.solver.score import *
-
-from dataclasses import dataclass, field
from typing import Annotated, List
@@ -94,3 +93,51 @@ def my_constraints(constraint_factory: ConstraintFactory):
solver = SolverFactory.create(solver_config).build_solver()
solution = solver.solve(problem)
assert solution.score.score == -2
+
+
+def test_planning_pin_to_index():
+ @planning_entity
+ @dataclass
+ class Point:
+ value: Annotated[list[int], PlanningListVariable]
+ unpinned_start: Annotated[int, PlanningPinToIndex] = field(default=0)
+
+ @planning_solution
+ @dataclass
+ class Solution:
+ values: Annotated[List[int], ValueRangeProvider]
+ points: Annotated[List[Point], PlanningEntityCollectionProperty]
+ score: Annotated[SimpleScore, PlanningScore] = field(default=None)
+
+ def penalty_function(point: Point):
+ penalty = 0
+ for i in range(len(point.value)):
+ penalty += point.value[i] * i
+ return penalty
+
+ @constraint_provider
+ def my_constraints(constraint_factory: ConstraintFactory):
+ return [
+ constraint_factory.for_each(Point)
+ .penalize(SimpleScore.ONE, penalty_function)
+ .as_constraint('Minimize Value')
+ ]
+
+ solver_config = SolverConfig(
+ solution_class=Solution,
+ entity_class_list=[Point],
+ score_director_factory_config=ScoreDirectorFactoryConfig(
+ constraint_provider_function=my_constraints
+ ),
+ termination_config=TerminationConfig(
+ unimproved_spent_limit=Duration(milliseconds=100)
+ )
+ )
+ problem: Solution = Solution([0, 1, 2],
+ [
+ Point([0, 2], unpinned_start=2),
+ Point([1]),
+ ])
+ solver = SolverFactory.create(solver_config).build_solver()
+ solution = solver.solve(problem)
+ assert solution.score.score == -2
diff --git a/tox.ini b/tox.ini
index ecffb96174..3e4309b781 100644
--- a/tox.ini
+++ b/tox.ini
@@ -4,7 +4,7 @@
# and then run "tox" from this directory.
[tox]
-env_list = py310,py311,p312
+env_list = py310,py311,py312
[testenv]
pass_env = * # needed by tox4, to pass JAVA_HOME