Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 50 additions & 10 deletions src/main/java/org/perlonjava/astrefactor/LargeBlockRefactorer.java
Original file line number Diff line number Diff line change
Expand Up @@ -240,8 +240,27 @@ private static boolean shouldRefactorBlock(BlockNode node, EmitterVisitor emitte
return false;
}

// Check if we're in a context that allows refactoring
return refactorEnabled || !emitterVisitor.ctx.javaClassInfo.gotoLabelStack.isEmpty();
// Codegen-time refactoring must be conservative.
// Wrapping statements in extracted closures changes control-flow semantics by losing the
// enclosing loop label stack. Only refactor when explicitly enabled and the block is
// actually large enough to risk hitting JVM method-size limits.
//
// If you need to re-enable/extend codegen-time refactoring in the future, do NOT wrap
// arbitrary statements into closures inside a loop body. In particular, moving
// `next/last/redo/goto` into an extracted closure will make codegen see an empty
// loop-label stack and compile these as non-local control flow.
//
// Safe options (in order of preference):
// - Keep any loop-control statements in the original method (never move them into a closure).
// - If you must extract, add an explicit mechanism to propagate and handle loop control across
// the extracted boundary (e.g. via RuntimeControlFlowRegistry + loop boundary checks, or by
// implementing/turning on loop handlers/call-site checks).
if (!refactorEnabled) {
return false;
}

long estimatedSize = estimateTotalBytecodeSizeCapped(node.elements, LARGE_BYTECODE_SIZE);
return estimatedSize > LARGE_BYTECODE_SIZE;
}

