From 63b860c483aecf6b624bbc2b58cca9511e6b01cd Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Thu, 29 Jan 2026 19:13:10 +0100 Subject: [PATCH 1/2] Fix eval string control-flow propagation; add unit test --- .../org/perlonjava/codegen/EmitBlock.java | 8 +- .../perlonjava/codegen/EmitControlFlow.java | 9 +- .../java/org/perlonjava/codegen/EmitEval.java | 205 +++++++++++++++++- .../org/perlonjava/codegen/EmitStatement.java | 6 +- .../org/perlonjava/codegen/JavaClassInfo.java | 23 ++ .../org/perlonjava/codegen/LoopLabels.java | 19 +- .../perlonjava/runtime/ControlFlowMarker.java | 16 +- .../org/perlonjava/runtime/RuntimeCode.java | 20 +- .../resources/unit/eval_next_propagation.t | 38 ++++ 9 files changed, 332 insertions(+), 12 deletions(-) create mode 100644 src/test/resources/unit/eval_next_propagation.t diff --git a/src/main/java/org/perlonjava/codegen/EmitBlock.java b/src/main/java/org/perlonjava/codegen/EmitBlock.java index f218bf3c5..d9e4a6da1 100644 --- a/src/main/java/org/perlonjava/codegen/EmitBlock.java +++ b/src/main/java/org/perlonjava/codegen/EmitBlock.java @@ -59,12 +59,18 @@ public static void emitBlock(EmitterVisitor emitterVisitor, BlockNode node) { Local.localTeardown(localRecord, mv); if (node.isLoop) { + // A labeled/bare block used as a loop target (e.g. SKIP: { ... }) is a + // pseudo-loop: it supports labeled next/last/redo (e.g. next SKIP), but + // an unlabeled next/last/redo must target the nearest enclosing true loop. emitterVisitor.ctx.javaClassInfo.pushLoopLabels( node.labelName, nextLabel, redoLabel, nextLabel, - emitterVisitor.ctx.contextType); + emitterVisitor.ctx.javaClassInfo.stackLevelManager.getStackLevel(), + emitterVisitor.ctx.contextType, + false, + false); } // Special case: detect pattern of `local $_` followed by `For1Node` with needsArrayOfAlias diff --git a/src/main/java/org/perlonjava/codegen/EmitControlFlow.java b/src/main/java/org/perlonjava/codegen/EmitControlFlow.java index b411d7211..01ff365e0 100644 --- a/src/main/java/org/perlonjava/codegen/EmitControlFlow.java +++ b/src/main/java/org/perlonjava/codegen/EmitControlFlow.java @@ -52,7 +52,14 @@ static void handleNextOperator(EmitterContext ctx, OperatorNode node) { String operator = node.operator; // Find loop labels by name. - LoopLabels loopLabels = ctx.javaClassInfo.findLoopLabelsByName(labelStr); + LoopLabels loopLabels; + if (labelStr == null) { + // Unlabeled next/last/redo target the nearest enclosing true loop. + // This avoids mis-targeting bare/labeled blocks like SKIP: { ... }. + loopLabels = ctx.javaClassInfo.findInnermostTrueLoopLabels(); + } else { + loopLabels = ctx.javaClassInfo.findLoopLabelsByName(labelStr); + } ctx.logDebug("visit(next) operator: " + operator + " label: " + labelStr + " labels: " + loopLabels); // Check if we're trying to use next/last/redo in a pseudo-loop (do-while/bare block) diff --git a/src/main/java/org/perlonjava/codegen/EmitEval.java b/src/main/java/org/perlonjava/codegen/EmitEval.java index 3492bee61..2b1e0f47a 100644 --- a/src/main/java/org/perlonjava/codegen/EmitEval.java +++ b/src/main/java/org/perlonjava/codegen/EmitEval.java @@ -1,5 +1,6 @@ package org.perlonjava.codegen; +import org.objectweb.asm.Label; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; import org.objectweb.asm.Type; @@ -146,6 +147,14 @@ static void handleEvalOperator(EmitterVisitor emitterVisitor, OperatorNode node) node.operand.accept(emitterVisitor.with(RuntimeContextType.SCALAR)); // Stack: [RuntimeScalar(String)] + // Perl clears $@ at entry to eval/evalbytes, before compilation/execution. + mv.visitLdcInsn("main::@"); + mv.visitLdcInsn(""); + mv.visitMethodInsn(Opcodes.INVOKESTATIC, + "org/perlonjava/runtime/GlobalVariable", + "setGlobalVariable", + "(Ljava/lang/String;Ljava/lang/String;)V", false); + if (node.operator.equals("evalbytes")) { // For evalbytes, verify the string contains valid bytes ctx.mv.visitInsn(Opcodes.DUP); @@ -261,11 +270,205 @@ static void handleEvalOperator(EmitterVisitor emitterVisitor, OperatorNode node) mv.visitMethodInsn( Opcodes.INVOKESTATIC, "org/perlonjava/runtime/RuntimeCode", - "apply", + "applyEval", "(Lorg/perlonjava/runtime/RuntimeScalar;Lorg/perlonjava/runtime/RuntimeArray;I)Lorg/perlonjava/runtime/RuntimeList;", false); // Stack: [RuntimeList] + // 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.visitMethodInsn(Opcodes.INVOKEVIRTUAL, + "org/perlonjava/runtime/RuntimeList", + "isNonLocalGoto", + "()Z", + false); + mv.visitJumpInsn(Opcodes.IFEQ, evalNoControlFlow); + + int cfSlot = emitterVisitor.ctx.symbolTable.allocateLocalVariable(); + mv.visitTypeInsn(Opcodes.CHECKCAST, "org/perlonjava/runtime/RuntimeControlFlowList"); + mv.visitVarInsn(Opcodes.ASTORE, cfSlot); + + // Load label (may be null) + int labelSlot = emitterVisitor.ctx.symbolTable.allocateLocalVariable(); + mv.visitVarInsn(Opcodes.ALOAD, cfSlot); + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, + "org/perlonjava/runtime/RuntimeControlFlowList", + "getControlFlowLabel", + "()Ljava/lang/String;", + false); + mv.visitVarInsn(Opcodes.ASTORE, labelSlot); + + // Load type ordinal + int typeSlot = emitterVisitor.ctx.symbolTable.allocateLocalVariable(); + mv.visitVarInsn(Opcodes.ALOAD, cfSlot); + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, + "org/perlonjava/runtime/RuntimeControlFlowList", + "getControlFlowType", + "()Lorg/perlonjava/runtime/ControlFlowType;", + false); + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, + "org/perlonjava/runtime/ControlFlowType", + "ordinal", + "()I", + 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) + mv.visitVarInsn(Opcodes.ILOAD, typeSlot); + mv.visitInsn(Opcodes.ICONST_2); + mv.visitJumpInsn(Opcodes.IF_ICMPGT, evalNotNextLastRedo); + + // 1) Labeled control flow: compare against each enclosing loop/block label + Label checkUnlabeled = new Label(); + mv.visitVarInsn(Opcodes.ALOAD, labelSlot); + mv.visitJumpInsn(Opcodes.IFNULL, checkUnlabeled); + + for (LoopLabels loopLabels : emitterVisitor.ctx.javaClassInfo.loopLabelStack) { + if (loopLabels != null && loopLabels.labelName != null) { + Label nextLabel = new Label(); + mv.visitVarInsn(Opcodes.ALOAD, labelSlot); + mv.visitLdcInsn(loopLabels.labelName); + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, + "java/lang/String", + "equals", + "(Ljava/lang/Object;)Z", + false); + mv.visitJumpInsn(Opcodes.IFEQ, nextLabel); + + // Matched label: jump based on type (0=LAST,1=NEXT,2=REDO) + Label isLast = new Label(); + Label isNext = new Label(); + Label isRedo = new Label(); + mv.visitVarInsn(Opcodes.ILOAD, typeSlot); + mv.visitInsn(Opcodes.ICONST_0); + mv.visitJumpInsn(Opcodes.IF_ICMPEQ, isLast); + mv.visitVarInsn(Opcodes.ILOAD, typeSlot); + mv.visitInsn(Opcodes.ICONST_1); + mv.visitJumpInsn(Opcodes.IF_ICMPEQ, isNext); + mv.visitVarInsn(Opcodes.ILOAD, typeSlot); + mv.visitInsn(Opcodes.ICONST_2); + mv.visitJumpInsn(Opcodes.IF_ICMPEQ, isRedo); + + // Other types are not handled here + mv.visitJumpInsn(Opcodes.GOTO, nextLabel); + + mv.visitLabel(isLast); + mv.visitJumpInsn(Opcodes.GOTO, loopLabels.lastLabel); + + mv.visitLabel(isNext); + mv.visitJumpInsn(Opcodes.GOTO, loopLabels.nextLabel); + + mv.visitLabel(isRedo); + mv.visitJumpInsn(Opcodes.GOTO, loopLabels.redoLabel); + + mv.visitLabel(nextLabel); + } + } + + // No labeled target matched: throw the marker's error + 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.visitJumpInsn(Opcodes.GOTO, evalNoControlFlow); + + // 2) Unlabeled control flow: target the innermost true loop + mv.visitLabel(checkUnlabeled); + LoopLabels unlabeledTarget = emitterVisitor.ctx.javaClassInfo.findInnermostTrueLoopLabels(); + if (unlabeledTarget != null) { + Label isLast = new Label(); + Label isNext = new Label(); + Label isRedo = new Label(); + mv.visitVarInsn(Opcodes.ILOAD, typeSlot); + mv.visitInsn(Opcodes.ICONST_0); + mv.visitJumpInsn(Opcodes.IF_ICMPEQ, isLast); + mv.visitVarInsn(Opcodes.ILOAD, typeSlot); + mv.visitInsn(Opcodes.ICONST_1); + mv.visitJumpInsn(Opcodes.IF_ICMPEQ, isNext); + mv.visitVarInsn(Opcodes.ILOAD, typeSlot); + mv.visitInsn(Opcodes.ICONST_2); + mv.visitJumpInsn(Opcodes.IF_ICMPEQ, isRedo); + // Any other control flow type was filtered out earlier; fall through. + mv.visitJumpInsn(Opcodes.GOTO, evalNotNextLastRedo); + + mv.visitLabel(isLast); + mv.visitJumpInsn(Opcodes.GOTO, unlabeledTarget.lastLabel); + + mv.visitLabel(isNext); + mv.visitJumpInsn(Opcodes.GOTO, unlabeledTarget.nextLabel); + + mv.visitLabel(isRedo); + mv.visitJumpInsn(Opcodes.GOTO, unlabeledTarget.redoLabel); + } else { + // next/last/redo outside any loop + 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.visitJumpInsn(Opcodes.GOTO, evalNoControlFlow); + } + + // Fallthrough for non NEXT/LAST/REDO control flow markers: treat as normal value + mv.visitLabel(evalNotNextLastRedo); + mv.visitVarInsn(Opcodes.ALOAD, cfSlot); + + mv.visitLabel(evalNoControlFlow); + // 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/EmitStatement.java b/src/main/java/org/perlonjava/codegen/EmitStatement.java index a5ce11e75..2e105f3ae 100644 --- a/src/main/java/org/perlonjava/codegen/EmitStatement.java +++ b/src/main/java/org/perlonjava/codegen/EmitStatement.java @@ -154,12 +154,16 @@ public static void emitFor3(EmitterVisitor emitterVisitor, For3Node node) { if (node.useNewScope) { // Register next/redo/last labels emitterVisitor.ctx.logDebug("FOR3 label: " + node.labelName); + boolean isUnlabeledTarget = !node.isSimpleBlock; emitterVisitor.ctx.javaClassInfo.pushLoopLabels( node.labelName, continueLabel, redoLabel, endLabel, - RuntimeContextType.VOID); + emitterVisitor.ctx.javaClassInfo.stackLevelManager.getStackLevel(), + RuntimeContextType.VOID, + true, + isUnlabeledTarget); // Visit the loop body node.body.accept(voidVisitor); diff --git a/src/main/java/org/perlonjava/codegen/JavaClassInfo.java b/src/main/java/org/perlonjava/codegen/JavaClassInfo.java index 39a58e80d..58d8621e1 100644 --- a/src/main/java/org/perlonjava/codegen/JavaClassInfo.java +++ b/src/main/java/org/perlonjava/codegen/JavaClassInfo.java @@ -150,6 +150,10 @@ public void pushLoopLabels(String labelName, Label nextLabel, Label redoLabel, L public void pushLoopLabels(String labelName, Label nextLabel, Label redoLabel, Label lastLabel, int stackLevel, int context, boolean isTrueLoop) { loopLabelStack.push(new LoopLabels(labelName, nextLabel, redoLabel, lastLabel, stackLevel, context, isTrueLoop)); } + + public void pushLoopLabels(String labelName, Label nextLabel, Label redoLabel, Label lastLabel, int stackLevel, int context, boolean isTrueLoop, boolean isUnlabeledControlFlowTarget) { + loopLabelStack.push(new LoopLabels(labelName, nextLabel, redoLabel, lastLabel, stackLevel, context, isTrueLoop, isUnlabeledControlFlowTarget)); + } /** * Pushes a LoopLabels object onto the loop label stack. @@ -213,6 +217,25 @@ public LoopLabels findLoopLabelsByName(String labelName) { return null; } + /** + * 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. + * + * @return the innermost LoopLabels with isTrueLoop=true, or null if none + */ + public LoopLabels findInnermostTrueLoopLabels() { + for (LoopLabels loopLabels : loopLabelStack) { + if (loopLabels != null && loopLabels.isTrueLoop && loopLabels.isUnlabeledControlFlowTarget) { + return loopLabels; + } + } + return null; + } + public void pushGotoLabels(String labelName, Label gotoLabel) { gotoLabelStack.push(new GotoLabels(labelName, gotoLabel, stackLevelManager.getStackLevel())); } diff --git a/src/main/java/org/perlonjava/codegen/LoopLabels.java b/src/main/java/org/perlonjava/codegen/LoopLabels.java index aa9e145bd..878248275 100644 --- a/src/main/java/org/perlonjava/codegen/LoopLabels.java +++ b/src/main/java/org/perlonjava/codegen/LoopLabels.java @@ -51,6 +51,18 @@ public class LoopLabels { */ public boolean isTrueLoop; + /** + * Whether unlabeled next/last/redo should target this loop/block. + * + * Perl semantics: + * - Unlabeled next/last/redo target the nearest enclosing true loop. + * - Labeled next/last/redo can target labeled blocks (e.g. next SKIP in SKIP: { ... }). + * + * We keep block loops on the stack so labeled control flow can find them, + * but prevent them from being selected as the target for unlabeled control flow. + */ + public boolean isUnlabeledControlFlowTarget; + /** * Creates a new LoopLabels instance with all necessary label information. * @@ -62,7 +74,7 @@ public class LoopLabels { * @param context The context type for this loop */ public LoopLabels(String labelName, Label nextLabel, Label redoLabel, Label lastLabel, int asmStackLevel, int context) { - this(labelName, nextLabel, redoLabel, lastLabel, asmStackLevel, context, true); + this(labelName, nextLabel, redoLabel, lastLabel, asmStackLevel, context, true, true); } /** @@ -77,6 +89,10 @@ public LoopLabels(String labelName, Label nextLabel, Label redoLabel, Label last * @param isTrueLoop Whether this is a true loop (for/while/until) or pseudo-loop (do-while/bare) */ public LoopLabels(String labelName, Label nextLabel, Label redoLabel, Label lastLabel, int asmStackLevel, int context, boolean isTrueLoop) { + this(labelName, nextLabel, redoLabel, lastLabel, asmStackLevel, context, isTrueLoop, true); + } + + public LoopLabels(String labelName, Label nextLabel, Label redoLabel, Label lastLabel, int asmStackLevel, int context, boolean isTrueLoop, boolean isUnlabeledControlFlowTarget) { this.labelName = labelName; this.nextLabel = nextLabel; this.redoLabel = redoLabel; @@ -84,6 +100,7 @@ public LoopLabels(String labelName, Label nextLabel, Label redoLabel, Label last this.asmStackLevel = asmStackLevel; this.context = context; this.isTrueLoop = isTrueLoop; + this.isUnlabeledControlFlowTarget = isUnlabeledControlFlowTarget; } /** diff --git a/src/main/java/org/perlonjava/runtime/ControlFlowMarker.java b/src/main/java/org/perlonjava/runtime/ControlFlowMarker.java index 3a95330aa..90d62ff3d 100644 --- a/src/main/java/org/perlonjava/runtime/ControlFlowMarker.java +++ b/src/main/java/org/perlonjava/runtime/ControlFlowMarker.java @@ -74,27 +74,31 @@ public void debugPrint(String context) { * * @throws PerlCompilerException Always throws with contextual error message */ - public void throwError() { + public String buildErrorMessage() { String location = " at " + fileName + " line " + lineNumber; if (type == ControlFlowType.TAILCALL) { // Tail call should have been handled by trampoline at returnLabel - throw new PerlCompilerException("Tail call escaped to top level (internal error)" + location); + return "Tail call escaped to top level (internal error)" + location; } else if (type == ControlFlowType.GOTO) { if (label != null) { - throw new PerlCompilerException("Can't find label " + label + location); + return "Can't find label " + label + location; } else { - throw new PerlCompilerException("goto must have a label" + location); + return "goto must have a label" + location; } } else { // last/next/redo String operation = type.name().toLowerCase(); if (label != null) { - throw new PerlCompilerException("Label not found for \"" + operation + " " + label + "\"" + location); + return "Label not found for \"" + operation + " " + label + "\"" + location; } else { - throw new PerlCompilerException("Can't \"" + operation + "\" outside a loop block" + location); + return "Can't \"" + operation + "\" outside a loop block" + location; } } } + + public void throwError() { + throw new PerlCompilerException(buildErrorMessage()); + } } diff --git a/src/main/java/org/perlonjava/runtime/RuntimeCode.java b/src/main/java/org/perlonjava/runtime/RuntimeCode.java index 3fbda9b21..f6e151be4 100644 --- a/src/main/java/org/perlonjava/runtime/RuntimeCode.java +++ b/src/main/java/org/perlonjava/runtime/RuntimeCode.java @@ -8,6 +8,7 @@ import org.perlonjava.codegen.JavaClassInfo; import org.perlonjava.lexer.Lexer; import org.perlonjava.lexer.LexerToken; +import org.perlonjava.operators.WarnDie; import org.perlonjava.parser.Parser; import org.perlonjava.mro.InheritanceResolver; import org.perlonjava.operators.ModuleOperators; @@ -265,7 +266,7 @@ public static Class evalStringHelper(RuntimeScalar code, String evalTag) thro true // use try-catch ); runUnitcheckBlocks(ctx.unitcheckBlocks); - } catch (Exception e) { + } catch (Throwable e) { // Compilation error in eval-string // Set the global error variable "$@" using GlobalContext.setGlobalVariable(key, value) @@ -700,6 +701,23 @@ public static RuntimeList apply(RuntimeScalar runtimeScalar, RuntimeArray a, int throw new PerlCompilerException("Not a CODE reference"); } + // Method to apply (execute) a subroutine reference for eval/evalbytes. + // Eval STRING must allow next/last/redo to propagate to the enclosing scope. + // The caller is responsible for handling RuntimeControlFlowList markers. + public static RuntimeList applyEval(RuntimeScalar runtimeScalar, RuntimeArray a, int callContext) { + try { + RuntimeList result = apply(runtimeScalar, a, callContext); + return result; + } catch (Throwable t) { + // Perl eval catches exceptions; set $@ and return undef / empty list. + WarnDie.catchEval(t); + if (callContext == RuntimeContextType.LIST) { + return new RuntimeList(); + } + return new RuntimeList(new RuntimeScalar()); + } + } + private static RuntimeScalar handleCodeOverload(RuntimeScalar runtimeScalar) { // Check if object is eligible for overloading int blessId = blessedId(runtimeScalar); diff --git a/src/test/resources/unit/eval_next_propagation.t b/src/test/resources/unit/eval_next_propagation.t new file mode 100644 index 000000000..e681b0991 --- /dev/null +++ b/src/test/resources/unit/eval_next_propagation.t @@ -0,0 +1,38 @@ +use strict; +use warnings; +use Test::More; + +# This test demonstrates Perl semantics for control flow from eval STRING. +# It is kept in dev/sandbox until PerlOnJava matches perl. + +sub run_eval_next_in_loop { + my $after_eval = 0; + for my $i (1..3) { + local $@; + eval q{ next; }; + $after_eval++; + } + return ($after_eval, $@); +} + +sub run_eval_next_in_labeled_block { + my $after_eval = 0; + for my $i (1..2) { + local $@; + BLOCK: { + eval q{ next BLOCK; }; + $after_eval++; + } + } + return ($after_eval, $@); +} + +my ($after_loop, $err_loop) = run_eval_next_in_loop(); +is($after_loop, 0, q{eval 'next' inside a loop should skip the rest of the iteration}); +is($err_loop, '', q{$@ should be empty after eval 'next' inside loop}); + +my ($after_block, $err_block) = run_eval_next_in_labeled_block(); +is($after_block, 0, q{eval 'next LABEL' should exit the labeled block}); +is($err_block, '', q{$@ should be empty after eval 'next LABEL'}); + +done_testing(); From b7e278cece56a32ed5c3ef3cf2644eff4f23fd63 Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Thu, 29 Jan 2026 19:53:34 +0100 Subject: [PATCH 2/2] Fix state vars skipped by goto in bare blocks --- .../org/perlonjava/codegen/EmitBlock.java | 81 ++++++++++++++++++- .../org/perlonjava/codegen/EmitStatement.java | 6 +- .../org/perlonjava/codegen/JavaClassInfo.java | 2 +- 3 files changed, 85 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/perlonjava/codegen/EmitBlock.java b/src/main/java/org/perlonjava/codegen/EmitBlock.java index d9e4a6da1..203565f63 100644 --- a/src/main/java/org/perlonjava/codegen/EmitBlock.java +++ b/src/main/java/org/perlonjava/codegen/EmitBlock.java @@ -8,9 +8,69 @@ import org.perlonjava.runtime.RuntimeContextType; import java.util.List; +import java.util.Set; +import java.util.LinkedHashSet; public class EmitBlock { + private static void collectStateDeclSigilNodes(Node node, Set out) { + if (node == null) { + return; + } + if (node instanceof OperatorNode op) { + if ("state".equals(op.operator) && op.operand instanceof OperatorNode sigilNode) { + if (sigilNode.operand instanceof IdentifierNode && "$@%".contains(sigilNode.operator)) { + out.add(sigilNode); + } + } + collectStateDeclSigilNodes(op.operand, out); + return; + } + if (node instanceof BinaryOperatorNode bin) { + collectStateDeclSigilNodes(bin.left, out); + collectStateDeclSigilNodes(bin.right, out); + return; + } + if (node instanceof ListNode list) { + for (Node child : list.elements) { + collectStateDeclSigilNodes(child, out); + } + return; + } + if (node instanceof BlockNode block) { + for (Node child : block.elements) { + collectStateDeclSigilNodes(child, out); + } + return; + } + if (node instanceof For1Node for1) { + collectStateDeclSigilNodes(for1.variable, out); + collectStateDeclSigilNodes(for1.list, out); + collectStateDeclSigilNodes(for1.body, out); + collectStateDeclSigilNodes(for1.continueBlock, out); + return; + } + if (node instanceof For3Node for3) { + collectStateDeclSigilNodes(for3.initialization, out); + collectStateDeclSigilNodes(for3.condition, out); + collectStateDeclSigilNodes(for3.increment, out); + collectStateDeclSigilNodes(for3.body, out); + collectStateDeclSigilNodes(for3.continueBlock, out); + return; + } + if (node instanceof IfNode ifNode) { + collectStateDeclSigilNodes(ifNode.condition, out); + collectStateDeclSigilNodes(ifNode.thenBranch, out); + collectStateDeclSigilNodes(ifNode.elseBranch, out); + return; + } + if (node instanceof TryNode tryNode) { + collectStateDeclSigilNodes(tryNode.tryBlock, out); + collectStateDeclSigilNodes(tryNode.catchBlock, out); + collectStateDeclSigilNodes(tryNode.finallyBlock, out); + } + } + /** * Emits bytecode for a block of statements. * @@ -32,6 +92,18 @@ public static void emitBlock(EmitterVisitor emitterVisitor, BlockNode node) { emitterVisitor.with(RuntimeContextType.VOID); // statements in the middle of the block have context VOID List list = node.elements; + // Hoist `state` declarations to the beginning of the block scope so that JVM local slots + // are initialized even if a `goto` skips the original declaration statement. + // This prevents NPEs when later code evaluates e.g. `defined $state_var`. + Set stateDeclSigilNodes = new LinkedHashSet<>(); + for (Node element : list) { + collectStateDeclSigilNodes(element, stateDeclSigilNodes); + } + for (OperatorNode sigilNode : stateDeclSigilNodes) { + new OperatorNode("state", sigilNode, sigilNode.tokenIndex) + .accept(voidVisitor); + } + int lastNonNullIndex = -1; for (int i = list.size() - 1; i >= 0; i--) { if (list.get(i) != null) { @@ -62,6 +134,11 @@ public static void emitBlock(EmitterVisitor emitterVisitor, BlockNode node) { // A labeled/bare block used as a loop target (e.g. SKIP: { ... }) is a // pseudo-loop: it supports labeled next/last/redo (e.g. next SKIP), but // an unlabeled next/last/redo must target the nearest enclosing true loop. + // + // However, a *bare* block with loop control (e.g. `{ ...; redo }` or + // `{ ... } continue { ... }`) is itself a valid target for *unlabeled* + // last/next/redo, matching Perl semantics. + boolean isBareBlock = node.labelName == null; emitterVisitor.ctx.javaClassInfo.pushLoopLabels( node.labelName, nextLabel, @@ -69,8 +146,8 @@ public static void emitBlock(EmitterVisitor emitterVisitor, BlockNode node) { nextLabel, emitterVisitor.ctx.javaClassInfo.stackLevelManager.getStackLevel(), emitterVisitor.ctx.contextType, - false, - false); + isBareBlock, + isBareBlock); } // Special case: detect pattern of `local $_` followed by `For1Node` with needsArrayOfAlias diff --git a/src/main/java/org/perlonjava/codegen/EmitStatement.java b/src/main/java/org/perlonjava/codegen/EmitStatement.java index 2e105f3ae..3cc9ed7dd 100644 --- a/src/main/java/org/perlonjava/codegen/EmitStatement.java +++ b/src/main/java/org/perlonjava/codegen/EmitStatement.java @@ -154,7 +154,11 @@ public static void emitFor3(EmitterVisitor emitterVisitor, For3Node node) { if (node.useNewScope) { // Register next/redo/last labels emitterVisitor.ctx.logDebug("FOR3 label: " + node.labelName); - boolean isUnlabeledTarget = !node.isSimpleBlock; + // A simple-block For3Node (isSimpleBlock=true) is used to model bare/labeled + // blocks like `{ ... }` and `LABEL: { ... }` (including `... } continue { ... }`). + // Unlabeled next/last/redo must be allowed for *bare* blocks (no label), but + // must *not* accidentally target pseudo-loops like `SKIP: { ... }`. + boolean isUnlabeledTarget = !node.isSimpleBlock || node.labelName == null; emitterVisitor.ctx.javaClassInfo.pushLoopLabels( node.labelName, continueLabel, diff --git a/src/main/java/org/perlonjava/codegen/JavaClassInfo.java b/src/main/java/org/perlonjava/codegen/JavaClassInfo.java index 58d8621e1..e29219ba4 100644 --- a/src/main/java/org/perlonjava/codegen/JavaClassInfo.java +++ b/src/main/java/org/perlonjava/codegen/JavaClassInfo.java @@ -229,7 +229,7 @@ public LoopLabels findLoopLabelsByName(String labelName) { */ public LoopLabels findInnermostTrueLoopLabels() { for (LoopLabels loopLabels : loopLabelStack) { - if (loopLabels != null && loopLabels.isTrueLoop && loopLabels.isUnlabeledControlFlowTarget) { + if (loopLabels != null && loopLabels.isUnlabeledControlFlowTarget) { return loopLabels; } }