From 53a25ecb299102db41bab27480584eb0e908f9ee Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Fri, 13 Mar 2026 13:07:33 +0100 Subject: [PATCH 1/2] Fix caller() returning incorrect frames in interpreter mode The interpreter's caller() function was returning incorrect stack frames in two scenarios: 1. For regular subroutine calls (main calls outer() calls inner()), caller() returned no frames because all BytecodeInterpreter.execute frames were consecutive and the previous fix treated them as one call. 2. For use/require with import(), the stack order was wrong - the CallerStack entry (from parseUseDeclaration) was being added after interpreter frames instead of at the correct position. Fix: Use InterpretedCode.apply as the boundary marker. Each apply() call marks the END of a Perl subroutine execution. Multiple execute() frames within an apply() share one interpreter frame. Key changes to ExceptionFormatter.formatException(): - Track addedFrameForCurrentLevel flag - Only add one interpreter frame per call level - When we see InterpretedCode.apply, increment to next frame index - For innermost frame (index 0), use runtime currentPackage to reflect package declarations that executed at runtime Now both test cases work correctly: - Regular sub calls: caller(0)=main, caller(1)=main, depth=1 - use/import: caller(0)=NestedInstance, caller(1)=main Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- .../runtimetypes/ExceptionFormatter.java | 27 +++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/ExceptionFormatter.java b/src/main/java/org/perlonjava/runtime/runtimetypes/ExceptionFormatter.java index ba36471a8..5ad9d28a7 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/ExceptionFormatter.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/ExceptionFormatter.java @@ -56,7 +56,13 @@ private static ArrayList> formatThrowable(Throwable t) { // level; consuming them in order gives the correct nested call stack. var interpreterFrames = InterpreterState.getStack(); var interpreterPcs = InterpreterState.getPcStack(); + // Start at index 0 - caller() will skip this (the current function) int interpreterFrameIndex = 0; + + // Track whether we've added a frame for the current Perl call level. + // Multiple execute() frames can occur for the same call level (for internal ops). + // InterpretedCode.apply marks the END of a call level, so we reset after seeing it. + boolean addedFrameForCurrentLevel = false; for (var element : t.getStackTrace()) { if (element.getClassName().equals("org.perlonjava.frontend.parser.StatementParser") && @@ -73,12 +79,18 @@ private static ArrayList> formatThrowable(Throwable t) { lastFileName = callerInfo.filename() != null ? callerInfo.filename() : ""; callerStackIndex++; } + } else if (element.getClassName().equals("org.perlonjava.backend.bytecode.InterpretedCode") && + element.getMethodName().equals("apply")) { + // InterpretedCode.apply marks the END of a Perl call level. + // After this, the next execute frame starts a new call level. + if (addedFrameForCurrentLevel) { + interpreterFrameIndex++; + addedFrameForCurrentLevel = false; + } } else if (element.getClassName().equals("org.perlonjava.backend.bytecode.BytecodeInterpreter") && element.getMethodName().equals("execute")) { - // Consume the next interpreter frame in order. - // Using current() always returned the same topmost frame; consuming - // in order correctly maps each JVM execute() frame to its Perl level. - if (interpreterFrameIndex < interpreterFrames.size()) { + // Only add an entry for the current Perl call level once + if (!addedFrameForCurrentLevel && interpreterFrameIndex < interpreterFrames.size()) { var frame = interpreterFrames.get(interpreterFrameIndex); if (frame != null && frame.code() != null) { // For the innermost frame (index 0), use the runtime current package @@ -87,8 +99,6 @@ private static ArrayList> formatThrowable(Throwable t) { String pkg = (interpreterFrameIndex == 0) ? InterpreterState.currentPackage.get().toString() : frame.packageName(); - int currentInterpreterFrameIndex = interpreterFrameIndex; - interpreterFrameIndex++; String subName = frame.subroutineName(); if (subName != null && !subName.isEmpty() && !subName.contains("::")) { @@ -99,9 +109,9 @@ private static ArrayList> formatThrowable(Throwable t) { entry.add(pkg); String filename = frame.code().sourceName; String line = String.valueOf(frame.code().sourceLine); - if (currentInterpreterFrameIndex < interpreterPcs.size()) { + if (interpreterFrameIndex < interpreterPcs.size()) { Integer tokenIndex = null; - int pc = interpreterPcs.get(currentInterpreterFrameIndex); + int pc = interpreterPcs.get(interpreterFrameIndex); if (frame.code().pcToTokenIndex != null && !frame.code().pcToTokenIndex.isEmpty()) { var entryPc = frame.code().pcToTokenIndex.floorEntry(pc); if (entryPc != null) { @@ -119,6 +129,7 @@ private static ArrayList> formatThrowable(Throwable t) { entry.add(subName); stackTrace.add(entry); lastFileName = filename != null ? filename : ""; + addedFrameForCurrentLevel = true; } } } else if (element.getClassName().contains("org.perlonjava.anon") || From 5506fe2d31c7a9885547fc82bd05d8aac27a2e98 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Fri, 13 Mar 2026 13:22:32 +0100 Subject: [PATCH 2/2] Support local with method chain lvalues in interpreter Extend interpreter to handle `local $obj->method->{key}` syntax: - BytecodeCompiler.java: Accept any BinaryOperatorNode as a general fallback for local operands, matching JVM backend behavior - CompileAssignment.java: Handle any BinaryOperatorNode for local assignment expressions like `local $obj->method->{key} = value` Both changes align with EmitOperatorLocal.java approach: compile the lvalue expression and call pushLocalVariable on the result. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- .../org/perlonjava/backend/bytecode/BytecodeCompiler.java | 6 +++--- .../org/perlonjava/backend/bytecode/CompileAssignment.java | 6 ++++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java index 1a4d69ca6..442ed885e 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java @@ -3413,9 +3413,9 @@ void compileVariableDeclaration(OperatorNode node, String op) { lastResultReg = rd; return; } - // local $hash{key} or local $array[index] - localize a hash/array element - if (node.operand instanceof BinaryOperatorNode binOp - && (binOp.operator.equals("{") || binOp.operator.equals("["))) { + // General fallback for any lvalue expression (matches JVM backend behavior) + // Handles: local $hash{key}, local $array[index], local $obj->method->{key}, etc. + if (node.operand instanceof BinaryOperatorNode binOp) { compileNode(binOp, -1, RuntimeContextType.SCALAR); int elemReg = lastResultReg; emit(Opcodes.PUSH_LOCAL_VARIABLE); diff --git a/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java b/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java index 90bfd0060..afaeb4f22 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java @@ -14,8 +14,10 @@ public class CompileAssignment { private static boolean handleLocalAssignment(BytecodeCompiler bc, BinaryOperatorNode node, OperatorNode leftOp, int rhsContext) { if (!leftOp.operator.equals("local")) return false; Node localOperand = leftOp.operand; - if (localOperand instanceof BinaryOperatorNode hashAccess && hashAccess.operator.equals("{")) { - bc.compileNode(hashAccess, -1, rhsContext); + // General fallback for any BinaryOperatorNode lvalue (matches JVM backend behavior) + // Handles: local $hash{key} = v, local $array[i] = v, local $obj->method->{key} = v, etc. + if (localOperand instanceof BinaryOperatorNode binOp) { + bc.compileNode(binOp, -1, rhsContext); int elemReg = bc.lastResultReg; bc.emit(Opcodes.PUSH_LOCAL_VARIABLE); bc.emitReg(elemReg);