/**
Expand Down Expand Up @@ -323,6 +342,20 @@ private static void trySmartChunking(BlockNode node, Parser parser, int maxNeste
ControlFlowFinder blockFinder = controlFlowFinderTl.get();
blockFinder.scan(node);
boolean hasAnyControlFlowInBlock = blockFinder.foundControlFlow;

// IMPORTANT: Smart chunking wraps segments in extracted closures.
// Loop control operators (next/last/redo/goto) must remain in the original scope
// so codegen can resolve loop labels and emit local jumps.
// Even when chunk boundaries try to "break" on control flow, the suffix stitching
// can still place the control-flow node inside a generated closure.
//
// Be conservative: if the block contains ANY control flow, do not chunk it.
if (hasAnyControlFlowInBlock) {
if (parser != null || node.annotations != null) {
node.setAnnotation("refactorSkipReason", "Smart chunking skipped: block contains control flow");
}
return;
}
boolean treatAllElementsAsSafe = !hasLabelElement && !hasAnyControlFlowInBlock;

if (treatAllElementsAsSafe) {
Expand Down Expand Up @@ -527,11 +560,16 @@ private static boolean shouldBreakChunk(Node element) {
* Try to refactor the entire block as a subroutine.
*/
private static boolean tryWholeBlockRefactoring(EmitterVisitor emitterVisitor, BlockNode node) {
// Check for unsafe control flow using ControlFlowDetectorVisitor
// This properly handles loop depth - unlabeled next/last/redo inside loops are safe
controlFlowDetector.reset();
controlFlowDetector.scan(node);
if (controlFlowDetector.hasUnsafeControlFlow()) {
// IMPORTANT: wrapping a block in an extracted closure/subroutine loses the enclosing
// loop label stack. Even control flow that is "safe" in-place (e.g. unlabeled `next`
// inside a real loop) becomes unsafe when moved into a closure because codegen will
// not find loop labels and will emit non-local tagged returns.
//
// Therefore, be conservative here: if the block contains ANY control flow statement,
// do not refactor the whole block into a subroutine.
ControlFlowFinder finder = controlFlowFinderTl.get();
finder.scan(node);
if (finder.foundControlFlow) {
return false;
}

Expand Down Expand Up @@ -581,10 +619,12 @@ private static boolean chunkWouldBeWrapped(List<Node> chunk) {
* @return true if unsafe control flow found
*/
private static boolean chunkHasUnsafeControlFlow(List<Node> chunk) {
controlFlowDetector.reset();
// Same reasoning as tryWholeBlockRefactoring(): moving a chunk into a closure loses
// loop context, so ANY control flow statement makes the chunk unsafe.
ControlFlowFinder finder = controlFlowFinderTl.get();
for (Node element : chunk) {
element.accept(controlFlowDetector);
if (controlFlowDetector.hasUnsafeControlFlow()) {
finder.scan(element);
if (finder.foundControlFlow) {
return true;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,12 @@ public void scan(Node root) {

if (root instanceof AbstractNode abstractNode) {
Boolean cached = abstractNode.getCachedHasAnyControlFlow();
if (cached != null) {
foundControlFlow = cached;
// IMPORTANT: Only trust positive cache entries.
// A cached FALSE can become stale if the AST is rewritten (e.g. during large-block
// refactoring / macros) and would cause us to miss control flow, leading to unsafe
// extraction of chunks that contain next/last/redo/goto.
if (Boolean.TRUE.equals(cached)) {
foundControlFlow = true;
return;
}
}
Expand Down
16 changes: 8 additions & 8 deletions src/main/java/org/perlonjava/codegen/EmitBlock.java
Original file line number Diff line number Diff line change
Expand Up @@ -132,22 +132,22 @@ public static void emitBlock(EmitterVisitor emitterVisitor, BlockNode node) {

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.
// 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;
// Perl semantics: unlabeled next/last/redo prefer a real loop if one exists,
// but outside any real loop a bare block can act as the target.
boolean isUnlabeledTarget = node.labelName == null;
emitterVisitor.ctx.javaClassInfo.pushLoopLabels(
node.labelName,
nextLabel,
redoLabel,
nextLabel,
emitterVisitor.ctx.javaClassInfo.stackLevelManager.getStackLevel(),
emitterVisitor.ctx.contextType,
isBareBlock,
isBareBlock);
true,
isUnlabeledTarget,
false);
}

// Special case: detect pattern of `local $_` followed by `For1Node` with needsArrayOfAlias
Expand Down
16 changes: 16 additions & 0 deletions src/main/java/org/perlonjava/codegen/EmitControlFlow.java
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,22 @@ static void handleNextOperator(EmitterContext ctx, OperatorNode node) {
}

if (loopLabels == null) {
if (ctx.compilerOptions != null && ctx.compilerOptions.debugEnabled) {
ctx.logDebug("visit(next): loopLabels == null; dumping loopLabelStack (top-first):");
int depth = 0;
for (LoopLabels ll : ctx.javaClassInfo.loopLabelStack) {
ctx.logDebug(
" [" + (depth++) + "] label=" + ll.labelName +
" isTrueLoop=" + ll.isTrueLoop +
" isRealLoop=" + ll.isRealLoop +
" isUnlabeledTarget=" + ll.isUnlabeledControlFlowTarget +
" asmStackLevel=" + ll.asmStackLevel +
" context=" + ll.context);
}
if (depth == 0) {
ctx.logDebug(" <empty>");
}
}
// Non-local control flow: return tagged RuntimeControlFlowList
ctx.logDebug("visit(next): Non-local control flow for " + operator + " " + labelStr);

Expand Down
7 changes: 6 additions & 1 deletion src/main/java/org/perlonjava/codegen/EmitForeach.java
Original file line number Diff line number Diff line change
Expand Up @@ -305,16 +305,21 @@ public static void emitFor1(EmitterVisitor emitterVisitor, For1Node node) {
emitterVisitor.ctx.javaClassInfo.stackLevelManager.getStackLevel(),
RuntimeContextType.VOID);
currentLoopLabels.controlFlowHandler = controlFlowHandler;

emitterVisitor.ctx.logDebug("FOR1 pushLoopLabels label=" + node.labelName + " asmStackLevel=" + currentLoopLabels.asmStackLevel);
emitterVisitor.ctx.javaClassInfo.pushLoopLabels(currentLoopLabels);

emitterVisitor.ctx.logDebug("FOR1 loopLabelStack depth after push=" + emitterVisitor.ctx.javaClassInfo.loopLabelStack.size());

node.body.accept(emitterVisitor.with(RuntimeContextType.VOID));

// Check RuntimeControlFlowRegistry for non-local control flow
emitRegistryCheck(mv, currentLoopLabels, redoLabel, continueLabel, loopEnd);

LoopLabels poppedLabels = emitterVisitor.ctx.javaClassInfo.popLoopLabels();

emitterVisitor.ctx.logDebug("FOR1 popLoopLabels label=" + (poppedLabels != null ? poppedLabels.labelName : null) +
" remainingDepth=" + emitterVisitor.ctx.javaClassInfo.loopLabelStack.size());

mv.visitLabel(continueLabel);

if (node.continueBlock != null) {
Expand Down
4 changes: 3 additions & 1 deletion src/main/java/org/perlonjava/codegen/EmitStatement.java
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ public static void emitFor3(EmitterVisitor emitterVisitor, For3Node node) {
// 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,
Expand All @@ -167,7 +168,8 @@ public static void emitFor3(EmitterVisitor emitterVisitor, For3Node node) {
emitterVisitor.ctx.javaClassInfo.stackLevelManager.getStackLevel(),
RuntimeContextType.VOID,
true,
isUnlabeledTarget);
isUnlabeledTarget,
!node.isSimpleBlock);

// Visit the loop body
node.body.accept(voidVisitor);
Expand Down
13 changes: 13 additions & 0 deletions src/main/java/org/perlonjava/codegen/JavaClassInfo.java
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,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, boolean isUnlabeledControlFlowTarget) {
loopLabelStack.push(new LoopLabels(labelName, nextLabel, redoLabel, lastLabel, stackLevel, context, isTrueLoop, isUnlabeledControlFlowTarget));
}

public void pushLoopLabels(String labelName, Label nextLabel, Label redoLabel, Label lastLabel, int stackLevel, int context, boolean isTrueLoop, boolean isUnlabeledControlFlowTarget, boolean isRealLoop) {
loopLabelStack.push(new LoopLabels(labelName, nextLabel, redoLabel, lastLabel, stackLevel, context, isTrueLoop, isUnlabeledControlFlowTarget, isRealLoop));
}

/**
* Pushes a LoopLabels object onto the loop label stack.
Expand Down Expand Up @@ -228,11 +232,20 @@ public LoopLabels findLoopLabelsByName(String labelName) {
* @return the innermost LoopLabels with isTrueLoop=true, or null if none
*/
public LoopLabels findInnermostTrueLoopLabels() {
// Prefer a real loop (for/foreach/while/until) if one exists.
for (LoopLabels loopLabels : loopLabelStack) {
if (loopLabels != null && loopLabels.isUnlabeledControlFlowTarget && loopLabels.isRealLoop) {
return loopLabels;
}
}

// Fallback: allow a block to act as a loop target only when there's no real loop.
for (LoopLabels loopLabels : loopLabelStack) {
if (loopLabels != null && loopLabels.isUnlabeledControlFlowTarget) {
return loopLabels;
}
}

return null;
}

Expand Down
15 changes: 15 additions & 0 deletions src/main/java/org/perlonjava/codegen/LoopLabels.java
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,15 @@ public class LoopLabels {
*/
public boolean isTrueLoop;

/**
* Whether this is a "real" loop construct (for/foreach/while/until).
*
* Perl semantics for *unlabeled* next/last/redo:
* - Prefer the nearest enclosing real loop.
* - Only when there is no real loop, a block can act as a loop target.
*/
public boolean isRealLoop;

/**
* Whether unlabeled next/last/redo should target this loop/block.
*
Expand Down Expand Up @@ -101,6 +110,12 @@ public LoopLabels(String labelName, Label nextLabel, Label redoLabel, Label last
this.context = context;
this.isTrueLoop = isTrueLoop;
this.isUnlabeledControlFlowTarget = isUnlabeledControlFlowTarget;
this.isRealLoop = true;
}

public LoopLabels(String labelName, Label nextLabel, Label redoLabel, Label lastLabel, int asmStackLevel, int context, boolean isTrueLoop, boolean isUnlabeledControlFlowTarget, boolean isRealLoop) {
this(labelName, nextLabel, redoLabel, lastLabel, asmStackLevel, context, isTrueLoop, isUnlabeledControlFlowTarget);
this.isRealLoop = isRealLoop;
}

/**
Expand Down
35 changes: 35 additions & 0 deletions src/test/resources/unit/next_if_should_skip_iteration.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
use strict;
use warnings;
use Test::More;

# Regression test for perl5_t/t/uni/variables.t drift.
# In perl, `next if (...)` must skip the *entire* loop iteration.
# jperl currently mis-handles this in a specific shape, causing +12 extra checks.

my $count = 0;

for my $ord (0x21 .. 0x2f) { # includes '#' (0x23) and '*' (0x2a)
my $chr = chr $ord;
my $tests = 0;

if ($chr =~ /[[:punct:][:digit:]]/a) {
next if ($chr eq '#' or $chr eq '*');

$count++;
$tests++;
$count++;
$tests++;
}

$count++;
$tests++;

SKIP: {
my $pad = 6 - $tests;
$count += $pad if $pad > 0;
}
}

is($count, 78, 'next-if must skip the whole iteration (no +12 drift)');

done_testing();