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

Commit 5f4c79d

Browse files
feat: Add support for Python 3.12 (#27)
* Python 3.12 changed the meaning of the arg for both COMPARE_OP and JUMP instructions: * For COMPARE_OP, the index now starts at the fifth-lowest bit. * For JUMP instructions, "The argument of a jump is the offset of the target instruction relative to the instruction that appears immediately after the jump instruction’s CACHE entries. As a consequence, the presence of the CACHE instructions is transparent for forward jumps but needs to be taken into account when reasoning about backward jumps." For both of these, we parse the argrepr. In particular: * For COMPARE_OP, it be "<", ">", "==", "!=", ">=" or "<=". * For JUMP instructions, it be "to BYTECODE_OFFSET", which is exactly twice the instruction index. * Python 3.12 changed the meaning of FOR_ITER to leave the iterator on the stack (and push an undocumented object) when exhausted, and added the instruction END_FOR to pop them both off. However, since the target for FOR_ITER is always END_FOR (see https://github.com/python/cpython/blob/17a82a1d16a46b6c008240bcc698619419ce5554/Python/bytecodes.c#L2289-L2293), we can "optimize" it by using the old implementation and making END_FOR a no-op. * Python 3.12 merged LOAD_ATTR and LOAD_METHOD into one opcode. * Python 3.12 added BINARY_SLICE and STORE_SLICE opcodes to implement `a[start:end]` and `a[start:end] = iterable` (previously, BUILD_SLICE was used followed by `GET_ITEM` or `SET_ITEM`). * Python 3.12 added END_SEND for generator cleanup, which we implement as a NOOP. * Python 3.12 added CLEANUP_THROW and INTRINSIC_STOPITERATION_ERROR, which replace TOS with its value if TOS a StopIteration, else reraise the error on TOS. CLEANUP_THROW additionally pops two additional values off the stack. * Python 3.12 added LOAD_FAST_CHECK and LOAD_FAST_AND_CLEAR. LOAD_FAST now will never raise unbound local error, LOAD_FAST_CHECK might raise an unbound local error, and LOAD_FAST_AND_CLEAR will load NONE when the local is unbound (and additionally sets the local to null). Since Java rightfully says "???" when it sees code that can access an unbound local, we need to set locals used by LOAD_FAST_AND_CLEAR to null before generating code for opcodes. * Python 3.12 added RETURN_CONST to return a constant. * Python 3.12 changed UNARY_POS from a "fast" opcode to a slow "intrinsic" opcode since "+a" is rarely used. This does not affect us (other than having a new alternative way of writing "UNARY_POS" as "INTRINSIC_1_INTRINSIC_UNARY_POSITIVE". * Simplified and fix some bugs in PythonSlice code * Add a workaround for jpype-project/jpype#1178
1 parent 458c5b9 commit 5f4c79d

File tree

59 files changed

+806
-398
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

59 files changed

+806
-398
lines changed

.github/workflows/pull_request.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -72,13 +72,13 @@ jobs:
7272
distribution: 'temurin'
7373
cache: 'maven'
7474
# Need to install all Python versions in the same run for tox
75-
- name: Python 3.9, Python 3.10, Python 3.11 Setup
75+
- name: Python 3.10, Python 3.11, Python 3.12 Setup
7676
uses: actions/setup-python@v4
7777
with:
7878
python-version: |
79-
3.9
8079
3.10
8180
3.11
81+
3.12
8282
cache: 'pip'
8383
cache-dependency-path: |
8484
**/setup.py

.github/workflows/sonarcloud.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -77,13 +77,13 @@ jobs:
7777
key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
7878
restore-keys: ${{ runner.os }}-m2
7979
# Need to install all Python versions in the same run for tox
80-
- name: Python 3.9, Python 3.10, Python 3.11 Setup
80+
- name: Python 3.10, Python 3.11, Python 3.12 Setup
8181
uses: actions/setup-python@v4
8282
with:
8383
python-version: |
84-
3.9
8584
3.10
8685
3.11
86+
3.12
8787
cache: 'pip'
8888
cache-dependency-path: |
8989
**/setup.py

jpyinterpreter/setup.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,9 @@ def run(self):
7777
'org-stubs': 'src/main/resources',
7878
},
7979
test_suite='tests',
80-
python_requires='>=3.9',
80+
python_requires='>=3.10',
8181
install_requires=[
82-
'JPype1>=1.4.1',
82+
'JPype1>=1.5.0',
8383
],
8484
cmdclass={'build_py': FetchDependencies},
8585
package_data={

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

+12-10
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
11
package ai.timefold.jpyinterpreter;
22

3+
import java.util.Objects;
4+
35
public enum CompareOp {
4-
LESS_THAN(0, "__lt__"),
5-
LESS_THAN_OR_EQUALS(1, "__le__"),
6-
EQUALS(2, "__eq__"),
7-
NOT_EQUALS(3, "__ne__"),
8-
GREATER_THAN(4, "__gt__"),
9-
GREATER_THAN_OR_EQUALS(5, "__ge__");
6+
LESS_THAN("<", "__lt__"),
7+
LESS_THAN_OR_EQUALS("<=", "__le__"),
8+
EQUALS("==", "__eq__"),
9+
NOT_EQUALS("!=", "__ne__"),
10+
GREATER_THAN(">", "__gt__"),
11+
GREATER_THAN_OR_EQUALS(">=", "__ge__");
1012

11-
public final int id;
13+
public final String id;
1214
public final String dunderMethod;
1315

14-
CompareOp(int id, String dunderMethod) {
16+
CompareOp(String id, String dunderMethod) {
1517
this.id = id;
1618
this.dunderMethod = dunderMethod;
1719
}
@@ -25,9 +27,9 @@ public static CompareOp getOpForDunderMethod(String dunderMethod) {
2527
throw new IllegalArgumentException("No Op corresponds to dunder method (" + dunderMethod + ")");
2628
}
2729

28-
public static CompareOp getOp(int id) {
30+
public static CompareOp getOp(String id) {
2931
for (CompareOp op : CompareOp.values()) {
30-
if (op.id == id) {
32+
if (Objects.equals(op.id, id)) {
3133
return op;
3234
}
3335
}

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

+12-6
Original file line numberDiff line numberDiff line change
@@ -4,30 +4,36 @@
44

55
import ai.timefold.jpyinterpreter.opcodes.descriptor.OpcodeDescriptor;
66

7-
public record PythonBytecodeInstruction(String opname, int offset, int arg, OptionalInt startsLine,
7+
public record PythonBytecodeInstruction(String opname, int offset, int arg,
8+
String argRepr, OptionalInt startsLine,
89
boolean isJumpTarget) {
910
public static PythonBytecodeInstruction atOffset(String opname, int offset) {
10-
return new PythonBytecodeInstruction(opname, offset, 0, OptionalInt.empty(), false);
11+
return new PythonBytecodeInstruction(opname, offset, 0, "", OptionalInt.empty(), false);
1112
}
1213

1314
public static PythonBytecodeInstruction atOffset(OpcodeDescriptor instruction, int offset) {
1415
return atOffset(instruction.name(), offset);
1516
}
1617

1718
public PythonBytecodeInstruction withArg(int newArg) {
18-
return new PythonBytecodeInstruction(opname, offset, newArg, startsLine, isJumpTarget);
19+
return new PythonBytecodeInstruction(opname, offset, newArg, argRepr, startsLine, isJumpTarget);
20+
}
21+
22+
public PythonBytecodeInstruction withArgRepr(String newArgRepr) {
23+
return new PythonBytecodeInstruction(opname, offset, arg, newArgRepr, startsLine, isJumpTarget);
1924
}
2025

2126
public PythonBytecodeInstruction startsLine(int lineNumber) {
22-
return new PythonBytecodeInstruction(opname, offset, arg, OptionalInt.of(lineNumber), isJumpTarget);
27+
return new PythonBytecodeInstruction(opname, offset, arg, argRepr, OptionalInt.of(lineNumber),
28+
isJumpTarget);
2329
}
2430

2531
public PythonBytecodeInstruction withIsJumpTarget(boolean isJumpTarget) {
26-
return new PythonBytecodeInstruction(opname, offset, arg, startsLine, isJumpTarget);
32+
return new PythonBytecodeInstruction(opname, offset, arg, argRepr, startsLine, isJumpTarget);
2733
}
2834

2935
public PythonBytecodeInstruction markAsJumpTarget() {
30-
return new PythonBytecodeInstruction(opname, offset, arg, startsLine, true);
36+
return new PythonBytecodeInstruction(opname, offset, arg, argRepr, startsLine, true);
3137
}
3238

3339
@Override

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

+23-6
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@
88
import java.nio.file.Path;
99
import java.util.ArrayList;
1010
import java.util.Arrays;
11+
import java.util.Collections;
1112
import java.util.HashMap;
1213
import java.util.HashSet;
1314
import java.util.List;
1415
import java.util.Map;
1516
import java.util.Set;
17+
import java.util.TreeSet;
1618
import java.util.function.BiFunction;
1719
import java.util.function.Consumer;
1820
import java.util.stream.Collectors;
@@ -30,6 +32,7 @@
3032
import ai.timefold.jpyinterpreter.opcodes.OpcodeWithoutSource;
3133
import ai.timefold.jpyinterpreter.opcodes.SelfOpcodeWithoutSource;
3234
import ai.timefold.jpyinterpreter.opcodes.descriptor.GeneratorOpDescriptor;
35+
import ai.timefold.jpyinterpreter.opcodes.variable.LoadFastAndClearOpcode;
3336
import ai.timefold.jpyinterpreter.types.BuiltinTypes;
3437
import ai.timefold.jpyinterpreter.types.PythonLikeFunction;
3538
import ai.timefold.jpyinterpreter.types.PythonLikeType;
@@ -175,7 +178,7 @@ public static <T> T forceTranslatePythonBytecodeToGenerator(PythonCompiledFuncti
175178
Method methodWithoutGenerics = getFunctionalInterfaceMethod(javaFunctionalInterfaceType);
176179
MethodDescriptor methodDescriptor = new MethodDescriptor(javaFunctionalInterfaceType,
177180
methodWithoutGenerics,
178-
List.of());
181+
Collections.emptyList());
179182
Class<T> compiledClass = forceTranslatePythonBytecodeToGeneratorClass(pythonCompiledFunction, methodDescriptor,
180183
methodWithoutGenerics, false);
181184
return FunctionImplementor.createInstance(new PythonLikeTuple(), new PythonLikeDict(),
@@ -252,7 +255,7 @@ public static <T> Class<T> translatePythonBytecodeToClass(PythonCompiledFunction
252255
null);
253256

254257
translatePythonBytecodeToMethod(methodDescriptor, internalClassName, methodVisitor, pythonCompiledFunction,
255-
isPythonLikeFunction, Integer.MAX_VALUE, isVirtual); // TODO: Use actual python version
258+
isPythonLikeFunction, isVirtual);
256259

257260
classWriter.visitEnd();
258261

@@ -296,7 +299,7 @@ public static <T> Class<T> translatePythonBytecodeToClass(PythonCompiledFunction
296299
null);
297300

298301
translatePythonBytecodeToMethod(methodDescriptor, internalClassName, methodVisitor, pythonCompiledFunction,
299-
isPythonLikeFunction, Integer.MAX_VALUE, isVirtual); // TODO: Use actual python version
302+
isPythonLikeFunction, isVirtual);
300303

301304
String withoutGenericsSignature = Type.getMethodDescriptor(methodWithoutGenerics);
302305
if (!withoutGenericsSignature.equals(methodDescriptor.getMethodDescriptor())) {
@@ -991,7 +994,7 @@ public static PythonFunctionType getFunctionType(PythonCompiledFunction pythonCo
991994
}
992995

993996
private static void translatePythonBytecodeToMethod(MethodDescriptor method, String className, MethodVisitor methodVisitor,
994-
PythonCompiledFunction pythonCompiledFunction, boolean isPythonLikeFunction, int pythonVersion, boolean isVirtual) {
997+
PythonCompiledFunction pythonCompiledFunction, boolean isPythonLikeFunction, boolean isVirtual) {
995998
// Apply Method Adapters, which reorder try blocks and check the bytecode to ensure it valid
996999
methodVisitor = MethodVisitorAdapters.adapt(methodVisitor, method);
9971000

@@ -1057,6 +1060,7 @@ private static void translatePythonBytecodeToMethod(MethodDescriptor method, Str
10571060

10581061
FlowGraph flowGraph = FlowGraph.createFlowGraph(functionMetadata, initialStackMetadata, opcodeList);
10591062
List<StackMetadata> stackMetadataForOpcodeIndex = flowGraph.getStackMetadataForOperations();
1063+
10601064
writeInstructionsForOpcodes(functionMetadata, stackMetadataForOpcodeIndex, opcodeList);
10611065

10621066
methodVisitor.visitLabel(end);
@@ -1140,6 +1144,19 @@ public static void writeInstructionsForOpcodes(FunctionMetadata functionMetadata
11401144
});
11411145
}
11421146

1147+
var requiredNullVariableSet = new TreeSet<Integer>();
1148+
for (Opcode opcode : opcodeList) {
1149+
if (opcode instanceof LoadFastAndClearOpcode loadAndClearOpcode) {
1150+
requiredNullVariableSet.add(loadAndClearOpcode.getInstruction().arg());
1151+
}
1152+
}
1153+
1154+
for (var requiredNullVariable : requiredNullVariableSet) {
1155+
methodVisitor.visitInsn(Opcodes.ACONST_NULL);
1156+
methodVisitor.visitVarInsn(Opcodes.ASTORE,
1157+
stackMetadataForOpcodeIndex.get(0).localVariableHelper.getPythonLocalVariableSlot(requiredNullVariable));
1158+
}
1159+
11431160
for (int i = 0; i < opcodeList.size(); i++) {
11441161
StackMetadata stackMetadata = stackMetadataForOpcodeIndex.get(i);
11451162
PythonBytecodeInstruction instruction = pythonCompiledFunction.instructionList.get(i);
@@ -1148,7 +1165,7 @@ public static void writeInstructionsForOpcodes(FunctionMetadata functionMetadata
11481165
Label label = exceptionTableTargetLabelMap.get(instruction.offset());
11491166
methodVisitor.visitLabel(label);
11501167
}
1151-
exceptionTableTryBlockMap.getOrDefault(instruction.offset(), List.of()).forEach(Runnable::run);
1168+
exceptionTableTryBlockMap.getOrDefault(instruction.offset(), Collections.emptyList()).forEach(Runnable::run);
11521169

11531170
if (instruction.isJumpTarget() || bytecodeCounterToLabelMap.containsKey(instruction.offset())) {
11541171
Label label = bytecodeCounterToLabelMap.computeIfAbsent(instruction.offset(), offset -> new Label());
@@ -1157,7 +1174,7 @@ public static void writeInstructionsForOpcodes(FunctionMetadata functionMetadata
11571174

11581175
runAfterLabelAndBeforeArgumentors.accept(instruction);
11591176

1160-
bytecodeIndexToArgumentorsMap.getOrDefault(instruction.offset(), List.of()).forEach(Runnable::run);
1177+
bytecodeIndexToArgumentorsMap.getOrDefault(instruction.offset(), Collections.emptyList()).forEach(Runnable::run);
11611178

11621179
if (exceptionTableStartLabelMap.containsKey(instruction.offset())) {
11631180
Label label = exceptionTableStartLabelMap.get(instruction.offset());

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

+5-1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import ai.timefold.jpyinterpreter.opcodes.AbstractOpcode;
2727
import ai.timefold.jpyinterpreter.opcodes.Opcode;
2828
import ai.timefold.jpyinterpreter.opcodes.SelfOpcodeWithoutSource;
29+
import ai.timefold.jpyinterpreter.opcodes.controlflow.ReturnConstantValueOpcode;
2930
import ai.timefold.jpyinterpreter.opcodes.controlflow.ReturnValueOpcode;
3031
import ai.timefold.jpyinterpreter.opcodes.object.DeleteAttrOpcode;
3132
import ai.timefold.jpyinterpreter.opcodes.object.LoadAttrOpcode;
@@ -788,7 +789,7 @@ private static void createInstanceMethod(PythonLikeType pythonLikeType, ClassWri
788789
function.getReturnType().orElse(BuiltinTypes.BASE_TYPE),
789790
function.totalArgCount() > 0
790791
? function.getParameterTypes().subList(1, function.getParameterTypes().size())
791-
: List.of()));
792+
: Collections.emptyList()));
792793
}
793794

794795
private static void createStaticMethod(PythonLikeType pythonLikeType, ClassWriter classWriter, String internalClassName,
@@ -1394,6 +1395,9 @@ public static PythonLikeType getPythonReturnTypeOfFunction(PythonCompiledFunctio
13941395
flowGraph.visitOperations(ReturnValueOpcode.class, (opcode, stackMetadata) -> {
13951396
possibleReturnTypeList.add(stackMetadata.getTOSType());
13961397
});
1398+
flowGraph.visitOperations(ReturnConstantValueOpcode.class, (opcode, stackMetadata) -> {
1399+
possibleReturnTypeList.add(opcode.getConstant(pythonCompiledFunction).$getGenericType());
1400+
});
13971401

13981402
return possibleReturnTypeList.stream()
13991403
.reduce(PythonLikeType::unifyWith)

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

+3-2
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,13 @@ public static PythonFunctionSignature forMethod(Method method) {
5656

5757
public PythonFunctionSignature(MethodDescriptor methodDescriptor,
5858
PythonLikeType returnType, PythonLikeType... parameterTypes) {
59-
this(methodDescriptor, List.of(), extractKeywordArgument(methodDescriptor), returnType, parameterTypes);
59+
this(methodDescriptor, Collections.emptyList(), extractKeywordArgument(methodDescriptor), returnType, parameterTypes);
6060
}
6161

6262
public PythonFunctionSignature(MethodDescriptor methodDescriptor,
6363
PythonLikeType returnType, List<PythonLikeType> parameterTypeList) {
64-
this(methodDescriptor, List.of(), extractKeywordArgument(methodDescriptor), returnType, parameterTypeList);
64+
this(methodDescriptor, Collections.emptyList(), extractKeywordArgument(methodDescriptor), returnType,
65+
parameterTypeList);
6566
}
6667

6768
public PythonFunctionSignature(MethodDescriptor methodDescriptor,

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@
55
public final class PythonVersion implements Comparable<PythonVersion> {
66
private final int hexversion;
77

8-
public static final PythonVersion PYTHON_3_9 = new PythonVersion(3, 9);
98
public static final PythonVersion PYTHON_3_10 = new PythonVersion(3, 10);
109
public static final PythonVersion PYTHON_3_11 = new PythonVersion(3, 11);
10+
public static final PythonVersion PYTHON_3_12 = new PythonVersion(3, 12);
1111

12-
public static final PythonVersion MINIMUM_PYTHON_VERSION = PYTHON_3_9;
12+
public static final PythonVersion MINIMUM_PYTHON_VERSION = PYTHON_3_10;
1313

1414
public PythonVersion(int hexversion) {
1515
this.hexversion = hexversion;

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

+4-42
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import java.util.ArrayList;
44
import java.util.Arrays;
5+
import java.util.Collections;
56
import java.util.List;
67
import java.util.Objects;
78
import java.util.stream.Collectors;
@@ -41,7 +42,7 @@ public StackMetadata(LocalVariableHelper localVariableHelper) {
4142
cellVariableValueSources.add(ValueSourceInfo.of(new OpcodeWithoutSource(),
4243
BuiltinTypes.BASE_TYPE));
4344
}
44-
this.callKeywordNameList = List.of();
45+
this.callKeywordNameList = Collections.emptyList();
4546
}
4647

4748
private StackMetadata(LocalVariableHelper localVariableHelper, List<ValueSourceInfo> stackValueSources,
@@ -62,11 +63,6 @@ public int getStackSize() {
6263
return stackValueSources.size();
6364
}
6465

65-
public List<PythonLikeType> getStackTypeList() {
66-
return stackValueSources.stream().map(ValueSourceInfo::getValueType)
67-
.collect(Collectors.toList());
68-
}
69-
7066
/**
7167
* Returns the list index for the given stack index (stack index is how many
7268
* elements below TOS (i.e. 0 is TOS, 1 is TOS1)).
@@ -113,7 +109,8 @@ public PythonLikeType getTypeAtStackIndex(int index) {
113109
if (valueSourceInfo != null) {
114110
return valueSourceInfo.valueType;
115111
}
116-
return null;
112+
// Unknown type
113+
return BuiltinTypes.BASE_TYPE;
117114
}
118115

119116
/**
@@ -126,20 +123,6 @@ public ValueSourceInfo getLocalVariableValueSource(int index) {
126123
return localVariableValueSources.get(index);
127124
}
128125

129-
/**
130-
* Returns the type for the local variable in slot {@code index}
131-
*
132-
* @param index The slot
133-
* @return The type for the local variable in the given slot
134-
*/
135-
public PythonLikeType getLocalVariableType(int index) {
136-
ValueSourceInfo valueSourceInfo = localVariableValueSources.get(index);
137-
if (valueSourceInfo != null) {
138-
return valueSourceInfo.valueType;
139-
}
140-
return null;
141-
}
142-
143126
/**
144127
* Returns the value source for the cell variable in slot {@code index}
145128
*
@@ -150,20 +133,6 @@ public ValueSourceInfo getCellVariableValueSource(int index) {
150133
return cellVariableValueSources.get(index);
151134
}
152135

153-
/**
154-
* Returns the type for the cell variable in slot {@code index}
155-
*
156-
* @param index The slot
157-
* @return The type for the cell variable in the given slot
158-
*/
159-
public PythonLikeType getCellVariableType(int index) {
160-
ValueSourceInfo valueSourceInfo = cellVariableValueSources.get(index);
161-
if (valueSourceInfo != null) {
162-
return valueSourceInfo.valueType;
163-
}
164-
return null;
165-
}
166-
167136
public PythonLikeType getTOSType() {
168137
return getTypeAtStackIndex(0);
169138
}
@@ -277,13 +246,6 @@ public StackMetadata pushTemps(PythonLikeType... types) {
277246
return out;
278247
}
279248

280-
public StackMetadata insertTemp(int tosIndex, PythonLikeType type) {
281-
StackMetadata out = copy();
282-
out.stackValueSources.add(stackValueSources.size() - tosIndex,
283-
ValueSourceInfo.of(new OpcodeWithoutSource(), type));
284-
return out;
285-
}
286-
287249
/**
288250
* Return a new StackMetadata with {@code types} as the stack;
289251
* The original stack is cleared.

0 commit comments

Comments
 (0)