Skip to content
Merged
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
85 changes: 84 additions & 1 deletion src/main/java/org/perlonjava/codegen/EmitBlock.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<OperatorNode> 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.
*
Expand All @@ -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<Node> 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<OperatorNode> 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) {
Expand Down Expand Up @@ -59,12 +131,23 @@ 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.
//
// 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,
redoLabel,
nextLabel,
emitterVisitor.ctx.contextType);
emitterVisitor.ctx.javaClassInfo.stackLevelManager.getStackLevel(),
emitterVisitor.ctx.contextType,
isBareBlock,
isBareBlock);
}

// Special case: detect pattern of `local $_` followed by `For1Node` with needsArrayOfAlias
Expand Down
9 changes: 8 additions & 1 deletion src/main/java/org/perlonjava/codegen/EmitControlFlow.java
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
205 changes: 204 additions & 1 deletion src/main/java/org/perlonjava/codegen/EmitEval.java
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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", "<init>", "()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", "<init>", "()V", false);
mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "org/perlonjava/runtime/RuntimeList", "<init>", "(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", "<init>", "()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", "<init>", "()V", false);
mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "org/perlonjava/runtime/RuntimeList", "<init>", "(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
Expand Down
10 changes: 9 additions & 1 deletion src/main/java/org/perlonjava/codegen/EmitStatement.java
Original file line number Diff line number Diff line change
Expand Up @@ -154,12 +154,20 @@ public static void emitFor3(EmitterVisitor emitterVisitor, For3Node node) {
if (node.useNewScope) {
// Register next/redo/last labels
emitterVisitor.ctx.logDebug("FOR3 label: " + node.labelName);
// 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,
redoLabel,
endLabel,
RuntimeContextType.VOID);
emitterVisitor.ctx.javaClassInfo.stackLevelManager.getStackLevel(),
RuntimeContextType.VOID,
true,
isUnlabeledTarget);

// Visit the loop body
node.body.accept(voidVisitor);
Expand Down
Loading