From fda8928f1755a16c3a3babc2ddf7950c406e40cf Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Thu, 26 Feb 2026 21:19:30 +0100 Subject: [PATCH 1/2] Fall back to interpreter on JVM VerifyError When the JVM bytecode verifier rejects a generated class (e.g. due to uninitialized local variable slots in complex subroutines), catch VerifyError alongside MethodTooLargeException and compile the AST to interpreter bytecode instead. This unblocks ExifTool.pm module loading which triggers VerifyError in anon787 (the module body with 22 captured hash variables). Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- .../org/perlonjava/backend/jvm/EmitterMethodCreator.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitterMethodCreator.java b/src/main/java/org/perlonjava/backend/jvm/EmitterMethodCreator.java index bd97a7750..de186fecc 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitterMethodCreator.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitterMethodCreator.java @@ -1519,6 +1519,12 @@ public static RuntimeCode createRuntimeCode( // If interpreter fallback disabled, re-throw to use existing AST splitter logic throw e; + } catch (VerifyError e) { + if (USE_INTERPRETER_FALLBACK) { + System.err.println("Note: JVM VerifyError (" + e.getMessage().split("\n")[0] + "), using interpreter backend."); + return compileToInterpreter(ast, ctx, useTryCatch); + } + throw new RuntimeException(e); } } From 0c15135e85c71a7b7dc9e739593d0a4acf60196f Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Thu, 26 Feb 2026 21:57:18 +0100 Subject: [PATCH 2/2] Fix interpreter fallback: package context, delete deref, and statement modifier my Three fixes for the eval interpreter catchup: 1. Pass EmitterContext to BytecodeCompiler.compile() so interpreter fallback inherits the correct package context. Unqualified sub calls now resolve in the correct package instead of main::. 2. Handle delete with arrow dereference ($$ref{key} parsed as $ref->{key}) in the interpreter bytecode compiler. Previously fell through to unimplemented slow path. 3. Fix 'my $x = expr if COND' in void context: extract the my declaration before the short-circuit jump so the variable is always initialized to undef, even when the condition is false. Also update SubroutineParser to update placeholder in-place for InterpretedCode subs, ensuring hash copies see the compiled code. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- .../backend/bytecode/BytecodeCompiler.java | 6 +++ .../backend/bytecode/CompileOperator.java | 39 +++++++++++++++++++ .../backend/jvm/EmitLogicalOperator.java | 35 +++++++++++++---- .../backend/jvm/EmitterMethodCreator.java | 4 +- .../frontend/parser/SubroutineParser.java | 12 ++++-- 5 files changed, 84 insertions(+), 12 deletions(-) diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java index 557bbbaf7..eedb514b2 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java @@ -493,6 +493,12 @@ public InterpretedCode compile(Node node, EmitterContext ctx) { // Use the calling context from EmitterContext for top-level expressions // This is crucial for eval STRING to propagate context correctly currentCallContext = ctx.contextType; + // Inherit package from the JVM compiler context so unqualified sub calls + // resolve in the correct package (not main) + if (ctx.symbolTable != null) { + symbolTable.setCurrentPackage(ctx.symbolTable.getCurrentPackage(), + ctx.symbolTable.currentPackageIsClass()); + } } // If we have captured variables, allocate registers for them diff --git a/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java index 49a2ceace..4374be4fe 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java @@ -1446,6 +1446,45 @@ public static void visitOperator(BytecodeCompiler bytecodeCompiler, OperatorNode bytecodeCompiler.emitReg(hashReg); bytecodeCompiler.emitReg(keyReg); + bytecodeCompiler.lastResultReg = rd; + } else if (arg instanceof BinaryOperatorNode && ((BinaryOperatorNode) arg).operator.equals("->")) { + // Arrow dereference: delete $ref->{key} + BinaryOperatorNode arrowAccess = (BinaryOperatorNode) arg; + // Compile the reference expression + arrowAccess.left.accept(bytecodeCompiler); + int scalarReg = bytecodeCompiler.lastResultReg; + + int hashReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emitWithToken(Opcodes.DEREF_HASH, node.getIndex()); + bytecodeCompiler.emitReg(hashReg); + bytecodeCompiler.emitReg(scalarReg); + + // Compile key + int keyReg; + if (arrowAccess.right instanceof HashLiteralNode keyNode && !keyNode.elements.isEmpty()) { + Node keyElement = keyNode.elements.get(0); + if (keyElement instanceof IdentifierNode) { + String keyString = ((IdentifierNode) keyElement).name; + keyReg = bytecodeCompiler.allocateRegister(); + int keyIdx = bytecodeCompiler.addToStringPool(keyString); + bytecodeCompiler.emit(Opcodes.LOAD_STRING); + bytecodeCompiler.emitReg(keyReg); + bytecodeCompiler.emit(keyIdx); + } else { + keyElement.accept(bytecodeCompiler); + keyReg = bytecodeCompiler.lastResultReg; + } + } else { + arrowAccess.right.accept(bytecodeCompiler); + keyReg = bytecodeCompiler.lastResultReg; + } + + int rd = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.HASH_DELETE); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(hashReg); + bytecodeCompiler.emitReg(keyReg); + bytecodeCompiler.lastResultReg = rd; } else { // For now, use SLOW_OP for other cases (hash slice delete, array delete, etc.) diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitLogicalOperator.java b/src/main/java/org/perlonjava/backend/jvm/EmitLogicalOperator.java index 55f8b79b5..2bd966f57 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitLogicalOperator.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitLogicalOperator.java @@ -287,14 +287,35 @@ private static void emitLogicalOperatorSimple(EmitterVisitor emitterVisitor, Bin if (emitterVisitor.ctx.contextType == RuntimeContextType.VOID) { evalTrace("EmitLogicalOperatorSimple VOID op=" + node.operator + " emit LHS in SCALAR; RHS in SCALAR"); - node.left.accept(emitterVisitor.with(RuntimeContextType.SCALAR)); - mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "org/perlonjava/runtime/runtimetypes/RuntimeBase", getBoolean, "()Z", false); - mv.visitJumpInsn(compareOpcode, endLabel); - node.right.accept(emitterVisitor.with(RuntimeContextType.SCALAR)); - mv.visitInsn(Opcodes.POP); - - mv.visitLabel(endLabel); + OperatorNode voidDeclaration = FindDeclarationVisitor.findOperator(node.right, "my"); + String voidSavedOperator = null; + Node voidSavedOperand = null; + boolean voidRewritten = false; + try { + if (voidDeclaration != null && voidDeclaration.operand instanceof OperatorNode voidOperatorNode) { + voidSavedOperator = voidDeclaration.operator; + voidSavedOperand = voidDeclaration.operand; + voidDeclaration.accept(emitterVisitor.with(RuntimeContextType.VOID)); + voidDeclaration.operator = voidOperatorNode.operator; + voidDeclaration.operand = voidOperatorNode.operand; + voidRewritten = true; + } + + node.left.accept(emitterVisitor.with(RuntimeContextType.SCALAR)); + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "org/perlonjava/runtime/runtimetypes/RuntimeBase", getBoolean, "()Z", false); + mv.visitJumpInsn(compareOpcode, endLabel); + + node.right.accept(emitterVisitor.with(RuntimeContextType.SCALAR)); + mv.visitInsn(Opcodes.POP); + + mv.visitLabel(endLabel); + } finally { + if (voidRewritten) { + voidDeclaration.operator = voidSavedOperator; + voidDeclaration.operand = voidSavedOperand; + } + } return; } diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitterMethodCreator.java b/src/main/java/org/perlonjava/backend/jvm/EmitterMethodCreator.java index de186fecc..6151612e7 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitterMethodCreator.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitterMethodCreator.java @@ -1619,8 +1619,8 @@ private static InterpretedCode compileToInterpreter( ctx.errorUtil ); - // Compile AST to interpreter bytecode - InterpretedCode code = compiler.compile(ast); + // Compile AST to interpreter bytecode (pass ctx for package context and closure detection) + InterpretedCode code = compiler.compile(ast, ctx); // Handle captured variables if needed (for closures) if (ctx.capturedEnv != null && ctx.capturedEnv.length > skipVariables) { diff --git a/src/main/java/org/perlonjava/frontend/parser/SubroutineParser.java b/src/main/java/org/perlonjava/frontend/parser/SubroutineParser.java index 8ada9151b..9cba34c7d 100644 --- a/src/main/java/org/perlonjava/frontend/parser/SubroutineParser.java +++ b/src/main/java/org/perlonjava/frontend/parser/SubroutineParser.java @@ -818,7 +818,11 @@ public static ListNode handleNamedSubWithFilter(Parser parser, String subName, S field.set(placeholder.codeObject, codeRef); } else if (runtimeCode instanceof InterpretedCode) { - // InterpretedCode path - replace codeRef.value entirely + // InterpretedCode path - update placeholder in-place (not replace codeRef.value) + // This is critical: hash assignments copy RuntimeScalar but share the same + // RuntimeCode value object. If we replace codeRef.value, hash copies won't see + // the update. By setting methodHandle/codeObject on the placeholder, ALL + // references (including hash copies) will see the compiled code. InterpretedCode interpretedCode = (InterpretedCode) runtimeCode; @@ -839,8 +843,10 @@ public static ListNode handleNamedSubWithFilter(Parser parser, String subName, S interpretedCode.subName = placeholder.subName; interpretedCode.packageName = placeholder.packageName; - // REPLACE the global reference - codeRef.value = interpretedCode; + // Update placeholder in-place: set methodHandle to delegate to InterpretedCode + placeholder.methodHandle = RuntimeCode.lookup.findVirtual( + InterpretedCode.class, "apply", RuntimeCode.methodType); + placeholder.codeObject = interpretedCode; } } catch (Exception e) { // Handle any exceptions during subroutine creation