diff --git a/dev/tools/perl_test_runner.pl b/dev/tools/perl_test_runner.pl index 98fe02ada..d659bb839 100755 --- a/dev/tools/perl_test_runner.pl +++ b/dev/tools/perl_test_runner.pl @@ -251,7 +251,9 @@ sub run_single_test { | op/sprintf.t | base/lex.t }x ? "warn" : ""; - local $ENV{JPERL_LARGECODE} = $test_file =~ m{opbasic/concat\.t$} + local $ENV{JPERL_LARGECODE} = $test_file =~ m{ + opbasic/concat\.t$ + | op/pack\.t$ }x ? "refactor" : ""; local $ENV{JPERL_OPTS} = $test_file =~ m{ re/pat.t diff --git a/src/main/java/org/perlonjava/codegen/EmitBinaryOperator.java b/src/main/java/org/perlonjava/codegen/EmitBinaryOperator.java index bcbf4eab5..d20b06c29 100644 --- a/src/main/java/org/perlonjava/codegen/EmitBinaryOperator.java +++ b/src/main/java/org/perlonjava/codegen/EmitBinaryOperator.java @@ -15,7 +15,7 @@ import static org.perlonjava.codegen.EmitOperator.emitOperator; public class EmitBinaryOperator { - static final boolean ENABLE_SPILL_BINARY_LHS = System.getenv("JPERL_NO_SPILL_BINARY_LHS") == null; + static final boolean ENABLE_SPILL_BINARY_LHS = true; static void handleBinaryOperator(EmitterVisitor emitterVisitor, BinaryOperatorNode node, OperatorHandler operatorHandler) { EmitterVisitor scalarVisitor = @@ -181,25 +181,20 @@ static void handleBinaryOperator(EmitterVisitor emitterVisitor, BinaryOperatorNo } MethodVisitor mv = emitterVisitor.ctx.mv; - if (ENABLE_SPILL_BINARY_LHS) { - node.left.accept(scalarVisitor); // left parameter - int leftSlot = emitterVisitor.ctx.javaClassInfo.acquireSpillSlot(); - boolean pooled = leftSlot >= 0; - if (!pooled) { - leftSlot = emitterVisitor.ctx.symbolTable.allocateLocalVariable(); - } - mv.visitVarInsn(Opcodes.ASTORE, leftSlot); + node.left.accept(scalarVisitor); // left parameter + int leftSlot = emitterVisitor.ctx.javaClassInfo.acquireSpillSlot(); + boolean pooled = leftSlot >= 0; + if (!pooled) { + leftSlot = emitterVisitor.ctx.symbolTable.allocateLocalVariable(); + } + mv.visitVarInsn(Opcodes.ASTORE, leftSlot); - right.accept(scalarVisitor); // right parameter + right.accept(scalarVisitor); // right parameter - mv.visitVarInsn(Opcodes.ALOAD, leftSlot); - mv.visitInsn(Opcodes.SWAP); - if (pooled) { - emitterVisitor.ctx.javaClassInfo.releaseSpillSlot(); - } - } else { - node.left.accept(scalarVisitor); // left parameter - right.accept(scalarVisitor); // right parameter + mv.visitVarInsn(Opcodes.ALOAD, leftSlot); + mv.visitInsn(Opcodes.SWAP); + if (pooled) { + emitterVisitor.ctx.javaClassInfo.releaseSpillSlot(); } // stack: [left, right] emitOperator(node, emitterVisitor); @@ -210,37 +205,31 @@ static void handleCompoundAssignment(EmitterVisitor emitterVisitor, BinaryOperat EmitterVisitor scalarVisitor = emitterVisitor.with(RuntimeContextType.SCALAR); // execute operands in scalar context MethodVisitor mv = emitterVisitor.ctx.mv; - if (ENABLE_SPILL_BINARY_LHS) { - node.left.accept(scalarVisitor); // target - left parameter - int leftSlot = emitterVisitor.ctx.javaClassInfo.acquireSpillSlot(); - boolean pooledLeft = leftSlot >= 0; - if (!pooledLeft) { - leftSlot = emitterVisitor.ctx.symbolTable.allocateLocalVariable(); - } - mv.visitVarInsn(Opcodes.ASTORE, leftSlot); + node.left.accept(scalarVisitor); // target - left parameter + int leftSlot = emitterVisitor.ctx.javaClassInfo.acquireSpillSlot(); + boolean pooledLeft = leftSlot >= 0; + if (!pooledLeft) { + leftSlot = emitterVisitor.ctx.symbolTable.allocateLocalVariable(); + } + mv.visitVarInsn(Opcodes.ASTORE, leftSlot); - node.right.accept(scalarVisitor); // right parameter - int rightSlot = emitterVisitor.ctx.javaClassInfo.acquireSpillSlot(); - boolean pooledRight = rightSlot >= 0; - if (!pooledRight) { - rightSlot = emitterVisitor.ctx.symbolTable.allocateLocalVariable(); - } - mv.visitVarInsn(Opcodes.ASTORE, rightSlot); + node.right.accept(scalarVisitor); // right parameter + int rightSlot = emitterVisitor.ctx.javaClassInfo.acquireSpillSlot(); + boolean pooledRight = rightSlot >= 0; + if (!pooledRight) { + rightSlot = emitterVisitor.ctx.symbolTable.allocateLocalVariable(); + } + mv.visitVarInsn(Opcodes.ASTORE, rightSlot); - mv.visitVarInsn(Opcodes.ALOAD, leftSlot); - mv.visitInsn(Opcodes.DUP); - mv.visitVarInsn(Opcodes.ALOAD, rightSlot); + mv.visitVarInsn(Opcodes.ALOAD, leftSlot); + mv.visitInsn(Opcodes.DUP); + mv.visitVarInsn(Opcodes.ALOAD, rightSlot); - if (pooledRight) { - emitterVisitor.ctx.javaClassInfo.releaseSpillSlot(); - } - if (pooledLeft) { - emitterVisitor.ctx.javaClassInfo.releaseSpillSlot(); - } - } else { - node.left.accept(scalarVisitor); // target - left parameter - mv.visitInsn(Opcodes.DUP); - node.right.accept(scalarVisitor); // right parameter + if (pooledRight) { + emitterVisitor.ctx.javaClassInfo.releaseSpillSlot(); + } + if (pooledLeft) { + emitterVisitor.ctx.javaClassInfo.releaseSpillSlot(); } // perform the operation String baseOperator = node.operator.substring(0, node.operator.length() - 1); diff --git a/src/main/java/org/perlonjava/codegen/EmitControlFlow.java b/src/main/java/org/perlonjava/codegen/EmitControlFlow.java index 01ff365e0..e878bd1e2 100644 --- a/src/main/java/org/perlonjava/codegen/EmitControlFlow.java +++ b/src/main/java/org/perlonjava/codegen/EmitControlFlow.java @@ -11,6 +11,7 @@ import org.perlonjava.runtime.ControlFlowType; import org.perlonjava.runtime.PerlCompilerException; import org.perlonjava.runtime.RuntimeContextType; +import org.perlonjava.runtime.RuntimeScalarType; /** * Handles the emission of control flow bytecode instructions for Perl-like language constructs. @@ -186,6 +187,7 @@ static void handleReturnOperator(EmitterVisitor emitterVisitor, OperatorNode nod node.operand.accept(emitterVisitor.with(RuntimeContextType.RUNTIME)); } + ctx.mv.visitVarInsn(Opcodes.ASTORE, ctx.javaClassInfo.returnValueSlot); ctx.mv.visitJumpInsn(Opcodes.GOTO, ctx.javaClassInfo.returnLabel); } @@ -252,6 +254,7 @@ static void handleGotoSubroutine(EmitterVisitor emitterVisitor, OperatorNode sub } // Jump to returnLabel (trampoline will handle it) + ctx.mv.visitVarInsn(Opcodes.ASTORE, ctx.javaClassInfo.returnValueSlot); ctx.mv.visitJumpInsn(Opcodes.GOTO, ctx.javaClassInfo.returnLabel); } @@ -297,38 +300,87 @@ static void handleGotoLabel(EmitterVisitor emitterVisitor, OperatorNode node) { // Evaluate the expression to get the label name at runtime arg.accept(emitterVisitor.with(RuntimeContextType.SCALAR)); - + + int targetSlot = ctx.javaClassInfo.acquireSpillSlot(); + boolean pooledTarget = targetSlot >= 0; + if (!pooledTarget) { + targetSlot = ctx.symbolTable.allocateLocalVariable(); + } + ctx.mv.visitVarInsn(Opcodes.ASTORE, targetSlot); + + // If EXPR evaluates to a CODE reference, treat it like `goto &NAME` (tail call). + Label notCodeRef = new Label(); + ctx.mv.visitVarInsn(Opcodes.ALOAD, targetSlot); + ctx.mv.visitFieldInsn(Opcodes.GETFIELD, + "org/perlonjava/runtime/RuntimeScalar", + "type", + "I"); + ctx.mv.visitLdcInsn(RuntimeScalarType.CODE); + ctx.mv.visitJumpInsn(Opcodes.IF_ICMPNE, notCodeRef); + + // Build a TAILCALL marker with the coderef and the current @_ array. + ctx.mv.visitTypeInsn(Opcodes.NEW, "org/perlonjava/runtime/RuntimeControlFlowList"); + ctx.mv.visitInsn(Opcodes.DUP); + ctx.mv.visitVarInsn(Opcodes.ALOAD, targetSlot); + ctx.mv.visitVarInsn(Opcodes.ALOAD, 1); // current @_ + ctx.mv.visitLdcInsn(ctx.compilerOptions.fileName != null ? ctx.compilerOptions.fileName : "(eval)"); + int tailLineNumber = ctx.errorUtil != null ? ctx.errorUtil.getLineNumber(node.tokenIndex) : 0; + ctx.mv.visitLdcInsn(tailLineNumber); + ctx.mv.visitMethodInsn(Opcodes.INVOKESPECIAL, + "org/perlonjava/runtime/RuntimeControlFlowList", + "", + "(Lorg/perlonjava/runtime/RuntimeScalar;Lorg/perlonjava/runtime/RuntimeArray;Ljava/lang/String;I)V", + false); + + ctx.javaClassInfo.resetStackLevel(); // Clean up stack before jumping + + if (pooledTarget) { + ctx.javaClassInfo.releaseSpillSlot(); + } + + ctx.mv.visitVarInsn(Opcodes.ASTORE, ctx.javaClassInfo.returnValueSlot); + ctx.mv.visitJumpInsn(Opcodes.GOTO, ctx.javaClassInfo.returnLabel); + + + // Otherwise, treat it as a computed label name (dynamic goto). + ctx.mv.visitLabel(notCodeRef); + + ctx.mv.visitVarInsn(Opcodes.ALOAD, targetSlot); // Convert to string (label name) ctx.mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "org/perlonjava/runtime/RuntimeScalar", "toString", "()Ljava/lang/String;", false); - - // For dynamic goto, we always create a RuntimeControlFlowList - // because we can't know at compile-time if the label is local or not + + // For dynamic goto, create a RuntimeControlFlowList marker + // because we can't know at compile-time if the label is local or not. ctx.mv.visitTypeInsn(Opcodes.NEW, "org/perlonjava/runtime/RuntimeControlFlowList"); ctx.mv.visitInsn(Opcodes.DUP_X1); // Stack: label, RuntimeControlFlowList, RuntimeControlFlowList ctx.mv.visitInsn(Opcodes.SWAP); // Stack: label, RuntimeControlFlowList, label, RuntimeControlFlowList - + ctx.mv.visitFieldInsn(Opcodes.GETSTATIC, "org/perlonjava/runtime/ControlFlowType", "GOTO", "Lorg/perlonjava/runtime/ControlFlowType;"); ctx.mv.visitInsn(Opcodes.SWAP); // Stack: ..., ControlFlowType, label - + // Push fileName ctx.mv.visitLdcInsn(ctx.compilerOptions.fileName != null ? ctx.compilerOptions.fileName : "(eval)"); // Push lineNumber int lineNumber = ctx.errorUtil != null ? ctx.errorUtil.getLineNumber(node.tokenIndex) : 0; ctx.mv.visitLdcInsn(lineNumber); - + ctx.mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "org/perlonjava/runtime/RuntimeControlFlowList", "", "(Lorg/perlonjava/runtime/ControlFlowType;Ljava/lang/String;Ljava/lang/String;I)V", false); + if (pooledTarget) { + ctx.javaClassInfo.releaseSpillSlot(); + } + int markerSlot = ctx.javaClassInfo.acquireSpillSlot(); boolean pooledMarker = markerSlot >= 0; if (!pooledMarker) { @@ -342,6 +394,7 @@ static void handleGotoLabel(EmitterVisitor emitterVisitor, OperatorNode node) { if (pooledMarker) { ctx.javaClassInfo.releaseSpillSlot(); } + ctx.mv.visitVarInsn(Opcodes.ASTORE, ctx.javaClassInfo.returnValueSlot); ctx.mv.visitJumpInsn(Opcodes.GOTO, ctx.javaClassInfo.returnLabel); return; } @@ -390,6 +443,7 @@ static void handleGotoLabel(EmitterVisitor emitterVisitor, OperatorNode node) { if (pooledMarker) { ctx.javaClassInfo.releaseSpillSlot(); } + ctx.mv.visitVarInsn(Opcodes.ASTORE, ctx.javaClassInfo.returnValueSlot); ctx.mv.visitJumpInsn(Opcodes.GOTO, ctx.javaClassInfo.returnLabel); return; } diff --git a/src/main/java/org/perlonjava/codegen/EmitEval.java b/src/main/java/org/perlonjava/codegen/EmitEval.java index 2b1e0f47a..b380f92a2 100644 --- a/src/main/java/org/perlonjava/codegen/EmitEval.java +++ b/src/main/java/org/perlonjava/codegen/EmitEval.java @@ -166,6 +166,15 @@ static void handleEvalOperator(EmitterVisitor emitterVisitor, OperatorNode node) false); } + // Wrap the runtime compile/instantiate/apply path so eval catches syntax errors + // thrown by RuntimeCode.evalStringHelper() (and any reflection errors). + Label tryStart = new Label(); + Label tryEnd = new Label(); + Label catchBlock = new Label(); + Label endCatch = new Label(); + mv.visitTryCatchBlock(tryStart, tryEnd, catchBlock, "java/lang/Throwable"); + mv.visitLabel(tryStart); + // Push the evalTag that links to the saved context mv.visitLdcInsn(evalTag); // Stack: [RuntimeScalar(String), String] @@ -275,12 +284,40 @@ static void handleEvalOperator(EmitterVisitor emitterVisitor, OperatorNode node) false); // Stack: [RuntimeList] + mv.visitLabel(tryEnd); + mv.visitJumpInsn(Opcodes.GOTO, endCatch); + + // Catch any exception from evalStringHelper / reflection / applyEval + mv.visitLabel(catchBlock); + mv.visitMethodInsn(Opcodes.INVOKESTATIC, + "org/perlonjava/operators/WarnDie", + "catchEval", + "(Ljava/lang/Throwable;)Lorg/perlonjava/runtime/RuntimeScalar;", + false); + mv.visitInsn(Opcodes.POP); + if (emitterVisitor.ctx.contextType == RuntimeContextType.LIST) { + mv.visitTypeInsn(Opcodes.NEW, "org/perlonjava/runtime/RuntimeList"); + mv.visitInsn(Opcodes.DUP); + mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "org/perlonjava/runtime/RuntimeList", "", "()V", false); + } else { + mv.visitTypeInsn(Opcodes.NEW, "org/perlonjava/runtime/RuntimeList"); + mv.visitInsn(Opcodes.DUP); + mv.visitTypeInsn(Opcodes.NEW, "org/perlonjava/runtime/RuntimeScalar"); + mv.visitInsn(Opcodes.DUP); + mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "org/perlonjava/runtime/RuntimeScalar", "", "()V", false); + mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "org/perlonjava/runtime/RuntimeList", "", "(Lorg/perlonjava/runtime/RuntimeScalar;)V", false); + } + + int evalResultSlot = emitterVisitor.ctx.symbolTable.allocateLocalVariable(); + mv.visitLabel(endCatch); + mv.visitVarInsn(Opcodes.ASTORE, evalResultSlot); + // If eval returned a non-local control flow marker (next/last/redo), // it must apply to the enclosing scope, matching Perl semantics. // We translate it into a local jump to the appropriate loop/block label. Label evalNoControlFlow = new Label(); Label evalNotNextLastRedo = new Label(); - mv.visitInsn(Opcodes.DUP); + mv.visitVarInsn(Opcodes.ALOAD, evalResultSlot); mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "org/perlonjava/runtime/RuntimeList", "isNonLocalGoto", @@ -288,6 +325,8 @@ static void handleEvalOperator(EmitterVisitor emitterVisitor, OperatorNode node) false); mv.visitJumpInsn(Opcodes.IFEQ, evalNoControlFlow); + mv.visitVarInsn(Opcodes.ALOAD, evalResultSlot); + int cfSlot = emitterVisitor.ctx.symbolTable.allocateLocalVariable(); mv.visitTypeInsn(Opcodes.CHECKCAST, "org/perlonjava/runtime/RuntimeControlFlowList"); mv.visitVarInsn(Opcodes.ASTORE, cfSlot); @@ -317,11 +356,49 @@ static void handleEvalOperator(EmitterVisitor emitterVisitor, OperatorNode node) false); mv.visitVarInsn(Opcodes.ISTORE, typeSlot); - // If this is not NEXT/LAST/REDO (ordinals 0..2), keep it as a normal value. - // (e.g. GOTO/TAILCALL are not handled here) + // If this is not NEXT/LAST/REDO (ordinals 0..2), treat it as an eval error. + // In particular, bad goto inside eval must set $@ and return undef/empty list, + // and must NOT escape as a marked RuntimeList (which would terminate the script). + Label evalIsNextLastRedo = new Label(); mv.visitVarInsn(Opcodes.ILOAD, typeSlot); mv.visitInsn(Opcodes.ICONST_2); - mv.visitJumpInsn(Opcodes.IF_ICMPGT, evalNotNextLastRedo); + mv.visitJumpInsn(Opcodes.IF_ICMPLE, evalIsNextLastRedo); + + // Set $@ = marker.buildErrorMessage() + mv.visitLdcInsn("main::@"); + mv.visitVarInsn(Opcodes.ALOAD, cfSlot); + mv.visitFieldInsn(Opcodes.GETFIELD, + "org/perlonjava/runtime/RuntimeControlFlowList", + "marker", + "Lorg/perlonjava/runtime/ControlFlowMarker;"); + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, + "org/perlonjava/runtime/ControlFlowMarker", + "buildErrorMessage", + "()Ljava/lang/String;", + false); + mv.visitMethodInsn(Opcodes.INVOKESTATIC, + "org/perlonjava/runtime/GlobalVariable", + "setGlobalVariable", + "(Ljava/lang/String;Ljava/lang/String;)V", + false); + + // Return undef/empty list from eval on error + if (emitterVisitor.ctx.contextType == RuntimeContextType.LIST) { + mv.visitTypeInsn(Opcodes.NEW, "org/perlonjava/runtime/RuntimeList"); + mv.visitInsn(Opcodes.DUP); + mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "org/perlonjava/runtime/RuntimeList", "", "()V", false); + } else { + mv.visitTypeInsn(Opcodes.NEW, "org/perlonjava/runtime/RuntimeList"); + mv.visitInsn(Opcodes.DUP); + mv.visitTypeInsn(Opcodes.NEW, "org/perlonjava/runtime/RuntimeScalar"); + mv.visitInsn(Opcodes.DUP); + mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "org/perlonjava/runtime/RuntimeScalar", "", "()V", false); + mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "org/perlonjava/runtime/RuntimeList", "", "(Lorg/perlonjava/runtime/RuntimeScalar;)V", false); + } + mv.visitVarInsn(Opcodes.ASTORE, evalResultSlot); + mv.visitJumpInsn(Opcodes.GOTO, evalNoControlFlow); + + mv.visitLabel(evalIsNextLastRedo); // 1) Labeled control flow: compare against each enclosing loop/block label Label checkUnlabeled = new Label(); @@ -400,6 +477,7 @@ static void handleEvalOperator(EmitterVisitor emitterVisitor, OperatorNode node) mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "org/perlonjava/runtime/RuntimeScalar", "", "()V", false); mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "org/perlonjava/runtime/RuntimeList", "", "(Lorg/perlonjava/runtime/RuntimeScalar;)V", false); } + mv.visitVarInsn(Opcodes.ASTORE, evalResultSlot); mv.visitJumpInsn(Opcodes.GOTO, evalNoControlFlow); // 2) Unlabeled control flow: target the innermost true loop @@ -422,10 +500,60 @@ static void handleEvalOperator(EmitterVisitor emitterVisitor, OperatorNode node) mv.visitJumpInsn(Opcodes.GOTO, evalNotNextLastRedo); mv.visitLabel(isLast); - mv.visitJumpInsn(Opcodes.GOTO, unlabeledTarget.lastLabel); + mv.visitVarInsn(Opcodes.ALOAD, cfSlot); + mv.visitFieldInsn(Opcodes.GETFIELD, + "org/perlonjava/runtime/RuntimeControlFlowList", + "marker", + "Lorg/perlonjava/runtime/ControlFlowMarker;"); + mv.visitMethodInsn(Opcodes.INVOKESTATIC, + "org/perlonjava/runtime/RuntimeControlFlowRegistry", + "register", + "(Lorg/perlonjava/runtime/ControlFlowMarker;)V", + false); + + // Return undef/empty list from eval after registering control flow + if (emitterVisitor.ctx.contextType == RuntimeContextType.LIST) { + mv.visitTypeInsn(Opcodes.NEW, "org/perlonjava/runtime/RuntimeList"); + mv.visitInsn(Opcodes.DUP); + mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "org/perlonjava/runtime/RuntimeList", "", "()V", false); + } else { + mv.visitTypeInsn(Opcodes.NEW, "org/perlonjava/runtime/RuntimeList"); + mv.visitInsn(Opcodes.DUP); + mv.visitTypeInsn(Opcodes.NEW, "org/perlonjava/runtime/RuntimeScalar"); + mv.visitInsn(Opcodes.DUP); + mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "org/perlonjava/runtime/RuntimeScalar", "", "()V", false); + mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "org/perlonjava/runtime/RuntimeList", "", "(Lorg/perlonjava/runtime/RuntimeScalar;)V", false); + } + mv.visitVarInsn(Opcodes.ASTORE, evalResultSlot); + mv.visitJumpInsn(Opcodes.GOTO, evalNoControlFlow); mv.visitLabel(isNext); - mv.visitJumpInsn(Opcodes.GOTO, unlabeledTarget.nextLabel); + mv.visitVarInsn(Opcodes.ALOAD, cfSlot); + mv.visitFieldInsn(Opcodes.GETFIELD, + "org/perlonjava/runtime/RuntimeControlFlowList", + "marker", + "Lorg/perlonjava/runtime/ControlFlowMarker;"); + mv.visitMethodInsn(Opcodes.INVOKESTATIC, + "org/perlonjava/runtime/RuntimeControlFlowRegistry", + "register", + "(Lorg/perlonjava/runtime/ControlFlowMarker;)V", + false); + + // Return undef/empty list from eval after registering control flow + if (emitterVisitor.ctx.contextType == RuntimeContextType.LIST) { + mv.visitTypeInsn(Opcodes.NEW, "org/perlonjava/runtime/RuntimeList"); + mv.visitInsn(Opcodes.DUP); + mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "org/perlonjava/runtime/RuntimeList", "", "()V", false); + } else { + mv.visitTypeInsn(Opcodes.NEW, "org/perlonjava/runtime/RuntimeList"); + mv.visitInsn(Opcodes.DUP); + mv.visitTypeInsn(Opcodes.NEW, "org/perlonjava/runtime/RuntimeScalar"); + mv.visitInsn(Opcodes.DUP); + mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "org/perlonjava/runtime/RuntimeScalar", "", "()V", false); + mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "org/perlonjava/runtime/RuntimeList", "", "(Lorg/perlonjava/runtime/RuntimeScalar;)V", false); + } + mv.visitVarInsn(Opcodes.ASTORE, evalResultSlot); + mv.visitJumpInsn(Opcodes.GOTO, evalNoControlFlow); mv.visitLabel(isRedo); mv.visitJumpInsn(Opcodes.GOTO, unlabeledTarget.redoLabel); @@ -460,15 +588,20 @@ static void handleEvalOperator(EmitterVisitor emitterVisitor, OperatorNode node) mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "org/perlonjava/runtime/RuntimeScalar", "", "()V", false); mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "org/perlonjava/runtime/RuntimeList", "", "(Lorg/perlonjava/runtime/RuntimeScalar;)V", false); } + mv.visitVarInsn(Opcodes.ASTORE, evalResultSlot); mv.visitJumpInsn(Opcodes.GOTO, evalNoControlFlow); } - // Fallthrough for non NEXT/LAST/REDO control flow markers: treat as normal value + // Fallthrough for non NEXT/LAST/REDO control flow markers is handled above as an eval error. mv.visitLabel(evalNotNextLastRedo); mv.visitVarInsn(Opcodes.ALOAD, cfSlot); + mv.visitVarInsn(Opcodes.ASTORE, evalResultSlot); + mv.visitLabel(evalNoControlFlow); + mv.visitVarInsn(Opcodes.ALOAD, evalResultSlot); + // Convert result based on calling context if (emitterVisitor.ctx.contextType == RuntimeContextType.SCALAR) { // In scalar context, extract the first element diff --git a/src/main/java/org/perlonjava/codegen/EmitLiteral.java b/src/main/java/org/perlonjava/codegen/EmitLiteral.java index 0e9c99437..2efa080e9 100644 --- a/src/main/java/org/perlonjava/codegen/EmitLiteral.java +++ b/src/main/java/org/perlonjava/codegen/EmitLiteral.java @@ -70,46 +70,28 @@ public static void emitArrayLiteral(EmitterVisitor emitterVisitor, ArrayLiteralN mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "org/perlonjava/runtime/RuntimeArray", "", "()V", false); // Stack: [RuntimeArray] - JavaClassInfo.SpillRef arrayRef = null; - arrayRef = emitterVisitor.ctx.javaClassInfo.tryAcquirePooledSpillRef(); - if (arrayRef != null) { - emitterVisitor.ctx.javaClassInfo.storeSpillRef(mv, arrayRef); - } - // Stack: [] (if arrayRef != null) else [RuntimeArray] + JavaClassInfo.SpillRef arrayRef = emitterVisitor.ctx.javaClassInfo.acquireSpillRefOrAllocate(emitterVisitor.ctx.symbolTable); + emitterVisitor.ctx.javaClassInfo.storeSpillRef(mv, arrayRef); + // Stack: [] // Populate the array with elements for (Node element : node.elements) { // Generate code for the element in LIST context - if (arrayRef != null) { - element.accept(elementContext); - JavaClassInfo.SpillRef elementRef = emitterVisitor.ctx.javaClassInfo.acquireSpillRefOrAllocate(emitterVisitor.ctx.symbolTable); - emitterVisitor.ctx.javaClassInfo.storeSpillRef(mv, elementRef); - - emitterVisitor.ctx.javaClassInfo.loadSpillRef(mv, arrayRef); - emitterVisitor.ctx.javaClassInfo.loadSpillRef(mv, elementRef); - emitterVisitor.ctx.javaClassInfo.releaseSpillRef(elementRef); - - // Add the element to the array - addElementToArray(mv, element); - // Stack: [] - } else { - mv.visitInsn(Opcodes.DUP); - // Stack: [RuntimeArray] + element.accept(elementContext); + JavaClassInfo.SpillRef elementRef = emitterVisitor.ctx.javaClassInfo.acquireSpillRefOrAllocate(emitterVisitor.ctx.symbolTable); + emitterVisitor.ctx.javaClassInfo.storeSpillRef(mv, elementRef); - emitterVisitor.ctx.javaClassInfo.incrementStackLevel(1); - element.accept(elementContext); - emitterVisitor.ctx.javaClassInfo.decrementStackLevel(1); + emitterVisitor.ctx.javaClassInfo.loadSpillRef(mv, arrayRef); + emitterVisitor.ctx.javaClassInfo.loadSpillRef(mv, elementRef); + emitterVisitor.ctx.javaClassInfo.releaseSpillRef(elementRef); - // Add the element to the array - addElementToArray(mv, element); - // Stack: [RuntimeArray] - } + // Add the element to the array + addElementToArray(mv, element); + // Stack: [] } - if (arrayRef != null) { - emitterVisitor.ctx.javaClassInfo.loadSpillRef(mv, arrayRef); - emitterVisitor.ctx.javaClassInfo.releaseSpillRef(arrayRef); - } + emitterVisitor.ctx.javaClassInfo.loadSpillRef(mv, arrayRef); + emitterVisitor.ctx.javaClassInfo.releaseSpillRef(arrayRef); // Convert the array to a reference (array literals produce references) mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "org/perlonjava/runtime/RuntimeBase", diff --git a/src/main/java/org/perlonjava/codegen/EmitOperator.java b/src/main/java/org/perlonjava/codegen/EmitOperator.java index a914761e3..bd503c543 100644 --- a/src/main/java/org/perlonjava/codegen/EmitOperator.java +++ b/src/main/java/org/perlonjava/codegen/EmitOperator.java @@ -20,7 +20,7 @@ */ public class EmitOperator { - private static final boolean ENABLE_SPILL_BINARY_LHS = System.getenv("JPERL_NO_SPILL_BINARY_LHS") == null; + private static final boolean ENABLE_SPILL_BINARY_LHS = true; static void emitOperator(Node node, EmitterVisitor emitterVisitor) { // Extract operator string from the node diff --git a/src/main/java/org/perlonjava/codegen/EmitRegex.java b/src/main/java/org/perlonjava/codegen/EmitRegex.java index 61774db65..e2a8981dd 100644 --- a/src/main/java/org/perlonjava/codegen/EmitRegex.java +++ b/src/main/java/org/perlonjava/codegen/EmitRegex.java @@ -237,8 +237,6 @@ static void handleQuoteRegex(EmitterVisitor emitterVisitor, OperatorNode node) { if (emitterVisitor.ctx.contextType == RuntimeContextType.VOID) { emitterVisitor.ctx.mv.visitInsn(Opcodes.POP); - } else { - emitterVisitor.ctx.javaClassInfo.incrementStackLevel(1); } } @@ -290,15 +288,16 @@ private static void emitMatchRegex(EmitterVisitor emitterVisitor) { "org/perlonjava/regex/RuntimeRegex", "matchRegex", "(Lorg/perlonjava/runtime/RuntimeScalar;Lorg/perlonjava/runtime/RuntimeScalar;I)Lorg/perlonjava/runtime/RuntimeBase;", false); - emitterVisitor.ctx.javaClassInfo.incrementStackLevel(1); + if (emitterVisitor.ctx.contextType == RuntimeContextType.VOID) { + // Discard result if in void context + emitterVisitor.ctx.mv.visitInsn(Opcodes.POP); + return; + } // Handle the result based on context type if (emitterVisitor.ctx.contextType == RuntimeContextType.SCALAR) { // Convert result to Scalar if in scalar context emitterVisitor.ctx.mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "org/perlonjava/runtime/RuntimeBase", "scalar", "()Lorg/perlonjava/runtime/RuntimeScalar;", false); - } else if (emitterVisitor.ctx.contextType == RuntimeContextType.VOID) { - // Discard result if in void context - emitterVisitor.ctx.mv.visitInsn(Opcodes.POP); } } diff --git a/src/main/java/org/perlonjava/codegen/EmitSubroutine.java b/src/main/java/org/perlonjava/codegen/EmitSubroutine.java index b94f332b1..1476ba75f 100644 --- a/src/main/java/org/perlonjava/codegen/EmitSubroutine.java +++ b/src/main/java/org/perlonjava/codegen/EmitSubroutine.java @@ -365,8 +365,6 @@ static void handleApplyOperator(EmitterVisitor emitterVisitor, BinaryOperatorNod "(Lorg/perlonjava/runtime/RuntimeScalar;Ljava/lang/String;[Lorg/perlonjava/runtime/RuntimeBase;I)Lorg/perlonjava/runtime/RuntimeList;", false); // Generate an .apply() call - emitterVisitor.ctx.javaClassInfo.incrementStackLevel(1); - if (pooledArgsArray) { emitterVisitor.ctx.javaClassInfo.releaseSpillSlot(); } @@ -510,6 +508,7 @@ static void handleApplyOperator(EmitterVisitor emitterVisitor, BinaryOperatorNod } } mv.visitVarInsn(Opcodes.ALOAD, emitterVisitor.ctx.javaClassInfo.controlFlowTempSlot); + mv.visitVarInsn(Opcodes.ASTORE, emitterVisitor.ctx.javaClassInfo.returnValueSlot); mv.visitJumpInsn(Opcodes.GOTO, emitterVisitor.ctx.javaClassInfo.returnLabel); // Not a control flow marker - load it back and continue @@ -520,11 +519,7 @@ static void handleApplyOperator(EmitterVisitor emitterVisitor, BinaryOperatorNod emitterVisitor.ctx.javaClassInfo.releaseSpillRef(ref); } } - if (belowResultStackLevel > 0) { - emitterVisitor.ctx.javaClassInfo.incrementStackLevel(belowResultStackLevel); - } mv.visitVarInsn(Opcodes.ALOAD, emitterVisitor.ctx.javaClassInfo.controlFlowTempSlot); - emitterVisitor.ctx.javaClassInfo.incrementStackLevel(1); } if (emitterVisitor.ctx.contextType == RuntimeContextType.SCALAR) { diff --git a/src/main/java/org/perlonjava/codegen/EmitVariable.java b/src/main/java/org/perlonjava/codegen/EmitVariable.java index c29ea7a19..a77c90802 100644 --- a/src/main/java/org/perlonjava/codegen/EmitVariable.java +++ b/src/main/java/org/perlonjava/codegen/EmitVariable.java @@ -506,8 +506,6 @@ static void handleVariableOperator(EmitterVisitor emitterVisitor, OperatorNode n "(Lorg/perlonjava/runtime/RuntimeScalar;Lorg/perlonjava/runtime/RuntimeArray;I)Lorg/perlonjava/runtime/RuntimeList;", false); // generate an .apply() call - emitterVisitor.ctx.javaClassInfo.incrementStackLevel(1); - // RuntimeCode.apply() can return a tagged RuntimeControlFlowList (last/next/redo). // Handle it before context conversion (especially before POP in VOID context). Label applyNoControlFlow = new Label(); @@ -598,6 +596,7 @@ static void handleVariableOperator(EmitterVisitor emitterVisitor, OperatorNode n // No labeled target matched: propagate via returnLabel if available. if (emitterVisitor.ctx.javaClassInfo.returnLabel != null) { mv.visitVarInsn(Opcodes.ALOAD, cfSlot); + mv.visitVarInsn(Opcodes.ASTORE, emitterVisitor.ctx.javaClassInfo.returnValueSlot); mv.visitJumpInsn(Opcodes.GOTO, emitterVisitor.ctx.javaClassInfo.returnLabel); } @@ -628,6 +627,7 @@ static void handleVariableOperator(EmitterVisitor emitterVisitor, OperatorNode n mv.visitJumpInsn(Opcodes.GOTO, unlabeledTarget.redoLabel); } else if (emitterVisitor.ctx.javaClassInfo.returnLabel != null) { mv.visitVarInsn(Opcodes.ALOAD, cfSlot); + mv.visitVarInsn(Opcodes.ASTORE, emitterVisitor.ctx.javaClassInfo.returnValueSlot); mv.visitJumpInsn(Opcodes.GOTO, emitterVisitor.ctx.javaClassInfo.returnLabel); } diff --git a/src/main/java/org/perlonjava/codegen/EmitterMethodCreator.java b/src/main/java/org/perlonjava/codegen/EmitterMethodCreator.java index 38d4f16e1..a93f221fc 100644 --- a/src/main/java/org/perlonjava/codegen/EmitterMethodCreator.java +++ b/src/main/java/org/perlonjava/codegen/EmitterMethodCreator.java @@ -622,6 +622,24 @@ private static byte[] getBytecodeInternal(EmitterContext ctx, Node ast, boolean // Setup local variables and environment for the method Local.localRecord localRecord = Local.localSetup(ctx, ast, mv); + // Store the computed RuntimeList return value in a dedicated local slot. + // This keeps the operand stack empty at join labels (endCatch), avoiding + // inconsistent stack map frames when multiple control-flow paths merge. + int returnListSlot = ctx.symbolTable.allocateLocalVariable(); + + // Spill the raw RuntimeBase return value for stack-neutral joins at returnLabel. + // Any path that jumps to returnLabel must arrive with an empty operand stack. + int returnValueSlot = ctx.symbolTable.allocateLocalVariable(); + ctx.javaClassInfo.returnValueSlot = returnValueSlot; + mv.visitInsn(Opcodes.ACONST_NULL); + mv.visitVarInsn(Opcodes.ASTORE, returnValueSlot); + + // Labels for eval-block try/catch wrapping (used only when useTryCatch=true) + Label tryStart = null; + Label tryEnd = null; + Label catchBlock = null; + Label endCatch = null; + if (useTryCatch) { ctx.logDebug("useTryCatch"); @@ -629,10 +647,10 @@ private static byte[] getBytecodeInternal(EmitterContext ctx, Node ast, boolean // Start of try-catch block // -------------------------------- - Label tryStart = new Label(); - Label tryEnd = new Label(); - Label catchBlock = new Label(); - Label endCatch = new Label(); + tryStart = new Label(); + tryEnd = new Label(); + catchBlock = new Label(); + endCatch = new Label(); // Define the try-catch block mv.visitTryCatchBlock(tryStart, tryEnd, catchBlock, "java/lang/Throwable"); @@ -652,47 +670,48 @@ private static byte[] getBytecodeInternal(EmitterContext ctx, Node ast, boolean ast.accept(visitor); + // Normal fallthrough return: spill and jump with empty operand stack. + mv.visitVarInsn(Opcodes.ASTORE, returnValueSlot); + mv.visitJumpInsn(Opcodes.GOTO, ctx.javaClassInfo.returnLabel); + // Handle the return value ctx.logDebug("Return the last value"); - mv.visitLabel(ctx.javaClassInfo.returnLabel); // "return" from other places arrive here + // -------------------------------- // End of the try block // -------------------------------- - mv.visitLabel(tryEnd); - - // Jump over the catch block if no exception occurs - mv.visitJumpInsn(Opcodes.GOTO, endCatch); - - // Start of the catch block - mv.visitLabel(catchBlock); - - // The throwable object is on the stack - // Catch the throwable - mv.visitMethodInsn(Opcodes.INVOKESTATIC, - "org/perlonjava/operators/WarnDie", - "catchEval", - "(Ljava/lang/Throwable;)Lorg/perlonjava/runtime/RuntimeScalar;", false); - - // End of the catch block - mv.visitLabel(endCatch); + // NOTE: We intentionally delay tryEnd/endCatch labels until after the + // return-value materialization and trampoline checks below. + // This ensures eval BLOCK catches control-flow marker errors raised + // during epilogue processing (e.g. bad goto), instead of escaping and + // terminating top-level execution. // -------------------------------- - // End of try-catch block + // End of try-catch block is emitted AFTER the epilogue/trampoline. // -------------------------------- } else { // No try-catch block is used ast.accept(visitor); + // Normal fallthrough return: spill and jump with empty operand stack. + mv.visitVarInsn(Opcodes.ASTORE, returnValueSlot); + mv.visitJumpInsn(Opcodes.GOTO, ctx.javaClassInfo.returnLabel); + // Handle the return value ctx.logDebug("Return the last value"); - mv.visitLabel(ctx.javaClassInfo.returnLabel); // "return" from other places arrive here } - // Transform the value in the stack to RuntimeList BEFORE local teardown - // This ensures that array/hash elements are expanded before local variables are restored + // Join point for all returns/gotos. Must be stack-neutral. + mv.visitLabel(ctx.javaClassInfo.returnLabel); + mv.visitVarInsn(Opcodes.ALOAD, returnValueSlot); + + // Transform the value in the stack to RuntimeList BEFORE local teardown. + // Materialize it into a local slot immediately so all subsequent control-flow + // checks operate from locals and join points don't depend on operand stack shape. mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "org/perlonjava/runtime/RuntimeBase", "getList", "()Lorg/perlonjava/runtime/RuntimeList;", false); + mv.visitVarInsn(Opcodes.ASTORE, returnListSlot); // Phase 3: Check for control flow markers // RuntimeList is on stack after getList() @@ -703,6 +722,7 @@ private static byte[] getBytecodeInternal(EmitterContext ctx, Node ast, boolean Label notTailcall = new Label(); Label normalReturn = new Label(); + mv.visitVarInsn(Opcodes.ALOAD, returnListSlot); mv.visitInsn(Opcodes.DUP); // Duplicate for checking mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "org/perlonjava/runtime/RuntimeList", @@ -855,16 +875,143 @@ private static byte[] getBytecodeInternal(EmitterContext ctx, Node ast, boolean false); mv.visitInsn(Opcodes.ICONST_4); mv.visitJumpInsn(Opcodes.IF_ICMPEQ, tailcallLoop); // Loop if still TAILCALL - // Otherwise fall through to normalReturn (propagate other control flow) - // Not TAILCALL: check if we're inside a loop and should jump to loop handler mv.visitLabel(notTailcall); + if (useTryCatch) { + // For eval BLOCK, any marked non-TAILCALL result is an eval failure. + // Stack here: [RuntimeControlFlowList] + int msgSlot = ctx.symbolTable.allocateLocalVariable(); + + // msg = marker.buildErrorMessage() + mv.visitInsn(Opcodes.DUP); + mv.visitFieldInsn(Opcodes.GETFIELD, + "org/perlonjava/runtime/RuntimeControlFlowList", + "marker", + "Lorg/perlonjava/runtime/ControlFlowMarker;"); + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, + "org/perlonjava/runtime/ControlFlowMarker", + "buildErrorMessage", + "()Ljava/lang/String;", + false); + mv.visitVarInsn(Opcodes.ASTORE, msgSlot); + + // $@ = msg + mv.visitLdcInsn("main::@"); + mv.visitVarInsn(Opcodes.ALOAD, msgSlot); + mv.visitMethodInsn(Opcodes.INVOKESTATIC, + "org/perlonjava/runtime/GlobalVariable", + "setGlobalVariable", + "(Ljava/lang/String;Ljava/lang/String;)V", + false); + + // Replace marker with undef/empty list + mv.visitInsn(Opcodes.POP); + Label evalBlockList = new Label(); + Label evalBlockDone = new Label(); + mv.visitVarInsn(Opcodes.ILOAD, 2); + mv.visitInsn(Opcodes.ICONST_2); // RuntimeContextType.LIST + mv.visitJumpInsn(Opcodes.IF_ICMPEQ, evalBlockList); + + mv.visitTypeInsn(Opcodes.NEW, "org/perlonjava/runtime/RuntimeList"); + mv.visitInsn(Opcodes.DUP); + mv.visitTypeInsn(Opcodes.NEW, "org/perlonjava/runtime/RuntimeScalar"); + mv.visitInsn(Opcodes.DUP); + mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "org/perlonjava/runtime/RuntimeScalar", "", "()V", false); + mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "org/perlonjava/runtime/RuntimeList", "", "(Lorg/perlonjava/runtime/RuntimeScalar;)V", false); + mv.visitJumpInsn(Opcodes.GOTO, evalBlockDone); + + mv.visitLabel(evalBlockList); + mv.visitTypeInsn(Opcodes.NEW, "org/perlonjava/runtime/RuntimeList"); + mv.visitInsn(Opcodes.DUP); + mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "org/perlonjava/runtime/RuntimeList", "", "()V", false); + mv.visitLabel(evalBlockDone); + + // Materialize return value in local slot and jump to endCatch with empty stack. + mv.visitVarInsn(Opcodes.ASTORE, returnListSlot); + + // Skip the success epilogue that clears $@. + // This path represents an eval failure (bad goto/other marker), + // so $@ must be preserved. + mv.visitJumpInsn(Opcodes.GOTO, endCatch); + } // TODO: Check ctx.javaClassInfo loop stack, if non-empty, jump to innermost loop handler // For now, just propagate (return to caller) // Normal return mv.visitLabel(normalReturn); + + // The RuntimeList is currently on stack when coming from the trampoline checks. + // When jumping here from the initial isNonLocalGoto check, we need to reload it. + // To normalize both paths, store any on-stack value and then reload from the slot. + mv.visitVarInsn(Opcodes.ASTORE, returnListSlot); } // End of if (ENABLE_TAILCALL_TRAMPOLINE) + + if (useTryCatch) { + // -------------------------------- + // End of the try block (includes epilogue/trampoline) + // -------------------------------- + mv.visitLabel(tryEnd); + + // Clear $@ on successful completion of eval (nested evals may have set it). + mv.visitLdcInsn("main::@"); + mv.visitLdcInsn(""); + mv.visitMethodInsn(Opcodes.INVOKESTATIC, + "org/perlonjava/runtime/GlobalVariable", + "setGlobalVariable", + "(Ljava/lang/String;Ljava/lang/String;)V", false); + + // Jump over the catch block if no exception occurs + mv.visitJumpInsn(Opcodes.GOTO, endCatch); + + // Start of the catch block + mv.visitLabel(catchBlock); + + // The throwable object is on the stack + // Catch the throwable + mv.visitMethodInsn(Opcodes.INVOKESTATIC, + "org/perlonjava/operators/WarnDie", + "catchEval", + "(Ljava/lang/Throwable;)Lorg/perlonjava/runtime/RuntimeScalar;", false); + + // Discard catchEval() return value; it only sets $@ + mv.visitInsn(Opcodes.POP); + + // Return undef/empty list from eval on error. + Label evalCatchList = new Label(); + Label evalCatchDone = new Label(); + mv.visitVarInsn(Opcodes.ILOAD, 2); + mv.visitInsn(Opcodes.ICONST_2); // RuntimeContextType.LIST + mv.visitJumpInsn(Opcodes.IF_ICMPEQ, evalCatchList); + + // Scalar/void: RuntimeList(new RuntimeScalar()) + mv.visitTypeInsn(Opcodes.NEW, "org/perlonjava/runtime/RuntimeList"); + mv.visitInsn(Opcodes.DUP); + mv.visitTypeInsn(Opcodes.NEW, "org/perlonjava/runtime/RuntimeScalar"); + mv.visitInsn(Opcodes.DUP); + mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "org/perlonjava/runtime/RuntimeScalar", "", "()V", false); + mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "org/perlonjava/runtime/RuntimeList", "", "(Lorg/perlonjava/runtime/RuntimeScalar;)V", false); + mv.visitJumpInsn(Opcodes.GOTO, evalCatchDone); + + // List: new RuntimeList() + mv.visitLabel(evalCatchList); + mv.visitTypeInsn(Opcodes.NEW, "org/perlonjava/runtime/RuntimeList"); + mv.visitInsn(Opcodes.DUP); + mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "org/perlonjava/runtime/RuntimeList", "", "()V", false); + mv.visitLabel(evalCatchDone); + + // Materialize return value in local slot. + mv.visitVarInsn(Opcodes.ASTORE, returnListSlot); + + // End of the catch block + mv.visitLabel(endCatch); + + // Load the return value for the method epilogue. + mv.visitVarInsn(Opcodes.ALOAD, returnListSlot); + } else { + // No try/catch: ensure the method epilogue sees the return value + // on the operand stack. + mv.visitVarInsn(Opcodes.ALOAD, returnListSlot); + } // Teardown local variables and environment after the return value is materialized Local.localTeardown(localRecord, mv); diff --git a/src/main/java/org/perlonjava/codegen/JavaClassInfo.java b/src/main/java/org/perlonjava/codegen/JavaClassInfo.java index e29219ba4..a1fe0b723 100644 --- a/src/main/java/org/perlonjava/codegen/JavaClassInfo.java +++ b/src/main/java/org/perlonjava/codegen/JavaClassInfo.java @@ -24,6 +24,15 @@ public class JavaClassInfo { * The label to return to after method execution. */ public Label returnLabel; + + /** + * Local variable slot used to spill the value returned by `return`, `goto` markers, + * and other non-local control-flow paths before jumping to {@link #returnLabel}. + * + * The {@link #returnLabel} join point must be stack-neutral: no incoming edge may + * rely on operand stack contents. + */ + public int returnValueSlot; /** * Local variable slot for tail call trampoline - stores codeRef. @@ -74,6 +83,7 @@ public SpillRef(int slot, boolean pooled) { public JavaClassInfo() { this.javaClassName = EmitterMethodCreator.generateClassName(); this.returnLabel = null; + this.returnValueSlot = -1; this.stackLevelManager = new StackLevelManager(); this.loopLabelStack = new ArrayDeque<>(); this.gotoLabelStack = new ArrayDeque<>(); @@ -218,18 +228,13 @@ public LoopLabels findLoopLabelsByName(String labelName) { } /** - * Finds the innermost "true" loop labels. - * This skips pseudo-loops like bare/labeled blocks (e.g. SKIP: { ... }) that - * may be present on the loop label stack to support redo/last/next on blocks. - * - * For unlabeled next/last/redo, Perl semantics target the nearest enclosing - * true loop, not a labeled block used for SKIP. + * Finds the innermost LoopLabels that is a true loop (for/while/until). * * @return the innermost LoopLabels with isTrueLoop=true, or null if none */ public LoopLabels findInnermostTrueLoopLabels() { for (LoopLabels loopLabels : loopLabelStack) { - if (loopLabels != null && loopLabels.isUnlabeledControlFlowTarget) { + if (loopLabels != null && loopLabels.isTrueLoop && loopLabels.isUnlabeledControlFlowTarget) { return loopLabels; } } @@ -253,24 +258,6 @@ public void popGotoLabels() { gotoLabelStack.pop(); } - /** - * Increments the stack level by a specified amount. - * - * @param level the amount to increment the stack level by - */ - public void incrementStackLevel(int level) { - stackLevelManager.increment(level); - } - - /** - * Decrements the stack level by a specified amount. - * - * @param level the amount to decrement the stack level by - */ - public void decrementStackLevel(int level) { - stackLevelManager.decrement(level); - } - /** * Resets the stack level to its initial state. */ diff --git a/src/main/java/org/perlonjava/codegen/StackLevelManager.java b/src/main/java/org/perlonjava/codegen/StackLevelManager.java index e56a6aa19..c0536bf85 100644 --- a/src/main/java/org/perlonjava/codegen/StackLevelManager.java +++ b/src/main/java/org/perlonjava/codegen/StackLevelManager.java @@ -28,28 +28,6 @@ public int getStackLevel() { return stackLevel; } - /** - * Increments the stack level by the specified amount. - * - * @param level the amount to increment the stack level by. - */ - public void increment(int level) { - stackLevel += level; - } - - /** - * Decrements the stack level by the specified amount. If the resulting - * stack level is negative, it is reset to zero to maintain a valid state. - * - * @param level the amount to decrement the stack level by. - */ - public void decrement(int level) { - stackLevel -= level; - if (stackLevel < 0) { - stackLevel = 0; - } - } - /** * Resets the stack level to zero. */ diff --git a/src/main/java/org/perlonjava/operators/FileTestOperator.java b/src/main/java/org/perlonjava/operators/FileTestOperator.java index d91a1927b..a2e2baf8a 100644 --- a/src/main/java/org/perlonjava/operators/FileTestOperator.java +++ b/src/main/java/org/perlonjava/operators/FileTestOperator.java @@ -130,9 +130,18 @@ private static boolean statForFileTest(RuntimeScalar arg, Path path, boolean lst BasicFileAttributes basicAttr = lstat ? Files.readAttributes(path, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS) : Files.readAttributes(path, BasicFileAttributes.class); - PosixFileAttributes posixAttr = lstat - ? Files.readAttributes(path, PosixFileAttributes.class, LinkOption.NOFOLLOW_LINKS) - : Files.readAttributes(path, PosixFileAttributes.class); + + // POSIX attributes are not available on all platforms (e.g. Windows). + // Perl filetest operators like -e/-f/-d only need the basic attributes. + PosixFileAttributes posixAttr = null; + try { + posixAttr = lstat + ? Files.readAttributes(path, PosixFileAttributes.class, LinkOption.NOFOLLOW_LINKS) + : Files.readAttributes(path, PosixFileAttributes.class); + } catch (UnsupportedOperationException | IOException ignored) { + // Leave posixAttr as null. + } + lastBasicAttr = basicAttr; lastPosixAttr = posixAttr; getGlobalVariable("main::!").set(0); @@ -142,7 +151,7 @@ private static boolean statForFileTest(RuntimeScalar arg, Path path, boolean lst getGlobalVariable("main::!").set(2); updateLastStat(arg, false, 2, lstat); return false; - } catch (IOException e) { + } catch (IOException | UnsupportedOperationException e) { getGlobalVariable("main::!").set(5); updateLastStat(arg, false, 5, lstat); return false; diff --git a/src/main/java/org/perlonjava/operators/WarnDie.java b/src/main/java/org/perlonjava/operators/WarnDie.java index a85942c43..b585e2634 100644 --- a/src/main/java/org/perlonjava/operators/WarnDie.java +++ b/src/main/java/org/perlonjava/operators/WarnDie.java @@ -22,6 +22,9 @@ private static Throwable unwrapException(Throwable throwable) { Throwable cause = current.getCause(); // Stop unwrapping if we find a meaningful exception + if (cause instanceof PerlDieException pde) { + return pde; + } if (cause instanceof PerlCompilerException pc) { return pc; } @@ -35,18 +38,38 @@ private static Throwable unwrapException(Throwable throwable) { */ public static RuntimeScalar catchEval(Throwable e) { e = unwrapException(e); - if (e instanceof PerlCompilerException && getGlobalVariable("main::@").getBoolean()) { - // $@ is already set + RuntimeScalar err = getGlobalVariable("main::@"); - // System.out.println("catchEval :" + e); - // System.out.println(" :" + getGlobalVariable("main::@")); + if (e instanceof PerlDieException pde) { + RuntimeBase payload = pde.getPayload(); + if (payload != null) { + err.set(payload.getFirst()); + } + // die() already invokes $SIG{__DIE__} (when defined). Perl's eval + // should not invoke it again while catching the exception. return scalarUndef; + } else { + if (!(e instanceof PerlCompilerException) || !err.getBoolean()) { + err.set(new RuntimeScalar(ErrorMessageUtil.stringifyException(e))); + } } - // System.out.println("catchEval : not PerlCompilerException: " + e); - // e.printStackTrace(); + RuntimeScalar sig = getGlobalHash("main::SIG").get("__DIE__"); + if (sig.getDefinedBoolean()) { + RuntimeArray args = new RuntimeArray(); + RuntimeArray.push(args, new RuntimeScalar(err)); + + RuntimeScalar sigHandler = new RuntimeScalar(sig); + + // Undefine $SIG{__DIE__} before calling the handler to avoid infinite recursion + int level = DynamicVariableManager.getLocalLevel(); + DynamicVariableManager.pushLocalVariable(sig); + + RuntimeCode.apply(sigHandler, args, RuntimeContextType.SCALAR); - getGlobalVariable("main::@").set(new RuntimeScalar(ErrorMessageUtil.stringifyException(e))); + // Restore $SIG{__DIE__} + DynamicVariableManager.popToLocalLevel(level); + } return scalarUndef; } @@ -187,10 +210,10 @@ public static RuntimeBase die(RuntimeBase message, RuntimeScalar where, String f // Restore $SIG{__DIE__} DynamicVariableManager.popToLocalLevel(level); - return res; + throw new PerlDieException(errVariable); } - throw new PerlCompilerException(errVariable.toString()); + throw new PerlDieException(errVariable); } private static RuntimeBase dieEmptyMessage(RuntimeScalar oldErr, String fileName, int lineNumber) { diff --git a/src/main/java/org/perlonjava/parser/OperatorParser.java b/src/main/java/org/perlonjava/parser/OperatorParser.java index a823a9c38..0bc3845e6 100644 --- a/src/main/java/org/perlonjava/parser/OperatorParser.java +++ b/src/main/java/org/perlonjava/parser/OperatorParser.java @@ -71,6 +71,12 @@ static AbstractNode parseEval(Parser parser, String operator) { TokenUtils.consume(parser, LexerTokenType.OPERATOR, "{"); block = ParseBlock.parseBlock(parser); TokenUtils.consume(parser, LexerTokenType.OPERATOR, "}"); + // Perl semantics: eval BLOCK behaves like a bare block for loop control. + // `last/next/redo` inside the eval block must target the eval block itself, + // not escape as non-local control flow. + if (block instanceof BlockNode blockNode) { + blockNode.isLoop = true; + } // transform: eval { 123 } // into: sub { 123 }->() with useTryCatch flag return new BinaryOperatorNode("->", diff --git a/src/main/java/org/perlonjava/runtime/GlobalContext.java b/src/main/java/org/perlonjava/runtime/GlobalContext.java index 663401bf5..d69df6461 100644 --- a/src/main/java/org/perlonjava/runtime/GlobalContext.java +++ b/src/main/java/org/perlonjava/runtime/GlobalContext.java @@ -44,6 +44,8 @@ public static void initializeGlobals(CompilerOptions compilerOptions) { String varName = "main::" + Character.toString(c - 'A' + 1); GlobalVariable.getGlobalVariable(varName); } + // $^N - last capture group closed (not yet implemented, but must be read-only) + GlobalVariable.globalVariables.put(encodeSpecialVar("N"), new RuntimeScalarReadOnly()); GlobalVariable.getGlobalVariable("main::" + Character.toString('O' - 'A' + 1)).set(SystemUtils.getPerlOsName()); // initialize $^O GlobalVariable.getGlobalVariable("main::" + Character.toString('V' - 'A' + 1)).set(Configuration.getPerlVersionVString()); // initialize $^V GlobalVariable.getGlobalVariable("main::" + Character.toString('T' - 'A' + 1)).set((int)(System.currentTimeMillis() / 1000)); // initialize $^T to epoch time @@ -108,8 +110,12 @@ public static void initializeGlobals(CompilerOptions compilerOptions) { GlobalVariable.getGlobalVariable(encodeSpecialVar("SAFE_LOCALES")); // TODO // Initialize arrays - GlobalVariable.getGlobalArray("main::+").elements = new ArraySpecialVariable(ArraySpecialVariable.Id.LAST_MATCH_END); // regex @+ - GlobalVariable.getGlobalArray("main::-").elements = new ArraySpecialVariable(ArraySpecialVariable.Id.LAST_MATCH_START); // regex @- + RuntimeArray matchEnd = GlobalVariable.getGlobalArray("main::+"); + matchEnd.type = RuntimeArray.READONLY_ARRAY; + matchEnd.elements = new ArraySpecialVariable(ArraySpecialVariable.Id.LAST_MATCH_END); // regex @+ + RuntimeArray matchStart = GlobalVariable.getGlobalArray("main::-"); + matchStart.type = RuntimeArray.READONLY_ARRAY; + matchStart.elements = new ArraySpecialVariable(ArraySpecialVariable.Id.LAST_MATCH_START); // regex @- GlobalVariable.getGlobalArray(encodeSpecialVar("CAPTURE")).elements = new ArraySpecialVariable(ArraySpecialVariable.Id.CAPTURE); // regex @{^CAPTURE} GlobalVariable.getGlobalArray("main::'"); // @' diff --git a/src/main/java/org/perlonjava/runtime/PerlDieException.java b/src/main/java/org/perlonjava/runtime/PerlDieException.java new file mode 100644 index 000000000..60d5c6479 --- /dev/null +++ b/src/main/java/org/perlonjava/runtime/PerlDieException.java @@ -0,0 +1,25 @@ +package org.perlonjava.runtime; + +import java.io.Serial; + +/** + * Exception used to implement Perl's die semantics. + * + * This carries the original die payload (string or reference) so eval can + * propagate it into $@ without stringifying. + */ +public class PerlDieException extends RuntimeException { + @Serial + private static final long serialVersionUID = 1L; + + private final RuntimeBase payload; + + public PerlDieException(RuntimeBase payload) { + super(payload == null ? null : payload.toString()); + this.payload = payload; + } + + public RuntimeBase getPayload() { + return payload; + } +} diff --git a/src/main/java/org/perlonjava/runtime/RuntimeArray.java b/src/main/java/org/perlonjava/runtime/RuntimeArray.java index 2ac4c6922..0341de17e 100644 --- a/src/main/java/org/perlonjava/runtime/RuntimeArray.java +++ b/src/main/java/org/perlonjava/runtime/RuntimeArray.java @@ -100,6 +100,7 @@ public static RuntimeScalar pop(RuntimeArray runtimeArray) { yield pop(runtimeArray); // Recursive call after vivification } case TIED_ARRAY -> TieArray.tiedPop(runtimeArray); + case READONLY_ARRAY -> throw new PerlCompilerException("Modification of a read-only value attempted"); default -> throw new IllegalStateException("Unknown array type: " + runtimeArray.type); }; } @@ -123,6 +124,7 @@ public static RuntimeScalar shift(RuntimeArray runtimeArray) { yield shift(runtimeArray); // Recursive call after vivification } case TIED_ARRAY -> TieArray.tiedShift(runtimeArray); + case READONLY_ARRAY -> throw new PerlCompilerException("Modification of a read-only value attempted"); default -> throw new IllegalStateException("Unknown array type: " + runtimeArray.type); }; } @@ -158,6 +160,7 @@ public static RuntimeScalar push(RuntimeArray runtimeArray, RuntimeBase value) { yield push(runtimeArray, value); } case TIED_ARRAY -> TieArray.tiedPush(runtimeArray, value); + case READONLY_ARRAY -> throw new PerlCompilerException("Modification of a read-only value attempted"); default -> throw new IllegalStateException("Unknown array type: " + runtimeArray.type); }; } @@ -297,6 +300,16 @@ public RuntimeScalar exists(int index) { } case AUTOVIVIFY_ARRAY -> scalarFalse; case TIED_ARRAY -> TieArray.tiedExists(this, getScalarInt(index)); + case READONLY_ARRAY -> { + if (index < 0) { + index = elements.size() + index; // Handle negative indices + } + if (index < 0 || index >= elements.size()) { + yield scalarFalse; + } + RuntimeScalar element = elements.get(index); + yield (element == null) ? scalarFalse : scalarTrue; + } default -> throw new IllegalStateException("Unknown array type: " + type); }; } @@ -329,6 +342,7 @@ public RuntimeScalar delete(int index) { } case AUTOVIVIFY_ARRAY -> scalarUndef; case TIED_ARRAY -> TieArray.tiedDelete(this, getScalarInt(index)); + case READONLY_ARRAY -> throw new PerlCompilerException("Modification of a read-only value attempted"); default -> throw new IllegalStateException("Unknown array type: " + type); }; } @@ -442,6 +456,9 @@ public RuntimeScalar get(RuntimeScalar value) { * @return The updated RuntimeArray. */ public RuntimeArray set(RuntimeScalar value) { + if (this.type == READONLY_ARRAY) { + throw new PerlCompilerException("Modification of a read-only value attempted"); + } this.elements.clear(); this.elements.add(value); return this; @@ -516,6 +533,7 @@ public RuntimeArray setFromList(RuntimeList list) { } yield this; } + case READONLY_ARRAY -> throw new PerlCompilerException("Modification of a read-only value attempted"); default -> throw new IllegalStateException("Unknown array type: " + type); }; } @@ -597,6 +615,12 @@ public RuntimeScalar scalar() { case AUTOVIVIFY_ARRAY -> throw new PerlCompilerException("Can't use an undefined value as an ARRAY reference"); case TIED_ARRAY -> TieArray.tiedFetchSize(this); + case READONLY_ARRAY -> { + if (scalarContextSize != null) { + yield getScalarInt(scalarContextSize); + } + yield getScalarInt(elements.size()); + } default -> throw new IllegalStateException("Unknown array type: " + type); }; } @@ -608,6 +632,7 @@ public int lastElementIndex() { case AUTOVIVIFY_ARRAY -> throw new PerlCompilerException("Can't use an undefined value as an ARRAY reference"); case TIED_ARRAY -> TieArray.tiedFetchSize(this).getInt() - 1; + case READONLY_ARRAY -> elements.size() - 1; default -> throw new IllegalStateException("Unknown array type: " + type); }; } @@ -636,6 +661,7 @@ public void setLastElementIndex(RuntimeScalar value) { setLastElementIndex(value); } case TIED_ARRAY -> TieArray.tiedStoreSize(this, new RuntimeScalar(value.getInt() + 1)); + case READONLY_ARRAY -> throw new PerlCompilerException("Modification of a read-only value attempted"); default -> throw new IllegalStateException("Unknown array type: " + type); } } diff --git a/src/main/java/org/perlonjava/runtime/RuntimeArrayProxyEntry.java b/src/main/java/org/perlonjava/runtime/RuntimeArrayProxyEntry.java index 403cd5513..7c237921b 100644 --- a/src/main/java/org/perlonjava/runtime/RuntimeArrayProxyEntry.java +++ b/src/main/java/org/perlonjava/runtime/RuntimeArrayProxyEntry.java @@ -37,6 +37,9 @@ public RuntimeArrayProxyEntry(RuntimeArray parent, int key) { */ void vivify() { if (lvalue == null) { + if (parent.type == RuntimeArray.READONLY_ARRAY) { + throw new PerlCompilerException("Modification of a read-only value attempted"); + } lvalue = new RuntimeScalar(); if (parent.type == RuntimeArray.AUTOVIVIFY_ARRAY) { diff --git a/src/main/java/org/perlonjava/runtime/RuntimeCode.java b/src/main/java/org/perlonjava/runtime/RuntimeCode.java index f6e151be4..ca21f33bb 100644 --- a/src/main/java/org/perlonjava/runtime/RuntimeCode.java +++ b/src/main/java/org/perlonjava/runtime/RuntimeCode.java @@ -236,7 +236,7 @@ public static Class evalStringHelper(RuntimeScalar code, String evalTag) thro // Process the string source code to create the LexerToken list Lexer lexer = new Lexer(evalString); List tokens = lexer.tokenize(); // Tokenize the Perl code - Node ast; + Node ast = null; Class generatedClass; try { // Create the AST @@ -263,7 +263,7 @@ public static Class evalStringHelper(RuntimeScalar code, String evalTag) thro generatedClass = EmitterMethodCreator.createClassWithMethod( evalCtx, ast, - true // use try-catch + false // use try-catch ); runUnitcheckBlocks(ctx.unitcheckBlocks); } catch (Throwable e) { @@ -272,16 +272,9 @@ public static Class evalStringHelper(RuntimeScalar code, String evalTag) thro // Set the global error variable "$@" using GlobalContext.setGlobalVariable(key, value) GlobalVariable.getGlobalVariable("main::@").set(e.getMessage()); - // In case of error return an "undef" ast and class - ast = new OperatorNode("undef", null, 1); - evalCtx.errorUtil = new ErrorMessageUtil(ctx.compilerOptions.fileName, tokens); - evalCtx.symbolTable = capturedSymbolTable; - setCurrentScope(evalCtx.symbolTable); - generatedClass = EmitterMethodCreator.createClassWithMethod( - evalCtx, - ast, - false - ); + // Rethrow so applyEval() can return undef/empty list as appropriate and avoid + // incorrectly treating this as a successful eval. + throw new PerlCompilerException(e.getMessage()); } finally { // Restore caller lexical flags (do not leak eval pragmas). capturedSymbolTable.warningFlagsStack.pop(); @@ -294,6 +287,11 @@ public static Class evalStringHelper(RuntimeScalar code, String evalTag) thro capturedSymbolTable.strictOptionsStack.push(savedStrictOptions); setCurrentScope(capturedSymbolTable); + + // Store source lines in symbol table if $^P flags are set + // Do this on both success and failure paths when flags require retention + // Use the original evalString and actualFileName; AST may be null on failure + storeSourceLines(evalString, actualFileName, ast, tokens); } // Cache the result (unless debugging is enabled) @@ -303,9 +301,6 @@ public static Class evalStringHelper(RuntimeScalar code, String evalTag) thro } } - // Store source lines in symbol table if $^P flags are set - storeSourceLines(evalString, actualFileName, ast); - return generatedClass; } @@ -314,9 +309,10 @@ public static Class evalStringHelper(RuntimeScalar code, String evalTag) thro * * @param evalString The source code string to store * @param filename The filename (e.g., "(eval 1)") - * @param ast The AST to check for subroutine definitions + * @param ast The AST to check for subroutine definitions (may be null on compilation failure) + * @param tokens Lexer tokens for #line directive processing */ - private static void storeSourceLines(String evalString, String filename, Node ast) { + private static void storeSourceLines(String evalString, String filename, Node ast, List tokens) { // Check $^P for debugger flags int debugFlags = GlobalVariable.getGlobalVariable(GlobalContext.encodeSpecialVar("P")).getInt(); // 0x02 (2): Line-by-line debugging (also saves source like 0x400) @@ -364,6 +360,56 @@ private static void storeSourceLines(String evalString, String filename, Node as // Index n+2: ";" sourceArray.elements.add(new RuntimeScalar(";")); + + // Process #line directives to populate @{"_ tokens) { + String currentFilename = null; + int currentLineOffset = 0; // 0-based index into lines array + + for (int i = 0; i < lines.length; i++) { + String line = lines[i]; + // Simple #line directive parsing: #line N "filename" + // Allow optional leading whitespace + java.util.regex.Matcher m = java.util.regex.Pattern.compile("^\\s*#line\\s+(\\d+)\\s+\"([^\"]+)\"").matcher(line); + if (m.find()) { + int targetLine = Integer.parseInt(m.group(1)); // 1-based line number in target file + currentFilename = m.group(2); + currentLineOffset = i + 1; // Next line in eval corresponds to targetLine + // Ensure the target array exists and is properly sized + String targetKey = "main::_<" + currentFilename; + RuntimeArray targetArray = GlobalVariable.getGlobalArray(targetKey); + // Ensure array is large enough (sparse behavior) + while (targetArray.elements.size() <= targetLine) { + targetArray.elements.add(RuntimeScalarCache.scalarUndef); + } + // Place the next line at the correct index + if (i + 1 < lines.length) { + targetArray.elements.set(targetLine, new RuntimeScalar(lines[i + 1] + "\n")); + } + } else if (currentFilename != null && i >= currentLineOffset) { + // Continue populating the current filename array + int targetLine = (i - currentLineOffset) + 1; // Convert to 1-based + String targetKey = "main::_<" + currentFilename; + RuntimeArray targetArray = GlobalVariable.getGlobalArray(targetKey); + // Ensure array is large enough (sparse behavior) + while (targetArray.elements.size() <= targetLine) { + targetArray.elements.add(RuntimeScalarCache.scalarUndef); + } + targetArray.elements.set(targetLine, new RuntimeScalar(line + "\n")); + } } } @@ -707,10 +753,28 @@ public static RuntimeList apply(RuntimeScalar runtimeScalar, RuntimeArray a, int public static RuntimeList applyEval(RuntimeScalar runtimeScalar, RuntimeArray a, int callContext) { try { RuntimeList result = apply(runtimeScalar, a, callContext); + // Perl clears $@ on successful eval (even if nested evals previously set it). + GlobalVariable.setGlobalVariable("main::@", ""); return result; } catch (Throwable t) { // Perl eval catches exceptions; set $@ and return undef / empty list. WarnDie.catchEval(t); + + // If $@ is set and $^P flags require source retention, we may need to retain lines + // for runtime errors (e.g., BEGIN/UNITCHECK die) where storeSourceLines wasn't called. + // Try to extract the eval string from the codeRef if available + String evalString = null; + String filename = null; + if (runtimeScalar.type == RuntimeScalarType.CODE) { + RuntimeCode code = (RuntimeCode) runtimeScalar.value; + // Use the evalString if it was captured in the codeRef + // Note: This is a best-effort fallback; the primary path is evalStringHelper + if (code.packageName != null && code.packageName.startsWith("(eval")) { + filename = code.packageName; + // We cannot reconstruct the exact eval string here, so skip retention + } + } + if (callContext == RuntimeContextType.LIST) { return new RuntimeList(); } diff --git a/src/test/java/org/perlonjava/PerlScriptExecutionTest.java b/src/test/java/org/perlonjava/PerlScriptExecutionTest.java index 234438576..74de27e17 100644 --- a/src/test/java/org/perlonjava/PerlScriptExecutionTest.java +++ b/src/test/java/org/perlonjava/PerlScriptExecutionTest.java @@ -10,6 +10,7 @@ import org.perlonjava.runtime.RuntimeArray; import org.perlonjava.runtime.RuntimeIO; import org.perlonjava.runtime.RuntimeScalar; +import org.perlonjava.runtime.GlobalVariable; import org.perlonjava.scriptengine.PerlLanguageProvider; import java.io.ByteArrayOutputStream; @@ -164,6 +165,10 @@ void setUp() { // Replace RuntimeIO.stdout with a new instance RuntimeIO.stdout = new RuntimeIO(newStdout); + // Keep Perl's global *STDOUT/*STDERR in sync with the RuntimeIO static fields. + // Some tests call `binmode STDOUT/STDERR` and expect it to affect the real globals. + GlobalVariable.getGlobalIO("main::STDOUT").setIO(RuntimeIO.stdout); + GlobalVariable.getGlobalIO("main::STDERR").setIO(RuntimeIO.stderr); // Also update System.out for any direct Java calls System.setOut(new PrintStream(outputStream)); @@ -176,6 +181,8 @@ void setUp() { void tearDown() { // Restore original stdout RuntimeIO.stdout = new RuntimeIO(new StandardIO(originalOut, true)); + GlobalVariable.getGlobalIO("main::STDOUT").setIO(RuntimeIO.stdout); + GlobalVariable.getGlobalIO("main::STDERR").setIO(RuntimeIO.stderr); System.setOut(originalOut); } @@ -278,7 +285,11 @@ private void executeTest(String filename) { Throwable rootCause = getRootCause(e); System.err.println("Root cause error in " + filename + ":"); rootCause.printStackTrace(System.err); - fail("Execution of " + filename + " failed: " + rootCause.getMessage()); + String msg = rootCause.getMessage(); + if (msg == null || msg.isEmpty()) { + msg = rootCause.toString(); + } + fail("Execution of " + filename + " failed: " + msg); } }