Skip to content
This repository was archived by the owner on Oct 6, 2025. It is now read-only.

Commit 4349d20

Browse files
chore: Refactor how Opcodes are created and looked up (#20)
- PythonBytecodeInstruction becomes a record - Removed the 200+ constants OpcodeIdentifier enum and replaced with a sealed interface OpcodeDescriptor, this allows the enum to be split across several classes (and for many switches to be total without a default branch). - Replace opcode lookup switches with a method call on the new OpcodeDescriptor class
1 parent b0e6874 commit 4349d20

File tree

112 files changed

+1824
-2759
lines changed

Some content is hidden

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

112 files changed

+1824
-2759
lines changed

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

Lines changed: 0 additions & 886 deletions
This file was deleted.

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

Lines changed: 32 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
import java.util.Optional;
44

5+
import ai.timefold.jpyinterpreter.opcodes.dunder.BinaryDunderOpcode;
6+
57
/**
68
* The list of all Python Binary Operators, which are performed
79
* by calling the left operand's corresponding dunder method on the
@@ -139,68 +141,39 @@ public Optional<PythonBinaryOperator> getFallbackOperation() {
139141
return Optional.ofNullable(fallbackOperation);
140142
}
141143

142-
public static PythonBinaryOperator lookup(int instructionArg) {
144+
public static BinaryDunderOpcode getBinaryOpcode(PythonBytecodeInstruction instruction) {
143145
// As defined by https://github.com/python/cpython/blob/0faa0ba240e815614e5a2900e48007acac41b214/Python/ceval.c#L299
144146

145147
// Binary operations are in alphabetical order (note: CPython refer to Modulo as Remainder),
146148
// and are followed by the inplace operations in the same order
147-
switch (instructionArg) {
148-
case 0:
149-
return PythonBinaryOperator.ADD;
150-
case 1:
151-
return PythonBinaryOperator.AND;
152-
case 2:
153-
return PythonBinaryOperator.FLOOR_DIVIDE;
154-
case 3:
155-
return PythonBinaryOperator.LSHIFT;
156-
case 4:
157-
return PythonBinaryOperator.MATRIX_MULTIPLY;
158-
case 5:
159-
return PythonBinaryOperator.MULTIPLY;
160-
case 6:
161-
return PythonBinaryOperator.MODULO;
162-
case 7:
163-
return PythonBinaryOperator.OR;
164-
case 8:
165-
return PythonBinaryOperator.POWER;
166-
case 9:
167-
return PythonBinaryOperator.RSHIFT;
168-
case 10:
169-
return PythonBinaryOperator.SUBTRACT;
170-
case 11:
171-
return PythonBinaryOperator.TRUE_DIVIDE;
172-
case 12:
173-
return PythonBinaryOperator.XOR;
174-
175-
case 13:
176-
return PythonBinaryOperator.INPLACE_ADD;
177-
case 14:
178-
return PythonBinaryOperator.INPLACE_AND;
179-
case 15:
180-
return PythonBinaryOperator.INPLACE_FLOOR_DIVIDE;
181-
case 16:
182-
return PythonBinaryOperator.INPLACE_LSHIFT;
183-
case 17:
184-
return PythonBinaryOperator.INPLACE_MATRIX_MULTIPLY;
185-
case 18:
186-
return PythonBinaryOperator.INPLACE_MULTIPLY;
187-
case 19:
188-
return PythonBinaryOperator.INPLACE_MODULO;
189-
case 20:
190-
return PythonBinaryOperator.INPLACE_OR;
191-
case 21:
192-
return PythonBinaryOperator.INPLACE_POWER;
193-
case 22:
194-
return PythonBinaryOperator.INPLACE_RSHIFT;
195-
case 23:
196-
return PythonBinaryOperator.INPLACE_SUBTRACT;
197-
case 24:
198-
return PythonBinaryOperator.INPLACE_TRUE_DIVIDE;
199-
case 25:
200-
return PythonBinaryOperator.INPLACE_XOR;
201-
202-
default:
203-
throw new IllegalArgumentException("Unknown binary op id: " + instructionArg);
204-
}
149+
return new BinaryDunderOpcode(instruction, switch (instruction.arg()) {
150+
case 0 -> PythonBinaryOperator.ADD;
151+
case 1 -> PythonBinaryOperator.AND;
152+
case 2 -> PythonBinaryOperator.FLOOR_DIVIDE;
153+
case 3 -> PythonBinaryOperator.LSHIFT;
154+
case 4 -> PythonBinaryOperator.MATRIX_MULTIPLY;
155+
case 5 -> PythonBinaryOperator.MULTIPLY;
156+
case 6 -> PythonBinaryOperator.MODULO;
157+
case 7 -> PythonBinaryOperator.OR;
158+
case 8 -> PythonBinaryOperator.POWER;
159+
case 9 -> PythonBinaryOperator.RSHIFT;
160+
case 10 -> PythonBinaryOperator.SUBTRACT;
161+
case 11 -> PythonBinaryOperator.TRUE_DIVIDE;
162+
case 12 -> PythonBinaryOperator.XOR;
163+
case 13 -> PythonBinaryOperator.INPLACE_ADD;
164+
case 14 -> PythonBinaryOperator.INPLACE_AND;
165+
case 15 -> PythonBinaryOperator.INPLACE_FLOOR_DIVIDE;
166+
case 16 -> PythonBinaryOperator.INPLACE_LSHIFT;
167+
case 17 -> PythonBinaryOperator.INPLACE_MATRIX_MULTIPLY;
168+
case 18 -> PythonBinaryOperator.INPLACE_MULTIPLY;
169+
case 19 -> PythonBinaryOperator.INPLACE_MODULO;
170+
case 20 -> PythonBinaryOperator.INPLACE_OR;
171+
case 21 -> PythonBinaryOperator.INPLACE_POWER;
172+
case 22 -> PythonBinaryOperator.INPLACE_RSHIFT;
173+
case 23 -> PythonBinaryOperator.INPLACE_SUBTRACT;
174+
case 24 -> PythonBinaryOperator.INPLACE_TRUE_DIVIDE;
175+
case 25 -> PythonBinaryOperator.INPLACE_XOR;
176+
default -> throw new IllegalArgumentException("Unknown binary op id: " + instruction.arg());
177+
});
205178
}
206179
}
Lines changed: 30 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,38 @@
11
package ai.timefold.jpyinterpreter;
22

3-
public class PythonBytecodeInstruction {
4-
/**
5-
* The {@link OpcodeIdentifier} for this operation
6-
*/
7-
public OpcodeIdentifier opcode;
8-
9-
/**
10-
* Human readable name for operation
11-
*/
12-
public String opname;
13-
14-
/**
15-
* Numeric argument to operation (if any), otherwise null
16-
*/
17-
public Integer arg;
18-
19-
/**
20-
* Start index of operation within bytecode sequence
21-
*/
22-
public int offset;
23-
24-
/**
25-
* Line started by this opcode (if any), otherwise None
26-
*/
27-
public Integer startsLine;
28-
29-
/**
30-
* True if other code jumps to here, otherwise False
31-
*/
32-
public boolean isJumpTarget;
33-
34-
public PythonBytecodeInstruction copy() {
35-
PythonBytecodeInstruction out = new PythonBytecodeInstruction();
36-
37-
out.opcode = opcode;
38-
out.opname = opname;
39-
out.arg = arg;
40-
out.offset = offset;
41-
out.startsLine = startsLine;
42-
out.isJumpTarget = isJumpTarget;
43-
44-
return out;
3+
import java.util.OptionalInt;
4+
5+
import ai.timefold.jpyinterpreter.opcodes.descriptor.OpcodeDescriptor;
6+
7+
public record PythonBytecodeInstruction(String opname, int offset, int arg, OptionalInt startsLine,
8+
boolean isJumpTarget) {
9+
public static PythonBytecodeInstruction atOffset(String opname, int offset) {
10+
return new PythonBytecodeInstruction(opname, offset, 0, OptionalInt.empty(), false);
11+
}
12+
13+
public static PythonBytecodeInstruction atOffset(OpcodeDescriptor instruction, int offset) {
14+
return atOffset(instruction.name(), offset);
15+
}
16+
17+
public PythonBytecodeInstruction withArg(int newArg) {
18+
return new PythonBytecodeInstruction(opname, offset, newArg, startsLine, isJumpTarget);
19+
}
20+
21+
public PythonBytecodeInstruction startsLine(int lineNumber) {
22+
return new PythonBytecodeInstruction(opname, offset, arg, OptionalInt.of(lineNumber), isJumpTarget);
23+
}
24+
25+
public PythonBytecodeInstruction withIsJumpTarget(boolean isJumpTarget) {
26+
return new PythonBytecodeInstruction(opname, offset, arg, startsLine, isJumpTarget);
27+
}
28+
29+
public PythonBytecodeInstruction markAsJumpTarget() {
30+
return new PythonBytecodeInstruction(opname, offset, arg, startsLine, true);
4531
}
4632

4733
@Override
4834
public String toString() {
49-
return "[" + offset + "] " + opcode.name() + " (" + arg + ")" + (isJumpTarget ? " {JUMP TARGET}" : "");
35+
return "[%d] %s (%d) %s"
36+
.formatted(offset, opname, arg, isJumpTarget ? "{JUMP TARGET}" : "");
5037
}
5138
}

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

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,11 @@
2525
import ai.timefold.jpyinterpreter.implementors.JavaPythonTypeConversionImplementor;
2626
import ai.timefold.jpyinterpreter.implementors.StackManipulationImplementor;
2727
import ai.timefold.jpyinterpreter.implementors.VariableImplementor;
28+
import ai.timefold.jpyinterpreter.opcodes.AbstractOpcode;
2829
import ai.timefold.jpyinterpreter.opcodes.Opcode;
2930
import ai.timefold.jpyinterpreter.opcodes.OpcodeWithoutSource;
3031
import ai.timefold.jpyinterpreter.opcodes.SelfOpcodeWithoutSource;
32+
import ai.timefold.jpyinterpreter.opcodes.descriptor.GeneratorOpDescriptor;
3133
import ai.timefold.jpyinterpreter.types.BuiltinTypes;
3234
import ai.timefold.jpyinterpreter.types.PythonLikeFunction;
3335
import ai.timefold.jpyinterpreter.types.PythonLikeType;
@@ -972,16 +974,18 @@ private static StackMetadata getPythonLikeFunctionInitialStackMetadata(LocalVari
972974

973975
public static PythonFunctionType getFunctionType(PythonCompiledFunction pythonCompiledFunction) {
974976
for (PythonBytecodeInstruction instruction : pythonCompiledFunction.instructionList) {
975-
switch (instruction.opcode) {
976-
case GEN_START:
977-
case RETURN_GENERATOR:
978-
case YIELD_VALUE:
979-
case YIELD_FROM:
980-
return PythonFunctionType.GENERATOR;
981-
982-
default:
983-
break; // Do nothing
984-
}
977+
var opcode = AbstractOpcode.lookupInstruction(instruction.opname());
978+
if (opcode instanceof GeneratorOpDescriptor generatorInstruction)
979+
switch (generatorInstruction) {
980+
case GEN_START:
981+
case RETURN_GENERATOR:
982+
case YIELD_VALUE:
983+
case YIELD_FROM:
984+
return PythonFunctionType.GENERATOR;
985+
986+
default:
987+
break; // Do nothing
988+
}
985989
}
986990
return PythonFunctionType.FUNCTION;
987991
}
@@ -1140,23 +1144,23 @@ public static void writeInstructionsForOpcodes(FunctionMetadata functionMetadata
11401144
StackMetadata stackMetadata = stackMetadataForOpcodeIndex.get(i);
11411145
PythonBytecodeInstruction instruction = pythonCompiledFunction.instructionList.get(i);
11421146

1143-
if (exceptionTableTargetLabelMap.containsKey(instruction.offset)) {
1144-
Label label = exceptionTableTargetLabelMap.get(instruction.offset);
1147+
if (exceptionTableTargetLabelMap.containsKey(instruction.offset())) {
1148+
Label label = exceptionTableTargetLabelMap.get(instruction.offset());
11451149
methodVisitor.visitLabel(label);
11461150
}
1147-
exceptionTableTryBlockMap.getOrDefault(instruction.offset, List.of()).forEach(Runnable::run);
1151+
exceptionTableTryBlockMap.getOrDefault(instruction.offset(), List.of()).forEach(Runnable::run);
11481152

1149-
if (instruction.isJumpTarget || bytecodeCounterToLabelMap.containsKey(instruction.offset)) {
1150-
Label label = bytecodeCounterToLabelMap.computeIfAbsent(instruction.offset, offset -> new Label());
1153+
if (instruction.isJumpTarget() || bytecodeCounterToLabelMap.containsKey(instruction.offset())) {
1154+
Label label = bytecodeCounterToLabelMap.computeIfAbsent(instruction.offset(), offset -> new Label());
11511155
methodVisitor.visitLabel(label);
11521156
}
11531157

11541158
runAfterLabelAndBeforeArgumentors.accept(instruction);
11551159

1156-
bytecodeIndexToArgumentorsMap.getOrDefault(instruction.offset, List.of()).forEach(Runnable::run);
1160+
bytecodeIndexToArgumentorsMap.getOrDefault(instruction.offset(), List.of()).forEach(Runnable::run);
11571161

1158-
if (exceptionTableStartLabelMap.containsKey(instruction.offset)) {
1159-
Label label = exceptionTableStartLabelMap.get(instruction.offset);
1162+
if (exceptionTableStartLabelMap.containsKey(instruction.offset())) {
1163+
Label label = exceptionTableStartLabelMap.get(instruction.offset());
11601164
methodVisitor.visitLabel(label);
11611165
}
11621166

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1184,14 +1184,14 @@ public static Set<String> getReferencedSelfAttributes(PythonCompiledFunction pyt
11841184
if (opcode instanceof LoadFastOpcode || opcode instanceof StoreAttrOpcode
11851185
|| opcode instanceof DeleteAttrOpcode) {
11861186
AbstractOpcode instructionOpcode = (AbstractOpcode) opcode;
1187-
return instructionOpcode.getInstruction().arg == 0;
1187+
return instructionOpcode.getInstruction().arg() == 0;
11881188
}
11891189
if (opcode instanceof SelfOpcodeWithoutSource) {
11901190
return true;
11911191
}
11921192
return false;
11931193
})) {
1194-
referencedSelfAttributeSet.add(pythonCompiledFunction.co_names.get(attributeOpcode.getInstruction().arg));
1194+
referencedSelfAttributeSet.add(pythonCompiledFunction.co_names.get(attributeOpcode.getInstruction().arg()));
11951195
}
11961196
};
11971197

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

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
import java.util.Map;
66
import java.util.Optional;
77
import java.util.function.BiFunction;
8-
import java.util.stream.Collectors;
98

109
import ai.timefold.jpyinterpreter.types.BuiltinTypes;
1110
import ai.timefold.jpyinterpreter.types.PythonLikeType;
@@ -130,19 +129,18 @@ public PythonCompiledFunction copy() {
130129

131130
out.module = module;
132131
out.qualifiedName = qualifiedName;
133-
out.instructionList = instructionList.stream().map(PythonBytecodeInstruction::copy)
134-
.collect(Collectors.toCollection(ArrayList::new));
132+
out.instructionList = List.copyOf(instructionList);
135133
out.closure = closure;
136134
out.globalsMap = globalsMap;
137135
out.typeAnnotations = typeAnnotations;
138136
out.defaultPositionalArguments = defaultPositionalArguments;
139137
out.defaultKeywordArguments = defaultKeywordArguments;
140138
out.co_exceptiontable = this.co_exceptiontable;
141-
out.co_names = new ArrayList<>(co_names);
142-
out.co_varnames = new ArrayList<>(co_varnames);
143-
out.co_cellvars = new ArrayList<>(co_cellvars);
144-
out.co_freevars = new ArrayList<>(co_freevars);
145-
out.co_constants = new ArrayList<>(co_constants);
139+
out.co_names = List.copyOf(co_names);
140+
out.co_varnames = List.copyOf(co_varnames);
141+
out.co_cellvars = List.copyOf(co_cellvars);
142+
out.co_freevars = List.copyOf(co_freevars);
143+
out.co_constants = List.copyOf(co_constants);
146144
out.co_argcount = co_argcount;
147145
out.co_kwonlyargcount = co_kwonlyargcount;
148146
out.pythonVersion = pythonVersion;

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

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import ai.timefold.jpyinterpreter.implementors.VariableImplementor;
1515
import ai.timefold.jpyinterpreter.opcodes.AbstractOpcode;
1616
import ai.timefold.jpyinterpreter.opcodes.Opcode;
17+
import ai.timefold.jpyinterpreter.opcodes.descriptor.GeneratorOpDescriptor;
1718
import ai.timefold.jpyinterpreter.opcodes.generator.GeneratorStartOpcode;
1819
import ai.timefold.jpyinterpreter.opcodes.generator.ResumeOpcode;
1920
import ai.timefold.jpyinterpreter.opcodes.generator.YieldFromOpcode;
@@ -426,17 +427,17 @@ private static void generateAdvanceGeneratorMethods(ClassWriter classWriter, Str
426427

427428
private static void generateAdvanceGeneratorMethod(ClassWriter classWriter, String internalClassName,
428429
GeneratorMethodPart generatorMethodPart) {
429-
switch (generatorMethodPart.instruction.opcode) {
430-
case YIELD_VALUE:
430+
var instruction = AbstractOpcode.lookupInstruction(generatorMethodPart.instruction.opname());
431+
if (!(instruction instanceof GeneratorOpDescriptor generatorInstruction)) {
432+
throw new IllegalArgumentException(
433+
"Invalid opcode for instruction: %s".formatted(generatorMethodPart.instruction.opname()));
434+
}
435+
switch (generatorInstruction) {
436+
case YIELD_VALUE ->
431437
generateAdvanceGeneratorMethodForYieldValue(classWriter, internalClassName, generatorMethodPart);
432-
return;
433-
434-
case YIELD_FROM:
435-
generateAdvanceGeneratorMethodForYieldFrom(classWriter, internalClassName, generatorMethodPart);
436-
return;
437-
438-
default:
439-
throw new IllegalArgumentException("Invalid opcode for instruction: " + generatorMethodPart.instruction.opcode);
438+
case YIELD_FROM -> generateAdvanceGeneratorMethodForYieldFrom(classWriter, internalClassName, generatorMethodPart);
439+
default -> throw new IllegalArgumentException(
440+
"Invalid opcode for instruction: " + generatorMethodPart.instruction.opname());
440441
}
441442
}
442443

@@ -491,7 +492,7 @@ private static void generateAdvanceGeneratorMethodForYieldValue(ClassWriter clas
491492
stackMetadataForOpcodeIndex, opcodeList,
492493
instruction -> {
493494
if (actualResumeType == ResumeOpcode.ResumeType.YIELD) {
494-
if (instruction.offset == generatorMethodPart.afterYield) {
495+
if (instruction.offset() == generatorMethodPart.afterYield) {
495496
// Put thrownValue on TOS
496497
methodVisitor.visitVarInsn(Opcodes.ALOAD, 0);
497498
methodVisitor.visitFieldInsn(Opcodes.GETFIELD, Type.getInternalName(PythonGenerator.class),
@@ -560,7 +561,7 @@ private static void generateAdvanceGeneratorMethodForYieldFrom(ClassWriter class
560561
PythonBytecodeToJavaBytecodeTranslator.writeInstructionsForOpcodes(generatorMethodPart.functionMetadata,
561562
stackMetadataForOpcodeIndex, opcodeList,
562563
instruction -> {
563-
if (instruction.offset == generatorMethodPart.afterYield) {
564+
if (instruction.offset() == generatorMethodPart.afterYield) {
564565
// 0 = next, 1 = send, 2 = throw
565566

566567
Label wasNotSentValue = new Label();
@@ -799,9 +800,7 @@ private static Map<Integer, GeneratorMethodPart> createGeneratorStateToMethod(Cl
799800
start.functionMetadata = functionMetadata;
800801
start.afterYield = 0;
801802
start.originalMethodDescriptor = stackMetadataMethod;
802-
start.instruction = new PythonBytecodeInstruction();
803-
start.instruction.opcode = OpcodeIdentifier.YIELD_VALUE;
804-
start.instruction.offset = 0;
803+
start.instruction = PythonBytecodeInstruction.atOffset(GeneratorOpDescriptor.YIELD_VALUE, 0);
805804
generatorStateToMethod.put(0, start);
806805

807806
return generatorStateToMethod;

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ public final class PythonVersion implements Comparable<PythonVersion> {
99
public static final PythonVersion PYTHON_3_10 = new PythonVersion(3, 10);
1010
public static final PythonVersion PYTHON_3_11 = new PythonVersion(3, 11);
1111

12+
public static final PythonVersion MINIMUM_PYTHON_VERSION = PYTHON_3_9;
13+
1214
public PythonVersion(int hexversion) {
1315
this.hexversion = hexversion;
1416
}

0 commit comments

Comments
 (0)