diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 4815079..79a6697 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -29,28 +29,23 @@ jobs: steps: # Need to check for stale repo, since Github is not aware of the build chain and therefore doesn't automate it. - - name: Checkout timefold-solver to access the scripts + - name: Checkout timefold-solver (PR) # Checkout the PR branch first, if it exists + id: checkout-solver uses: actions/checkout@v4 + continue-on-error: true with: - path: './timefold-solver' - repository: 'TimefoldAI/timefold-solver' - - name: Find the proper timefold-solver repo and branch - env: - CHAIN_USER: ${{ github.event.pull_request.head.repo.owner.login }} - CHAIN_BRANCH: ${{ github.head_ref }} - CHAIN_REPO: "timefold-solver" - CHAIN_DEFAULT_BRANCH: ${{ endsWith(github.head_ref, '.x') && github.head_ref || 'main' }} - shell: bash - run: | - ./timefold-solver/.github/scripts/check_chain_repo.sh - rm -rf ./timefold-solver - - name: Checkout the proper timefold-solver branch + repository: ${{ github.actor }}/timefold-solver + ref: ${{ github.head_ref }} + path: ./timefold-solver + fetch-depth: 0 # Otherwise merge will fail on account of not having history. + - name: Checkout timefold-solver (main) # Checkout the main branch if the PR branch does not exist + if: steps.checkout-solver.outcome != 'success' uses: actions/checkout@v4 with: - repository: ${{ env.TARGET_CHAIN_USER }}/${{ env.TARGET_CHAIN_REPO }} - ref: ${{ env.TARGET_CHAIN_BRANCH }} - path: './timefold-solver' - fetch-depth: 0 # Otherwise merge in the next step will fail on account of not having history. + repository: TimefoldAI/timefold-solver + ref: main + path: ./timefold-solver + fetch-depth: 0 # Otherwise merge will fail on account of not having history. - name: Prevent stale fork of timefold-solver env: BLESSED_REPO: "timefold-solver" diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index a71e990..10e0c96 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -23,28 +23,23 @@ jobs: steps: # Need to check for stale repo, since Github is not aware of the build chain and therefore doesn't automate it. - - name: Checkout timefold-solver to access the scripts + - name: Checkout timefold-solver (PR) # Checkout the PR branch first, if it exists + id: checkout-solver uses: actions/checkout@v4 + continue-on-error: true with: - path: './timefold-solver' - repository: 'TimefoldAI/timefold-solver' - - name: Find the proper timefold-solver repo and branch - env: - CHAIN_USER: ${{ github.event.pull_request.head.repo.owner.login }} - CHAIN_BRANCH: ${{ github.head_ref }} - CHAIN_REPO: "timefold-solver" - CHAIN_DEFAULT_BRANCH: ${{ endsWith(github.head_ref, '.x') && github.head_ref || 'main' }} - shell: bash - run: | - ./timefold-solver/.github/scripts/check_chain_repo.sh - rm -rf ./timefold-solver - - name: Checkout the proper timefold-solver branch + repository: ${{ github.actor }}/timefold-solver + ref: ${{ github.head_ref }} + path: ./timefold-solver + fetch-depth: 0 # Otherwise merge will fail on account of not having history. + - name: Checkout timefold-solver (main) # Checkout the main branch if the PR branch does not exist + if: steps.checkout-solver.outcome != 'success' uses: actions/checkout@v4 with: - repository: ${{ env.TARGET_CHAIN_USER }}/${{ env.TARGET_CHAIN_REPO }} - ref: ${{ env.TARGET_CHAIN_BRANCH }} - path: './timefold-solver' - fetch-depth: 0 # Otherwise merge in the next step will fail on account of not having history. + repository: TimefoldAI/timefold-solver + ref: main + path: ./timefold-solver + fetch-depth: 0 # Otherwise merge will fail on account of not having history. - name: Prevent stale fork of timefold-solver env: BLESSED_REPO: "timefold-solver" diff --git a/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/PythonBytecodeToJavaBytecodeTranslator.java b/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/PythonBytecodeToJavaBytecodeTranslator.java index e07e60b..a688203 100644 --- a/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/PythonBytecodeToJavaBytecodeTranslator.java +++ b/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/PythonBytecodeToJavaBytecodeTranslator.java @@ -245,6 +245,8 @@ public static Class translatePythonBytecodeToClass(PythonCompiledFunction final boolean isPythonLikeFunction = methodDescriptor.getDeclaringClassInternalName().equals(Type.getInternalName(PythonLikeFunction.class)); + classWriter.visitSource(pythonCompiledFunction.moduleFilePath, null); + createFields(classWriter); createConstructor(classWriter, internalClassName); @@ -289,6 +291,8 @@ public static Class translatePythonBytecodeToClass(PythonCompiledFunction final boolean isPythonLikeFunction = methodDescriptor.getDeclaringClassInternalName().equals(Type.getInternalName(PythonLikeFunction.class)); + classWriter.visitSource(pythonCompiledFunction.moduleFilePath, null); + createFields(classWriter); createConstructor(classWriter, internalClassName); @@ -308,6 +312,7 @@ public static Class translatePythonBytecodeToClass(PythonCompiledFunction null); methodVisitor.visitCode(); + visitGeneratedLineNumber(methodVisitor); methodVisitor.visitVarInsn(Opcodes.ALOAD, 0); for (int i = 0; i < methodWithoutGenerics.getParameterCount(); i++) { Type parameterType = Type.getType(methodWithoutGenerics.getParameterTypes()[i]); @@ -349,6 +354,8 @@ public static Class translatePythonBytecodeToPythonWrapperClass(PythonCom classWriter.visit(Opcodes.V11, Modifier.PUBLIC, internalClassName, null, Type.getInternalName(Object.class), new String[] { Type.getInternalName(PythonLikeFunction.class) }); + classWriter.visitSource(pythonCompiledFunction.moduleFilePath, null); + createFields(classWriter); classWriter.visitField(Modifier.PUBLIC | Modifier.STATIC, PYTHON_WRAPPER_CODE_STATIC_FIELD_NAME, Type.getDescriptor(OpaquePythonReference.class), @@ -368,6 +375,7 @@ public static Class translatePythonBytecodeToPythonWrapperClass(PythonCom null); methodVisitor.visitCode(); + visitGeneratedLineNumber(methodVisitor); methodVisitor.visitVarInsn(Opcodes.ALOAD, 0); methodVisitor.visitFieldInsn(Opcodes.GETFIELD, internalClassName, PYTHON_WRAPPER_FUNCTION_INSTANCE_FIELD_NAME, Type.getDescriptor(PythonObjectWrapper.class)); @@ -417,6 +425,8 @@ public static Class forceTranslatePythonBytecodeToGeneratorClass(PythonCo classWriter.visit(Opcodes.V11, Modifier.PUBLIC, internalClassName, null, Type.getInternalName(Object.class), new String[] { methodDescriptor.getDeclaringClassInternalName() }); + classWriter.visitSource(pythonCompiledFunction.moduleFilePath, null); + final boolean isPythonLikeFunction = methodDescriptor.getDeclaringClassInternalName().equals(Type.getInternalName(PythonLikeFunction.class)); @@ -460,6 +470,7 @@ public static Class forceTranslatePythonBytecodeToGeneratorClass(PythonCo null); methodVisitor.visitCode(); + visitGeneratedLineNumber(methodVisitor); methodVisitor.visitVarInsn(Opcodes.ALOAD, 0); for (int i = 0; i < methodWithoutGenerics.getParameterCount(); i++) { Type parameterType = Type.getType(methodWithoutGenerics.getParameterTypes()[i]); @@ -494,6 +505,7 @@ private static void createConstructor(ClassWriter classWriter, String className) null, null); methodVisitor.visitCode(); + visitGeneratedLineNumber(methodVisitor); methodVisitor.visitVarInsn(Opcodes.ALOAD, 0); methodVisitor.visitInsn(Opcodes.DUP); methodVisitor.visitMethodInsn(Opcodes.INVOKESPECIAL, Type.getInternalName(Object.class), "", @@ -574,6 +586,8 @@ private static void createConstructor(ClassWriter classWriter, String className) Type.getType(PythonInterpreter.class)), null, null); methodVisitor.visitCode(); + + visitGeneratedLineNumber(methodVisitor); methodVisitor.visitVarInsn(Opcodes.ALOAD, 0); methodVisitor.visitInsn(Opcodes.DUP); methodVisitor.visitMethodInsn(Opcodes.INVOKESPECIAL, Type.getInternalName(Object.class), "", @@ -647,6 +661,7 @@ private static void createPythonWrapperConstructor(ClassWriter classWriter, Stri null, null); methodVisitor.visitCode(); + visitGeneratedLineNumber(methodVisitor); methodVisitor.visitVarInsn(Opcodes.ALOAD, 0); methodVisitor.visitInsn(Opcodes.DUP); methodVisitor.visitMethodInsn(Opcodes.INVOKESPECIAL, Type.getInternalName(Object.class), "", @@ -759,6 +774,8 @@ private static void createPythonWrapperConstructor(ClassWriter classWriter, Stri Type.getType(PythonInterpreter.class)), null, null); methodVisitor.visitCode(); + + visitGeneratedLineNumber(methodVisitor); methodVisitor.visitVarInsn(Opcodes.ALOAD, 0); methodVisitor.visitInsn(Opcodes.DUP); methodVisitor.visitMethodInsn(Opcodes.INVOKESPECIAL, Type.getInternalName(Object.class), "", @@ -1007,6 +1024,7 @@ private static void translatePythonBytecodeToMethod(MethodDescriptor method, Str } methodVisitor.visitCode(); + visitGeneratedLineNumber(methodVisitor); Label start = new Label(); Label end = new Label(); @@ -1172,6 +1190,12 @@ public static void writeInstructionsForOpcodes(FunctionMetadata functionMetadata methodVisitor.visitLabel(label); } + if (instruction.startsLine().isPresent()) { + Label label = new Label(); + methodVisitor.visitLabel(label); + methodVisitor.visitLineNumber(instruction.startsLine().getAsInt(), label); + } + runAfterLabelAndBeforeArgumentors.accept(instruction); bytecodeIndexToArgumentorsMap.getOrDefault(instruction.offset(), Collections.emptyList()).forEach(Runnable::run); @@ -1361,4 +1385,10 @@ public static String getPythonBytecodeListing(PythonCompiledFunction pythonCompi out.append("\nco_exceptiontable = ").append(pythonCompiledFunction.co_exceptiontable).append("\n"); return out.toString(); } + + public static void visitGeneratedLineNumber(MethodVisitor methodVisitor) { + Label label = new Label(); + methodVisitor.visitLabel(label); + methodVisitor.visitLineNumber(0, label); + } } diff --git a/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/PythonClassTranslator.java b/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/PythonClassTranslator.java index 9daa15a..5d08569 100644 --- a/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/PythonClassTranslator.java +++ b/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/PythonClassTranslator.java @@ -168,6 +168,8 @@ public static PythonLikeType translatePythonClass(PythonCompiledClass pythonComp classWriter.visit(Opcodes.V11, Modifier.PUBLIC, internalClassName, null, superClassType.getJavaTypeInternalName(), interfaces); + classWriter.visitSource(pythonCompiledClass.moduleFilePath, null); + for (var annotation : pythonCompiledClass.annotations) { annotation.addAnnotationTo(classWriter); } @@ -236,6 +238,7 @@ public static PythonLikeType translatePythonClass(PythonCompiledClass pythonComp classWriter.visitMethod(Modifier.PUBLIC, "", Type.getMethodDescriptor(Type.VOID_TYPE), null, null); methodVisitor.visitCode(); + PythonBytecodeToJavaBytecodeTranslator.visitGeneratedLineNumber(methodVisitor); methodVisitor.visitVarInsn(Opcodes.ALOAD, 0); methodVisitor.visitFieldInsn(Opcodes.GETSTATIC, Type.getInternalName(PythonInterpreter.class), "DEFAULT", Type.getDescriptor(PythonInterpreter.class)); @@ -259,6 +262,7 @@ public static PythonLikeType translatePythonClass(PythonCompiledClass pythonComp methodVisitor.visitParameter("subclassType", 0); methodVisitor.visitCode(); + PythonBytecodeToJavaBytecodeTranslator.visitGeneratedLineNumber(methodVisitor); methodVisitor.visitVarInsn(Opcodes.ALOAD, 0); methodVisitor.visitVarInsn(Opcodes.ALOAD, 1); methodVisitor.visitVarInsn(Opcodes.ALOAD, 2); @@ -541,6 +545,8 @@ private static Class createPythonWrapperMethod(String methodName, PythonCompi classWriter.visit(Opcodes.V11, Modifier.PUBLIC, internalClassName, null, Type.getInternalName(Object.class), new String[] { interfaceDeclaration.interfaceName }); + classWriter.visitSource("", null); + classWriter.visitField(Modifier.PUBLIC | Modifier.FINAL, "$binaryType", Type.getDescriptor(PythonLikeType.class), null, null); classWriter.visitField(Modifier.STATIC | Modifier.PUBLIC, ARGUMENT_SPEC_INSTANCE_FIELD_NAME, @@ -554,7 +560,7 @@ private static Class createPythonWrapperMethod(String methodName, PythonCompi null, null); methodVisitor.visitCode(); - + PythonBytecodeToJavaBytecodeTranslator.visitGeneratedLineNumber(methodVisitor); methodVisitor.visitVarInsn(Opcodes.ALOAD, 0); methodVisitor.visitMethodInsn(Opcodes.INVOKESPECIAL, Type.getInternalName(Object.class), "", Type.getMethodDescriptor(Type.VOID_TYPE), false); @@ -578,7 +584,7 @@ private static Class createPythonWrapperMethod(String methodName, PythonCompi } methodVisitor.visitCode(); - + PythonBytecodeToJavaBytecodeTranslator.visitGeneratedLineNumber(methodVisitor); methodVisitor.visitVarInsn(Opcodes.ALOAD, 0); methodVisitor.visitFieldInsn(Opcodes.GETFIELD, internalClassName, "$binaryType", Type.getDescriptor(PythonLikeType.class)); @@ -651,6 +657,8 @@ private static PythonLikeFunction createConstructor(String classInternalName, Type.getInternalName(PythonLikeFunction.class) }); + classWriter.visitSource(initFunction != null ? initFunction.moduleFilePath : "", null); + classWriter.visitField(Modifier.STATIC | Modifier.PUBLIC, ARGUMENT_SPEC_INSTANCE_FIELD_NAME, Type.getDescriptor(ArgumentSpec.class), null, null); @@ -659,6 +667,7 @@ private static PythonLikeFunction createConstructor(String classInternalName, classWriter.visitMethod(Modifier.PUBLIC, "", Type.getMethodDescriptor(Type.VOID_TYPE), null, null); methodVisitor.visitCode(); + PythonBytecodeToJavaBytecodeTranslator.visitGeneratedLineNumber(methodVisitor); methodVisitor.visitVarInsn(Opcodes.ALOAD, 0); methodVisitor.visitMethodInsn(Opcodes.INVOKESPECIAL, Type.getInternalName(Object.class), "", Type.getMethodDescriptor(Type.VOID_TYPE), false); @@ -674,13 +683,16 @@ private static PythonLikeFunction createConstructor(String classInternalName, null, null); methodVisitor.visitCode(); - + PythonBytecodeToJavaBytecodeTranslator.visitGeneratedLineNumber(methodVisitor); methodVisitor.visitTypeInsn(Opcodes.NEW, classInternalName); methodVisitor.visitInsn(Opcodes.DUP); methodVisitor.visitMethodInsn(Opcodes.INVOKESPECIAL, classInternalName, "", Type.getMethodDescriptor(Type.VOID_TYPE), false); if (initFunction != null) { + Label start = new Label(); + methodVisitor.visitLabel(start); + methodVisitor.visitLineNumber(initFunction.getFirstLine(), start); methodVisitor.visitInsn(Opcodes.DUP); methodVisitor.visitFieldInsn(Opcodes.GETSTATIC, constructorInternalClassName, ARGUMENT_SPEC_INSTANCE_FIELD_NAME, @@ -844,6 +856,10 @@ private static void createClassMethod(PythonLikeType pythonLikeType, ClassWriter addAnnotationsToMethod(function, methodVisitor); methodVisitor.visitCode(); + Label start = new Label(); + methodVisitor.visitLabel(start); + methodVisitor.visitLineNumber(function.getFirstLine(), start); + methodVisitor.visitFieldInsn(Opcodes.GETSTATIC, internalClassName, javaMethodName, interfaceDescriptor); for (int i = 0; i < function.totalArgCount(); i++) { @@ -880,6 +896,10 @@ private static void createInstanceOrStaticMethodBody(String internalClassName, S addAnnotationsToMethod(function, methodVisitor); methodVisitor.visitCode(); + Label start = new Label(); + methodVisitor.visitLabel(start); + methodVisitor.visitLineNumber(function.getFirstLine(), start); + methodVisitor.visitFieldInsn(Opcodes.GETSTATIC, internalClassName, javaMethodName, interfaceDescriptor); for (int i = 0; i < function.totalArgCount(); i++) { methodVisitor.visitVarInsn(Opcodes.ALOAD, i); @@ -929,6 +949,8 @@ public static void createGetAttribute(ClassWriter classWriter, String classInter methodVisitor.visitCode(); + PythonBytecodeToJavaBytecodeTranslator.visitGeneratedLineNumber(methodVisitor); + methodVisitor.visitVarInsn(Opcodes.ALOAD, 1); BytecodeSwitchImplementor.createStringSwitch(methodVisitor, instanceAttributes, 2, field -> { methodVisitor.visitVarInsn(Opcodes.ALOAD, 0); @@ -976,6 +998,8 @@ public static void createSetAttribute(ClassWriter classWriter, String classInter methodVisitor.visitCode(); + PythonBytecodeToJavaBytecodeTranslator.visitGeneratedLineNumber(methodVisitor); + methodVisitor.visitVarInsn(Opcodes.ALOAD, 1); BytecodeSwitchImplementor.createStringSwitch(methodVisitor, instanceAttributes, 3, field -> { var type = fieldToType.get(field); @@ -1023,6 +1047,8 @@ public static void createDeleteAttribute(ClassWriter classWriter, String classIn methodVisitor.visitCode(); + PythonBytecodeToJavaBytecodeTranslator.visitGeneratedLineNumber(methodVisitor); + methodVisitor.visitVarInsn(Opcodes.ALOAD, 1); BytecodeSwitchImplementor.createStringSwitch(methodVisitor, instanceAttributes, 2, field -> { methodVisitor.visitVarInsn(Opcodes.ALOAD, 0); @@ -1063,6 +1089,8 @@ public static void createReadFromCPythonReference(ClassWriter classWriter, Strin null); methodVisitor.visitCode(); + PythonBytecodeToJavaBytecodeTranslator.visitGeneratedLineNumber(methodVisitor); + methodVisitor.visitVarInsn(Opcodes.ALOAD, 0); methodVisitor.visitInsn(Opcodes.DUP); methodVisitor.visitMethodInsn(Opcodes.INVOKESPECIAL, superClassInternalName, @@ -1157,6 +1185,8 @@ public static void createWriteToCPythonReference(ClassWriter classWriter, String null); methodVisitor.visitCode(); + PythonBytecodeToJavaBytecodeTranslator.visitGeneratedLineNumber(methodVisitor); + methodVisitor.visitVarInsn(Opcodes.ALOAD, 0); methodVisitor.visitInsn(Opcodes.DUP); methodVisitor.visitVarInsn(Opcodes.ALOAD, 1); @@ -1305,6 +1335,8 @@ public static InterfaceDeclaration createInterfaceForFunctionSignature(FunctionS classWriter.visit(Opcodes.V11, Modifier.PUBLIC | Modifier.INTERFACE | Modifier.ABSTRACT, internalClassName, null, Type.getInternalName(Object.class), null); + classWriter.visitSource("", null); + Type returnType = Type.getType(functionSignature.returnType); Type[] parameterTypes = new Type[functionSignature.parameterTypes.length]; for (int i = 0; i < parameterTypes.length; i++) { diff --git a/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/PythonCompiledClass.java b/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/PythonCompiledClass.java index ea08ac2..3b0a6aa 100644 --- a/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/PythonCompiledClass.java +++ b/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/PythonCompiledClass.java @@ -14,6 +14,11 @@ public class PythonCompiledClass { */ public String module; + /** + * The path to the file that defines the module. + */ + public String moduleFilePath; + /** * The qualified name of the class. Does not include module. */ diff --git a/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/PythonCompiledFunction.java b/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/PythonCompiledFunction.java index 923f4d4..3558284 100644 --- a/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/PythonCompiledFunction.java +++ b/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/PythonCompiledFunction.java @@ -22,6 +22,11 @@ public class PythonCompiledFunction { */ public String module; + /** + * The path to the file that defines the module. + */ + public String moduleFilePath; + /** * The qualified name of the function. Does not include module. */ @@ -128,6 +133,7 @@ public PythonCompiledFunction copy() { PythonCompiledFunction out = new PythonCompiledFunction(); out.module = module; + out.moduleFilePath = moduleFilePath; out.qualifiedName = qualifiedName; out.instructionList = List.copyOf(instructionList); out.closure = closure; @@ -282,4 +288,13 @@ public int totalArgCount() { return co_argcount + co_kwonlyargcount + extraArgs; } + + public int getFirstLine() { + for (var instruction : instructionList) { + if (instruction.startsLine().isPresent()) { + return instruction.startsLine().getAsInt(); + } + } + return -1; + } } diff --git a/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/builtins/GlobalBuiltins.java b/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/builtins/GlobalBuiltins.java index e0721af..38e7e24 100644 --- a/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/builtins/GlobalBuiltins.java +++ b/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/builtins/GlobalBuiltins.java @@ -1085,7 +1085,7 @@ public static PythonBoolean issubclass(List positionalArgs, public static PythonLikeDict locals(List positionalArgs, Map keywordArgs, PythonLikeObject instance) { - throw new ValueError("builtin locals is not supported when executed in Java bytecode"); + throw new ValueError("builtin locals() is not supported when executed in Java bytecode"); } public static PythonIterator map(List positionalArgs, diff --git a/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/implementors/ExceptionImplementor.java b/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/implementors/ExceptionImplementor.java index 04325f5..727b9fa 100644 --- a/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/implementors/ExceptionImplementor.java +++ b/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/implementors/ExceptionImplementor.java @@ -141,11 +141,13 @@ public static void raiseWithOptionalExceptionAndCause(MethodVisitor methodVisito * are handled via the {@link ControlOpDescriptor#JUMP_IF_NOT_EXC_MATCH} instruction. * {@code instruction.arg} is the difference in bytecode offset to the first catch/finally block. */ - public static void createTryFinallyBlock(MethodVisitor methodVisitor, String className, - int handlerLocation, + public static void createTryFinallyBlock(FunctionMetadata functionMetadata, StackMetadata stackMetadata, + int handlerLocation, Map bytecodeCounterToLabelMap, BiConsumer bytecodeCounterCodeArgumentConsumer) { + var methodVisitor = functionMetadata.methodVisitor; + var className = functionMetadata.className; // Store the stack in local variables so the except block has access to them int[] stackLocals = StackManipulationImplementor.storeStack(methodVisitor, stackMetadata); @@ -159,7 +161,10 @@ public static void createTryFinallyBlock(MethodVisitor methodVisitor, String cla methodVisitor.visitLabel(tryStart); // At finallyStart, stack is expected to be: - // [(stack-before-try), instruction, level, label, tb, exception, exception_class] ; where: + // in Python 3.10 and below + // [(stack-before-try), instruction, level, label, tb, value, exception] + // or in Python 3.11 and above + // [(stack-before-try), instruction, level, label, tb, value, exception] ; where: // (stack-before-try) = the stack state before the try statement // (see https://github.com/python/cpython/blob/b6558d768f19584ad724be23030603280f9e6361/Python/compile.c#L3241-L3268 ) // instruction = instruction that created the block @@ -168,7 +173,11 @@ public static void createTryFinallyBlock(MethodVisitor methodVisitor, String cla // (see https://stackoverflow.com/a/66720684) // tb = stack trace // exception = exception instance - // exception_class = the exception class + // value = the exception instance again? + // Results from Python 3.10 seems to indicate both exception and value are exception + // instances, since Python 3.10 use RERAISE on exception (TOS) + // and stores value into the exception variable (TOS1) + // Python 3.11 and above use a different code path bytecodeCounterCodeArgumentConsumer.accept(handlerLocation, () -> { // Stack is exception // Duplicate exception to the current exception variable slot so we can reraise it if needed @@ -188,13 +197,12 @@ public static void createTryFinallyBlock(MethodVisitor methodVisitor, String cla "valueOf", Type.getMethodDescriptor(Type.getType(PythonInteger.class), Type.INT_TYPE), false); - // Stack is (stack-before-try), instruction, stack-size, exception + // Stack is (stack-before-try), instruction, stack-size // Label PythonConstantsImplementor.loadNone(methodVisitor); // We don't use it // Stack is (stack-before-try), instruction, stack-size, label - methodVisitor.visitVarInsn(Opcodes.ALOAD, 0); methodVisitor.visitTypeInsn(Opcodes.CHECKCAST, className); // needed cast; type confusion on this? methodVisitor.visitFieldInsn(Opcodes.GETFIELD, className, @@ -212,13 +220,9 @@ public static void createTryFinallyBlock(MethodVisitor methodVisitor, String cla // Stack is (stack-before-try), instruction, stack-size, label, traceback, exception - // Get exception class + // Get exception value methodVisitor.visitInsn(Opcodes.DUP); - methodVisitor.visitMethodInsn(Opcodes.INVOKEINTERFACE, Type.getInternalName(PythonLikeObject.class), - "$getType", Type.getMethodDescriptor(Type.getType(PythonLikeType.class)), - true); - - // Stack is (stack-before-try), instruction, stack-size, label, traceback, exception, exception_class + // Stack is (stack-before-try), instruction, stack-size, label, traceback, value, exception }); } @@ -285,8 +289,7 @@ public static void startWith(int jumpTarget, FunctionMetadata functionMetadata, .push(ValueSourceInfo.of(new OpcodeWithoutSource(), PythonLikeFunction.getFunctionType(), stackMetadata.getTOSValueSource())); - createTryFinallyBlock(methodVisitor, functionMetadata.className, jumpTarget, - currentStackMetadata, + createTryFinallyBlock(functionMetadata, currentStackMetadata, jumpTarget, functionMetadata.bytecodeCounterToLabelMap, (bytecodeIndex, runnable) -> { functionMetadata.bytecodeCounterToCodeArgumenterList @@ -351,16 +354,16 @@ public static void handleExceptionInWith(FunctionMetadata functionMetadata, Stac LocalVariableHelper localVariableHelper = stackMetadata.localVariableHelper; // First, store the top 7 items in the stack to be restored later - int exceptionType = localVariableHelper.newLocal(); int exception = localVariableHelper.newLocal(); + int exceptionArgs = localVariableHelper.newLocal(); int traceback = localVariableHelper.newLocal(); int label = localVariableHelper.newLocal(); int stackSize = localVariableHelper.newLocal(); int instruction = localVariableHelper.newLocal(); int exitFunction = localVariableHelper.newLocal(); - localVariableHelper.writeTemp(methodVisitor, Type.getType(PythonLikeObject.class), exceptionType); localVariableHelper.writeTemp(methodVisitor, Type.getType(PythonLikeObject.class), exception); + localVariableHelper.writeTemp(methodVisitor, Type.getType(PythonLikeObject.class), exceptionArgs); localVariableHelper.writeTemp(methodVisitor, Type.getType(PythonLikeObject.class), traceback); localVariableHelper.writeTemp(methodVisitor, Type.getType(PythonLikeObject.class), label); localVariableHelper.writeTemp(methodVisitor, Type.getType(PythonLikeObject.class), stackSize); @@ -381,7 +384,9 @@ public static void handleExceptionInWith(FunctionMetadata functionMetadata, Stac false); methodVisitor.visitInsn(Opcodes.DUP); - localVariableHelper.readTemp(methodVisitor, Type.getType(PythonLikeObject.class), exceptionType); + localVariableHelper.readTemp(methodVisitor, Type.getType(PythonLikeObject.class), exception); + methodVisitor.visitMethodInsn(Opcodes.INVOKEINTERFACE, Type.getInternalName(PythonLikeObject.class), + "$getType", Type.getMethodDescriptor(Type.getType(PythonLikeType.class)), true); methodVisitor.visitMethodInsn(Opcodes.INVOKEINTERFACE, Type.getInternalName(Collection.class), "add", Type.getMethodDescriptor(Type.BOOLEAN_TYPE, Type.getType(Object.class)), true); @@ -431,10 +436,10 @@ public static void handleExceptionInWith(FunctionMetadata functionMetadata, Stac localVariableHelper.readTemp(methodVisitor, Type.getType(PythonLikeObject.class), traceback); methodVisitor.visitInsn(Opcodes.SWAP); - localVariableHelper.readTemp(methodVisitor, Type.getType(PythonLikeObject.class), exception); + localVariableHelper.readTemp(methodVisitor, Type.getType(PythonLikeObject.class), exceptionArgs); methodVisitor.visitInsn(Opcodes.SWAP); - localVariableHelper.readTemp(methodVisitor, Type.getType(PythonLikeObject.class), exceptionType); + localVariableHelper.readTemp(methodVisitor, Type.getType(PythonLikeObject.class), exception); methodVisitor.visitInsn(Opcodes.SWAP); // Free the 7 temps diff --git a/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/implementors/JumpImplementor.java b/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/implementors/JumpImplementor.java index 587121a..c37ab3e 100644 --- a/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/implementors/JumpImplementor.java +++ b/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/implementors/JumpImplementor.java @@ -3,6 +3,7 @@ import java.util.Map; import ai.timefold.jpyinterpreter.FunctionMetadata; +import ai.timefold.jpyinterpreter.PythonLikeObject; import ai.timefold.jpyinterpreter.PythonUnaryOperator; import ai.timefold.jpyinterpreter.StackMetadata; import ai.timefold.jpyinterpreter.types.BuiltinTypes; @@ -83,7 +84,8 @@ public static void popAndJumpIfIsNone(FunctionMetadata functionMetadata, StackMe } /** - * TOS and TOS1 are an exception types. If TOS1 is not an instance of TOS, set the bytecode counter to the + * TOS is an exception type and TOS1 is an exception. + * If TOS1 is not an instance of TOS, set the bytecode counter to the * {@code instruction} argument. * Pop TOS and TOS1 off the stack. */ @@ -95,6 +97,8 @@ public static void popAndJumpIfExceptionDoesNotMatch(FunctionMetadata functionMe methodVisitor.visitTypeInsn(Opcodes.CHECKCAST, Type.getInternalName(PythonLikeType.class)); StackManipulationImplementor.swap(methodVisitor); + methodVisitor.visitMethodInsn(Opcodes.INVOKEINTERFACE, Type.getInternalName(PythonLikeObject.class), + "$getType", Type.getMethodDescriptor(Type.getType(PythonLikeType.class)), true); methodVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, Type.getInternalName(PythonLikeType.class), "isSubclassOf", Type.getMethodDescriptor(Type.BOOLEAN_TYPE, Type.getType(PythonLikeType.class)), false); diff --git a/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/opcodes/exceptions/SetupFinallyOpcode.java b/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/opcodes/exceptions/SetupFinallyOpcode.java index 55ad3ad..11f270e 100644 --- a/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/opcodes/exceptions/SetupFinallyOpcode.java +++ b/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/opcodes/exceptions/SetupFinallyOpcode.java @@ -36,14 +36,13 @@ public List getStackMetadataAfterInstructionForBranches(FunctionM .pushTemp(BuiltinTypes.NONE_TYPE) .pushTemp(PythonTraceback.TRACEBACK_TYPE) .pushTemp(PythonBaseException.BASE_EXCEPTION_TYPE) - .pushTemp(BuiltinTypes.TYPE_TYPE)); + .pushTemp(PythonBaseException.BASE_EXCEPTION_TYPE)); } @Override public void implement(FunctionMetadata functionMetadata, StackMetadata stackMetadata) { - ExceptionImplementor.createTryFinallyBlock(functionMetadata.methodVisitor, functionMetadata.className, + ExceptionImplementor.createTryFinallyBlock(functionMetadata, stackMetadata, jumpTarget, - stackMetadata, functionMetadata.bytecodeCounterToLabelMap, (bytecodeIndex, runnable) -> { functionMetadata.bytecodeCounterToCodeArgumenterList diff --git a/jpyinterpreter/src/main/python/conversions.py b/jpyinterpreter/src/main/python/conversions.py index 1ba0e7f..6b36afe 100644 --- a/jpyinterpreter/src/main/python/conversions.py +++ b/jpyinterpreter/src/main/python/conversions.py @@ -2,38 +2,58 @@ import inspect from dataclasses import dataclass from typing import TYPE_CHECKING +from traceback import TracebackException, StackSummary, FrameSummary -from jpype import JLong, JDouble, JBoolean, JProxy, JImplementationFor +from jpype import JLong, JDouble, JBoolean, JProxy if TYPE_CHECKING: from java.util import IdentityHashMap - - -# Workaround for https://github.com/jpype-project/jpype/issues/1178 -@JImplementationFor('java.lang.Throwable') -class _JavaException: - @staticmethod - def _get_exception_with_cause(exception): - if exception is None: - return None - try: - raise Exception(f'{exception.getClass().getSimpleName()}: {exception.getMessage()}') - except Exception as e: - cause = _JavaException._get_exception_with_cause(exception.getCause()) - if cause is not None: - try: - raise e from cause - except Exception as return_value: - return return_value - else: - return e - @property - def __cause__(self): - if self.getCause() is not None: - return _JavaException._get_exception_with_cause(self.getCause()) + from java.lang import Throwable + + +def extract_frames_from_java_error(java_error: 'Throwable'): + stack_trace = java_error.getStackTrace() + start_index = 0 + stop_index = len(stack_trace) + while start_index < stop_index and (stack_trace[start_index].getFileName() is None or + not stack_trace[start_index].getFileName().endswith('.py')): + start_index += 1 + + # If there is no python part, keep the entire exception + if start_index == stop_index: + start_index = 0 + + for i in range(start_index, stop_index): + stack_trace_element = stack_trace[stop_index - (i - start_index) - 1] + file_name = stack_trace_element.getFileName() or '' + if file_name.endswith('.py'): + class_name = stack_trace_element.getClassName() or '' + function_name = class_name.rsplit('.', 1)[-1].split('$', 1)[0] else: - return None + function_name = stack_trace_element.getMethodName() or '' + line_number = stack_trace_element.getLineNumber() or 0 + yield FrameSummary(file_name, line_number, function_name) + + +def get_traceback_exception(java_error: 'Throwable', python_exception_type: type, clone_map: 'PythonCloneMap') -> TracebackException: + out = object.__new__(TracebackException) + if java_error.getCause() is not None: + out.__cause__ = get_traceback_exception(java_error.getCause(), + unwrap_python_like_object(java_error.getCause(), + clone_map).__class__.__bases__[0], + clone_map) + else: + out.__cause__ = None + + out.__suppress_context__ = False + out.__context__ = None + out.__notes__ = None + out.exceptions = None + out.exc_type = python_exception_type + out.stack = StackSummary.from_list(extract_frames_from_java_error(java_error)) + out._str = java_error.getMessage() + return out def get_translated_java_system_error_message(error): @@ -624,7 +644,17 @@ def throw(self, thrown): exception_python_type = getattr(builtins, exception_name) args = unwrap_python_like_object(getattr(python_like_object, '$getArgs')(), clone_map, default) - return clone_map.add_clone(python_like_object, exception_python_type(*args)) + traceback_exception = get_traceback_exception(python_like_object, exception_python_type, clone_map) + + class WrappedException(exception_python_type): + wrapped_type: type + def __init__(self, *args): + super().__init__(*args) + + def __str__(self): + return ''.join(traceback_exception.format()) + + return clone_map.add_clone(python_like_object, WrappedException(*args)) except AttributeError: return clone_map.add_clone(python_like_object, TranslatedJavaSystemError(python_like_object)) elif isinstance(python_like_object, PythonLikeType): @@ -700,4 +730,4 @@ def unwrap_python_like_builtin_module_object(python_like_object, clone_map, defa unwrap_python_like_object(python_like_object.seconds, clone_map, default), unwrap_python_like_object(python_like_object.microseconds, clone_map, default))) - return None \ No newline at end of file + return None diff --git a/jpyinterpreter/src/main/python/translator.py b/jpyinterpreter/src/main/python/translator.py index 5a59996..90c399e 100644 --- a/jpyinterpreter/src/main/python/translator.py +++ b/jpyinterpreter/src/main/python/translator.py @@ -18,6 +18,28 @@ function_interface_pair_to_class = dict() +def get_file_for_module(module_name): + import pathlib + import sys + + if module_name is None: + return '' + + module = sys.modules[module_name] + if hasattr(module, '__file__'): + file_path = sys.modules[module_name].__file__ + if file_path is not None: + return file_path + + # Do not know file for module; predict file from module name + if module_name == '__main__': + return '' + + path_parts = module_name.split('.') + path_parts[-1] = f'{path_parts[-1]}.py' + return str(pathlib.Path(*path_parts)) + + def is_python_version_supported(python_version): python_version_major_minor = python_version[0:2] return MINIMUM_SUPPORTED_PYTHON_VERSION <= python_version_major_minor <= MAXIMUM_SUPPORTED_PYTHON_VERSION @@ -213,6 +235,7 @@ def get_function_bytecode_object(python_function): instruction_list.add(java_instruction) python_compiled_function.module = python_function.__module__ + python_compiled_function.moduleFilePath = get_file_for_module(python_function.__module__) python_compiled_function.qualifiedName = python_function.__qualname__ python_compiled_function.instructionList = instruction_list python_compiled_function.co_exceptiontable = get_python_exception_table(python_function.__code__) @@ -386,10 +409,19 @@ def wrapped_function(*args, **kwargs): java_kwargs.put(convert_to_java_python_like_object(key, instance_map), convert_to_java_python_like_object(value, instance_map)) + out = None + error = None + try: - return unwrap_python_like_object(getattr(java_function, '$call')(java_args, java_kwargs, None)) + out = unwrap_python_like_object(getattr(java_function, '$call')(java_args, java_kwargs, None)) except Exception as e: - raise unwrap_python_like_object(e) + error = unwrap_python_like_object(e) + + if error is not None: + raise error + + return out + return wrapped_function @@ -402,10 +434,18 @@ def wrapped_function(*args): instance_map = HashMap() java_args = [convert_to_java_python_like_object(arg, instance_map) for arg in args] + out = None + error = None + try: - return unwrap_python_like_object(java_function.invoke(*java_args)) + out = unwrap_python_like_object(java_function.invoke(*java_args)) except Exception as e: - raise unwrap_python_like_object(e) + error = unwrap_python_like_object(e) + + if error is not None: + raise error + + return out return wrapped_function @@ -595,6 +635,7 @@ def translate_python_class_to_java_class(python_class): python_compiled_class.binaryType = CPythonType.getType(JProxy(OpaquePythonReference, inst=python_class, convert=True)) python_compiled_class.module = python_class.__module__ + python_compiled_class.moduleFilePath = get_file_for_module(python_class.__module__) python_compiled_class.qualifiedName = python_class.__qualname__ python_compiled_class.className = python_class.__name__ python_compiled_class.typeAnnotations = copy_type_annotations(python_class, diff --git a/jpyinterpreter/src/test/java/ai/timefold/jpyinterpreter/dag/FlowGraphTest.java b/jpyinterpreter/src/test/java/ai/timefold/jpyinterpreter/dag/FlowGraphTest.java index 9a08236..1c3468a 100644 --- a/jpyinterpreter/src/test/java/ai/timefold/jpyinterpreter/dag/FlowGraphTest.java +++ b/jpyinterpreter/src/test/java/ai/timefold/jpyinterpreter/dag/FlowGraphTest.java @@ -217,16 +217,20 @@ public void testStackMetadataForExceptions() { new FrameData(9).stack(), // LOAD_ASSERTION_ERROR new FrameData(10).stack(PythonAssertionError.ASSERTION_ERROR_TYPE), // RAISE new FrameData(11).stack(BuiltinTypes.NONE_TYPE, BuiltinTypes.INT_TYPE, BuiltinTypes.NONE_TYPE, - PythonTraceback.TRACEBACK_TYPE, PythonBaseException.BASE_EXCEPTION_TYPE, BuiltinTypes.TYPE_TYPE), // except handler; DUP_TOP, + PythonTraceback.TRACEBACK_TYPE, PythonBaseException.BASE_EXCEPTION_TYPE, + PythonBaseException.BASE_EXCEPTION_TYPE), // except handler; DUP_TOP, new FrameData(12).stack(BuiltinTypes.NONE_TYPE, BuiltinTypes.INT_TYPE, BuiltinTypes.NONE_TYPE, - PythonTraceback.TRACEBACK_TYPE, PythonBaseException.BASE_EXCEPTION_TYPE, BuiltinTypes.TYPE_TYPE, - BuiltinTypes.TYPE_TYPE), // LOAD_CONSTANT + PythonTraceback.TRACEBACK_TYPE, PythonBaseException.BASE_EXCEPTION_TYPE, + PythonBaseException.BASE_EXCEPTION_TYPE, + PythonBaseException.BASE_EXCEPTION_TYPE), // LOAD_CONSTANT new FrameData(13).stack(BuiltinTypes.NONE_TYPE, BuiltinTypes.INT_TYPE, BuiltinTypes.NONE_TYPE, - PythonTraceback.TRACEBACK_TYPE, PythonBaseException.BASE_EXCEPTION_TYPE, BuiltinTypes.TYPE_TYPE, - BuiltinTypes.TYPE_TYPE, + PythonTraceback.TRACEBACK_TYPE, PythonBaseException.BASE_EXCEPTION_TYPE, + PythonBaseException.BASE_EXCEPTION_TYPE, + PythonBaseException.BASE_EXCEPTION_TYPE, BuiltinTypes.TYPE_TYPE), // JUMP_IF_NOT_EXC_MATCH new FrameData(14).stack(BuiltinTypes.NONE_TYPE, BuiltinTypes.INT_TYPE, BuiltinTypes.NONE_TYPE, - PythonTraceback.TRACEBACK_TYPE, PythonBaseException.BASE_EXCEPTION_TYPE, BuiltinTypes.TYPE_TYPE), // POP_TOP + PythonTraceback.TRACEBACK_TYPE, PythonBaseException.BASE_EXCEPTION_TYPE, + PythonBaseException.BASE_EXCEPTION_TYPE), // POP_TOP new FrameData(15).stack(BuiltinTypes.NONE_TYPE, BuiltinTypes.INT_TYPE, BuiltinTypes.NONE_TYPE, PythonTraceback.TRACEBACK_TYPE, PythonBaseException.BASE_EXCEPTION_TYPE), // POP_TOP new FrameData(16).stack(BuiltinTypes.NONE_TYPE, BuiltinTypes.INT_TYPE, BuiltinTypes.NONE_TYPE, @@ -236,13 +240,13 @@ public void testStackMetadataForExceptions() { new FrameData(19).stack(BuiltinTypes.STRING_TYPE), // RETURN new FrameData(20).stack(BuiltinTypes.NONE_TYPE, BuiltinTypes.INT_TYPE, BuiltinTypes.NONE_TYPE, PythonTraceback.TRACEBACK_TYPE, PythonBaseException.BASE_EXCEPTION_TYPE, - BuiltinTypes.TYPE_TYPE), // POP_TOP + PythonBaseException.BASE_EXCEPTION_TYPE), // POP_TOP new FrameData(21).stack(BuiltinTypes.NONE_TYPE, BuiltinTypes.INT_TYPE, BuiltinTypes.NONE_TYPE, - PythonTraceback.TRACEBACK_TYPE, PythonBaseException.BASE_EXCEPTION_TYPE, BuiltinTypes.TYPE_TYPE), // POP_TOP + PythonTraceback.TRACEBACK_TYPE, PythonBaseException.BASE_EXCEPTION_TYPE, + PythonBaseException.BASE_EXCEPTION_TYPE), // POP_TOP new FrameData(22).stack(BuiltinTypes.NONE_TYPE, BuiltinTypes.INT_TYPE, BuiltinTypes.NONE_TYPE, - PythonTraceback.TRACEBACK_TYPE, PythonBaseException.BASE_EXCEPTION_TYPE), // RERAISE - new FrameData(23).stack(BuiltinTypes.NONE_TYPE, BuiltinTypes.INT_TYPE, BuiltinTypes.NONE_TYPE, - PythonTraceback.TRACEBACK_TYPE, PythonBaseException.BASE_EXCEPTION_TYPE, BuiltinTypes.TYPE_TYPE) // RERAISE + PythonTraceback.TRACEBACK_TYPE, PythonBaseException.BASE_EXCEPTION_TYPE, + PythonBaseException.BASE_EXCEPTION_TYPE) // RERAISE ); } @@ -301,16 +305,20 @@ public void testStackMetadataForTryFinally() { new FrameData(15).stack(), // NOP new FrameData(16).stack(), // JUMP_ABSOLUTE new FrameData(17).stack(BuiltinTypes.NONE_TYPE, BuiltinTypes.INT_TYPE, BuiltinTypes.NONE_TYPE, - PythonTraceback.TRACEBACK_TYPE, PythonBaseException.BASE_EXCEPTION_TYPE, BuiltinTypes.TYPE_TYPE), // except handler; DUP_TOP, + PythonTraceback.TRACEBACK_TYPE, PythonBaseException.BASE_EXCEPTION_TYPE, + PythonBaseException.BASE_EXCEPTION_TYPE), // except handler; DUP_TOP, new FrameData(18).stack(BuiltinTypes.NONE_TYPE, BuiltinTypes.INT_TYPE, BuiltinTypes.NONE_TYPE, - PythonTraceback.TRACEBACK_TYPE, PythonBaseException.BASE_EXCEPTION_TYPE, BuiltinTypes.TYPE_TYPE, - BuiltinTypes.TYPE_TYPE), // LOAD_CONSTANT + PythonTraceback.TRACEBACK_TYPE, PythonBaseException.BASE_EXCEPTION_TYPE, + PythonBaseException.BASE_EXCEPTION_TYPE, + PythonBaseException.BASE_EXCEPTION_TYPE), // LOAD_CONSTANT new FrameData(19).stack(BuiltinTypes.NONE_TYPE, BuiltinTypes.INT_TYPE, BuiltinTypes.NONE_TYPE, - PythonTraceback.TRACEBACK_TYPE, PythonBaseException.BASE_EXCEPTION_TYPE, BuiltinTypes.TYPE_TYPE, - BuiltinTypes.TYPE_TYPE, + PythonTraceback.TRACEBACK_TYPE, PythonBaseException.BASE_EXCEPTION_TYPE, + PythonBaseException.BASE_EXCEPTION_TYPE, + PythonBaseException.BASE_EXCEPTION_TYPE, BuiltinTypes.TYPE_TYPE), // JUMP_IF_NOT_EXC_MATCH new FrameData(20).stack(BuiltinTypes.NONE_TYPE, BuiltinTypes.INT_TYPE, BuiltinTypes.NONE_TYPE, - PythonTraceback.TRACEBACK_TYPE, PythonBaseException.BASE_EXCEPTION_TYPE, BuiltinTypes.TYPE_TYPE), // POP_TOP + PythonTraceback.TRACEBACK_TYPE, PythonBaseException.BASE_EXCEPTION_TYPE, + PythonBaseException.BASE_EXCEPTION_TYPE), // POP_TOP new FrameData(21).stack(BuiltinTypes.NONE_TYPE, BuiltinTypes.INT_TYPE, BuiltinTypes.NONE_TYPE, PythonTraceback.TRACEBACK_TYPE, PythonBaseException.BASE_EXCEPTION_TYPE), // POP_TOP new FrameData(22).stack(BuiltinTypes.NONE_TYPE, BuiltinTypes.INT_TYPE, BuiltinTypes.NONE_TYPE, @@ -320,30 +328,31 @@ public void testStackMetadataForTryFinally() { new FrameData(25).stack(BuiltinTypes.STRING_TYPE), // STORE_GLOBAL new FrameData(26).stack(), // JUMP_ABSOLUTE new FrameData(27).stack(BuiltinTypes.NONE_TYPE, BuiltinTypes.INT_TYPE, BuiltinTypes.NONE_TYPE, - PythonTraceback.TRACEBACK_TYPE, PythonBaseException.BASE_EXCEPTION_TYPE, BuiltinTypes.TYPE_TYPE), // RERAISE + PythonTraceback.TRACEBACK_TYPE, PythonBaseException.BASE_EXCEPTION_TYPE, + PythonBaseException.BASE_EXCEPTION_TYPE), // RERAISE new FrameData(28).stack(BuiltinTypes.NONE_TYPE, BuiltinTypes.INT_TYPE, BuiltinTypes.NONE_TYPE, - PythonTraceback.TRACEBACK_TYPE, PythonBaseException.BASE_EXCEPTION_TYPE, BuiltinTypes.TYPE_TYPE), // POP_TOP - new FrameData(29).stack(BuiltinTypes.NONE_TYPE, BuiltinTypes.INT_TYPE, BuiltinTypes.NONE_TYPE, - PythonTraceback.TRACEBACK_TYPE, PythonBaseException.BASE_EXCEPTION_TYPE), // POP_TOP + PythonTraceback.TRACEBACK_TYPE, PythonBaseException.BASE_EXCEPTION_TYPE, + PythonBaseException.BASE_EXCEPTION_TYPE), // POP_TOP + new FrameData(29).stack(), // POP_TOP new FrameData(30).stack(), // POP_TOP - new FrameData(31).stack(), // Load constant - new FrameData(32).stack(BuiltinTypes.STRING_TYPE), // STORE - new FrameData(33).stack(), // JUMP_ABSOLUTE + new FrameData(31).stack(BuiltinTypes.STRING_TYPE), // STORE + new FrameData(32).stack(), // JUMP_ABSOLUTE + new FrameData(33).stack(BuiltinTypes.NONE_TYPE, BuiltinTypes.INT_TYPE, BuiltinTypes.NONE_TYPE, + PythonTraceback.TRACEBACK_TYPE, PythonBaseException.BASE_EXCEPTION_TYPE, + PythonBaseException.BASE_EXCEPTION_TYPE), // NO-OP; Uncaught exception handler new FrameData(34).stack(BuiltinTypes.NONE_TYPE, BuiltinTypes.INT_TYPE, BuiltinTypes.NONE_TYPE, - PythonTraceback.TRACEBACK_TYPE, PythonBaseException.BASE_EXCEPTION_TYPE, BuiltinTypes.TYPE_TYPE), // NO-OP; Uncaught exception handler + PythonTraceback.TRACEBACK_TYPE, PythonBaseException.BASE_EXCEPTION_TYPE, + PythonBaseException.BASE_EXCEPTION_TYPE), // LOAD-CONSTANT new FrameData(35).stack(BuiltinTypes.NONE_TYPE, BuiltinTypes.INT_TYPE, BuiltinTypes.NONE_TYPE, - PythonTraceback.TRACEBACK_TYPE, PythonBaseException.BASE_EXCEPTION_TYPE, BuiltinTypes.TYPE_TYPE), // LOAD-CONSTANT - new FrameData(36).stack(BuiltinTypes.NONE_TYPE, BuiltinTypes.INT_TYPE, BuiltinTypes.NONE_TYPE, - PythonTraceback.TRACEBACK_TYPE, PythonBaseException.BASE_EXCEPTION_TYPE, BuiltinTypes.TYPE_TYPE, + PythonTraceback.TRACEBACK_TYPE, PythonBaseException.BASE_EXCEPTION_TYPE, + PythonBaseException.BASE_EXCEPTION_TYPE, BuiltinTypes.STRING_TYPE), // STORE - new FrameData(37).stack(BuiltinTypes.NONE_TYPE, BuiltinTypes.INT_TYPE, BuiltinTypes.NONE_TYPE, - PythonTraceback.TRACEBACK_TYPE, PythonBaseException.BASE_EXCEPTION_TYPE, BuiltinTypes.TYPE_TYPE), // POP-TOP - new FrameData(38).stack(BuiltinTypes.NONE_TYPE, BuiltinTypes.INT_TYPE, BuiltinTypes.NONE_TYPE, - PythonTraceback.TRACEBACK_TYPE, PythonBaseException.BASE_EXCEPTION_TYPE), // RERAISE - new FrameData(39).stack(), // NO-OP; After try - new FrameData(40).stack(), // LOAD_CONSTANT - new FrameData(41).stack(BuiltinTypes.INT_TYPE) // RETURN - ); + new FrameData(36).stack(BuiltinTypes.NONE_TYPE, BuiltinTypes.INT_TYPE, BuiltinTypes.NONE_TYPE, + PythonTraceback.TRACEBACK_TYPE, PythonBaseException.BASE_EXCEPTION_TYPE, + PythonBaseException.BASE_EXCEPTION_TYPE), // POP-TOP + new FrameData(37).stack(), // RERAISE + new FrameData(38).stack(), // NO-OP; After try + new FrameData(39).stack(BuiltinTypes.INT_TYPE)); } @Test diff --git a/jpyinterpreter/src/test/java/ai/timefold/jpyinterpreter/util/ExceptBuilder.java b/jpyinterpreter/src/test/java/ai/timefold/jpyinterpreter/util/ExceptBuilder.java index aa7d140..c2da00a 100644 --- a/jpyinterpreter/src/test/java/ai/timefold/jpyinterpreter/util/ExceptBuilder.java +++ b/jpyinterpreter/src/test/java/ai/timefold/jpyinterpreter/util/ExceptBuilder.java @@ -96,7 +96,6 @@ public ExceptBuilder andFinally(Consumer finallyBuilder, .markAsJumpTarget(); delegate.instructionList.add(exceptGotoTarget); - delegate.op(StackOpDescriptor.POP_TOP); delegate.op(ExceptionOpDescriptor.RERAISE, 0); if (tryEndGoto != null) { @@ -127,7 +126,6 @@ public ExceptBuilder andFinally(Consumer finallyBuilder, finallyBuilder.accept(delegate); - delegate.op(StackOpDescriptor.POP_TOP); delegate.op(ExceptionOpDescriptor.RERAISE); delegate.update(finallyEndInstruction.withArg(delegate.instructionList.size())); @@ -150,7 +148,6 @@ public PythonFunctionBuilder tryEnd() { .markAsJumpTarget(); delegate.instructionList.add(exceptGotoTarget); - delegate.op(StackOpDescriptor.POP_TOP); delegate.op(ExceptionOpDescriptor.RERAISE, 0); } diff --git a/jpyinterpreter/src/test/java/ai/timefold/jpyinterpreter/util/PythonFunctionBuilder.java b/jpyinterpreter/src/test/java/ai/timefold/jpyinterpreter/util/PythonFunctionBuilder.java index 765fd7c..bb2ca73 100644 --- a/jpyinterpreter/src/test/java/ai/timefold/jpyinterpreter/util/PythonFunctionBuilder.java +++ b/jpyinterpreter/src/test/java/ai/timefold/jpyinterpreter/util/PythonFunctionBuilder.java @@ -321,7 +321,7 @@ public PythonFunctionBuilder with(Consumer blockBuilder) update(instruction.withArg(exceptionHandler.offset() - instruction.offset() - 1)); ifFalse(reraiseExceptionBlock -> reraiseExceptionBlock - .op(StackOpDescriptor.POP_TOP).op(ExceptionOpDescriptor.RERAISE)); + .op(ExceptionOpDescriptor.RERAISE)); op(StackOpDescriptor.POP_TOP); op(StackOpDescriptor.POP_TOP); diff --git a/jpyinterpreter/tests/conftest.py b/jpyinterpreter/tests/conftest.py index 4cd11a9..20ae99a 100644 --- a/jpyinterpreter/tests/conftest.py +++ b/jpyinterpreter/tests/conftest.py @@ -67,7 +67,7 @@ def verify_property(self, *args, predicate, clone_arguments=True): f'the property ({inspect.getsource(predicate)}) ' f'for arguments {args}.') elif not predicate(untyped_java_result): - raise AssertionError(f'Untyped translated bytecode result ({java_result}) does not satisfy the ' + raise AssertionError(f'Untyped translated bytecode result ({untyped_java_result}) does not satisfy the ' f'property ({inspect.getsource(predicate)}) ' f'for arguments {args}.') else: @@ -75,6 +75,51 @@ def verify_property(self, *args, predicate, clone_arguments=True): f'property ({inspect.getsource(predicate)}) ' f'for arguments {args}.') + def verify_error_property(self, *args, predicate, clone_arguments=True): + cloner = get_argument_cloner(clone_arguments) + try: + python_result = self.python_function(*cloner(args)) + raise AssertionError(f'Python function did not raise an error and returned ({python_result})') + except AssertionError: + raise + except Exception as python_result: + if not predicate(python_result): + import inspect + raise AssertionError(f'Python function error ({python_result}) does not satisfy the property ' + f'({inspect.getsource(predicate).strip()}) ' + f'for arguments {args}.') + + try: + java_result = self.java_function(*cloner(args)) + raise AssertionError(f'Typed Java function did not raise an error and returned ({java_result})') + except AssertionError: + raise + except Exception as err: + java_result = err + + try: + untyped_java_result = self.untyped_java_function(*cloner(args)) + raise AssertionError(f'Untyped Java function did not raise an error and returned ({untyped_java_result})') + except AssertionError: + raise + except Exception as err: + untyped_java_result = err + + if not predicate(java_result) or not predicate(untyped_java_result): + import inspect + if not predicate(java_result) and not predicate(untyped_java_result): + raise AssertionError(f'Typed and untyped translated bytecode error ({java_result}) does not satisfy ' + f'the property ({inspect.getsource(predicate)}) ' + f'for arguments {args}.') + elif not predicate(untyped_java_result): + raise AssertionError(f'Untyped translated bytecode error ({untyped_java_result}) does not satisfy the ' + f'property ({inspect.getsource(predicate)}) ' + f'for arguments {args}.') + else: + raise AssertionError(f'Typed translated bytecode error ({java_result}) does not satisfy the ' + f'property ({inspect.getsource(predicate)}) ' + f'for arguments {args}.') + def expect(self, expected, *args, clone_arguments=True, type_check=True): cloner = get_argument_cloner(clone_arguments) java_result = self.java_function(*cloner(args)) diff --git a/jpyinterpreter/tests/test_builtins.py b/jpyinterpreter/tests/test_builtins.py index d39628f..b210b84 100644 --- a/jpyinterpreter/tests/test_builtins.py +++ b/jpyinterpreter/tests/test_builtins.py @@ -2,9 +2,38 @@ import pytest import sys from typing import SupportsAbs, Iterable, Callable, Sequence, Union, Iterator, Sized, Reversible, SupportsIndex + +from jpype import JImplementationFor + from .conftest import verifier_for +# Workaround for https://github.com/jpype-project/jpype/issues/1178 +@JImplementationFor('java.lang.Throwable') +class _JavaException: + @staticmethod + def _get_exception_with_cause(exception): + if exception is None: + return None + try: + raise Exception(f'{exception.getClass().getSimpleName()}: {exception.getMessage()}') + except Exception as e: + cause = _JavaException._get_exception_with_cause(exception.getCause()) + if cause is not None: + try: + raise e from cause + except Exception as return_value: + return return_value + else: + return e + @property + def __cause__(self): + if self.getCause() is not None: + return _JavaException._get_exception_with_cause(self.getCause()) + else: + return None + + def test_abs(): def my_function(x: SupportsAbs) -> object: return abs(x) @@ -377,7 +406,7 @@ def my_function(): with pytest.raises(ValueError) as excinfo: java_function() - assert 'builtin locals is not supported when executed in Java bytecode' == str(excinfo.value) + assert 'builtin locals() is not supported when executed in Java bytecode' in str(excinfo.value) def test_map(): @@ -748,4 +777,5 @@ def my_function() -> exception_class: verifier = verifier_for(my_function) verifier.verify_property(predicate= - lambda error: type(error) == exception_class and error.args == ('my argument',)) + lambda error: isinstance(error, exception_class) and len(error.args) == 1 and + 'my argument' in error.args[0]) diff --git a/jpyinterpreter/tests/test_traceback.py b/jpyinterpreter/tests/test_traceback.py new file mode 100644 index 0000000..4ac2ac6 --- /dev/null +++ b/jpyinterpreter/tests/test_traceback.py @@ -0,0 +1,100 @@ +from .conftest import verifier_for + +def test_function_traceback(): + def my_function_1(): + my_function_2() + + def my_function_2(): + raise Exception('Message') + + def check_traceback(error: Exception): + from traceback import format_exception + traceback = '\n'.join(format_exception(type(error), error, error.__traceback__)) + if 'test_traceback.py", line 5, in my_function_1\n' not in traceback: + return False + + if 'test_traceback.py", line 8, in my_function_2\n' not in traceback: + return False + + if not traceback.strip().endswith('Exception: Message'): + return False + + if 'File "PythonException.java"' in traceback: + return False + + return True + + verifier = verifier_for(my_function_1) + verifier.verify_error_property(predicate=check_traceback) + + +def test_class_traceback(): + class A: + def my_function(self): + raise ValueError('Message') + + def check_traceback(error: Exception): + from traceback import format_exception + traceback = '\n'.join(format_exception(type(error), error, error.__traceback__)) + if 'test_traceback.py", line 34, in my_function\n' not in traceback: + return False + + if not traceback.strip().endswith('ValueError: Message'): + return False + + if 'File "PythonException.java"' in traceback: + return False + + return True + + def call_class_function(): + return A().my_function() + + verifier = verifier_for(call_class_function) + verifier.verify_error_property(predicate=check_traceback) + + +def test_chained_traceback(): + def first(): + return second() + + def second(): + try: + return third() + except ValueError as e: + raise RuntimeError('Consequence') from e + + def third(): + raise ValueError("Cause") + + def check_traceback(error: Exception): + from traceback import format_exception + traceback = '\n'.join(format_exception(type(error), error, error.__traceback__)) + if 'test_traceback.py", line 59, in first\n' not in traceback: + return False + + if 'test_traceback.py", line 63, in second\n' not in traceback: + return False + + if 'ValueError: Cause' not in traceback: + return False + + if 'The above exception was the direct cause of the following exception:\n' not in traceback: + return False + + if 'test_traceback.py", line 65, in second\n' not in traceback: + return False + + if 'test_traceback.py", line 68, in third\n' not in traceback: + return False + + if not traceback.strip().endswith('RuntimeError: Consequence'): + return False + + if 'File "PythonException.java"' in traceback: + return False + + return True + + verifier = verifier_for(first) + verifier.verify_error_property(predicate=check_traceback) diff --git a/timefold-solver-python-core/src/main/python/timefold_api_wrappers.py b/timefold-solver-python-core/src/main/python/timefold_api_wrappers.py index c761471..ac9f054 100644 --- a/timefold-solver-python-core/src/main/python/timefold_api_wrappers.py +++ b/timefold-solver-python-core/src/main/python/timefold_api_wrappers.py @@ -306,9 +306,7 @@ def solve(self, problem: Solution_): try: java_solution = self._delegate.solve(java_problem) except PythonBaseException as e: - python_error = unwrap_python_like_object(e) - raise RuntimeError(f'Solving failed due to an error: {e.getMessage()}.\n' - f'Java stack trace: {e.stacktrace()}') from python_error + raise unwrap_python_like_object(e) except JavaException as e: raise RuntimeError(f'Solving failed due to an error: {e.getMessage()}.\n' f'Java stack trace: {e.stacktrace()}') from e